From 81ed93045b713292545d3f1ff867987bb4abe7bc Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 6 Aug 2026 23:15:25 -0400 Subject: [PATCH] feat: switch BOM export to a formatted xlsx template instead of generated sheets Copy a pre-built "BOM Template.xlsx" (with headers/formatting) and write into it rather than deleting/recreating worksheets from scratch, so output keeps consistent styling. Cut Templates sheet no longer stores a File Name column; the DXF filename is now derived from the stored revision, simplifying the existing-templates map and removing redundant state. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 ++ ExportDXF/ExportDXF.csproj | 3 ++ ExportDXF/Services/DxfExportService.cs | 16 +++--- ExportDXF/Services/ExcelExportService.cs | 61 +++++++++++++---------- ExportDXF/Templates/BOM Template.xlsx | Bin 0 -> 9407 bytes 5 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 ExportDXF/Templates/BOM Template.xlsx diff --git a/.gitignore b/.gitignore index 00902a3..38233a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +# Stray Windows redirect artifact +/nul + # User-specific files *.suo *.user diff --git a/ExportDXF/ExportDXF.csproj b/ExportDXF/ExportDXF.csproj index 405ba86..0d6545a 100644 --- a/ExportDXF/ExportDXF.csproj +++ b/ExportDXF/ExportDXF.csproj @@ -37,6 +37,9 @@ Always + + Always + diff --git a/ExportDXF/Services/DxfExportService.cs b/ExportDXF/Services/DxfExportService.cs index b7615b3..aab8a6c 100644 --- a/ExportDXF/Services/DxfExportService.cs +++ b/ExportDXF/Services/DxfExportService.cs @@ -130,7 +130,7 @@ namespace ExportDXF.Services ExportContext context, string tempDir, string outputFolder, - Dictionary existingTemplates, + Dictionary existingTemplates, List bomItems) { LogProgress(context, "Active document is a Part"); @@ -159,7 +159,7 @@ namespace ExportDXF.Services ExportContext context, string tempDir, string outputFolder, - Dictionary existingTemplates, + Dictionary existingTemplates, List bomItems) { LogProgress(context, "Active document is an Assembly"); @@ -200,7 +200,7 @@ namespace ExportDXF.Services ExportContext context, string tempDir, string outputFolder, - Dictionary existingTemplates, + Dictionary existingTemplates, List bomItems) { LogProgress(context, "Active document is a Drawing"); @@ -294,7 +294,7 @@ namespace ExportDXF.Services string tempDir, string outputFolder, ExportContext context, - Dictionary existingTemplates, + Dictionary existingTemplates, List bomItems) { int successCount = 0; @@ -362,7 +362,7 @@ namespace ExportDXF.Services Item item, ExportContext context, string outputFolder, - Dictionary existingTemplates, + Dictionary existingTemplates, BomItem bomItem) { var baseName = FilenameTemplateParser.Evaluate(context.FilenameTemplate, item); @@ -377,15 +377,15 @@ namespace ExportDXF.Services if (existing.ContentHash == contentHash) { // Unchanged — skip file write, keep existing - dxfFileName = existing.FileName; revision = existing.Revision; + dxfFileName = GetRevisionFileName(baseName, revision); LogProgress(context, $"Unchanged: {dxfFileName}", LogLevel.Info, item.PartName); _logFileService.LogInfo($"Unchanged: {dxfFileName}"); bomItem.CutTemplate = new CutTemplate { - DxfFilePath = dxfFileName, + DxfFilePath = Path.GetFileNameWithoutExtension(dxfFileName), ContentHash = contentHash, Revision = revision, Thickness = item.Thickness > 0 ? item.Thickness : (double?)null, @@ -402,7 +402,7 @@ namespace ExportDXF.Services dxfFileName = GetRevisionFileName(baseName, revision); LogProgress(context, $"Updated: {dxfFileName} (Rev{revision})", LogLevel.Info, item.PartName); - _logFileService.LogInfo($"Updated: {dxfFileName} (was {existing.FileName})"); + _logFileService.LogInfo($"Updated: {dxfFileName}"); } } else diff --git a/ExportDXF/Services/ExcelExportService.cs b/ExportDXF/Services/ExcelExportService.cs index d097b49..29cf599 100644 --- a/ExportDXF/Services/ExcelExportService.cs +++ b/ExportDXF/Services/ExcelExportService.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using ClosedXML.Excel; using ExportDXF.Models; @@ -8,14 +10,18 @@ namespace ExportDXF.Services { public class ExcelExportService { + private static readonly string TemplatePath = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + "Templates", + "BOM Template.xlsx"); /// /// Reads existing Cut Templates from an xlsx file to compare content hashes. /// Returns empty dictionary if file doesn't exist or has no Cut Templates sheet. - /// Key = Item #, Value = (ContentHash, Revision, FileName) + /// Key = Item #, Value = (ContentHash, Revision) /// - public Dictionary ReadExistingCutTemplates(string xlsxPath) + public Dictionary ReadExistingCutTemplates(string xlsxPath) { - var result = new Dictionary(); + var result = new Dictionary(); if (!File.Exists(xlsxPath)) return result; @@ -49,12 +55,9 @@ namespace ExportDXF.Services var revision = headers.ContainsKey("Revision") ? ws.Cell(row, headers["Revision"]).GetValue() : 1; - var fileName = headers.ContainsKey("File Name") - ? ws.Cell(row, headers["File Name"]).GetString() - : ""; if (!string.IsNullOrEmpty(itemNo)) - result[itemNo] = (hash, revision, fileName); + result[itemNo] = (hash, revision); } } @@ -71,9 +74,12 @@ namespace ExportDXF.Services List> rawBomTable, List bomItems) { - using (var workbook = File.Exists(xlsxPath) - ? new XLWorkbook(xlsxPath) - : new XLWorkbook()) + if (!File.Exists(TemplatePath)) + throw new FileNotFoundException("Excel template not found.", TemplatePath); + + File.Copy(TemplatePath, xlsxPath, overwrite: true); + + using (var workbook = new XLWorkbook(xlsxPath)) { WriteBomSheet(workbook, rawBomTable); WriteCutTemplatesSheet(workbook, bomItems); @@ -86,10 +92,9 @@ namespace ExportDXF.Services if (rawBomTable == null || rawBomTable.Count == 0) return; - if (workbook.TryGetWorksheet("BOM", out _)) - workbook.Worksheets.Delete("BOM"); + var sheet = GetOrCreateSheet(workbook, "BOM"); + sheet.Clear(); - var sheet = workbook.Worksheets.Add("BOM"); var columns = rawBomTable[0].Keys.ToList(); for (int col = 0; col < columns.Count; col++) @@ -110,30 +115,34 @@ namespace ExportDXF.Services private void WriteCutTemplatesSheet(XLWorkbook workbook, List bomItems) { - if (workbook.TryGetWorksheet("Cut Templates", out _)) - workbook.Worksheets.Delete("Cut Templates"); + var sheet = GetOrCreateSheet(workbook, "Cut Templates"); - var sheet = workbook.Worksheets.Add("Cut Templates"); - - var headers = new[] { "Item #", "File Name", "Revision", "Thickness", "K-Factor", "Bend Radius", "Content Hash" }; - for (int col = 0; col < headers.Length; col++) - sheet.Cell(1, col + 1).Value = headers[col]; + // Clear data rows only (preserve template headers/formatting in row 1) + var lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1; + if (lastRow > 1) + sheet.Range(2, 1, lastRow, sheet.LastColumnUsed()?.ColumnNumber() ?? 1).Clear(); int row = 2; foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo)) { var ct = item.CutTemplate; sheet.Cell(row, 1).Value = item.ItemNo; - sheet.Cell(row, 2).Value = ct.DxfFilePath; - sheet.Cell(row, 3).Value = ct.Revision; - sheet.Cell(row, 4).Value = ct.Thickness ?? 0; - sheet.Cell(row, 5).Value = ct.KFactor ?? 0; - sheet.Cell(row, 6).Value = ct.DefaultBendRadius ?? 0; - sheet.Cell(row, 7).Value = ct.ContentHash ?? ""; + sheet.Cell(row, 2).Value = ct.Revision; + sheet.Cell(row, 3).Value = ct.Thickness ?? 0; + sheet.Cell(row, 4).Value = ct.KFactor ?? 0; + sheet.Cell(row, 5).Value = ct.DefaultBendRadius ?? 0; + sheet.Cell(row, 6).Value = ct.ContentHash ?? ""; row++; } sheet.Columns().AdjustToContents(); } + + private IXLWorksheet GetOrCreateSheet(XLWorkbook workbook, string name) + { + if (workbook.TryGetWorksheet(name, out var sheet)) + return sheet; + return workbook.Worksheets.Add(name); + } } } diff --git a/ExportDXF/Templates/BOM Template.xlsx b/ExportDXF/Templates/BOM Template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..105d7302b338561908dc53309e8de0315b08a4be GIT binary patch literal 9407 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