From 254066c989c1792541a8c169068ae7d32d69c738 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 4 Feb 2026 23:37:31 -0500 Subject: [PATCH] feat: Add stock transaction tracking system - Add StockTransaction entity for audit trail - Track received, used, adjusted, scrapped, and returned stock - Include unit price tracking for cost analysis - Link transactions to jobs and suppliers Co-Authored-By: Claude Opus 4.5 --- CutList.Web/Data/Entities/StockTransaction.cs | 27 + ...7_MergeStockAndAddTransactions.Designer.cs | 471 +++++++++++++++++ ...0204220547_MergeStockAndAddTransactions.cs | 144 ++++++ ...AddPriceTrackingToTransactions.Designer.cs | 487 ++++++++++++++++++ ...04221152_AddPriceTrackingToTransactions.cs | 61 +++ 5 files changed, 1190 insertions(+) create mode 100644 CutList.Web/Data/Entities/StockTransaction.cs create mode 100644 CutList.Web/Migrations/20260204220547_MergeStockAndAddTransactions.Designer.cs create mode 100644 CutList.Web/Migrations/20260204220547_MergeStockAndAddTransactions.cs create mode 100644 CutList.Web/Migrations/20260204221152_AddPriceTrackingToTransactions.Designer.cs create mode 100644 CutList.Web/Migrations/20260204221152_AddPriceTrackingToTransactions.cs diff --git a/CutList.Web/Data/Entities/StockTransaction.cs b/CutList.Web/Data/Entities/StockTransaction.cs new file mode 100644 index 0000000..1dc6839 --- /dev/null +++ b/CutList.Web/Data/Entities/StockTransaction.cs @@ -0,0 +1,27 @@ +namespace CutList.Web.Data.Entities; + +public class StockTransaction +{ + public int Id { get; set; } + public int StockItemId { get; set; } + public int Quantity { get; set; } + public StockTransactionType Type { get; set; } + public int? JobId { get; set; } + public int? SupplierId { get; set; } + public decimal? UnitPrice { get; set; } + public string? Notes { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public StockItem StockItem { get; set; } = null!; + public Job? Job { get; set; } + public Supplier? Supplier { get; set; } +} + +public enum StockTransactionType +{ + Received, + Used, + Adjustment, + Scrapped, + Returned +} diff --git a/CutList.Web/Migrations/20260204220547_MergeStockAndAddTransactions.Designer.cs b/CutList.Web/Migrations/20260204220547_MergeStockAndAddTransactions.Designer.cs new file mode 100644 index 0000000..988e578 --- /dev/null +++ b/CutList.Web/Migrations/20260204220547_MergeStockAndAddTransactions.Designer.cs @@ -0,0 +1,471 @@ +// +using System; +using CutList.Web.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CutList.Web.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260204220547_MergeStockAndAddTransactions")] + partial class MergeStockAndAddTransactions + { + /// + 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("CutList.Web.Data.Entities.CuttingTool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("KerfInches") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("CuttingTools"); + + b.HasData( + new + { + Id = 1, + IsActive = true, + IsDefault = true, + KerfInches = 0.0625m, + Name = "Bandsaw" + }, + new + { + Id = 2, + IsActive = true, + IsDefault = false, + KerfInches = 0.125m, + Name = "Chop Saw" + }, + new + { + Id = 3, + IsActive = true, + IsDefault = false, + KerfInches = 0.0625m, + Name = "Cold Cut Saw" + }, + new + { + Id = 4, + IsActive = true, + IsDefault = false, + KerfInches = 0.0625m, + Name = "Hacksaw" + }); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Customer") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CuttingToolId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CuttingToolId"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("MaterialId"); + + b.ToTable("JobParts"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Material", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Shape") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Size") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Materials"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("QuantityOnHand") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("MaterialId", "LengthInches") + .IsUnique(); + + b.ToTable("StockItems"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("StockItemId"); + + b.ToTable("StockTransactions"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContactInfo") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("PartNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierDescription") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("StockItemId"); + + b.HasIndex("SupplierId", "StockItemId") + .IsUnique(); + + b.ToTable("SupplierOfferings"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Job", b => + { + b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool") + .WithMany("Jobs") + .HasForeignKey("CuttingToolId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CuttingTool"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b => + { + b.HasOne("CutList.Web.Data.Entities.Job", "Job") + .WithMany("Parts") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Material", "Material") + .WithMany("JobParts") + .HasForeignKey("MaterialId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("Material"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b => + { + b.HasOne("CutList.Web.Data.Entities.Material", "Material") + .WithMany("StockItems") + .HasForeignKey("MaterialId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Material"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b => + { + b.HasOne("CutList.Web.Data.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem") + .WithMany("Transactions") + .HasForeignKey("StockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("StockItem"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b => + { + b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem") + .WithMany("SupplierOfferings") + .HasForeignKey("StockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier") + .WithMany("Offerings") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("StockItem"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b => + { + b.Navigation("Jobs"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Job", b => + { + b.Navigation("Parts"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Material", b => + { + b.Navigation("JobParts"); + + b.Navigation("StockItems"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b => + { + b.Navigation("SupplierOfferings"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b => + { + b.Navigation("Offerings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CutList.Web/Migrations/20260204220547_MergeStockAndAddTransactions.cs b/CutList.Web/Migrations/20260204220547_MergeStockAndAddTransactions.cs new file mode 100644 index 0000000..f078ba2 --- /dev/null +++ b/CutList.Web/Migrations/20260204220547_MergeStockAndAddTransactions.cs @@ -0,0 +1,144 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CutList.Web.Migrations +{ + /// + public partial class MergeStockAndAddTransactions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // First add the new columns to StockItems + migrationBuilder.AddColumn( + name: "Notes", + table: "StockItems", + type: "nvarchar(255)", + maxLength: 255, + nullable: true); + + migrationBuilder.AddColumn( + name: "QuantityOnHand", + table: "StockItems", + type: "int", + nullable: false, + defaultValue: 0); + + // Migrate data from MaterialStockLengths to StockItems + // Update existing StockItems with matching MaterialId + LengthInches + migrationBuilder.Sql(@" + UPDATE si + SET si.QuantityOnHand = msl.Quantity, + si.Notes = COALESCE(si.Notes, msl.Notes) + FROM StockItems si + INNER JOIN MaterialStockLengths msl + ON si.MaterialId = msl.MaterialId + AND si.LengthInches = msl.LengthInches + WHERE msl.IsActive = 1 + "); + + // Insert MaterialStockLengths that don't have a matching StockItem + migrationBuilder.Sql(@" + INSERT INTO StockItems (MaterialId, LengthInches, QuantityOnHand, Notes, IsActive, CreatedAt) + SELECT msl.MaterialId, msl.LengthInches, msl.Quantity, msl.Notes, 1, GETUTCDATE() + FROM MaterialStockLengths msl + WHERE msl.IsActive = 1 + AND NOT EXISTS ( + SELECT 1 FROM StockItems si + WHERE si.MaterialId = msl.MaterialId + AND si.LengthInches = msl.LengthInches + ) + "); + + // Now drop the old table + migrationBuilder.DropTable( + name: "MaterialStockLengths"); + + migrationBuilder.CreateTable( + name: "StockTransactions", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + StockItemId = table.Column(type: "int", nullable: false), + Quantity = table.Column(type: "int", nullable: false), + Type = table.Column(type: "int", nullable: false), + JobId = table.Column(type: "int", nullable: true), + Notes = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()") + }, + constraints: table => + { + table.PrimaryKey("PK_StockTransactions", x => x.Id); + table.ForeignKey( + name: "FK_StockTransactions_Jobs_JobId", + column: x => x.JobId, + principalTable: "Jobs", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_StockTransactions_StockItems_StockItemId", + column: x => x.StockItemId, + principalTable: "StockItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_StockTransactions_JobId", + table: "StockTransactions", + column: "JobId"); + + migrationBuilder.CreateIndex( + name: "IX_StockTransactions_StockItemId", + table: "StockTransactions", + column: "StockItemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StockTransactions"); + + migrationBuilder.DropColumn( + name: "Notes", + table: "StockItems"); + + migrationBuilder.DropColumn( + name: "QuantityOnHand", + table: "StockItems"); + + migrationBuilder.CreateTable( + name: "MaterialStockLengths", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + MaterialId = table.Column(type: "int", nullable: false), + IsActive = table.Column(type: "bit", nullable: false), + LengthInches = table.Column(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false), + Notes = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: true), + Quantity = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MaterialStockLengths", x => x.Id); + table.ForeignKey( + name: "FK_MaterialStockLengths_Materials_MaterialId", + column: x => x.MaterialId, + principalTable: "Materials", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_MaterialStockLengths_MaterialId_LengthInches", + table: "MaterialStockLengths", + columns: new[] { "MaterialId", "LengthInches" }, + unique: true); + } + } +} diff --git a/CutList.Web/Migrations/20260204221152_AddPriceTrackingToTransactions.Designer.cs b/CutList.Web/Migrations/20260204221152_AddPriceTrackingToTransactions.Designer.cs new file mode 100644 index 0000000..c9b689a --- /dev/null +++ b/CutList.Web/Migrations/20260204221152_AddPriceTrackingToTransactions.Designer.cs @@ -0,0 +1,487 @@ +// +using System; +using CutList.Web.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CutList.Web.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260204221152_AddPriceTrackingToTransactions")] + partial class AddPriceTrackingToTransactions + { + /// + 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("CutList.Web.Data.Entities.CuttingTool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("KerfInches") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("CuttingTools"); + + b.HasData( + new + { + Id = 1, + IsActive = true, + IsDefault = true, + KerfInches = 0.0625m, + Name = "Bandsaw" + }, + new + { + Id = 2, + IsActive = true, + IsDefault = false, + KerfInches = 0.125m, + Name = "Chop Saw" + }, + new + { + Id = 3, + IsActive = true, + IsDefault = false, + KerfInches = 0.0625m, + Name = "Cold Cut Saw" + }, + new + { + Id = 4, + IsActive = true, + IsDefault = false, + KerfInches = 0.0625m, + Name = "Hacksaw" + }); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Customer") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CuttingToolId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CuttingToolId"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("MaterialId"); + + b.ToTable("JobParts"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Material", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Shape") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Size") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Materials"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LengthInches") + .HasPrecision(10, 4) + .HasColumnType("decimal(10,4)"); + + b.Property("MaterialId") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("QuantityOnHand") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("MaterialId", "LengthInches") + .IsUnique(); + + b.ToTable("StockItems"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("StockItemId"); + + b.HasIndex("SupplierId"); + + b.ToTable("StockTransactions"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContactInfo") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("PartNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("StockItemId") + .HasColumnType("int"); + + b.Property("SupplierDescription") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SupplierId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("StockItemId"); + + b.HasIndex("SupplierId", "StockItemId") + .IsUnique(); + + b.ToTable("SupplierOfferings"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Job", b => + { + b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool") + .WithMany("Jobs") + .HasForeignKey("CuttingToolId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CuttingTool"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b => + { + b.HasOne("CutList.Web.Data.Entities.Job", "Job") + .WithMany("Parts") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Material", "Material") + .WithMany("JobParts") + .HasForeignKey("MaterialId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("Material"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b => + { + b.HasOne("CutList.Web.Data.Entities.Material", "Material") + .WithMany("StockItems") + .HasForeignKey("MaterialId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Material"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b => + { + b.HasOne("CutList.Web.Data.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem") + .WithMany("Transactions") + .HasForeignKey("StockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier") + .WithMany() + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Job"); + + b.Navigation("StockItem"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b => + { + b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem") + .WithMany("SupplierOfferings") + .HasForeignKey("StockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier") + .WithMany("Offerings") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("StockItem"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b => + { + b.Navigation("Jobs"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Job", b => + { + b.Navigation("Parts"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Material", b => + { + b.Navigation("JobParts"); + + b.Navigation("StockItems"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b => + { + b.Navigation("SupplierOfferings"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b => + { + b.Navigation("Offerings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CutList.Web/Migrations/20260204221152_AddPriceTrackingToTransactions.cs b/CutList.Web/Migrations/20260204221152_AddPriceTrackingToTransactions.cs new file mode 100644 index 0000000..d7b417b --- /dev/null +++ b/CutList.Web/Migrations/20260204221152_AddPriceTrackingToTransactions.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CutList.Web.Migrations +{ + /// + public partial class AddPriceTrackingToTransactions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SupplierId", + table: "StockTransactions", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "UnitPrice", + table: "StockTransactions", + type: "decimal(10,2)", + precision: 10, + scale: 2, + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_StockTransactions_SupplierId", + table: "StockTransactions", + column: "SupplierId"); + + migrationBuilder.AddForeignKey( + name: "FK_StockTransactions_Suppliers_SupplierId", + table: "StockTransactions", + column: "SupplierId", + principalTable: "Suppliers", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_StockTransactions_Suppliers_SupplierId", + table: "StockTransactions"); + + migrationBuilder.DropIndex( + name: "IX_StockTransactions_SupplierId", + table: "StockTransactions"); + + migrationBuilder.DropColumn( + name: "SupplierId", + table: "StockTransactions"); + + migrationBuilder.DropColumn( + name: "UnitPrice", + table: "StockTransactions"); + } + } +}