feat: add production templates API and documentation for manufacturing phase 2

- Implemented CRUD operations for production templates, including listing, retrieving, creating, updating, and deactivating templates.
- Introduced a new API contract for production runs, detailing the lifecycle from creation to completion, including handling of stock inputs and outputs.
- Documented the architecture, requirements, entity model, and API contract for the manufacturing phase 2, ensuring clarity on the production process and its integration with existing systems.
This commit is contained in:
2026-07-31 10:22:40 +05:30
parent 415ac94ab2
commit 7d6e597389
80 changed files with 11037 additions and 886 deletions
@@ -0,0 +1,277 @@
using ERPCore.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ERPCore.Infra.Persistence.Configurations;
// Manufacturing / Production Lines (docs/30 Part C). All eleven tables live in this one
// file, following the StockConfiguration.cs precedent of grouping an aggregate's
// configurations together.
//
// Cascade shape (deliberate): the two aggregate roots cascade to their own children —
// template → stages/edges → inputs/outputs, run → stages/edges/events → inputs/outputs.
// Every *cross* reference is Restrict, because making them cascade would give EF two
// delete paths to the same table and the model validator rejects that. The consequence
// is an ordering rule for the service layer: when replacing a graph, delete edges before
// stages and inputs before outputs.
public sealed class ProductionTemplateConfiguration : IEntityTypeConfiguration<ProductionTemplate>
{
public void Configure(EntityTypeBuilder<ProductionTemplate> builder)
{
builder.ToTable("production_templates");
builder.HasKey(t => t.TemplateId);
builder.Property(t => t.Code).IsRequired().HasMaxLength(30);
builder.HasIndex(t => t.Code).IsUnique();
builder.Property(t => t.Name).IsRequired().HasMaxLength(150);
builder.Property(t => t.Description).HasMaxLength(500);
builder.Property(t => t.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
// Same jsonb-as-string mapping as TemplateStage.FieldDefs and AuditLog.ChangeSet: a
// typed/owned mapping would make AuditScribe emit audit rows for the nested entries.
builder.Property(t => t.Annotations).HasColumnType("jsonb");
builder.Property(t => t.CreatedAt).IsRequired();
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
builder.Property(t => t.RowVersion).IsRowVersion();
builder.HasOne(t => t.Creator).WithMany().HasForeignKey(t => t.CreatedBy).OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(t => t.Status);
}
}
public sealed class TemplateStageConfiguration : IEntityTypeConfiguration<TemplateStage>
{
public void Configure(EntityTypeBuilder<TemplateStage> builder)
{
builder.ToTable("template_stages");
builder.HasKey(s => s.StageId);
builder.Property(s => s.Name).IsRequired().HasMaxLength(150);
builder.Property(s => s.RoleLabel).HasMaxLength(60);
builder.Property(s => s.PosX).HasPrecision(18, 4);
builder.Property(s => s.PosY).HasPrecision(18, 4);
builder.Property(s => s.FieldDefs).IsRequired().HasColumnType("jsonb");
builder.HasOne(s => s.Template).WithMany(t => t.Stages)
.HasForeignKey(s => s.TemplateId).OnDelete(DeleteBehavior.Cascade);
}
}
public sealed class StageEdgeConfiguration : IEntityTypeConfiguration<StageEdge>
{
public void Configure(EntityTypeBuilder<StageEdge> builder)
{
builder.ToTable("stage_edges");
builder.HasKey(e => e.EdgeId);
builder.HasOne(e => e.Template).WithMany(t => t.Edges)
.HasForeignKey(e => e.TemplateId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(e => e.ParentStage).WithMany()
.HasForeignKey(e => e.ParentStageId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.ChildStage).WithMany()
.HasForeignKey(e => e.ChildStageId).OnDelete(DeleteBehavior.Restrict);
// One arrow per ordered pair; self-loops and cycles are rejected by the validator.
builder.HasIndex(e => new { e.ParentStageId, e.ChildStageId }).IsUnique();
}
}
public sealed class StageInputConfiguration : IEntityTypeConfiguration<StageInput>
{
public void Configure(EntityTypeBuilder<StageInput> builder)
{
builder.ToTable("stage_inputs");
builder.HasKey(i => i.InputId);
builder.Property(i => i.Source).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(i => i.QtyPerBatch).HasPrecision(18, 4);
builder.HasOne(i => i.Stage).WithMany(s => s.Inputs)
.HasForeignKey(i => i.StageId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(i => i.Item).WithMany().HasForeignKey(i => i.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(i => i.Uom).WithMany().HasForeignKey(i => i.UomId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(i => i.FromOutput).WithMany()
.HasForeignKey(i => i.FromOutputId).OnDelete(DeleteBehavior.Restrict);
}
}
public sealed class StageOutputConfiguration : IEntityTypeConfiguration<StageOutput>
{
public void Configure(EntityTypeBuilder<StageOutput> builder)
{
builder.ToTable("stage_outputs");
builder.HasKey(o => o.OutputId);
builder.Property(o => o.Name).IsRequired().HasMaxLength(150);
builder.Property(o => o.QtyPerBatch).HasPrecision(18, 4);
builder.HasOne(o => o.Stage).WithMany(s => s.Outputs)
.HasForeignKey(o => o.StageId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(o => o.Item).WithMany().HasForeignKey(o => o.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(o => o.Uom).WithMany().HasForeignKey(o => o.UomId).OnDelete(DeleteBehavior.Restrict);
}
}
public sealed class ProductionRunConfiguration : IEntityTypeConfiguration<ProductionRun>
{
public void Configure(EntityTypeBuilder<ProductionRun> builder)
{
builder.ToTable("production_runs");
builder.HasKey(r => r.RunId);
builder.Property(r => r.DocNo).IsRequired().HasMaxLength(30);
builder.HasIndex(r => r.DocNo).IsUnique();
builder.Property(r => r.TargetQty).HasPrecision(18, 4);
builder.Property(r => r.ScaleFactor).HasPrecision(18, 6);
builder.Property(r => r.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(r => r.CreatedAt).IsRequired();
builder.Property(r => r.RowVersion).IsRowVersion();
builder.HasOne(r => r.Template).WithMany(t => t.Runs)
.HasForeignKey(r => r.TemplateId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(r => r.Warehouse).WithMany().HasForeignKey(r => r.WarehouseId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(r => r.OutputBin).WithMany().HasForeignKey(r => r.OutputBinId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(r => r.CancelReason).WithMany().HasForeignKey(r => r.CancelReasonCodeId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(r => r.Creator).WithMany().HasForeignKey(r => r.CreatedBy).OnDelete(DeleteBehavior.Restrict);
// Run board filters (docs/30 §D.2) and the template edit-lock count (FR-MFG-06).
builder.HasIndex(r => r.Status);
builder.HasIndex(r => new { r.TemplateId, r.Status });
}
}
public sealed class RunStageConfiguration : IEntityTypeConfiguration<RunStage>
{
public void Configure(EntityTypeBuilder<RunStage> builder)
{
builder.ToTable("run_stages");
builder.HasKey(s => s.RunStageId);
builder.Property(s => s.Name).IsRequired().HasMaxLength(150);
builder.Property(s => s.RoleLabel).HasMaxLength(60);
builder.Property(s => s.PosX).HasPrecision(18, 4);
builder.Property(s => s.PosY).HasPrecision(18, 4);
builder.Property(s => s.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(s => s.FieldDefs).IsRequired().HasColumnType("jsonb");
builder.Property(s => s.FieldValues).HasColumnType("jsonb");
builder.Property(s => s.RowVersion).IsRowVersion();
builder.HasOne(s => s.Run).WithMany(r => r.Stages)
.HasForeignKey(s => s.RunId).OnDelete(DeleteBehavior.Cascade);
// SetNull, not Restrict: a template edit may delete a stage that completed or
// cancelled runs still point at. Everything needed to render such a run is copied
// onto this row, so losing the provenance link is the intended trade (FR-MFG-06).
builder.HasOne(s => s.TemplateStage).WithMany()
.HasForeignKey(s => s.TemplateStageId).OnDelete(DeleteBehavior.SetNull);
builder.HasIndex(s => new { s.RunId, s.Status });
}
}
public sealed class RunEdgeConfiguration : IEntityTypeConfiguration<RunEdge>
{
public void Configure(EntityTypeBuilder<RunEdge> builder)
{
builder.ToTable("run_edges");
builder.HasKey(e => e.RunEdgeId);
builder.HasOne(e => e.Run).WithMany(r => r.Edges)
.HasForeignKey(e => e.RunId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(e => e.ParentRunStage).WithMany()
.HasForeignKey(e => e.ParentRunStageId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.ChildRunStage).WithMany()
.HasForeignKey(e => e.ChildRunStageId).OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => new { e.ParentRunStageId, e.ChildRunStageId }).IsUnique();
}
}
public sealed class RunStageInputConfiguration : IEntityTypeConfiguration<RunStageInput>
{
public void Configure(EntityTypeBuilder<RunStageInput> builder)
{
builder.ToTable("run_stage_inputs");
builder.HasKey(i => i.RunInputId);
builder.Property(i => i.Source).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(i => i.PlannedQty).HasPrecision(18, 4);
builder.Property(i => i.ConsumedQty).HasPrecision(18, 4);
builder.Property(i => i.ConsumedValue).HasPrecision(18, 4);
builder.Property(i => i.DeliveredQty).HasPrecision(18, 4);
builder.Property(i => i.ReturnedQty).HasPrecision(18, 4);
builder.Property(i => i.ReturnedValue).HasPrecision(18, 4);
builder.HasOne(i => i.RunStage).WithMany(s => s.Inputs)
.HasForeignKey(i => i.RunStageId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(i => i.Item).WithMany().HasForeignKey(i => i.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(i => i.Uom).WithMany().HasForeignKey(i => i.UomId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(i => i.FromRunOutput).WithMany()
.HasForeignKey(i => i.FromRunOutputId).OnDelete(DeleteBehavior.Restrict);
// Transfers route by the source output, so this is the hot lookup.
builder.HasIndex(i => i.FromRunOutputId);
}
}
public sealed class RunStageOutputConfiguration : IEntityTypeConfiguration<RunStageOutput>
{
public void Configure(EntityTypeBuilder<RunStageOutput> builder)
{
builder.ToTable("run_stage_outputs");
builder.HasKey(o => o.RunOutputId);
builder.Property(o => o.Name).IsRequired().HasMaxLength(150);
builder.Property(o => o.PlannedQty).HasPrecision(18, 4);
builder.Property(o => o.ProducedQty).HasPrecision(18, 4);
builder.Property(o => o.ScrappedQty).HasPrecision(18, 4);
builder.Property(o => o.TransferredQty).HasPrecision(18, 4);
builder.HasOne(o => o.RunStage).WithMany(s => s.Outputs)
.HasForeignKey(o => o.RunStageId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne(o => o.Item).WithMany().HasForeignKey(o => o.ItemId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(o => o.Uom).WithMany().HasForeignKey(o => o.UomId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(o => o.ScrapReason).WithMany().HasForeignKey(o => o.ScrapReasonCodeId).OnDelete(DeleteBehavior.Restrict);
}
}
public sealed class RunStageEventConfiguration : IEntityTypeConfiguration<RunStageEvent>
{
public void Configure(EntityTypeBuilder<RunStageEvent> builder)
{
// Append-only history: the app never updates or deletes these rows.
builder.ToTable("run_stage_events");
builder.HasKey(e => e.EventId);
builder.Property(e => e.EventType).HasConversion<string>().HasMaxLength(20).IsRequired();
builder.Property(e => e.Note).HasMaxLength(500);
builder.Property(e => e.Payload).HasColumnType("jsonb");
builder.Property(e => e.CreatedAt).IsRequired();
builder.HasOne(e => e.Run).WithMany(r => r.Events)
.HasForeignKey(e => e.RunId).OnDelete(DeleteBehavior.Cascade);
// SetNull rather than Cascade: the run-level cascade above already removes these
// rows, and a second cascade path (run → stage → event) is what EF rejects.
builder.HasOne(e => e.RunStage).WithMany(s => s.Events)
.HasForeignKey(e => e.RunStageId).OnDelete(DeleteBehavior.SetNull);
builder.HasOne(e => e.User).WithMany().HasForeignKey(e => e.UserId).OnDelete(DeleteBehavior.Restrict);
// The run-detail timeline reads the whole run's history in one ordered pass.
builder.HasIndex(e => new { e.RunId, e.EventId });
}
}
@@ -30,6 +30,12 @@ public static class DataSeeder
("WRONG", "Wrong Item", ReasonContext.Return),
("OVER", "Over-supply", ReasonContext.Return),
("QREJ", "Quality Reject", ReasonContext.Return),
// Manufacturing (docs/30 §A.2) — scrap at stage complete, leftover return before
// receipt, and the mandatory reason on a run cancel.
("PRD-SCRAP", "Production Scrap", ReasonContext.Production),
("PRD-LEFTOVER", "Production Leftover Return", ReasonContext.Production),
("PRD-CANCEL", "Production Run Cancelled", ReasonContext.Production),
("PRD-REWORK-LOSS", "Production Rework Loss", ReasonContext.Production),
];
public static async Task SeedAsync(ErpDbContext db, CancellationToken ct = default)
@@ -126,6 +126,21 @@ public class ErpDbContext : DbContext
public DbSet<PayrollLineComponent> PayrollLineComponents => Set<PayrollLineComponent>();
public DbSet<Payslip> Payslips => Set<Payslip>();
// --- Manufacturing: production templates (docs/30-BACKEND-PHASE2.md Part C) ---
public DbSet<ProductionTemplate> ProductionTemplates => Set<ProductionTemplate>();
public DbSet<TemplateStage> TemplateStages => Set<TemplateStage>();
public DbSet<StageEdge> StageEdges => Set<StageEdge>();
public DbSet<StageInput> StageInputs => Set<StageInput>();
public DbSet<StageOutput> StageOutputs => Set<StageOutput>();
// --- Manufacturing: production runs (docs/30-BACKEND-PHASE2.md Part C) ---
public DbSet<ProductionRun> ProductionRuns => Set<ProductionRun>();
public DbSet<RunStage> RunStages => Set<RunStage>();
public DbSet<RunEdge> RunEdges => Set<RunEdge>();
public DbSet<RunStageInput> RunStageInputs => Set<RunStageInput>();
public DbSet<RunStageOutput> RunStageOutputs => Set<RunStageOutput>();
public DbSet<RunStageEvent> RunStageEvents => Set<RunStageEvent>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
@@ -2400,6 +2400,136 @@ namespace ERPCore.Infra.Persistence.Migrations
});
});
modelBuilder.Entity("ERPCore.Domain.Entities.ProductionRun", b =>
{
b.Property<int>("RunId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("RunId"));
b.Property<int?>("CancelReasonCodeId")
.HasColumnType("integer");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("CreatedBy")
.HasColumnType("integer");
b.Property<string>("DocNo")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int?>("OutputBinId")
.HasColumnType("integer");
b.Property<int>("ReworkCount")
.HasColumnType("integer");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.Property<decimal>("ScaleFactor")
.HasPrecision(18, 6)
.HasColumnType("numeric(18,6)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<decimal>("TargetQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.Property<int>("WarehouseId")
.HasColumnType("integer");
b.HasKey("RunId");
b.HasIndex("CancelReasonCodeId");
b.HasIndex("CreatedBy");
b.HasIndex("DocNo")
.IsUnique();
b.HasIndex("OutputBinId");
b.HasIndex("Status");
b.HasIndex("WarehouseId");
b.HasIndex("TemplateId", "Status");
b.ToTable("production_runs", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.ProductionTemplate", b =>
{
b.Property<int>("TemplateId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("TemplateId"));
b.Property<string>("Annotations")
.HasColumnType("jsonb");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("CreatedBy")
.HasColumnType("integer");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("TemplateId");
b.HasIndex("Code")
.IsUnique();
b.HasIndex("CreatedBy");
b.HasIndex("Status");
b.ToTable("production_templates", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
{
b.Property<int>("PoId")
@@ -2769,6 +2899,261 @@ namespace ERPCore.Infra.Persistence.Migrations
b.ToTable("role_permissions", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunEdge", b =>
{
b.Property<int>("RunEdgeId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("RunEdgeId"));
b.Property<int>("ChildRunStageId")
.HasColumnType("integer");
b.Property<int>("ParentRunStageId")
.HasColumnType("integer");
b.Property<int>("RunId")
.HasColumnType("integer");
b.HasKey("RunEdgeId");
b.HasIndex("ChildRunStageId");
b.HasIndex("RunId");
b.HasIndex("ParentRunStageId", "ChildRunStageId")
.IsUnique();
b.ToTable("run_edges", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b =>
{
b.Property<int>("RunStageId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("RunStageId"));
b.Property<DateTime?>("ActualEndAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ActualStartAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("EstimatedMinutes")
.HasColumnType("integer");
b.Property<string>("FieldDefs")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("FieldValues")
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<decimal>("PosX")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<decimal>("PosY")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<string>("RoleLabel")
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.Property<int>("RunId")
.HasColumnType("integer");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int?>("TemplateStageId")
.HasColumnType("integer");
b.HasKey("RunStageId");
b.HasIndex("TemplateStageId");
b.HasIndex("RunId", "Status");
b.ToTable("run_stages", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStageEvent", b =>
{
b.Property<int>("EventId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("EventId"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("Payload")
.HasColumnType("jsonb");
b.Property<int>("RunId")
.HasColumnType("integer");
b.Property<int?>("RunStageId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("EventId");
b.HasIndex("RunStageId");
b.HasIndex("UserId");
b.HasIndex("RunId", "EventId");
b.ToTable("run_stage_events", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStageInput", b =>
{
b.Property<int>("RunInputId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("RunInputId"));
b.Property<decimal>("ConsumedQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<decimal>("ConsumedValue")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<decimal>("DeliveredQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<int?>("FromRunOutputId")
.HasColumnType("integer");
b.Property<int?>("ItemId")
.HasColumnType("integer");
b.Property<decimal>("PlannedQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<decimal>("ReturnedQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<decimal>("ReturnedValue")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<int>("RunStageId")
.HasColumnType("integer");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int>("UomId")
.HasColumnType("integer");
b.HasKey("RunInputId");
b.HasIndex("FromRunOutputId");
b.HasIndex("ItemId");
b.HasIndex("RunStageId");
b.HasIndex("UomId");
b.ToTable("run_stage_inputs", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStageOutput", b =>
{
b.Property<int>("RunOutputId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("RunOutputId"));
b.Property<int?>("ItemId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<decimal>("PlannedQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<decimal>("ProducedQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<int>("RunStageId")
.HasColumnType("integer");
b.Property<int?>("ScrapReasonCodeId")
.HasColumnType("integer");
b.Property<decimal>("ScrappedQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<decimal>("TransferredQty")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<int>("UomId")
.HasColumnType("integer");
b.HasKey("RunOutputId");
b.HasIndex("ItemId");
b.HasIndex("RunStageId");
b.HasIndex("ScrapReasonCodeId");
b.HasIndex("UomId");
b.ToTable("run_stage_outputs", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.SalaryComponent", b =>
{
b.Property<int>("SalaryComponentId")
@@ -2856,6 +3241,114 @@ namespace ERPCore.Infra.Persistence.Migrations
b.ToTable("serials", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.StageEdge", b =>
{
b.Property<int>("EdgeId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("EdgeId"));
b.Property<int>("ChildStageId")
.HasColumnType("integer");
b.Property<int>("ParentStageId")
.HasColumnType("integer");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.HasKey("EdgeId");
b.HasIndex("ChildStageId");
b.HasIndex("TemplateId");
b.HasIndex("ParentStageId", "ChildStageId")
.IsUnique();
b.ToTable("stage_edges", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.StageInput", b =>
{
b.Property<int>("InputId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("InputId"));
b.Property<int?>("FromOutputId")
.HasColumnType("integer");
b.Property<int?>("ItemId")
.HasColumnType("integer");
b.Property<decimal>("QtyPerBatch")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int>("StageId")
.HasColumnType("integer");
b.Property<int>("UomId")
.HasColumnType("integer");
b.HasKey("InputId");
b.HasIndex("FromOutputId");
b.HasIndex("ItemId");
b.HasIndex("StageId");
b.HasIndex("UomId");
b.ToTable("stage_inputs", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.StageOutput", b =>
{
b.Property<int>("OutputId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("OutputId"));
b.Property<int?>("ItemId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<decimal>("QtyPerBatch")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<int>("StageId")
.HasColumnType("integer");
b.Property<int>("UomId")
.HasColumnType("integer");
b.HasKey("OutputId");
b.HasIndex("ItemId");
b.HasIndex("StageId");
b.HasIndex("UomId");
b.ToTable("stage_outputs", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b =>
{
b.Property<int>("AdjustmentId")
@@ -3537,6 +4030,48 @@ namespace ERPCore.Infra.Persistence.Migrations
b.ToTable("hr_tax_slabs", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b =>
{
b.Property<int>("StageId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("StageId"));
b.Property<int>("EstimatedMinutes")
.HasColumnType("integer");
b.Property<string>("FieldDefs")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<decimal>("PosX")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<decimal>("PosY")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)");
b.Property<string>("RoleLabel")
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<int>("TemplateId")
.HasColumnType("integer");
b.HasKey("StageId");
b.HasIndex("TemplateId");
b.ToTable("template_stages", (string)null);
});
modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b =>
{
b.Property<int>("UomId")
@@ -4392,6 +4927,58 @@ namespace ERPCore.Infra.Persistence.Migrations
b.Navigation("UpdatedByUser");
});
modelBuilder.Entity("ERPCore.Domain.Entities.ProductionRun", b =>
{
b.HasOne("ERPCore.Domain.Entities.ReasonCode", "CancelReason")
.WithMany()
.HasForeignKey("CancelReasonCodeId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.Bin", "OutputBin")
.WithMany()
.HasForeignKey("OutputBinId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.ProductionTemplate", "Template")
.WithMany("Runs")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
.WithMany()
.HasForeignKey("WarehouseId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CancelReason");
b.Navigation("Creator");
b.Navigation("OutputBin");
b.Navigation("Template");
b.Navigation("Warehouse");
});
modelBuilder.Entity("ERPCore.Domain.Entities.ProductionTemplate", b =>
{
b.HasOne("ERPCore.Domain.Entities.User", "Creator")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Creator");
});
modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
{
b.HasOne("ERPCore.Domain.Entities.User", "Creator")
@@ -4558,6 +5145,143 @@ namespace ERPCore.Infra.Persistence.Migrations
b.Navigation("Role");
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunEdge", b =>
{
b.HasOne("ERPCore.Domain.Entities.RunStage", "ChildRunStage")
.WithMany()
.HasForeignKey("ChildRunStageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.RunStage", "ParentRunStage")
.WithMany()
.HasForeignKey("ParentRunStageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run")
.WithMany("Edges")
.HasForeignKey("RunId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ChildRunStage");
b.Navigation("ParentRunStage");
b.Navigation("Run");
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b =>
{
b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run")
.WithMany("Stages")
.HasForeignKey("RunId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.TemplateStage", "TemplateStage")
.WithMany()
.HasForeignKey("TemplateStageId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Run");
b.Navigation("TemplateStage");
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStageEvent", b =>
{
b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run")
.WithMany("Events")
.HasForeignKey("RunId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage")
.WithMany("Events")
.HasForeignKey("RunStageId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ERPCore.Domain.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Run");
b.Navigation("RunStage");
b.Navigation("User");
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStageInput", b =>
{
b.HasOne("ERPCore.Domain.Entities.RunStageOutput", "FromRunOutput")
.WithMany()
.HasForeignKey("FromRunOutputId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
.WithMany()
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage")
.WithMany("Inputs")
.HasForeignKey("RunStageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.Uom", "Uom")
.WithMany()
.HasForeignKey("UomId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("FromRunOutput");
b.Navigation("Item");
b.Navigation("RunStage");
b.Navigation("Uom");
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStageOutput", b =>
{
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
.WithMany()
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage")
.WithMany("Outputs")
.HasForeignKey("RunStageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ScrapReason")
.WithMany()
.HasForeignKey("ScrapReasonCodeId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.Uom", "Uom")
.WithMany()
.HasForeignKey("UomId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Item");
b.Navigation("RunStage");
b.Navigation("ScrapReason");
b.Navigation("Uom");
});
modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b =>
{
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
@@ -4569,6 +5293,92 @@ namespace ERPCore.Infra.Persistence.Migrations
b.Navigation("Item");
});
modelBuilder.Entity("ERPCore.Domain.Entities.StageEdge", b =>
{
b.HasOne("ERPCore.Domain.Entities.TemplateStage", "ChildStage")
.WithMany()
.HasForeignKey("ChildStageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.TemplateStage", "ParentStage")
.WithMany()
.HasForeignKey("ParentStageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.ProductionTemplate", "Template")
.WithMany("Edges")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ChildStage");
b.Navigation("ParentStage");
b.Navigation("Template");
});
modelBuilder.Entity("ERPCore.Domain.Entities.StageInput", b =>
{
b.HasOne("ERPCore.Domain.Entities.StageOutput", "FromOutput")
.WithMany()
.HasForeignKey("FromOutputId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
.WithMany()
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.TemplateStage", "Stage")
.WithMany("Inputs")
.HasForeignKey("StageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.Uom", "Uom")
.WithMany()
.HasForeignKey("UomId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("FromOutput");
b.Navigation("Item");
b.Navigation("Stage");
b.Navigation("Uom");
});
modelBuilder.Entity("ERPCore.Domain.Entities.StageOutput", b =>
{
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
.WithMany()
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ERPCore.Domain.Entities.TemplateStage", "Stage")
.WithMany("Outputs")
.HasForeignKey("StageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ERPCore.Domain.Entities.Uom", "Uom")
.WithMany()
.HasForeignKey("UomId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Item");
b.Navigation("Stage");
b.Navigation("Uom");
});
modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b =>
{
b.HasOne("ERPCore.Domain.Entities.User", "Creator")
@@ -4837,6 +5647,17 @@ namespace ERPCore.Infra.Persistence.Migrations
b.Navigation("NavItem");
});
modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b =>
{
b.HasOne("ERPCore.Domain.Entities.ProductionTemplate", "Template")
.WithMany("Stages")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Template");
});
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
{
b.HasOne("ERPCore.Domain.Entities.Uom", "FromUom")
@@ -4954,6 +5775,24 @@ namespace ERPCore.Infra.Persistence.Migrations
b.Navigation("Lines");
});
modelBuilder.Entity("ERPCore.Domain.Entities.ProductionRun", b =>
{
b.Navigation("Edges");
b.Navigation("Events");
b.Navigation("Stages");
});
modelBuilder.Entity("ERPCore.Domain.Entities.ProductionTemplate", b =>
{
b.Navigation("Edges");
b.Navigation("Runs");
b.Navigation("Stages");
});
modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
{
b.Navigation("Lines");
@@ -4976,6 +5815,15 @@ namespace ERPCore.Infra.Persistence.Migrations
b.Navigation("Quotations");
});
modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b =>
{
b.Navigation("Events");
b.Navigation("Inputs");
b.Navigation("Outputs");
});
modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b =>
{
b.Navigation("Lines");
@@ -4991,6 +5839,13 @@ namespace ERPCore.Infra.Persistence.Migrations
b.Navigation("Lines");
});
modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b =>
{
b.Navigation("Inputs");
b.Navigation("Outputs");
});
modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b =>
{
b.Navigation("Lines");