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 <noreply@anthropic.com>
This commit is contained in:
aj
2026-08-06 23:15:25 -04:00
co-authored by Claude Opus 4.6
parent 14fa1e6906
commit 81ed93045b
5 changed files with 49 additions and 34 deletions
+3
View File
@@ -1,6 +1,9 @@
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
# Stray Windows redirect artifact
/nul
# User-specific files # User-specific files
*.suo *.suo
*.user *.user
+3
View File
@@ -37,6 +37,9 @@
<Content Include="Templates\Blank.drwdot"> <Content Include="Templates\Blank.drwdot">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Templates\BOM Template.xlsx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
+8 -8
View File
@@ -130,7 +130,7 @@ namespace ExportDXF.Services
ExportContext context, ExportContext context,
string tempDir, string tempDir,
string outputFolder, string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates, Dictionary<string, (string ContentHash, int Revision)> existingTemplates,
List<BomItem> bomItems) List<BomItem> bomItems)
{ {
LogProgress(context, "Active document is a Part"); LogProgress(context, "Active document is a Part");
@@ -159,7 +159,7 @@ namespace ExportDXF.Services
ExportContext context, ExportContext context,
string tempDir, string tempDir,
string outputFolder, string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates, Dictionary<string, (string ContentHash, int Revision)> existingTemplates,
List<BomItem> bomItems) List<BomItem> bomItems)
{ {
LogProgress(context, "Active document is an Assembly"); LogProgress(context, "Active document is an Assembly");
@@ -200,7 +200,7 @@ namespace ExportDXF.Services
ExportContext context, ExportContext context,
string tempDir, string tempDir,
string outputFolder, string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates, Dictionary<string, (string ContentHash, int Revision)> existingTemplates,
List<BomItem> bomItems) List<BomItem> bomItems)
{ {
LogProgress(context, "Active document is a Drawing"); LogProgress(context, "Active document is a Drawing");
@@ -294,7 +294,7 @@ namespace ExportDXF.Services
string tempDir, string tempDir,
string outputFolder, string outputFolder,
ExportContext context, ExportContext context,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates, Dictionary<string, (string ContentHash, int Revision)> existingTemplates,
List<BomItem> bomItems) List<BomItem> bomItems)
{ {
int successCount = 0; int successCount = 0;
@@ -362,7 +362,7 @@ namespace ExportDXF.Services
Item item, Item item,
ExportContext context, ExportContext context,
string outputFolder, string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates, Dictionary<string, (string ContentHash, int Revision)> existingTemplates,
BomItem bomItem) BomItem bomItem)
{ {
var baseName = FilenameTemplateParser.Evaluate(context.FilenameTemplate, item); var baseName = FilenameTemplateParser.Evaluate(context.FilenameTemplate, item);
@@ -377,15 +377,15 @@ namespace ExportDXF.Services
if (existing.ContentHash == contentHash) if (existing.ContentHash == contentHash)
{ {
// Unchanged — skip file write, keep existing // Unchanged — skip file write, keep existing
dxfFileName = existing.FileName;
revision = existing.Revision; revision = existing.Revision;
dxfFileName = GetRevisionFileName(baseName, revision);
LogProgress(context, $"Unchanged: {dxfFileName}", LogLevel.Info, item.PartName); LogProgress(context, $"Unchanged: {dxfFileName}", LogLevel.Info, item.PartName);
_logFileService.LogInfo($"Unchanged: {dxfFileName}"); _logFileService.LogInfo($"Unchanged: {dxfFileName}");
bomItem.CutTemplate = new CutTemplate bomItem.CutTemplate = new CutTemplate
{ {
DxfFilePath = dxfFileName, DxfFilePath = Path.GetFileNameWithoutExtension(dxfFileName),
ContentHash = contentHash, ContentHash = contentHash,
Revision = revision, Revision = revision,
Thickness = item.Thickness > 0 ? item.Thickness : (double?)null, Thickness = item.Thickness > 0 ? item.Thickness : (double?)null,
@@ -402,7 +402,7 @@ namespace ExportDXF.Services
dxfFileName = GetRevisionFileName(baseName, revision); dxfFileName = GetRevisionFileName(baseName, revision);
LogProgress(context, $"Updated: {dxfFileName} (Rev{revision})", LogLevel.Info, item.PartName); LogProgress(context, $"Updated: {dxfFileName} (Rev{revision})", LogLevel.Info, item.PartName);
_logFileService.LogInfo($"Updated: {dxfFileName} (was {existing.FileName})"); _logFileService.LogInfo($"Updated: {dxfFileName}");
} }
} }
else else
+35 -26
View File
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using ClosedXML.Excel; using ClosedXML.Excel;
using ExportDXF.Models; using ExportDXF.Models;
@@ -8,14 +10,18 @@ namespace ExportDXF.Services
{ {
public class ExcelExportService public class ExcelExportService
{ {
private static readonly string TemplatePath = Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
"Templates",
"BOM Template.xlsx");
/// <summary> /// <summary>
/// Reads existing Cut Templates from an xlsx file to compare content hashes. /// 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. /// 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)
/// </summary> /// </summary>
public Dictionary<string, (string ContentHash, int Revision, string FileName)> ReadExistingCutTemplates(string xlsxPath) public Dictionary<string, (string ContentHash, int Revision)> ReadExistingCutTemplates(string xlsxPath)
{ {
var result = new Dictionary<string, (string, int, string)>(); var result = new Dictionary<string, (string, int)>();
if (!File.Exists(xlsxPath)) if (!File.Exists(xlsxPath))
return result; return result;
@@ -49,12 +55,9 @@ namespace ExportDXF.Services
var revision = headers.ContainsKey("Revision") var revision = headers.ContainsKey("Revision")
? ws.Cell(row, headers["Revision"]).GetValue<int>() ? ws.Cell(row, headers["Revision"]).GetValue<int>()
: 1; : 1;
var fileName = headers.ContainsKey("File Name")
? ws.Cell(row, headers["File Name"]).GetString()
: "";
if (!string.IsNullOrEmpty(itemNo)) if (!string.IsNullOrEmpty(itemNo))
result[itemNo] = (hash, revision, fileName); result[itemNo] = (hash, revision);
} }
} }
@@ -71,9 +74,12 @@ namespace ExportDXF.Services
List<Dictionary<string, string>> rawBomTable, List<Dictionary<string, string>> rawBomTable,
List<BomItem> bomItems) List<BomItem> bomItems)
{ {
using (var workbook = File.Exists(xlsxPath) if (!File.Exists(TemplatePath))
? new XLWorkbook(xlsxPath) throw new FileNotFoundException("Excel template not found.", TemplatePath);
: new XLWorkbook())
File.Copy(TemplatePath, xlsxPath, overwrite: true);
using (var workbook = new XLWorkbook(xlsxPath))
{ {
WriteBomSheet(workbook, rawBomTable); WriteBomSheet(workbook, rawBomTable);
WriteCutTemplatesSheet(workbook, bomItems); WriteCutTemplatesSheet(workbook, bomItems);
@@ -86,10 +92,9 @@ namespace ExportDXF.Services
if (rawBomTable == null || rawBomTable.Count == 0) if (rawBomTable == null || rawBomTable.Count == 0)
return; return;
if (workbook.TryGetWorksheet("BOM", out _)) var sheet = GetOrCreateSheet(workbook, "BOM");
workbook.Worksheets.Delete("BOM"); sheet.Clear();
var sheet = workbook.Worksheets.Add("BOM");
var columns = rawBomTable[0].Keys.ToList(); var columns = rawBomTable[0].Keys.ToList();
for (int col = 0; col < columns.Count; col++) for (int col = 0; col < columns.Count; col++)
@@ -110,30 +115,34 @@ namespace ExportDXF.Services
private void WriteCutTemplatesSheet(XLWorkbook workbook, List<BomItem> bomItems) private void WriteCutTemplatesSheet(XLWorkbook workbook, List<BomItem> bomItems)
{ {
if (workbook.TryGetWorksheet("Cut Templates", out _)) var sheet = GetOrCreateSheet(workbook, "Cut Templates");
workbook.Worksheets.Delete("Cut Templates");
var sheet = workbook.Worksheets.Add("Cut Templates"); // Clear data rows only (preserve template headers/formatting in row 1)
var lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1;
var headers = new[] { "Item #", "File Name", "Revision", "Thickness", "K-Factor", "Bend Radius", "Content Hash" }; if (lastRow > 1)
for (int col = 0; col < headers.Length; col++) sheet.Range(2, 1, lastRow, sheet.LastColumnUsed()?.ColumnNumber() ?? 1).Clear();
sheet.Cell(1, col + 1).Value = headers[col];
int row = 2; int row = 2;
foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo)) foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo))
{ {
var ct = item.CutTemplate; var ct = item.CutTemplate;
sheet.Cell(row, 1).Value = item.ItemNo; sheet.Cell(row, 1).Value = item.ItemNo;
sheet.Cell(row, 2).Value = ct.DxfFilePath; sheet.Cell(row, 2).Value = ct.Revision;
sheet.Cell(row, 3).Value = ct.Revision; sheet.Cell(row, 3).Value = ct.Thickness ?? 0;
sheet.Cell(row, 4).Value = ct.Thickness ?? 0; sheet.Cell(row, 4).Value = ct.KFactor ?? 0;
sheet.Cell(row, 5).Value = ct.KFactor ?? 0; sheet.Cell(row, 5).Value = ct.DefaultBendRadius ?? 0;
sheet.Cell(row, 6).Value = ct.DefaultBendRadius ?? 0; sheet.Cell(row, 6).Value = ct.ContentHash ?? "";
sheet.Cell(row, 7).Value = ct.ContentHash ?? "";
row++; row++;
} }
sheet.Columns().AdjustToContents(); sheet.Columns().AdjustToContents();
} }
private IXLWorksheet GetOrCreateSheet(XLWorkbook workbook, string name)
{
if (workbook.TryGetWorksheet(name, out var sheet))
return sheet;
return workbook.Worksheets.Add(name);
}
} }
} }
Binary file not shown.