refactor: extract CutTemplate from BomItem for all-item BOM tracking

BomItems are now created for every BOM item regardless of whether they
produce a DXF. Sheet metal cut data (thickness, k-factor, bend radius,
DXF path, content hash) moved to a new CutTemplate entity with a 1:1
optional relationship. Non-sheet-metal items are counted as "skipped"
instead of "failed" in the export summary. Added Cut Templates tab to
the UI with a DataGridView for viewing cut template records.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 15:32:17 -05:00
parent 0ace378eff
commit 32e8379e9b
9 changed files with 841 additions and 82 deletions

View File

@@ -8,6 +8,7 @@ namespace ExportDXF.Data
{
public DbSet<ExportRecord> ExportRecords { get; set; }
public DbSet<BomItem> BomItems { get; set; }
public DbSet<CutTemplate> CutTemplates { get; set; }
public ExportDxfDbContext() : base()
{
@@ -38,6 +39,7 @@ namespace ExportDXF.Data
entity.Property(e => e.SourceFilePath).HasMaxLength(500);
entity.Property(e => e.OutputFolder).HasMaxLength(500);
entity.Property(e => e.ExportedBy).HasMaxLength(100);
entity.Property(e => e.PdfContentHash).HasMaxLength(64);
entity.HasMany(e => e.BomItems)
.WithOne(b => b.ExportRecord)
@@ -54,7 +56,18 @@ namespace ExportDXF.Data
entity.Property(e => e.PartName).HasMaxLength(200);
entity.Property(e => e.ConfigurationName).HasMaxLength(100);
entity.Property(e => e.Material).HasMaxLength(100);
entity.HasOne(e => e.CutTemplate)
.WithOne(ct => ct.BomItem)
.HasForeignKey<CutTemplate>(ct => ct.BomItemId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<CutTemplate>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.CutTemplateName).HasMaxLength(100);
entity.Property(e => e.ContentHash).HasMaxLength(64);
});
}
}

View File

@@ -36,6 +36,8 @@ namespace ExportDXF.Forms
logEventsDataGrid = new System.Windows.Forms.DataGridView();
bomTab = new System.Windows.Forms.TabPage();
bomDataGrid = new System.Windows.Forms.DataGridView();
cutTemplatesTab = new System.Windows.Forms.TabPage();
cutTemplatesDataGrid = new System.Windows.Forms.DataGridView();
equipmentBox = new System.Windows.Forms.ComboBox();
label1 = new System.Windows.Forms.Label();
label2 = new System.Windows.Forms.Label();
@@ -45,12 +47,14 @@ namespace ExportDXF.Forms
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).BeginInit();
bomTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)bomDataGrid).BeginInit();
cutTemplatesTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)cutTemplatesDataGrid).BeginInit();
SuspendLayout();
//
// runButton
//
runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
runButton.Location = new System.Drawing.Point(656, 13);
runButton.Location = new System.Drawing.Point(514, 13);
runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
runButton.Name = "runButton";
runButton.Size = new System.Drawing.Size(100, 30);
@@ -74,7 +78,7 @@ namespace ExportDXF.Forms
viewFlipDeciderBox.FormattingEnabled = true;
viewFlipDeciderBox.Location = new System.Drawing.Point(137, 43);
viewFlipDeciderBox.Name = "viewFlipDeciderBox";
viewFlipDeciderBox.Size = new System.Drawing.Size(502, 25);
viewFlipDeciderBox.Size = new System.Drawing.Size(365, 25);
viewFlipDeciderBox.TabIndex = 3;
//
// mainTabControl
@@ -82,11 +86,12 @@ namespace ExportDXF.Forms
mainTabControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
mainTabControl.Controls.Add(logEventsTab);
mainTabControl.Controls.Add(bomTab);
mainTabControl.Controls.Add(cutTemplatesTab);
mainTabControl.Location = new System.Drawing.Point(15, 74);
mainTabControl.Name = "mainTabControl";
mainTabControl.Padding = new System.Drawing.Point(20, 5);
mainTabControl.SelectedIndex = 0;
mainTabControl.Size = new System.Drawing.Size(741, 586);
mainTabControl.Size = new System.Drawing.Size(599, 330);
mainTabControl.TabIndex = 12;
//
// logEventsTab
@@ -95,7 +100,7 @@ namespace ExportDXF.Forms
logEventsTab.Location = new System.Drawing.Point(4, 30);
logEventsTab.Name = "logEventsTab";
logEventsTab.Padding = new System.Windows.Forms.Padding(3);
logEventsTab.Size = new System.Drawing.Size(733, 552);
logEventsTab.Size = new System.Drawing.Size(591, 296);
logEventsTab.TabIndex = 0;
logEventsTab.Text = "Log Events";
logEventsTab.UseVisualStyleBackColor = true;
@@ -107,7 +112,7 @@ namespace ExportDXF.Forms
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
logEventsDataGrid.Name = "logEventsDataGrid";
logEventsDataGrid.Size = new System.Drawing.Size(721, 540);
logEventsDataGrid.Size = new System.Drawing.Size(579, 282);
logEventsDataGrid.TabIndex = 0;
//
// bomTab
@@ -116,7 +121,7 @@ namespace ExportDXF.Forms
bomTab.Location = new System.Drawing.Point(4, 30);
bomTab.Name = "bomTab";
bomTab.Padding = new System.Windows.Forms.Padding(3);
bomTab.Size = new System.Drawing.Size(733, 552);
bomTab.Size = new System.Drawing.Size(982, 549);
bomTab.TabIndex = 1;
bomTab.Text = "Bill Of Materials";
bomTab.UseVisualStyleBackColor = true;
@@ -128,9 +133,30 @@ namespace ExportDXF.Forms
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
bomDataGrid.Location = new System.Drawing.Point(6, 6);
bomDataGrid.Name = "bomDataGrid";
bomDataGrid.Size = new System.Drawing.Size(721, 540);
bomDataGrid.Size = new System.Drawing.Size(970, 535);
bomDataGrid.TabIndex = 1;
//
//
// cutTemplatesTab
//
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
cutTemplatesTab.Location = new System.Drawing.Point(4, 30);
cutTemplatesTab.Name = "cutTemplatesTab";
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
cutTemplatesTab.Size = new System.Drawing.Size(982, 549);
cutTemplatesTab.TabIndex = 2;
cutTemplatesTab.Text = "Cut Templates";
cutTemplatesTab.UseVisualStyleBackColor = true;
//
// cutTemplatesDataGrid
//
cutTemplatesDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
cutTemplatesDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
cutTemplatesDataGrid.Size = new System.Drawing.Size(970, 535);
cutTemplatesDataGrid.TabIndex = 2;
//
// equipmentBox
//
equipmentBox.FormattingEnabled = true;
@@ -151,7 +177,7 @@ namespace ExportDXF.Forms
// label2
//
label2.AutoSize = true;
label2.Location = new System.Drawing.Point(354, 15);
label2.Location = new System.Drawing.Point(321, 15);
label2.Name = "label2";
label2.Size = new System.Drawing.Size(56, 17);
label2.TabIndex = 2;
@@ -160,15 +186,15 @@ namespace ExportDXF.Forms
// drawingNoBox
//
drawingNoBox.FormattingEnabled = true;
drawingNoBox.Location = new System.Drawing.Point(416, 12);
drawingNoBox.Location = new System.Drawing.Point(383, 12);
drawingNoBox.Name = "drawingNoBox";
drawingNoBox.Size = new System.Drawing.Size(223, 25);
drawingNoBox.Size = new System.Drawing.Size(119, 25);
drawingNoBox.TabIndex = 13;
//
// MainForm
//
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
ClientSize = new System.Drawing.Size(768, 672);
ClientSize = new System.Drawing.Size(626, 416);
Controls.Add(drawingNoBox);
Controls.Add(equipmentBox);
Controls.Add(mainTabControl);
@@ -180,7 +206,7 @@ namespace ExportDXF.Forms
Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
MaximizeBox = false;
MinimumSize = new System.Drawing.Size(643, 355);
MinimumSize = new System.Drawing.Size(642, 455);
Name = "MainForm";
StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
Text = "ExportDXF";
@@ -189,6 +215,8 @@ namespace ExportDXF.Forms
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).EndInit();
bomTab.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)bomDataGrid).EndInit();
cutTemplatesTab.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)cutTemplatesDataGrid).EndInit();
ResumeLayout(false);
PerformLayout();
}
@@ -203,6 +231,8 @@ namespace ExportDXF.Forms
private System.Windows.Forms.TabPage bomTab;
private System.Windows.Forms.DataGridView logEventsDataGrid;
private System.Windows.Forms.DataGridView bomDataGrid;
private System.Windows.Forms.TabPage cutTemplatesTab;
private System.Windows.Forms.DataGridView cutTemplatesDataGrid;
private System.Windows.Forms.ComboBox equipmentBox;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;

View File

@@ -23,6 +23,7 @@ namespace ExportDXF.Forms
private CancellationTokenSource _cancellationTokenSource;
private readonly BindingList<LogEvent> _logEvents;
private readonly BindingList<BomItem> _bomItems;
private readonly BindingList<CutTemplate> _cutTemplates;
private List<DrawingInfo> _allDrawings;
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFileExportService fileExportService, Func<ExportDxfDbContext> dbContextFactory = null)
@@ -38,10 +39,12 @@ namespace ExportDXF.Forms
_dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext());
_logEvents = new BindingList<LogEvent>();
_bomItems = new BindingList<BomItem>();
_cutTemplates = new BindingList<CutTemplate>();
_allDrawings = new List<DrawingInfo>();
InitializeViewFlipDeciders();
InitializeLogEventsGrid();
InitializeBomGrid();
InitializeCutTemplatesGrid();
InitializeDrawingDropdowns();
}
@@ -215,6 +218,61 @@ namespace ExportDXF.Forms
bomDataGrid.DataSource = _bomItems;
}
private void InitializeCutTemplatesGrid()
{
cutTemplatesDataGrid.Columns.Clear();
cutTemplatesDataGrid.AutoGenerateColumns = false;
cutTemplatesDataGrid.AllowUserToAddRows = false;
cutTemplatesDataGrid.AllowUserToDeleteRows = false;
cutTemplatesDataGrid.ReadOnly = true;
cutTemplatesDataGrid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.CutTemplateName),
HeaderText = "Template Name",
Width = 150
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.DxfFilePath),
HeaderText = "DXF File",
Width = 250
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.Thickness),
HeaderText = "Thickness",
Width = 80
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.KFactor),
HeaderText = "K-Factor",
Width = 80
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.DefaultBendRadius),
HeaderText = "Bend Radius",
Width = 90
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.ContentHash),
HeaderText = "Content Hash",
Width = 150
});
cutTemplatesDataGrid.DataSource = _cutTemplates;
}
private void InitializeDrawingDropdowns()
{
try
@@ -319,9 +377,12 @@ namespace ExportDXF.Forms
return;
}
// Parse drawing number from active document title
var drawingInfo = DrawingInfo.Parse(activeDoc.Title);
var filePrefix = drawingInfo != null ? $"{drawingInfo.EquipmentNo} {drawingInfo.DrawingNo}" : activeDoc.Title;
// Use equipment/drawing values from the UI dropdowns
var equipment = equipmentBox.Text?.Trim();
var drawingNo = drawingNoBox.Text?.Trim();
var filePrefix = !string.IsNullOrEmpty(equipment) && !string.IsNullOrEmpty(drawingNo)
? $"{equipment} {drawingNo}"
: activeDoc.Title;
var viewFlipDecider = GetSelectedViewFlipDecider();
var exportContext = new ExportContext
@@ -335,8 +396,9 @@ namespace ExportDXF.Forms
BomItemCallback = AddBomItem
};
// Clear previous BOM items
// Clear previous BOM items and cut templates
_bomItems.Clear();
_cutTemplates.Clear();
LogMessage($"Started at {DateTime.Now:t}");
LogMessage($"Exporting to: {_fileExportService.OutputFolder}");
@@ -405,6 +467,22 @@ namespace ExportDXF.Forms
var activeDoc = _solidWorksService.GetActiveDocument();
var docTitle = activeDoc?.Title ?? "No Document Open";
this.Text = $"ExportDXF - {docTitle}";
// Parse the file name and fill Equipment/Drawing dropdowns
if (activeDoc != null)
{
var drawingInfo = DrawingInfo.Parse(activeDoc.Title);
if (drawingInfo != null)
{
if (!equipmentBox.Items.Contains(drawingInfo.EquipmentNo))
equipmentBox.Items.Add(drawingInfo.EquipmentNo);
equipmentBox.Text = drawingInfo.EquipmentNo;
if (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo))
drawingNoBox.Items.Add(drawingInfo.DrawingNo);
drawingNoBox.Text = drawingInfo.DrawingNo;
}
}
}
private void LogMessage(string message, LogLevel level = LogLevel.Info, string file = null)
@@ -451,6 +529,11 @@ namespace ExportDXF.Forms
return;
}
_bomItems.Add(item);
if (item.CutTemplate != null)
{
_cutTemplates.Add(item.CutTemplate);
}
}
private void LogEventsDataGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)

View File

@@ -0,0 +1,188 @@
// <auto-generated />
using System;
using ExportDXF.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ExportDXF.Migrations
{
[DbContext(typeof(ExportDxfDbContext))]
[Migration("20260214195856_ExtractCutTemplate")]
partial class ExtractCutTemplate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
b.Property<string>("ConfigurationName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Material")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PartName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("PartNo")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("Qty")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("CutTemplateName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<double?>("DefaultBendRadius")
.HasColumnType("float");
b.Property<string>("DxfFilePath")
.HasColumnType("nvarchar(max)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("ExportedAt")
.HasColumnType("datetime2");
b.Property<string>("ExportedBy")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("OutputFolder")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("SourceFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
{
b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b =>
{
b.HasOne("ExportDXF.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("ExportDXF.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
});
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,114 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ExportDXF.Migrations
{
/// <inheritdoc />
public partial class ExtractCutTemplate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ContentHash",
table: "BomItems");
migrationBuilder.DropColumn(
name: "CutTemplateName",
table: "BomItems");
migrationBuilder.DropColumn(
name: "DefaultBendRadius",
table: "BomItems");
migrationBuilder.DropColumn(
name: "DxfFilePath",
table: "BomItems");
migrationBuilder.DropColumn(
name: "KFactor",
table: "BomItems");
migrationBuilder.DropColumn(
name: "Thickness",
table: "BomItems");
migrationBuilder.CreateTable(
name: "CutTemplates",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DxfFilePath = table.Column<string>(type: "nvarchar(max)", nullable: true),
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
CutTemplateName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Thickness = table.Column<double>(type: "float", nullable: true),
KFactor = table.Column<double>(type: "float", nullable: true),
DefaultBendRadius = table.Column<double>(type: "float", nullable: true),
BomItemId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CutTemplates", x => x.Id);
table.ForeignKey(
name: "FK_CutTemplates_BomItems_BomItemId",
column: x => x.BomItemId,
principalTable: "BomItems",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CutTemplates_BomItemId",
table: "CutTemplates",
column: "BomItemId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CutTemplates");
migrationBuilder.AddColumn<string>(
name: "ContentHash",
table: "BomItems",
type: "nvarchar(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CutTemplateName",
table: "BomItems",
type: "nvarchar(100)",
maxLength: 100,
nullable: true);
migrationBuilder.AddColumn<double>(
name: "DefaultBendRadius",
table: "BomItems",
type: "float",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "DxfFilePath",
table: "BomItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<double>(
name: "KFactor",
table: "BomItems",
type: "float",
nullable: true);
migrationBuilder.AddColumn<double>(
name: "Thickness",
table: "BomItems",
type: "float",
nullable: true);
}
}
}

View File

@@ -0,0 +1,185 @@
// <auto-generated />
using System;
using ExportDXF.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ExportDXF.Migrations
{
[DbContext(typeof(ExportDxfDbContext))]
partial class ExportDxfDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
b.Property<string>("ConfigurationName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Material")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PartName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("PartNo")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("Qty")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("CutTemplateName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<double?>("DefaultBendRadius")
.HasColumnType("float");
b.Property<string>("DxfFilePath")
.HasColumnType("nvarchar(max)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("ExportedAt")
.HasColumnType("datetime2");
b.Property<string>("ExportedBy")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("OutputFolder")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("SourceFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
{
b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b =>
{
b.HasOne("ExportDXF.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("ExportDXF.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
});
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,5 +1,3 @@
using System;
namespace ExportDXF.Models
{
public class BomItem
@@ -14,29 +12,13 @@ namespace ExportDXF.Models
public string PartName { get; set; } = "";
public string ConfigurationName { get; set; } = "";
public string Material { get; set; } = "";
public string CutTemplateName { get; set; } = "";
public string DxfFilePath { get; set; } = "";
// Sheet metal properties
private double? _thickness;
public double? Thickness
{
get => _thickness;
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public double? KFactor { get; set; }
private double? _defaultBendRadius;
public double? DefaultBendRadius
{
get => _defaultBendRadius;
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
}
// EF Core relationship to ExportRecord
public int ExportRecordId { get; set; }
public virtual ExportRecord ExportRecord { get; set; }
// Optional 1:1 relationship to CutTemplate (only for sheet metal parts)
public virtual CutTemplate CutTemplate { get; set; }
}
public struct Size

View File

@@ -0,0 +1,33 @@
using System;
namespace ExportDXF.Models
{
public class CutTemplate
{
public int Id { get; set; }
public string DxfFilePath { get; set; } = "";
public string ContentHash { get; set; }
public string CutTemplateName { get; set; } = "";
// Sheet metal properties (moved from BomItem)
private double? _thickness;
public double? Thickness
{
get => _thickness;
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public double? KFactor { get; set; }
private double? _defaultBendRadius;
public double? DefaultBendRadius
{
get => _defaultBendRadius;
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
}
// FK back to BomItem
public int BomItemId { get; set; }
public virtual BomItem BomItem { get; set; }
}
}

View File

@@ -2,11 +2,14 @@ using ExportDXF.Data;
using ExportDXF.Extensions;
using ExportDXF.ItemExtractors;
using ExportDXF.Models;
using ExportDXF.Utilities;
using ExportDXF;
using Microsoft.EntityFrameworkCore;
using SolidWorks.Interop.sldworks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace ExportDXF.Services
{
@@ -161,18 +164,29 @@ namespace ExportDXF.Services
// Determine drawing number for file naming
var drawingNumber = ParseDrawingNumber(context);
// Resolve output folder: /{outputDir}/{equipmentNo}/{drawingNo}/ or flat fallback
var drawingOutputFolder = _fileExportService.GetDrawingOutputFolder(drawingNumber);
// Export drawing to PDF
var tempDir = CreateTempWorkDir();
_drawingExporter.ExportToPdf(drawing, tempDir, context);
// Copy PDF to output folder
// Copy PDF to output folder with versioning
string pdfStashPath = null;
string savedPdfPath = null;
try
{
var pdfs = Directory.GetFiles(tempDir, "*.pdf");
if (pdfs.Length > 0)
{
var savedPath = _fileExportService.SavePdfFile(pdfs[0], drawingNumber);
LogProgress(context, $"Saved PDF: {Path.GetFileName(savedPath)}", LogLevel.Info);
// Determine the destination path to stash the existing file
var pdfFileName = !string.IsNullOrEmpty(drawingNumber)
? $"{drawingNumber}.pdf"
: Path.GetFileName(pdfs[0]);
var pdfDestPath = Path.Combine(drawingOutputFolder, pdfFileName);
pdfStashPath = _fileExportService.StashFile(pdfDestPath);
savedPdfPath = _fileExportService.SavePdfFile(pdfs[0], drawingNumber, drawingOutputFolder);
}
}
catch (Exception ex)
@@ -186,15 +200,40 @@ namespace ExportDXF.Services
{
using (var db = _dbContextFactory())
{
db.Database.EnsureCreated();
db.Database.Migrate();
exportRecord = new ExportRecord
{
DrawingNumber = drawingNumber ?? context.ActiveDocument.Title,
SourceFilePath = context.ActiveDocument.FilePath,
OutputFolder = _fileExportService.OutputFolder,
OutputFolder = drawingOutputFolder,
ExportedAt = DateTime.Now,
ExportedBy = System.Environment.UserName
};
// Handle PDF versioning - compute hash and compare with previous
if (savedPdfPath != null)
{
HandlePdfVersioning(savedPdfPath, exportRecord.DrawingNumber, exportRecord, context);
// Archive or discard old PDF based on hash comparison
if (pdfStashPath != null)
{
var previousRecord = db.ExportRecords
.Where(r => r.DrawingNumber == exportRecord.DrawingNumber && r.PdfContentHash != null)
.OrderByDescending(r => r.Id)
.FirstOrDefault();
if (previousRecord != null && previousRecord.PdfContentHash == exportRecord.PdfContentHash)
{
_fileExportService.DiscardStash(pdfStashPath);
}
else
{
_fileExportService.ArchiveFile(pdfStashPath, savedPdfPath);
}
}
}
db.ExportRecords.Add(exportRecord);
db.SaveChanges();
LogProgress(context, $"Created export record (ID: {exportRecord.Id})", LogLevel.Info);
@@ -202,11 +241,13 @@ namespace ExportDXF.Services
}
catch (Exception ex)
{
// Clean up stash on error
_fileExportService.DiscardStash(pdfStashPath);
LogProgress(context, $"Database error creating export record: {ex.Message}", LogLevel.Error);
}
// Export parts to DXF (directly to output folder) and save BOM items
ExportItems(items, _fileExportService.OutputFolder, context, exportRecord?.Id);
// Export parts to DXF and save BOM items
ExportItems(items, drawingOutputFolder, context, exportRecord?.Id);
}
#endregion
@@ -268,6 +309,7 @@ namespace ExportDXF.Services
private void ExportItems(List<Item> items, string saveDirectory, ExportContext context, int? exportRecordId = null)
{
int successCount = 0;
int skippedCount = 0;
int failureCount = 0;
int sortOrder = 0;
@@ -284,54 +326,62 @@ namespace ExportDXF.Services
// PartExporter will handle template drawing creation through context
_partExporter.ExportItem(item, saveDirectory, context);
// Always create BomItem for every item (sheet metal or not)
var bomItem = new BomItem
{
ExportRecordId = exportRecordId ?? 0,
ItemNo = item.ItemNo ?? "",
PartNo = item.FileName ?? item.PartName ?? "",
SortOrder = sortOrder++,
Qty = item.Quantity,
TotalQty = item.Quantity,
Description = item.Description ?? "",
PartName = item.PartName ?? "",
ConfigurationName = item.Configuration ?? "",
Material = item.Material ?? ""
};
// Only create CutTemplate if DXF was exported successfully
if (!string.IsNullOrEmpty(item.FileName))
{
successCount++;
LogProgress(context, $"Exported: {item.FileName}.dxf", LogLevel.Info);
// Create BOM item
var dxfPath = Path.Combine(saveDirectory, item.FileName + ".dxf");
var bomItem = new BomItem
bomItem.CutTemplate = new CutTemplate
{
ExportRecordId = exportRecordId ?? 0,
ItemNo = item.ItemNo ?? "",
PartNo = item.FileName ?? item.PartName ?? "",
SortOrder = sortOrder++,
Qty = item.Quantity,
TotalQty = item.Quantity,
Description = item.Description ?? "",
PartName = item.PartName ?? "",
ConfigurationName = item.Configuration ?? "",
Material = item.Material ?? "",
DxfFilePath = dxfPath,
ContentHash = item.ContentHash,
Thickness = item.Thickness > 0 ? item.Thickness : null,
KFactor = item.KFactor > 0 ? item.KFactor : null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null,
DxfFilePath = dxfPath
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
};
// Add to UI
context.BomItemCallback?.Invoke(bomItem);
// Save BOM item to database if we have an export record
if (exportRecordId.HasValue)
{
try
{
using (var db = _dbContextFactory())
{
db.BomItems.Add(bomItem);
db.SaveChanges();
}
}
catch (Exception dbEx)
{
LogProgress(context, $"Database error saving BOM item: {dbEx.Message}", LogLevel.Error);
}
}
// Compare hash with previous export to decide archive/discard
HandleDxfVersioning(item, dxfPath, context);
}
else
{
failureCount++;
skippedCount++;
}
// Add to UI
context.BomItemCallback?.Invoke(bomItem);
// Save BOM item to database if we have an export record
if (exportRecordId.HasValue)
{
try
{
using (var db = _dbContextFactory())
{
db.BomItems.Add(bomItem);
db.SaveChanges();
}
}
catch (Exception dbEx)
{
LogProgress(context, $"Database error saving BOM item: {dbEx.Message}", LogLevel.Error);
}
}
}
catch (Exception ex)
@@ -341,8 +391,10 @@ namespace ExportDXF.Services
}
}
LogProgress(context, $"Export complete: {successCount} succeeded, {failureCount} failed",
failureCount > 0 ? LogLevel.Warning : LogLevel.Info);
var summary = $"Export complete: {successCount} exported, {skippedCount} skipped";
if (failureCount > 0)
summary += $", {failureCount} failed";
LogProgress(context, summary, failureCount > 0 ? LogLevel.Warning : LogLevel.Info);
if (exportRecordId.HasValue)
{
@@ -352,6 +404,85 @@ namespace ExportDXF.Services
#endregion
#region Versioning
private void HandleDxfVersioning(Item item, string dxfPath, ExportContext context)
{
if (string.IsNullOrEmpty(item.ContentHash))
return;
try
{
using (var db = _dbContextFactory())
{
var previousCutTemplate = db.CutTemplates
.Where(ct => ct.DxfFilePath == dxfPath && ct.ContentHash != null)
.OrderByDescending(ct => ct.Id)
.FirstOrDefault();
if (previousCutTemplate != null && previousCutTemplate.ContentHash == item.ContentHash)
{
// Content unchanged - discard the stashed file
_fileExportService.DiscardStash(item.StashedFilePath);
LogProgress(context, $"DXF unchanged: {item.FileName}.dxf", LogLevel.Info);
}
else
{
// Content changed or first export - archive the old file
if (!string.IsNullOrEmpty(item.StashedFilePath))
{
_fileExportService.ArchiveFile(item.StashedFilePath, dxfPath);
LogProgress(context, $"DXF updated, previous version archived: {item.FileName}.dxf", LogLevel.Info);
}
else
{
LogProgress(context, $"Exported: {item.FileName}.dxf", LogLevel.Info);
}
}
}
}
catch (Exception ex)
{
// Don't fail the export if versioning fails - just discard the stash
_fileExportService.DiscardStash(item.StashedFilePath);
LogProgress(context, $"Versioning check failed for {item.FileName}: {ex.Message}", LogLevel.Warning);
}
}
private void HandlePdfVersioning(string pdfPath, string drawingNumber, ExportRecord exportRecord, ExportContext context)
{
try
{
var newHash = ContentHasher.ComputeFileHash(pdfPath);
using (var db = _dbContextFactory())
{
var previousRecord = db.ExportRecords
.Where(r => r.DrawingNumber == drawingNumber && r.PdfContentHash != null)
.OrderByDescending(r => r.Id)
.FirstOrDefault();
if (previousRecord != null && previousRecord.PdfContentHash == newHash)
{
LogProgress(context, $"PDF unchanged: {Path.GetFileName(pdfPath)}", LogLevel.Info);
}
else
{
LogProgress(context, $"Saved PDF: {Path.GetFileName(pdfPath)}", LogLevel.Info);
}
}
if (exportRecord != null)
exportRecord.PdfContentHash = newHash;
}
catch (Exception ex)
{
LogProgress(context, $"PDF versioning check failed: {ex.Message}", LogLevel.Warning);
}
}
#endregion
#region Helper Methods
private string CreateTempWorkDir()