From 32e8379e9b501fc22e131d06014de38b62e5e4b2 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 14 Feb 2026 15:32:17 -0500 Subject: [PATCH] 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 --- ExportDXF/Data/ExportDxfDbContext.cs | 13 ++ ExportDXF/Forms/MainForm.Designer.cs | 56 +++-- ExportDXF/Forms/MainForm.cs | 91 +++++++- ...60214195856_ExtractCutTemplate.Designer.cs | 188 +++++++++++++++ .../20260214195856_ExtractCutTemplate.cs | 114 +++++++++ .../ExportDxfDbContextModelSnapshot.cs | 185 +++++++++++++++ ExportDXF/Models/BomItem.cs | 24 +- ExportDXF/Models/CutTemplate.cs | 33 +++ ExportDXF/Services/DxfExportService.cs | 219 ++++++++++++++---- 9 files changed, 841 insertions(+), 82 deletions(-) create mode 100644 ExportDXF/Migrations/20260214195856_ExtractCutTemplate.Designer.cs create mode 100644 ExportDXF/Migrations/20260214195856_ExtractCutTemplate.cs create mode 100644 ExportDXF/Migrations/ExportDxfDbContextModelSnapshot.cs create mode 100644 ExportDXF/Models/CutTemplate.cs diff --git a/ExportDXF/Data/ExportDxfDbContext.cs b/ExportDXF/Data/ExportDxfDbContext.cs index d949690..b0024ac 100644 --- a/ExportDXF/Data/ExportDxfDbContext.cs +++ b/ExportDXF/Data/ExportDxfDbContext.cs @@ -8,6 +8,7 @@ namespace ExportDXF.Data { public DbSet ExportRecords { get; set; } public DbSet BomItems { get; set; } + public DbSet 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(ct => ct.BomItemId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); entity.Property(e => e.CutTemplateName).HasMaxLength(100); + entity.Property(e => e.ContentHash).HasMaxLength(64); }); } } diff --git a/ExportDXF/Forms/MainForm.Designer.cs b/ExportDXF/Forms/MainForm.Designer.cs index 302bbbd..f3344c2 100644 --- a/ExportDXF/Forms/MainForm.Designer.cs +++ b/ExportDXF/Forms/MainForm.Designer.cs @@ -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; diff --git a/ExportDXF/Forms/MainForm.cs b/ExportDXF/Forms/MainForm.cs index c003f14..f7d149b 100644 --- a/ExportDXF/Forms/MainForm.cs +++ b/ExportDXF/Forms/MainForm.cs @@ -23,6 +23,7 @@ namespace ExportDXF.Forms private CancellationTokenSource _cancellationTokenSource; private readonly BindingList _logEvents; private readonly BindingList _bomItems; + private readonly BindingList _cutTemplates; private List _allDrawings; public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFileExportService fileExportService, Func dbContextFactory = null) @@ -38,10 +39,12 @@ namespace ExportDXF.Forms _dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext()); _logEvents = new BindingList(); _bomItems = new BindingList(); + _cutTemplates = new BindingList(); _allDrawings = new List(); 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) diff --git a/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.Designer.cs b/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.Designer.cs new file mode 100644 index 0000000..6c06ec6 --- /dev/null +++ b/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.Designer.cs @@ -0,0 +1,188 @@ +// +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 + { + /// + 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("ID") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ID")); + + b.Property("ConfigurationName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ExportRecordId") + .HasColumnType("int"); + + b.Property("ItemNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Material") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartNo") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Qty") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TotalQty") + .HasColumnType("int"); + + b.HasKey("ID"); + + b.HasIndex("ExportRecordId"); + + b.ToTable("BomItems"); + }); + + modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BomItemId") + .HasColumnType("int"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CutTemplateName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DefaultBendRadius") + .HasColumnType("float"); + + b.Property("DxfFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("KFactor") + .HasColumnType("float"); + + b.Property("Thickness") + .HasColumnType("float"); + + b.HasKey("Id"); + + b.HasIndex("BomItemId") + .IsUnique(); + + b.ToTable("CutTemplates"); + }); + + modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DrawingNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ExportedAt") + .HasColumnType("datetime2"); + + b.Property("ExportedBy") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OutputFolder") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PdfContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("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 + } + } +} diff --git a/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.cs b/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.cs new file mode 100644 index 0000000..bdb1d95 --- /dev/null +++ b/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ExportDXF.Migrations +{ + /// + public partial class ExtractCutTemplate : Migration + { + /// + 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(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DxfFilePath = table.Column(type: "nvarchar(max)", nullable: true), + ContentHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + CutTemplateName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + Thickness = table.Column(type: "float", nullable: true), + KFactor = table.Column(type: "float", nullable: true), + DefaultBendRadius = table.Column(type: "float", nullable: true), + BomItemId = table.Column(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); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CutTemplates"); + + migrationBuilder.AddColumn( + name: "ContentHash", + table: "BomItems", + type: "nvarchar(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "CutTemplateName", + table: "BomItems", + type: "nvarchar(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.AddColumn( + name: "DefaultBendRadius", + table: "BomItems", + type: "float", + nullable: true); + + migrationBuilder.AddColumn( + name: "DxfFilePath", + table: "BomItems", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "KFactor", + table: "BomItems", + type: "float", + nullable: true); + + migrationBuilder.AddColumn( + name: "Thickness", + table: "BomItems", + type: "float", + nullable: true); + } + } +} diff --git a/ExportDXF/Migrations/ExportDxfDbContextModelSnapshot.cs b/ExportDXF/Migrations/ExportDxfDbContextModelSnapshot.cs new file mode 100644 index 0000000..130ebad --- /dev/null +++ b/ExportDXF/Migrations/ExportDxfDbContextModelSnapshot.cs @@ -0,0 +1,185 @@ +// +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("ID") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ID")); + + b.Property("ConfigurationName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ExportRecordId") + .HasColumnType("int"); + + b.Property("ItemNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Material") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartNo") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Qty") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TotalQty") + .HasColumnType("int"); + + b.HasKey("ID"); + + b.HasIndex("ExportRecordId"); + + b.ToTable("BomItems"); + }); + + modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BomItemId") + .HasColumnType("int"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CutTemplateName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DefaultBendRadius") + .HasColumnType("float"); + + b.Property("DxfFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("KFactor") + .HasColumnType("float"); + + b.Property("Thickness") + .HasColumnType("float"); + + b.HasKey("Id"); + + b.HasIndex("BomItemId") + .IsUnique(); + + b.ToTable("CutTemplates"); + }); + + modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DrawingNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ExportedAt") + .HasColumnType("datetime2"); + + b.Property("ExportedBy") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OutputFolder") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PdfContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("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 + } + } +} diff --git a/ExportDXF/Models/BomItem.cs b/ExportDXF/Models/BomItem.cs index 16c7dc0..c986ca9 100644 --- a/ExportDXF/Models/BomItem.cs +++ b/ExportDXF/Models/BomItem.cs @@ -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 diff --git a/ExportDXF/Models/CutTemplate.cs b/ExportDXF/Models/CutTemplate.cs new file mode 100644 index 0000000..ee04b68 --- /dev/null +++ b/ExportDXF/Models/CutTemplate.cs @@ -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; } + } +} diff --git a/ExportDXF/Services/DxfExportService.cs b/ExportDXF/Services/DxfExportService.cs index 77e54a5..ff9bb42 100644 --- a/ExportDXF/Services/DxfExportService.cs +++ b/ExportDXF/Services/DxfExportService.cs @@ -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 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()