feat: add revision tracking to CutTemplate and scope BOM items to export record
Each export record now keeps a complete BOM snapshot instead of moving BomItems between records. CutTemplate gains a Revision field that auto-increments when the content hash changes across exports for the same drawing+item, and stays the same when the geometry is unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ namespace ExportDXF.ApiClient
|
||||
public int Id { get; set; }
|
||||
public string DxfFilePath { get; set; }
|
||||
public string ContentHash { get; set; }
|
||||
public int Revision { get; set; }
|
||||
public double? Thickness { get; set; }
|
||||
public double? KFactor { get; set; }
|
||||
public double? DefaultBendRadius { get; set; }
|
||||
|
||||
@@ -53,12 +53,16 @@ namespace FabWorks.Api.Controllers
|
||||
var export = await _db.ExportRecords.FindAsync(exportId);
|
||||
if (export == null) return NotFound("Export record not found");
|
||||
|
||||
// Look for existing BomItem with same PartName + ConfigurationName under the same drawing
|
||||
// Look up the latest CutTemplate for this drawing+item across all previous exports
|
||||
// to determine the revision number
|
||||
var newContentHash = dto.CutTemplate?.ContentHash;
|
||||
int revision = await ResolveRevisionAsync(export.DrawingNumber, dto.ItemNo, newContentHash);
|
||||
|
||||
// Look for existing BomItem with same PartName + ConfigurationName within this export record
|
||||
var existing = await _db.BomItems
|
||||
.Include(b => b.CutTemplate)
|
||||
.Include(b => b.FormProgram)
|
||||
.Include(b => b.ExportRecord)
|
||||
.Where(b => b.ExportRecord.DrawingNumber == export.DrawingNumber
|
||||
.Where(b => b.ExportRecordId == exportId
|
||||
&& b.PartName == (dto.PartName ?? "")
|
||||
&& b.ConfigurationName == (dto.ConfigurationName ?? ""))
|
||||
.OrderByDescending(b => b.ID)
|
||||
@@ -66,8 +70,7 @@ namespace FabWorks.Api.Controllers
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
// Update existing: move to new export record and refresh fields
|
||||
existing.ExportRecordId = exportId;
|
||||
// Update existing fields
|
||||
existing.PartNo = dto.PartNo ?? "";
|
||||
existing.SortOrder = dto.SortOrder;
|
||||
existing.Qty = dto.Qty;
|
||||
@@ -81,6 +84,7 @@ namespace FabWorks.Api.Controllers
|
||||
{
|
||||
existing.CutTemplate.DxfFilePath = dto.CutTemplate.DxfFilePath ?? "";
|
||||
existing.CutTemplate.ContentHash = dto.CutTemplate.ContentHash;
|
||||
existing.CutTemplate.Revision = revision;
|
||||
existing.CutTemplate.Thickness = dto.CutTemplate.Thickness;
|
||||
existing.CutTemplate.KFactor = dto.CutTemplate.KFactor;
|
||||
existing.CutTemplate.DefaultBendRadius = dto.CutTemplate.DefaultBendRadius;
|
||||
@@ -91,6 +95,7 @@ namespace FabWorks.Api.Controllers
|
||||
{
|
||||
DxfFilePath = dto.CutTemplate.DxfFilePath ?? "",
|
||||
ContentHash = dto.CutTemplate.ContentHash,
|
||||
Revision = revision,
|
||||
Thickness = dto.CutTemplate.Thickness,
|
||||
KFactor = dto.CutTemplate.KFactor,
|
||||
DefaultBendRadius = dto.CutTemplate.DefaultBendRadius
|
||||
@@ -156,6 +161,7 @@ namespace FabWorks.Api.Controllers
|
||||
{
|
||||
DxfFilePath = dto.CutTemplate.DxfFilePath ?? "",
|
||||
ContentHash = dto.CutTemplate.ContentHash,
|
||||
Revision = revision,
|
||||
Thickness = dto.CutTemplate.Thickness,
|
||||
KFactor = dto.CutTemplate.KFactor,
|
||||
DefaultBendRadius = dto.CutTemplate.DefaultBendRadius
|
||||
@@ -185,6 +191,33 @@ namespace FabWorks.Api.Controllers
|
||||
return CreatedAtAction(nameof(GetByExport), new { exportId }, MapToDto(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the revision number for a CutTemplate by looking at the most recent
|
||||
/// CutTemplate for the same drawing number and item number across all exports.
|
||||
/// Returns 1 if no previous version exists, the same revision if the hash matches,
|
||||
/// or previous revision + 1 if the hash changed.
|
||||
/// </summary>
|
||||
private async Task<int> ResolveRevisionAsync(string drawingNumber, string itemNo, string contentHash)
|
||||
{
|
||||
if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo) || string.IsNullOrEmpty(contentHash))
|
||||
return 1;
|
||||
|
||||
var previous = await _db.CutTemplates
|
||||
.Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
|
||||
&& c.BomItem.ItemNo == itemNo
|
||||
&& c.ContentHash != null)
|
||||
.OrderByDescending(c => c.Id)
|
||||
.Select(c => new { c.ContentHash, c.Revision })
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (previous == null)
|
||||
return 1;
|
||||
|
||||
return previous.ContentHash == contentHash
|
||||
? previous.Revision
|
||||
: previous.Revision + 1;
|
||||
}
|
||||
|
||||
private static BomItemDto MapToDto(BomItem b) => new()
|
||||
{
|
||||
ID = b.ID,
|
||||
@@ -202,6 +235,7 @@ namespace FabWorks.Api.Controllers
|
||||
Id = b.CutTemplate.Id,
|
||||
DxfFilePath = b.CutTemplate.DxfFilePath,
|
||||
ContentHash = b.CutTemplate.ContentHash,
|
||||
Revision = b.CutTemplate.Revision,
|
||||
Thickness = b.CutTemplate.Thickness,
|
||||
KFactor = b.CutTemplate.KFactor,
|
||||
DefaultBendRadius = b.CutTemplate.DefaultBendRadius
|
||||
|
||||
@@ -239,6 +239,7 @@ namespace FabWorks.Api.Controllers
|
||||
Id = ct.Id,
|
||||
DxfFilePath = ct.DxfFilePath,
|
||||
ContentHash = ct.ContentHash,
|
||||
Revision = ct.Revision,
|
||||
Thickness = ct.Thickness,
|
||||
KFactor = ct.KFactor,
|
||||
DefaultBendRadius = ct.DefaultBendRadius
|
||||
@@ -324,6 +325,7 @@ namespace FabWorks.Api.Controllers
|
||||
Id = b.CutTemplate.Id,
|
||||
DxfFilePath = b.CutTemplate.DxfFilePath,
|
||||
ContentHash = b.CutTemplate.ContentHash,
|
||||
Revision = b.CutTemplate.Revision,
|
||||
Thickness = b.CutTemplate.Thickness,
|
||||
KFactor = b.CutTemplate.KFactor,
|
||||
DefaultBendRadius = b.CutTemplate.DefaultBendRadius
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace FabWorks.Api.DTOs
|
||||
public int Id { get; set; }
|
||||
public string DxfFilePath { get; set; }
|
||||
public string ContentHash { get; set; }
|
||||
public int Revision { get; set; }
|
||||
public double? Thickness { get; set; }
|
||||
public double? KFactor { get; set; }
|
||||
public double? DefaultBendRadius { get; set; }
|
||||
|
||||
273
FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs
generated
Normal file
273
FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs
generated
Normal file
@@ -0,0 +1,273 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260219134027_AddCutTemplateRevision")]
|
||||
partial class AddCutTemplateRevision
|
||||
{
|
||||
/// <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("FabWorks.Core.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("FabWorks.Core.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")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
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.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCutTemplateRevision : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Revision",
|
||||
table: "CutTemplates",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Revision",
|
||||
table: "CutTemplates");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,9 @@ namespace FabWorks.Core.Migrations
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace FabWorks.Core.Models
|
||||
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
|
||||
}
|
||||
|
||||
public int Revision { get; set; } = 1;
|
||||
|
||||
public double? KFactor { get; set; }
|
||||
|
||||
private double? _defaultBendRadius;
|
||||
|
||||
Reference in New Issue
Block a user