222 lines
9.2 KiB
C#
222 lines
9.2 KiB
C#
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;
|
||
}
|
||
}
|