Files

222 lines
9.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ClosedXML.Excel;
using DongfangHydro.Dashboard.Api.Importing;
using Xunit;
namespace DongfangHydro.Dashboard.Tests;
public sealed class BomWorkbookParserTests
{
[Fact]
public void Parse_builds_hierarchy_and_preserves_repeated_material_occurrences()
{
using var stream = CreateWorkbook();
var document = BomWorkbookParser.Parse(stream, "test-bom.xlsx");
Assert.Equal("SG2026-06-033", document.Metadata.SourceDocumentCode);
Assert.Equal("2437-QM0201", document.Metadata.WorkOrderCode);
Assert.Equal("青峪口水库工程", document.Metadata.ProjectName);
Assert.Equal("青峪口尾水单向门机 2×160/10", document.Metadata.ContractProductName);
Assert.Equal("电站尾水2×160kN/100kN单向门式启闭机", document.Metadata.DrawingProductName);
Assert.Equal(1, document.Metadata.BatchQuantity);
Assert.Equal(3, document.BlockCount);
Assert.Single(document.Warnings);
Assert.Contains("第 11 行", document.Warnings[0]);
Assert.Equal(5, document.BomNodes.Count);
Assert.Equal(2, document.BomNodes.Count(node => node.Level == 1));
Assert.Single(document.BomNodes, node => node.Level == 2);
Assert.Equal(2, document.BomNodes.Count(node => node.Level == 3));
var component = Assert.Single(document.BomNodes, node => node.DrawingNumber == "C-01");
Assert.Equal(2, component.ParentSourceRow);
Assert.Equal("component", component.NodeType);
Assert.Equal("self_made", component.SupplyType);
Assert.Equal("焊接件", component.Specification);
Assert.Equal("装配件", component.Material);
Assert.Equal(4m, component.UnitWeight);
Assert.Equal(8m, component.TotalWeight);
Assert.Equal("备注", component.Remark);
var bolts = document.BomNodes.Where(node => node.Name == "螺栓").OrderBy(node => node.SourceRow).ToList();
Assert.Equal(2, bolts.Count);
Assert.All(bolts, node => Assert.Equal(6, node.ParentSourceRow));
Assert.Null(bolts[0].Quantity);
Assert.Equal("/", bolts[0].QuantityText);
Assert.Equal(-8m, bolts[1].Quantity);
Assert.Null(bolts[1].UnitWeight);
Assert.Null(bolts[1].TotalWeight);
Assert.Equal("95L", bolts[1].UnitWeightText);
Assert.Equal("170L", bolts[1].TotalWeightText);
Assert.All(bolts, node => Assert.Equal("material", node.NodeType));
Assert.All(bolts, node => Assert.Equal("purchased", node.SupplyType));
Assert.NotEqual(bolts[0].SourceRow, bolts[1].SourceRow);
}
[Fact]
public void Parse_reads_material_quota_categories_and_nullable_quantity()
{
using var stream = CreateWorkbook();
var document = BomWorkbookParser.Parse(stream, "test-bom.xlsx");
Assert.Equal(2, document.MaterialQuotas.Count);
var steel = document.MaterialQuotas[0];
Assert.Equal("钢板", steel.Category);
Assert.Equal("1010100182", steel.MaterialCode);
Assert.Equal("钢板/Q235B", steel.MaterialName);
Assert.Equal("δ2-1500", steel.Specification);
Assert.Equal("kg", steel.Unit);
Assert.Null(steel.Quantity);
Assert.Null(steel.NetWeight);
Assert.Equal("/", steel.NetWeightText);
Assert.Equal(21m, steel.ConsumptionQuota);
Assert.Equal("21件", steel.ConsumptionQuotaText);
Assert.Equal("无", steel.Brand);
Assert.Equal("外协外购", document.MaterialQuotas[1].Category);
}
[Fact]
public void Parse_rejects_a_block_whose_parent_was_not_declared()
{
using var stream = CreateWorkbook(orphanParent: true);
var exception = Assert.Throws<BomImportException>(() =>
BomWorkbookParser.Parse(stream, "test-bom.xlsx"));
Assert.Contains("父节点", exception.Message);
Assert.Contains("P-X", exception.Message);
}
[Fact]
public void Parse_imports_a_material_quota_row_without_a_material_code()
{
using var stream = CreateWorkbook(malformedQuota: true);
var document = BomWorkbookParser.Parse(stream, "test-bom.xlsx");
var uncoded = Assert.Single(document.MaterialQuotas, item => item.SourceRow == 13);
Assert.Equal(string.Empty, uncoded.MaterialCode);
Assert.Equal("缺少编码的物料", uncoded.MaterialName);
Assert.Equal("件", uncoded.Unit);
}
private static MemoryStream CreateWorkbook(bool orphanParent = false, bool malformedQuota = false)
{
using var workbook = new XLWorkbook();
var detail = workbook.Worksheets.Add("明细");
detail.Cell("A1").Value = "序号";
detail.Cell("B1").Value = "图号";
detail.Cell("C1").Value = "名称";
detail.Cell("D1").Value = "型号规格";
detail.Cell("E1").Value = "数量";
detail.Cell("F1").Value = "材料";
detail.Cell("G1").Value = "单重";
detail.Cell("H1").Value = "总重";
detail.Cell("I1").Value = "备注";
SetRow(detail, 2, 1, "P-01", "主起升机构", null, 1, "部件", 10, 10, null);
SetRow(detail, 3, 2, "P-02", "门架", null, 1, "装焊件", 20, 20, null);
SetRow(detail, 5, 1, orphanParent ? "P-X" : "P-01", "主起升机构", null, 1, "部件", 10, 10, null);
SetRow(detail, 6, 1.1, "C-01", "机架", "焊接件", 2, "装配件", 4, 8, "备注");
SetRow(detail, 8, 1.1, "C-01", "机架", "焊接件", 2, "装配件", 4, 8, "备注");
SetRow(detail, 9, 1.11, "GB/T5783", "螺栓", "M12", 4, "标准件", 0.1, 0.4, null);
detail.Cell(9, 5).Value = "/";
SetRow(detail, 10, 1.12, "GB/T5783", "螺栓", "M16", -8, "标准件", 0.2, 1.6, "厂家提供");
detail.Cell(10, 7).Value = "95L";
detail.Cell(10, 8).Value = "170L";
detail.Cell(11, 1).Value = "1.1.3";
var quota = workbook.Worksheets.Add("材料定额");
quota.Cell("A1").Value = "产品材料定额汇总表";
quota.Cell("A2").Value = "编号:SG2026-06-033";
quota.Cell("A3").Value = "合同(项目)名称:青峪口水库工程";
quota.Cell("H3").Value = "工作令号";
quota.Cell("I3").Value = "2437-QM0201";
quota.Cell("A4").Value = "产品合同名称:青峪口尾水单向门机 2×160/10";
quota.Cell("A5").Value = "产品图纸名称:电站尾水2×160kN/100kN单向门式启闭机";
quota.Cell("A6").Value = "说明:本表为1套产品定额";
var headers = new[] { "序号", "物料编码", "物料名称", "型号规格", "单位", "数量", "净重", "消耗定额", "品牌", "备注" };
for (var column = 1; column <= headers.Length; column++)
{
quota.Cell(7, column).Value = headers[column - 1];
}
quota.Cell("A8").Value = "一";
quota.Cell("B8").Value = "钢板";
SetQuotaRow(quota, 9, 1, "1010100182", "钢板/Q235B", "δ2-1500", "kg", null, 33.61, 35.6266, "无", null);
quota.Cell(9, 7).Value = "/";
quota.Cell(9, 8).Value = "21件";
quota.Cell("A11").Value = "二";
quota.Cell("B11").Value = "外协外购";
SetQuotaRow(quota, 12, 1, "2050000001", "减速器", "ZQ-500", "台", 1, 100, 100, "", "外购");
if (malformedQuota)
{
quota.Cell(13, 1).Value = 2;
quota.Cell(13, 3).Value = "缺少编码的物料";
quota.Cell(13, 5).Value = "件";
}
quota.Cell(14, 1).Value = "编码: 编制: 校核: 审核: 日期:2026.6.16";
var stream = new MemoryStream();
workbook.SaveAs(stream);
stream.Position = 0;
return stream;
}
private static void SetRow(
IXLWorksheet worksheet,
int row,
double sequence,
string drawingNumber,
string name,
string? specification,
int quantity,
string material,
double unitWeight,
double totalWeight,
string? remark)
{
worksheet.Cell(row, 1).Value = sequence;
worksheet.Cell(row, 2).Value = drawingNumber;
worksheet.Cell(row, 3).Value = name;
worksheet.Cell(row, 4).Value = specification;
worksheet.Cell(row, 5).Value = quantity;
worksheet.Cell(row, 6).Value = material;
worksheet.Cell(row, 7).Value = unitWeight;
worksheet.Cell(row, 8).Value = totalWeight;
worksheet.Cell(row, 9).Value = remark;
}
private static void SetQuotaRow(
IXLWorksheet worksheet,
int row,
int sequence,
string materialCode,
string materialName,
string specification,
string unit,
double? quantity,
double netWeight,
double consumptionQuota,
string brand,
string? remark)
{
worksheet.Cell(row, 1).Value = sequence;
worksheet.Cell(row, 2).Value = materialCode;
worksheet.Cell(row, 3).Value = materialName;
worksheet.Cell(row, 4).Value = specification;
worksheet.Cell(row, 5).Value = unit;
if (quantity.HasValue)
{
worksheet.Cell(row, 6).Value = quantity.Value;
}
worksheet.Cell(row, 7).Value = netWeight;
worksheet.Cell(row, 8).Value = consumptionQuota;
worksheet.Cell(row, 9).Value = brand;
worksheet.Cell(row, 10).Value = remark;
}
}