diff --git a/Backend/ERPCore/Controllers/BundleSalesController.cs b/Backend/ERPCore/Controllers/BundleSalesController.cs index b1d99a5..d768c97 100644 --- a/Backend/ERPCore/Controllers/BundleSalesController.cs +++ b/Backend/ERPCore/Controllers/BundleSalesController.cs @@ -29,6 +29,14 @@ public sealed class BundleSalesController : ApiControllerBase return result is null ? NotFound() : Ok(result); } + [HttpPost("templates")] + [ProducesResponseType(typeof(BundleSaleTemplateDto), StatusCodes.Status201Created)] + public async Task> CreateTemplate([FromBody] CreateBundleSaleTemplateRequest request, CancellationToken ct) + { + var result = await _bundles.CreateTemplateAsync(request, ct); + return Created($"/api/v1/bundle-sales/templates/{result.BundleSaleTemplateId}", result); + } + [HttpGet] [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)] public async Task>> List( diff --git a/Backend/ERPCore/Domain/DocumentTypes.cs b/Backend/ERPCore/Domain/DocumentTypes.cs index a034541..6f39a9b 100644 --- a/Backend/ERPCore/Domain/DocumentTypes.cs +++ b/Backend/ERPCore/Domain/DocumentTypes.cs @@ -20,6 +20,7 @@ public static class DocumentTypes public const string SalesInvoice = "SI"; public const string SalesSlip = "SSL"; public const string BundleSale = "BND"; + public const string BundleSaleTemplate = "TPL"; public const string SalesReturn = "SRET"; /// Cashier day-end close-out (Sales) — DEND-2026-00001. diff --git a/Backend/ERPCore/Domain/Entities/Item.cs b/Backend/ERPCore/Domain/Entities/Item.cs index 94b22a7..2a6fa07 100644 --- a/Backend/ERPCore/Domain/Entities/Item.cs +++ b/Backend/ERPCore/Domain/Entities/Item.cs @@ -44,11 +44,12 @@ public class Item public string? TaxClass { get; set; } /// - /// Optional fixed selling price used by Sales only. null means "use stock value" - /// (the item is sold at its FIFO stock cost at sale time); a value is the fixed sale price. - /// Never enters costing/GRN/FIFO (docs/10 Part C.1, C.9). + /// Optional selling price range and flat price used by Sales only. + /// null values mean the field is not set. These values never enter costing/GRN/FIFO. /// - public decimal? SalePrice { get; set; } + public decimal? SellingMinPrice { get; set; } + public decimal? SellingMaxPrice { get; set; } + public decimal? SellingFlatPrice { get; set; } /// /// How much one pack holds, as the user entered it — 500 with diff --git a/Backend/ERPCore/Dtos/Items/ItemDtos.cs b/Backend/ERPCore/Dtos/Items/ItemDtos.cs index 35f8d62..32a584d 100644 --- a/Backend/ERPCore/Dtos/Items/ItemDtos.cs +++ b/Backend/ERPCore/Dtos/Items/ItemDtos.cs @@ -10,7 +10,7 @@ public sealed record ItemListItemDto( int ItemId, string Sku, string Name, int CategoryId, int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode, Warranty Warranty, int? WarrantyPeriodMonths, - string? TaxClass, decimal? SalePrice, + string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice, decimal? ContentQty, MeasureUnit? ContentUnit, decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit, EntityStatus Status); @@ -31,7 +31,7 @@ public sealed record ItemDetailDto( int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode, Warranty Warranty, int? WarrantyPeriodMonths, - string? TaxClass, decimal? SalePrice, + string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice, decimal? ContentQty, MeasureUnit? ContentUnit, decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit, EntityStatus Status, IReadOnlyList Reorder, @@ -66,7 +66,9 @@ public sealed class CreateItemRequest public int? WarrantyPeriodMonths { get; set; } [StringLength(20)] public string? TaxClass { get; set; } /// Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value. - [Range(0, double.MaxValue)] public decimal? SalePrice { get; set; } + [Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; } + [Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; } + [Range(0, double.MaxValue)] public decimal? SellingFlatPrice { get; set; } /// /// How much one pack holds. Supply with or leave both null @@ -96,7 +98,9 @@ public sealed class UpdateItemRequest public int? WarrantyPeriodMonths { get; set; } [StringLength(20)] public string? TaxClass { get; set; } /// Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value. - [Range(0, double.MaxValue)] public decimal? SalePrice { get; set; } + [Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; } + [Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; } + [Range(0, double.MaxValue)] public decimal? SellingFlatPrice { get; set; } /// /// How much one pack holds. Supply with or leave both null diff --git a/Backend/ERPCore/Dtos/Sales/BundleSaleDtos.cs b/Backend/ERPCore/Dtos/Sales/BundleSaleDtos.cs index a31d3d4..9ab6b6b 100644 --- a/Backend/ERPCore/Dtos/Sales/BundleSaleDtos.cs +++ b/Backend/ERPCore/Dtos/Sales/BundleSaleDtos.cs @@ -51,7 +51,6 @@ public sealed class CreateBundleSaleTemplateLineRequest public sealed class CreateBundleSaleTemplateRequest { - [Required] public string TemplateCode { get; set; } = string.Empty; [Required] public string TemplateName { get; set; } = string.Empty; public string? Description { get; set; } [Required, MinLength(1)] public List Lines { get; set; } = new(); diff --git a/Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs b/Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs index ea97dc9..398e266 100644 --- a/Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs +++ b/Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs @@ -8,11 +8,21 @@ namespace ERPCore.Dtos.Sales; /// Merges sales-slip and bundle-sale lines for the same item into one row. public sealed record SalesDayEndItemLineDto(int ItemId, string Sku, string Name, decimal Qty, decimal Revenue); +/// Read-only visibility into that cashier's Sales Invoices for the date — informational +/// only, appended to and . +/// Invoices post their own GL entry on Confirm (SalesPostingService.PostInvoiceAsync) and have no +/// DayEndId, so they're never locked against a close: these fields are computed live from current +/// data on every read, unlike the frozen Slip/Bundle totals alongside them. +public sealed record SalesDayEndInvoiceSummaryDto( + int InvoiceCount, decimal InvoiceSubtotal, decimal InvoiceDiscountTotal, decimal InvoiceTaxTotal, + decimal InvoiceGrandTotal, IReadOnlyList InvoiceNumbers); + public sealed record SalesDayEndDto( int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate, int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal, decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt, - IReadOnlyList SlipNumbers, IReadOnlyList BundleNumbers, IReadOnlyList ItemBreakdown); + IReadOnlyList SlipNumbers, IReadOnlyList BundleNumbers, IReadOnlyList ItemBreakdown, + SalesDayEndInvoiceSummaryDto Invoices); public sealed record SalesDayEndSummaryDto( int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate, @@ -25,7 +35,8 @@ public sealed record SalesDayEndPreviewDto( int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal, IReadOnlyList SlipNumbers, IReadOnlyList BundleNumbers, IReadOnlyList DraftSlipsBlockingClose, - IReadOnlyList DraftBundleSalesBlockingClose); + IReadOnlyList DraftBundleSalesBlockingClose, + SalesDayEndInvoiceSummaryDto Invoices); public sealed record DraftSlipBlockingCloseDto(int SalesSlipId, string SlipNo, decimal GrandTotal); public sealed record DraftBundleBlockingCloseDto(int BundleSaleId, string BundleNo, decimal GrandTotal); diff --git a/Backend/ERPCore/Dtos/Sales/SalesPromotionSuggestionDtos.cs b/Backend/ERPCore/Dtos/Sales/SalesPromotionSuggestionDtos.cs index dffd1c8..cdd19b4 100644 --- a/Backend/ERPCore/Dtos/Sales/SalesPromotionSuggestionDtos.cs +++ b/Backend/ERPCore/Dtos/Sales/SalesPromotionSuggestionDtos.cs @@ -4,7 +4,7 @@ public sealed record SalesFreeIssueRewardOptionDto( int ItemId, string Sku, string Name, - decimal? SalePrice); + decimal? SellingFlatPrice); public sealed record SalesFreeIssueSuggestionLineDto( int SalesSlipLineId, @@ -20,4 +20,4 @@ public sealed record SalesFreeIssueSuggestionDto( int SalesSlipId, string SlipNo, DateTime SlipDate, - IReadOnlyList Lines); \ No newline at end of file + IReadOnlyList Lines); diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/ItemConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/ItemConfiguration.cs index b14cb0e..79d409e 100644 --- a/Backend/ERPCore/Infra/Persistence/Configurations/ItemConfiguration.cs +++ b/Backend/ERPCore/Infra/Persistence/Configurations/ItemConfiguration.cs @@ -20,7 +20,9 @@ public sealed class ItemConfiguration : IEntityTypeConfiguration builder.Property(i => i.TaxClass).HasMaxLength(20); // Sales-only fixed selling price; nullable (null ⇒ sell at stock/FIFO value). - builder.Property(i => i.SalePrice).HasPrecision(18, 4); + builder.Property(i => i.SellingMinPrice).HasPrecision(18, 4); + builder.Property(i => i.SellingMaxPrice).HasPrecision(18, 4); + builder.Property(i => i.SellingFlatPrice).HasPrecision(18, 4); // Optional content size (how much one stocked pack holds). All four are nullable // together: null ⇒ the item has no measurable content. The base pair is derived diff --git a/Backend/ERPCore/Infra/Persistence/DataSeeder.cs b/Backend/ERPCore/Infra/Persistence/DataSeeder.cs index ff802f6..da53ab5 100644 --- a/Backend/ERPCore/Infra/Persistence/DataSeeder.cs +++ b/Backend/ERPCore/Infra/Persistence/DataSeeder.cs @@ -182,7 +182,7 @@ public static class DataSeeder WarehouseId = warehouse.WarehouseId, QtyReceived = 100m, QtyRemaining = 100m, - UnitCost = item.SalePrice.GetValueOrDefault() > 0m ? item.SalePrice.GetValueOrDefault() / 2m : 25m, + UnitCost = item.SellingFlatPrice.GetValueOrDefault() > 0m ? item.SellingFlatPrice.GetValueOrDefault() / 2m : 25m, ReceiptDate = DateTime.UtcNow.AddDays(-7) }); db.StockLayers.Add(new StockLayer @@ -191,7 +191,7 @@ public static class DataSeeder WarehouseId = warehouse.WarehouseId, QtyReceived = 5m, QtyRemaining = 5m, - UnitCost = secondItem.SalePrice.GetValueOrDefault() > 0m ? secondItem.SalePrice.GetValueOrDefault() / 2m : 15m, + UnitCost = secondItem.SellingFlatPrice.GetValueOrDefault() > 0m ? secondItem.SellingFlatPrice.GetValueOrDefault() / 2m : 15m, ReceiptDate = DateTime.UtcNow.AddDays(-6) }); return true; @@ -278,7 +278,7 @@ public static class DataSeeder BaseUomId = uom.UomId, StockNature = StockNature.Stocked, TrackingMode = TrackingMode.None, - SalePrice = 100m, + SellingFlatPrice = 100m, Status = EntityStatus.Active, CreatedAt = now }, @@ -292,7 +292,7 @@ public static class DataSeeder BaseUomId = uom.UomId, StockNature = StockNature.Stocked, TrackingMode = TrackingMode.None, - SalePrice = 50m, + SellingFlatPrice = 50m, ContentQty = 500m, ContentUnit = MeasureUnit.Ml, ContentBaseQty = 500m, @@ -445,7 +445,7 @@ public static class DataSeeder ItemId = items[0].ItemId, WarehouseId = warehouse.WarehouseId, Qty = 1m, - UnitPrice = items[0].SalePrice.GetValueOrDefault(), + UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(), IncludeInBundle = true, SortOrder = 1 }, @@ -454,7 +454,7 @@ public static class DataSeeder ItemId = items[1].ItemId, WarehouseId = warehouse.WarehouseId, Qty = 1m, - UnitPrice = items[1].SalePrice.GetValueOrDefault(), + UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(), IncludeInBundle = true, SortOrder = 2 } @@ -478,10 +478,10 @@ public static class DataSeeder BundleName = "Demo Bundle Draft", BundleCode = "BND-DEMO-001", Status = BundleSaleStatus.Draft, - ComponentSubtotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(), + ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(), BundlePrice = 0m, - MarginAmount = -(items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault()), - DiscountTotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(), + MarginAmount = -(items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault()), + DiscountTotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(), TaxTotal = 0m, GrandTotal = 0m, CreatedAt = now.AddDays(-2), @@ -493,8 +493,8 @@ public static class DataSeeder Description = items[0].Name, Qty = 1m, WarehouseId = warehouse.WarehouseId, - UnitPrice = items[0].SalePrice.GetValueOrDefault(), - LineTotal = items[0].SalePrice.GetValueOrDefault(), + UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(), + LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(), IncludeInBundle = true, IsComponent = true }, @@ -504,8 +504,8 @@ public static class DataSeeder Description = items[1].Name, Qty = 1m, WarehouseId = warehouse.WarehouseId, - UnitPrice = items[1].SalePrice.GetValueOrDefault(), - LineTotal = items[1].SalePrice.GetValueOrDefault(), + UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(), + LineTotal = items[1].SellingFlatPrice.GetValueOrDefault(), IncludeInBundle = true, IsComponent = true } @@ -523,12 +523,12 @@ public static class DataSeeder BundleName = "Demo Bundle Posted", BundleCode = "BND-DEMO-001", Status = BundleSaleStatus.Posted, - ComponentSubtotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(), - BundlePrice = (items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault()) - 10m, + ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(), + BundlePrice = (items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault()) - 10m, MarginAmount = -10m, DiscountTotal = 10m, TaxTotal = 0m, - GrandTotal = (items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault()) - 10m, + GrandTotal = (items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault()) - 10m, CreatedAt = now.AddDays(-1), UpdatedAt = now.AddHours(-2), Lines = @@ -539,8 +539,8 @@ public static class DataSeeder Description = items[0].Name, Qty = 1m, WarehouseId = warehouse.WarehouseId, - UnitPrice = items[0].SalePrice.GetValueOrDefault(), - LineTotal = items[0].SalePrice.GetValueOrDefault(), + UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(), + LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(), IncludeInBundle = true, IsComponent = true }, @@ -550,8 +550,8 @@ public static class DataSeeder Description = items[1].Name, Qty = 1m, WarehouseId = warehouse.WarehouseId, - UnitPrice = items[1].SalePrice.GetValueOrDefault(), - LineTotal = items[1].SalePrice.GetValueOrDefault(), + UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(), + LineTotal = items[1].SellingFlatPrice.GetValueOrDefault(), IncludeInBundle = true, IsComponent = true } @@ -569,12 +569,12 @@ public static class DataSeeder BundleName = "Demo Bundle Cancelled", BundleCode = "BND-DEMO-001", Status = BundleSaleStatus.Cancelled, - ComponentSubtotal = items[0].SalePrice.GetValueOrDefault(), - BundlePrice = items[0].SalePrice.GetValueOrDefault(), + ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault(), + BundlePrice = items[0].SellingFlatPrice.GetValueOrDefault(), MarginAmount = 0m, DiscountTotal = 0m, TaxTotal = 0m, - GrandTotal = items[0].SalePrice.GetValueOrDefault(), + GrandTotal = items[0].SellingFlatPrice.GetValueOrDefault(), CreatedAt = now, UpdatedAt = now, Lines = @@ -585,8 +585,8 @@ public static class DataSeeder Description = items[0].Name, Qty = 1m, WarehouseId = secondaryWarehouse.WarehouseId, - UnitPrice = items[0].SalePrice.GetValueOrDefault(), - LineTotal = items[0].SalePrice.GetValueOrDefault(), + UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(), + LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(), IncludeInBundle = true, IsComponent = true } diff --git a/Backend/ERPCore/Migrations/20260820083810_itemModel.Designer.cs b/Backend/ERPCore/Migrations/20260820083810_itemModel.Designer.cs new file mode 100644 index 0000000..b9a9604 --- /dev/null +++ b/Backend/ERPCore/Migrations/20260820083810_itemModel.Designer.cs @@ -0,0 +1,7476 @@ +// +using System; +using ERPCore.Infra.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ERPCore.Migrations +{ + [DbContext(typeof(ErpDbContext))] + [Migration("20260820083810_itemModel")] + partial class itemModel + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceRecord", b => + { + b.Property("AttendanceRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AttendanceRecordId")); + + b.Property("AttendanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("AttendanceStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("AttendanceUploadBatchId") + .HasColumnType("integer"); + + b.Property("CheckIn") + .HasColumnType("interval"); + + b.Property("CheckOut") + .HasColumnType("interval"); + + b.Property("DuplicateOfAttendanceRecordId") + .HasColumnType("integer"); + + b.Property("EarlyLeaveMinutes") + .HasColumnType("integer"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EditedBy") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("IsManualOverride") + .HasColumnType("boolean"); + + b.Property("LateMinutes") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("OvertimeMinutes") + .HasColumnType("integer"); + + b.Property("RowValidationStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WorkShiftId") + .HasColumnType("integer"); + + b.Property("WorkingMinutes") + .HasColumnType("integer"); + + b.HasKey("AttendanceRecordId"); + + b.HasIndex("AttendanceUploadBatchId"); + + b.HasIndex("WorkShiftId"); + + b.HasIndex("EmployeeId", "AttendanceDate"); + + b.ToTable("hr_attendance_records", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceUploadBatch", b => + { + b.Property("AttendanceUploadBatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AttendanceUploadBatchId")); + + b.Property("ConfirmedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConfirmedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OriginalFileName") + .HasMaxLength(260) + .HasColumnType("character varying(260)"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("RowCountDuplicate") + .HasColumnType("integer"); + + b.Property("RowCountError") + .HasColumnType("integer"); + + b.Property("RowCountTotal") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedBy") + .HasColumnType("integer"); + + b.HasKey("AttendanceUploadBatchId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("hr_attendance_upload_batches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AuditLog", b => + { + b.Property("AuditId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AuditId")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("ChangeSet") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .HasColumnType("integer"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("AuditId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UserId"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("audit_logs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b => + { + b.Property("BatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BatchId")); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ExpiryDate") + .HasColumnType("date"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.HasKey("BatchId"); + + b.HasIndex("ItemId", "BatchNo") + .IsUnique(); + + b.ToTable("batches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b => + { + b.Property("BinId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BinId")); + + b.Property("BinType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("BinId"); + + b.HasIndex("WarehouseId", "Code") + .IsUnique(); + + b.ToTable("bins", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Branch", b => + { + b.Property("BranchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BranchId")); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BranchId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_branches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Brand", b => + { + b.Property("BrandId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BrandId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BrandId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("brands", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.Property("BundleSaleId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleId")); + + b.Property("BundleCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BundleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("BundleName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BundleNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BundlePrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("BundleSaleTemplateId") + .HasColumnType("integer"); + + b.Property("CashierUserId") + .HasColumnType("integer"); + + b.Property("ComponentSubtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DayEndId") + .HasColumnType("integer"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("MarginAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("BundleSaleId"); + + b.HasIndex("BundleNo") + .IsUnique(); + + b.HasIndex("BundleSaleTemplateId"); + + b.HasIndex("CashierUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DayEndId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sales", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleLine", b => + { + b.Property("BundleSaleLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleLineId")); + + b.Property("BundleSaleId") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IncludeInBundle") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsComponent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("BundleSaleLineId"); + + b.HasIndex("BundleSaleId"); + + b.HasIndex("ItemId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sale_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplate", b => + { + b.Property("BundleSaleTemplateId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleTemplateId")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TemplateName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BundleSaleTemplateId"); + + b.HasIndex("TemplateCode") + .IsUnique(); + + b.ToTable("bundle_sale_templates", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplateLine", b => + { + b.Property("BundleSaleTemplateLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleTemplateLineId")); + + b.Property("BundleSaleTemplateId") + .HasColumnType("integer"); + + b.Property("IncludeInBundle") + .HasColumnType("boolean"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("BundleSaleTemplateLineId"); + + b.HasIndex("BundleSaleTemplateId"); + + b.HasIndex("ItemId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sale_template_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CategoryId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CategoryId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Customer", b => + { + b.Property("CustomerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CustomerId")); + + b.Property("AddressLine1") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("AddressLine2") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Country") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditDays") + .HasColumnType("integer"); + + b.Property("CreditLimit") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CustomerCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CustomerType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("B2C"); + + b.Property("DefaultWarehouseId") + .HasColumnType("integer"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("TaxRegistrationNo") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CustomerId"); + + b.HasIndex("CustomerCode") + .IsUnique(); + + b.HasIndex("CustomerType"); + + b.HasIndex("DefaultWarehouseId"); + + b.HasIndex("Status"); + + b.ToTable("customers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Department", b => + { + b.Property("DepartmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DepartmentId")); + + b.Property("BranchId") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HeadEmployeeId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParentDepartmentId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DepartmentId"); + + b.HasIndex("BranchId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("HeadEmployeeId"); + + b.HasIndex("ParentDepartmentId"); + + b.HasIndex("Status"); + + b.ToTable("hr_departments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Designation", b => + { + b.Property("DesignationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DesignationId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DesignationId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_designations", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Employee", b => + { + b.Property("EmployeeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeId")); + + b.Property("AddressLine1") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AddressLine2") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BranchId") + .HasColumnType("integer"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ConfirmationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Country") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DateOfBirth") + .HasColumnType("timestamp with time zone"); + + b.Property("DepartmentId") + .HasColumnType("integer"); + + b.Property("DesignationId") + .HasColumnType("integer"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("EmergencyContactName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EmergencyContactRelationship") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmployeeCode") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EmploymentTypeId") + .HasColumnType("integer"); + + b.Property("EpfNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EtfNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Gender") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastWorkingDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Nationality") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Nic") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PersonalMobile") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PostalCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ProfilePhotoPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReportingManagerId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("TaxIdentificationNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WorkShiftId") + .HasColumnType("integer"); + + b.HasKey("EmployeeId"); + + b.HasIndex("BranchId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("DesignationId"); + + b.HasIndex("Email"); + + b.HasIndex("EmployeeCode") + .IsUnique(); + + b.HasIndex("EmploymentTypeId"); + + b.HasIndex("ReportingManagerId"); + + b.HasIndex("Status"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("WorkShiftId"); + + b.ToTable("hr_employees", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeBankDetail", b => + { + b.Property("EmployeeBankDetailId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeBankDetailId")); + + b.Property("AccountHolderName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AccountNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BankName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BranchName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("SwiftCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("EmployeeBankDetailId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("hr_employee_bank_details", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeDocument", b => + { + b.Property("EmployeeDocumentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeDocumentId")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HrDocumentTypeId") + .HasColumnType("integer"); + + b.Property("IssueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("character varying(260)"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("StoredFileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("character varying(260)"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedBy") + .HasColumnType("integer"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedBy") + .HasColumnType("integer"); + + b.HasKey("EmployeeDocumentId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ExpiryDate"); + + b.HasIndex("HrDocumentTypeId"); + + b.ToTable("hr_employee_documents", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.Property("EmployeeLoanId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeLoanId")); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("InstallmentAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("InterestRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)"); + + b.Property("LoanKind") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("NumberOfInstallments") + .HasColumnType("integer"); + + b.Property("OutstandingBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PrincipalAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartMonth") + .HasColumnType("integer"); + + b.Property("StartYear") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("EmployeeLoanId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("EmployeeId"); + + b.HasIndex("Status"); + + b.ToTable("hr_employee_loans", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.Property("EmployeeSalaryStructureId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeSalaryStructureId")); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("integer"); + + b.Property("BasicSalary") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("EmployeeSalaryStructureId"); + + b.HasIndex("EmployeeId", "EffectiveTo"); + + b.ToTable("hr_employee_salary_structures", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructureLine", b => + { + b.Property("EmployeeSalaryStructureLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeSalaryStructureLineId")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeSalaryStructureId") + .HasColumnType("integer"); + + b.Property("SalaryComponentId") + .HasColumnType("integer"); + + b.HasKey("EmployeeSalaryStructureLineId"); + + b.HasIndex("EmployeeSalaryStructureId"); + + b.HasIndex("SalaryComponentId"); + + b.ToTable("hr_employee_salary_structure_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmploymentType", b => + { + b.Property("EmploymentTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmploymentTypeId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("EmploymentTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_employment_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.Property("GrnId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnId")); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PoId") + .HasColumnType("integer"); + + b.Property("PostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("GrnId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("PoId"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("grns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.Property("GrnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnLineId")); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("BinId") + .HasColumnType("integer"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("GrnId") + .HasColumnType("integer"); + + b.Property("HoldStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("NetUnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("PoLineId") + .HasColumnType("integer"); + + b.Property("PoUnitPrice") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReceivedValue") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("VatAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("VatPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.HasKey("GrnLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("GrnId"); + + b.HasIndex("ItemId"); + + b.HasIndex("PoLineId"); + + b.ToTable("grn_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLineWarrantyNumber", b => + { + b.Property("GrnLineWarrantyNumberId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnLineWarrantyNumberId")); + + b.Property("GrnLineId") + .HasColumnType("integer"); + + b.Property("WarrantyNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("WarrantyPeriodMonths") + .HasColumnType("integer"); + + b.HasKey("GrnLineWarrantyNumberId"); + + b.HasIndex("GrnLineId", "WarrantyNo") + .IsUnique(); + + b.ToTable("grn_line_warranty_numbers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnPayment", b => + { + b.Property("GrnPaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnPaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GrnId") + .HasColumnType("integer"); + + b.Property("PaymentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("GrnPaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("GrnId"); + + b.ToTable("grn_payments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.HrDocumentType", b => + { + b.Property("HrDocumentTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HrDocumentTypeId")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiryTracked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiredAtOnboarding") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("HrDocumentTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_document_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => + { + b.Property("ItemId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ItemId")); + + b.Property("BaseUomId") + .HasColumnType("integer"); + + b.Property("BrandId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ContentBaseQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ContentBaseUnit") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ContentQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ContentUnit") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultVendorId") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SellingFlatPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SellingMaxPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SellingMinPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("StockNature") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubCategoryId") + .HasColumnType("integer"); + + b.Property("TaxClass") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TrackingMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Warranty") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("NonWarranty"); + + b.Property("WarrantyPeriodMonths") + .HasColumnType("integer"); + + b.HasKey("ItemId"); + + b.HasIndex("BaseUomId"); + + b.HasIndex("BrandId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DefaultVendorId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("SubCategoryId"); + + b.ToTable("items", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b => + { + b.Property("ReorderId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReorderId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("ReorderPoint") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReorderQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("ReorderId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ItemId", "WarehouseId") + .IsUnique(); + + b.ToTable("item_reorders", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemType", b => + { + b.Property("ItemTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ItemTypeId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsMeasurable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ItemTypeId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("item_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.JournalEntryStub", b => + { + b.Property("JournalId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("JournalId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CreditAccount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("DebitAccount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SourceDocId") + .HasColumnType("integer"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("JournalId"); + + b.HasIndex("SourceDocType", "SourceDocId"); + + b.ToTable("journal_entry_stubs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveBalance", b => + { + b.Property("LeaveBalanceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LeaveBalanceId")); + + b.Property("AdjustmentDays") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("CarriedForwardDays") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("EntitledDays") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("LeaveTypeId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TakenDays") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("LeaveBalanceId"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("EmployeeId", "LeaveTypeId", "Year") + .IsUnique(); + + b.ToTable("hr_leave_balances", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveRequest", b => + { + b.Property("LeaveRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LeaveRequestId")); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DaysCount") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LeaveTypeId") + .HasColumnType("integer"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RejectionReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("LeaveRequestId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("EmployeeId"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("Status"); + + b.HasIndex("StartDate", "EndDate"); + + b.ToTable("hr_leave_requests", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveType", b => + { + b.Property("LeaveTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LeaveTypeId")); + + b.Property("AccrualPerYear") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("CarryForwardAllowed") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CountsAsNoPay") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MaxCarryForwardDays") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiresApproval") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("LeaveTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_leave_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LoanInstallment", b => + { + b.Property("LoanInstallmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LoanInstallmentId")); + + b.Property("DueMonth") + .HasColumnType("integer"); + + b.Property("DueYear") + .HasColumnType("integer"); + + b.Property("EmployeeLoanId") + .HasColumnType("integer"); + + b.Property("InstallmentNumber") + .HasColumnType("integer"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PayrollRunId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ScheduledAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("LoanInstallmentId"); + + b.HasIndex("EmployeeLoanId"); + + b.HasIndex("PayrollRunId"); + + b.HasIndex("DueYear", "DueMonth"); + + b.ToTable("hr_loan_installments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NavItem", b => + { + b.Property("NavItemId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NavItemId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Href") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.HasKey("NavItemId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("nav_items", (string)null); + + b.HasData( + new + { + NavItemId = 1, + Code = "dashboard", + Href = "/dashboard", + Label = "Dashboard", + SortOrder = 1, + Status = "Active" + }, + new + { + NavItemId = 2, + Code = "products", + Href = "/dashboard/products", + Label = "Products", + SortOrder = 2, + Status = "Active" + }, + new + { + NavItemId = 3, + Code = "vendors", + Href = "/dashboard/vendors", + Label = "Vendors", + SortOrder = 3, + Status = "Active" + }, + new + { + NavItemId = 4, + Code = "procurement", + Href = "/dashboard/procurement", + Label = "Procurement", + SortOrder = 4, + Status = "Active" + }, + new + { + NavItemId = 5, + Code = "receiving", + Href = "/dashboard/receiving/grn", + Label = "Receiving", + SortOrder = 5, + Status = "Active" + }, + new + { + NavItemId = 6, + Code = "stock", + Href = "/dashboard/stock", + Label = "Stock", + SortOrder = 6, + Status = "Active" + }, + new + { + NavItemId = 7, + Code = "warehouses", + Href = "/dashboard/warehouse", + Label = "Warehouses", + SortOrder = 7, + Status = "Active" + }, + new + { + NavItemId = 8, + Code = "orders", + Href = "/dashboard/orders", + Label = "Orders", + SortOrder = 8, + Status = "Active" + }, + new + { + NavItemId = 9, + Code = "settings", + Href = "/dashboard/settings", + Label = "Settings", + SortOrder = 9, + Status = "Active" + }, + new + { + NavItemId = 10, + Code = "help", + Href = "/dashboard/help", + Label = "Help", + SortOrder = 10, + Status = "Active" + }, + new + { + NavItemId = 11, + Code = "ledgers", + Href = "/dashboard/ledgers", + Label = "Ledgers", + SortOrder = 11, + Status = "Active" + }, + new + { + NavItemId = 12, + Code = "accounts", + Href = "/dashboard/accounts", + Label = "Accounts", + SortOrder = 12, + Status = "Active" + }, + new + { + NavItemId = 13, + Code = "sales", + Href = "/dashboard/sales", + Label = "Sales", + SortOrder = 13, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NumberSequence", b => + { + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceId")); + + b.Property("DocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("doc_type"); + + b.Property("LastNumber") + .HasColumnType("integer") + .HasColumnName("last_number"); + + b.Property("Year") + .HasColumnType("integer") + .HasColumnName("year"); + + b.HasKey("SequenceId"); + + b.HasIndex("DocType", "Year") + .IsUnique(); + + b.ToTable("number_sequences", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLine", b => + { + b.Property("PayrollLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PayrollLineId")); + + b.Property("AbsentDays") + .HasColumnType("integer"); + + b.Property("BasicSalary") + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("EpfEmployeeAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("EpfEmployerAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("EtfEmployerAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("GrossSalary") + .HasColumnType("numeric(18,2)"); + + b.Property("LateDeductionAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("LateMinutesTotal") + .HasColumnType("integer"); + + b.Property("LeaveDays") + .HasColumnType("integer"); + + b.Property("LoanDeductionAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("NetSalary") + .HasColumnType("numeric(18,2)"); + + b.Property("NoPayAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("OtMinutesTotal") + .HasColumnType("integer"); + + b.Property("OtherDeductionsAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("OvertimeAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("PayrollRunId") + .HasColumnType("integer"); + + b.Property("PresentDays") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TaxAmount") + .HasColumnType("numeric(18,2)"); + + b.Property("TotalAllowances") + .HasColumnType("numeric(18,2)"); + + b.Property("WorkingDays") + .HasColumnType("integer"); + + b.HasKey("PayrollLineId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("PayrollRunId", "EmployeeId") + .IsUnique(); + + b.ToTable("hr_payroll_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLineComponent", b => + { + b.Property("PayrollLineComponentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PayrollLineComponentId")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ComponentCategory") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PayrollLineId") + .HasColumnType("integer"); + + b.Property("SalaryComponentId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("PayrollLineComponentId"); + + b.HasIndex("PayrollLineId"); + + b.HasIndex("SalaryComponentId"); + + b.ToTable("hr_payroll_line_components", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollRun", b => + { + b.Property("PayrollRunId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PayrollRunId")); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("integer"); + + b.Property("BranchId") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .HasColumnType("integer"); + + b.Property("LockedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockedBy") + .HasColumnType("integer"); + + b.Property("PeriodMonth") + .HasColumnType("integer"); + + b.Property("PeriodYear") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UnlockReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UnlockedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UnlockedBy") + .HasColumnType("integer"); + + b.HasKey("PayrollRunId"); + + b.HasIndex("BranchId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("PeriodYear", "PeriodMonth", "BranchId"); + + b.ToTable("hr_payroll_runs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollStatutorySetting", b => + { + b.Property("PayrollStatutorySettingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PayrollStatutorySettingId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("EpfEmployeeRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)"); + + b.Property("EpfEmployerRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)"); + + b.Property("EtfEmployerRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)"); + + b.Property("OtMultiplierDefault") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("PayrollStatutorySettingId"); + + b.HasIndex("EffectiveFrom"); + + b.ToTable("hr_payroll_statutory_settings", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Payslip", b => + { + b.Property("PayslipId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PayslipId")); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PayrollLineId") + .HasColumnType("integer"); + + b.Property("ReleasedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReleasedBy") + .HasColumnType("integer"); + + b.HasKey("PayslipId"); + + b.HasIndex("PayrollLineId") + .IsUnique(); + + b.ToTable("hr_payslips", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PermissionId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("NavItemId") + .HasColumnType("integer"); + + b.Property("SubNavItemId") + .HasColumnType("integer"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("NavItemId"); + + b.HasIndex("SubNavItemId"); + + b.ToTable("permissions", (string)null); + + b.HasData( + new + { + PermissionId = 1, + Code = "NAV:dashboard", + NavItemId = 1 + }, + new + { + PermissionId = 2, + Code = "NAV:products", + NavItemId = 2 + }, + new + { + PermissionId = 3, + Code = "NAV:vendors", + NavItemId = 3 + }, + new + { + PermissionId = 4, + Code = "NAV:procurement", + NavItemId = 4 + }, + new + { + PermissionId = 5, + Code = "NAV:receiving", + NavItemId = 5 + }, + new + { + PermissionId = 6, + Code = "NAV:stock", + NavItemId = 6 + }, + new + { + PermissionId = 7, + Code = "NAV:warehouses", + NavItemId = 7 + }, + new + { + PermissionId = 8, + Code = "NAV:orders", + NavItemId = 8 + }, + new + { + PermissionId = 9, + Code = "NAV:settings", + NavItemId = 9 + }, + new + { + PermissionId = 10, + Code = "NAV:help", + NavItemId = 10 + }, + new + { + PermissionId = 11, + Code = "NAV:products.item", + SubNavItemId = 1 + }, + new + { + PermissionId = 12, + Code = "NAV:products.category", + SubNavItemId = 2 + }, + new + { + PermissionId = 13, + Code = "NAV:products.brand", + SubNavItemId = 3 + }, + new + { + PermissionId = 14, + Code = "NAV:products.item-type", + SubNavItemId = 4 + }, + new + { + PermissionId = 15, + Code = "NAV:products.uom", + SubNavItemId = 5 + }, + new + { + PermissionId = 16, + Code = "NAV:products.configuration", + SubNavItemId = 6 + }, + new + { + PermissionId = 17, + Code = "NAV:settings.roles", + SubNavItemId = 7 + }, + new + { + PermissionId = 18, + Code = "NAV:settings.users", + SubNavItemId = 8 + }, + new + { + PermissionId = 28, + Code = "NAV:procurement.requisitions", + SubNavItemId = 17 + }, + new + { + PermissionId = 29, + Code = "NAV:procurement.rfqs", + SubNavItemId = 18 + }, + new + { + PermissionId = 30, + Code = "NAV:procurement.purchase-orders", + SubNavItemId = 19 + }, + new + { + PermissionId = 31, + Code = "NAV:procurement.purchase-returns", + SubNavItemId = 20 + }, + new + { + PermissionId = 19, + Code = "NAV:ledgers", + NavItemId = 11 + }, + new + { + PermissionId = 20, + Code = "NAV:ledgers.trial-balance", + SubNavItemId = 9 + }, + new + { + PermissionId = 21, + Code = "NAV:ledgers.balance-sheet", + SubNavItemId = 10 + }, + new + { + PermissionId = 22, + Code = "NAV:ledgers.general-ledger", + SubNavItemId = 11 + }, + new + { + PermissionId = 23, + Code = "NAV:ledgers.profit-and-loss", + SubNavItemId = 12 + }, + new + { + PermissionId = 24, + Code = "NAV:ledgers.cash-flow", + SubNavItemId = 13 + }, + new + { + PermissionId = 25, + Code = "NAV:ledgers.budget-vs-actual", + SubNavItemId = 14 + }, + new + { + PermissionId = 27, + Code = "NAV:ledgers.tax-report", + SubNavItemId = 16 + }, + new + { + PermissionId = 26, + Code = "NAV:accounts.bank-accounts", + SubNavItemId = 15 + }, + new + { + PermissionId = 32, + Code = "NAV:accounts", + NavItemId = 12 + }, + new + { + PermissionId = 33, + Code = "NAV:accounts.cheque-books", + SubNavItemId = 21 + }, + new + { + PermissionId = 34, + Code = "NAV:accounts.received-cheques", + SubNavItemId = 22 + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b => + { + b.Property("PoLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PoLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("PoId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyReceived") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Tax") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("PoLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("PoId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("po_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductConfig", b => + { + b.Property("ConfigId") + .HasColumnType("integer"); + + b.Property("BrandsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ItemTypesEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SubcategoriesEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("integer"); + + b.HasKey("ConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("product_config", null, t => + { + t.HasCheckConstraint("ck_product_config_singleton", "\"ConfigId\" = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductionRun", b => + { + b.Property("RunId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunId")); + + b.Property("CancelReasonCodeId") + .HasColumnType("integer"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OutputBinId") + .HasColumnType("integer"); + + b.Property("ReworkCount") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ScaleFactor") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TargetQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TemplateId") + .HasColumnType("integer"); + + b.Property("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("TemplateId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TemplateId")); + + b.Property("Annotations") + .HasColumnType("jsonb"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("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("PoId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PoId")); + + b.Property("ApprovalRequired") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RequisitionId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.HasKey("PoId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequisitionId"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.ToTable("purchase_orders", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.Property("ReturnId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReasonCodeId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("ReturnId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("VendorId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("purchase_returns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturnLine", b => + { + b.Property("ReturnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnLineId")); + + b.Property("GrnLineId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReturnId") + .HasColumnType("integer"); + + b.HasKey("ReturnLineId"); + + b.HasIndex("GrnLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReturnId"); + + b.ToTable("purchase_return_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ReasonCode", b => + { + b.Property("ReasonCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReasonCodeId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Context") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("ReasonCodeId"); + + b.HasIndex("Context", "Code") + .IsUnique(); + + b.ToTable("reason_codes", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.Property("RequisitionId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RequisitionId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RequestedBy") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("RequisitionId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequestedBy"); + + b.HasIndex("Status"); + + b.ToTable("requisitions", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b => + { + b.Property("ReqLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReqLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RequiredBy") + .HasColumnType("date"); + + b.Property("RequisitionId") + .HasColumnType("integer"); + + b.HasKey("ReqLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RequisitionId"); + + b.ToTable("requisition_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.Property("RfqId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RfqId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RequisitionId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("RfqId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequisitionId"); + + b.ToTable("rfqs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b => + { + b.Property("RfqLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RfqLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RfqId") + .HasColumnType("integer"); + + b.HasKey("RfqLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RfqId"); + + b.ToTable("rfq_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Role", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RoleId")); + + b.Property("AuthRoleId") + .HasColumnType("uuid") + .HasColumnName("auth_role_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsSystemRole") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("AuthRoleId") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("roles", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("PermissionId") + .HasColumnType("integer"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.ToTable("role_permissions", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunEdge", b => + { + b.Property("RunEdgeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunEdgeId")); + + b.Property("ChildRunStageId") + .HasColumnType("integer"); + + b.Property("ParentRunStageId") + .HasColumnType("integer"); + + b.Property("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("RunStageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunStageId")); + + b.Property("ActualEndAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ActualStartAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedMinutes") + .HasColumnType("integer"); + + b.Property("FieldDefs") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("FieldValues") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("PosX") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PosY") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RoleLabel") + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("RunId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("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("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EventId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("RunId") + .HasColumnType("integer"); + + b.Property("RunStageId") + .HasColumnType("integer"); + + b.Property("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("RunInputId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunInputId")); + + b.Property("ConsumedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ConsumedValue") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DeliveredQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("FromRunOutputId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("PlannedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ReturnedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReturnedValue") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RunStageId") + .HasColumnType("integer"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("RunInputId"); + + b.HasIndex("FromRunOutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RunStageId"); + + b.ToTable("run_stage_inputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageOutput", b => + { + b.Property("RunOutputId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunOutputId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("PlannedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ProducedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RunStageId") + .HasColumnType("integer"); + + b.Property("ScrapReasonCodeId") + .HasColumnType("integer"); + + b.Property("ScrappedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TransferredQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("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("SalaryComponentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalaryComponentId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ComponentType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEpfEtfApplicable") + .HasColumnType("boolean"); + + b.Property("IsTaxable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("SalaryComponentId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_salary_components", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.Property("SalesDayEndId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesDayEndId")); + + b.Property("BundleCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("BusinessDate") + .HasColumnType("date"); + + b.Property("CashierUserId") + .HasColumnType("integer"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedBy") + .HasColumnType("integer"); + + b.Property("CostOfGoodsSold") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SlipCount") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.HasKey("SalesDayEndId"); + + b.HasIndex("ClosedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("CashierUserId", "BusinessDate") + .IsUnique(); + + b.ToTable("sales_day_ends", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.Property("SalesInvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesInvoiceId")); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("CreatorUserId") + .HasColumnType("integer"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CustomerSnapshotTaxNo") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("InvoiceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("B2C"); + + b.Property("NetPayable") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RoundOff") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("SalesInvoiceId"); + + b.HasIndex("CreatorUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("InvoiceDate"); + + b.HasIndex("InvoiceNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_invoices", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLine", b => + { + b.Property("SalesInvoiceLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesInvoiceLineId")); + + b.Property("BaseCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DiscountAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DiscountMode") + .HasColumnType("integer"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("FreeQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("IsFreeIssue") + .HasColumnType("boolean"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("NetUnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("integer"); + + b.Property("PriceSource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SalesInvoiceId") + .HasColumnType("integer"); + + b.Property("TaxAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("SalesInvoiceLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SalesInvoiceId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_invoice_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoicePayment", b => + { + b.Property("SalesInvoicePaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesInvoicePaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PaymentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SalesInvoiceId") + .HasColumnType("integer"); + + b.HasKey("SalesInvoicePaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("SalesInvoiceId"); + + b.ToTable("sales_invoice_payments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.Property("ReturnId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReasonCodeId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("ReturnId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_returns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLine", b => + { + b.Property("ReturnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReturnId") + .HasColumnType("integer"); + + b.Property("SalesInvoiceLineId") + .HasColumnType("integer"); + + b.HasKey("ReturnLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReturnId"); + + b.HasIndex("SalesInvoiceLineId"); + + b.ToTable("sales_return_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.Property("SalesSlipId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesSlipId")); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CashierUserId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DayEndId") + .HasColumnType("integer"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SlipDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SlipNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("SalesSlipId"); + + b.HasIndex("CashierUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DayEndId"); + + b.HasIndex("SlipDate"); + + b.HasIndex("SlipNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_slips", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLine", b => + { + b.Property("SalesSlipLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesSlipLineId")); + + b.Property("BaseCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DiscountAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DiscountMode") + .HasColumnType("integer"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("FreeQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("IsFreeIssue") + .HasColumnType("boolean"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("NetUnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("integer"); + + b.Property("PriceSource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SalesSlipId") + .HasColumnType("integer"); + + b.Property("TaxAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("SalesSlipLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SalesSlipId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_slip_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b => + { + b.Property("SerialId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SerialId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("SerialNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("SerialId"); + + b.HasIndex("ItemId", "SerialNo") + .IsUnique(); + + b.ToTable("serials", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageEdge", b => + { + b.Property("EdgeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EdgeId")); + + b.Property("ChildStageId") + .HasColumnType("integer"); + + b.Property("ParentStageId") + .HasColumnType("integer"); + + b.Property("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("InputId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InputId")); + + b.Property("FromOutputId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("QtyPerBatch") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StageId") + .HasColumnType("integer"); + + b.HasKey("InputId"); + + b.HasIndex("FromOutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("StageId"); + + b.ToTable("stage_inputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageOutput", b => + { + b.Property("OutputId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("OutputId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("QtyPerBatch") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("StageId") + .HasColumnType("integer"); + + b.Property("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("AdjustmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AdjustmentId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReasonCodeId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("AdjustmentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("stock_adjustments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustmentLine", b => + { + b.Property("AdjLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AdjLineId")); + + b.Property("AdjustmentId") + .HasColumnType("integer"); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("BinId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("QtyDelta") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SerialId") + .HasColumnType("integer"); + + b.HasKey("AdjLineId"); + + b.HasIndex("AdjustmentId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SerialId"); + + b.ToTable("stock_adjustment_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.Property("CountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CountId")); + + b.Property("CountType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("CountId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("stock_counts", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCountLine", b => + { + b.Property("CountLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CountLineId")); + + b.Property("BinId") + .HasColumnType("integer"); + + b.Property("CountId") + .HasColumnType("integer"); + + b.Property("CountedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("SystemQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Variance") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.HasKey("CountLineId"); + + b.HasIndex("BinId"); + + b.HasIndex("CountId"); + + b.HasIndex("ItemId"); + + b.ToTable("stock_count_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLayer", b => + { + b.Property("LayerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LayerId")); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("GrnLineId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("QtyReceived") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyRemaining") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReceiptDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SerialId") + .HasColumnType("integer"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("LayerId"); + + b.HasIndex("BatchId"); + + b.HasIndex("GrnLineId"); + + b.HasIndex("SerialId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ItemId", "WarehouseId", "ReceiptDate", "LayerId"); + + b.ToTable("stock_layers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLedger", b => + { + b.Property("LedgerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LedgerId")); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("BinId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("QtyBase") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RunningBalance") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SerialId") + .HasColumnType("integer"); + + b.Property("SourceDocId") + .HasColumnType("integer"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("LedgerId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("SerialId"); + + b.HasIndex("UserId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("SourceDocType", "SourceDocId"); + + b.HasIndex("ItemId", "WarehouseId", "CreatedAt"); + + b.HasIndex("ItemId", "WarehouseId", "LedgerId"); + + b.ToTable("stock_ledger", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + b.Property("TransferId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TransferId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DestWarehouseId") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SrcWarehouseId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("TransferId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DestWarehouseId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("SrcWarehouseId"); + + b.HasIndex("Status"); + + b.ToTable("stock_transfers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLine", b => + { + b.Property("TransferLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TransferLineId")); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("DestBinId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyReceived") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SerialId") + .HasColumnType("integer"); + + b.Property("SrcBinId") + .HasColumnType("integer"); + + b.Property("TransferId") + .HasColumnType("integer"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.HasKey("TransferLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("DestBinId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SerialId"); + + b.HasIndex("SrcBinId"); + + b.HasIndex("TransferId"); + + b.ToTable("stock_transfer_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubCategory", b => + { + b.Property("SubCategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SubCategoryId")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("SubCategoryId"); + + b.HasIndex("Status"); + + b.HasIndex("CategoryId", "Name") + .IsUnique(); + + b.ToTable("subcategories", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubNavItem", b => + { + b.Property("SubNavItemId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SubNavItemId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Href") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NavItemId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.HasKey("SubNavItemId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("NavItemId"); + + b.ToTable("sub_nav_items", (string)null); + + b.HasData( + new + { + SubNavItemId = 1, + Code = "products.item", + Href = "/dashboard/products", + Label = "Item", + NavItemId = 2, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 2, + Code = "products.category", + Href = "/dashboard/products/categories", + Label = "Category", + NavItemId = 2, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 3, + Code = "products.brand", + Href = "/dashboard/products/brands", + Label = "Brand", + NavItemId = 2, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 4, + Code = "products.item-type", + Href = "/dashboard/products/item-types", + Label = "Item Type", + NavItemId = 2, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 5, + Code = "products.uom", + Href = "/dashboard/products/uoms", + Label = "UOM", + NavItemId = 2, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 6, + Code = "products.configuration", + Href = "/dashboard/products/settings", + Label = "Configuration", + NavItemId = 2, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 7, + Code = "settings.roles", + Href = "/dashboard/settings/roles", + Label = "Roles", + NavItemId = 9, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 8, + Code = "settings.users", + Href = "/dashboard/settings/users", + Label = "Users", + NavItemId = 9, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 23, + Code = "sales.bundle-sales", + Href = "/dashboard/sales/bundles", + Label = "Bundle Sales", + NavItemId = 13, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 17, + Code = "procurement.requisitions", + Href = "/dashboard/procurement/requisitions", + Label = "Requisitions", + NavItemId = 4, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 18, + Code = "procurement.rfqs", + Href = "/dashboard/procurement/rfqs", + Label = "RFQs", + NavItemId = 4, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 19, + Code = "procurement.purchase-orders", + Href = "/dashboard/procurement/purchase-orders", + Label = "Purchase Orders", + NavItemId = 4, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 20, + Code = "procurement.purchase-returns", + Href = "/dashboard/procurement/purchase-returns", + Label = "Purchase Returns", + NavItemId = 4, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 9, + Code = "ledgers.trial-balance", + Href = "/dashboard/ledgers/trial-balance", + Label = "Trial Balance", + NavItemId = 11, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 10, + Code = "ledgers.balance-sheet", + Href = "/dashboard/ledgers/balance-sheet", + Label = "Balance Sheet", + NavItemId = 11, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 11, + Code = "ledgers.general-ledger", + Href = "/dashboard/ledgers/general-ledger", + Label = "General Ledger", + NavItemId = 11, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 12, + Code = "ledgers.profit-and-loss", + Href = "/dashboard/ledgers/profit-and-loss", + Label = "Profit & Loss", + NavItemId = 11, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 13, + Code = "ledgers.cash-flow", + Href = "/dashboard/ledgers/cash-flow", + Label = "Cash Flow", + NavItemId = 11, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 14, + Code = "ledgers.budget-vs-actual", + Href = "/dashboard/ledgers/budget-vs-actual", + Label = "Budget vs Actual", + NavItemId = 11, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 16, + Code = "ledgers.tax-report", + Href = "/dashboard/ledgers/tax-report", + Label = "Tax Report", + NavItemId = 11, + SortOrder = 7, + Status = "Active" + }, + new + { + SubNavItemId = 15, + Code = "accounts.bank-accounts", + Href = "/dashboard/accounts/bank-accounts", + Label = "Cash / Bank Accounts", + NavItemId = 12, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 21, + Code = "accounts.cheque-books", + Href = "/dashboard/accounts/cheque-books", + Label = "Cheque Books", + NavItemId = 12, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 22, + Code = "accounts.received-cheques", + Href = "/dashboard/accounts/received-cheques", + Label = "Received Cheques", + NavItemId = 12, + SortOrder = 3, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TaxSlab", b => + { + b.Property("TaxSlabId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TaxSlabId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("LowerBound") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Rate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpperBound") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("TaxSlabId"); + + b.HasIndex("EffectiveFrom"); + + b.ToTable("hr_tax_slabs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b => + { + b.Property("StageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StageId")); + + b.Property("EstimatedMinutes") + .HasColumnType("integer"); + + b.Property("FieldDefs") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("PosX") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PosY") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RoleLabel") + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("TemplateId") + .HasColumnType("integer"); + + b.HasKey("StageId"); + + b.HasIndex("TemplateId"); + + b.ToTable("template_stages", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b => + { + b.Property("UomId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UomId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("UomId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("uoms", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId")); + + b.Property("AuthUserId") + .HasColumnType("uuid") + .HasColumnName("auth_user_id"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("UserId"); + + b.HasIndex("AuthUserId") + .IsUnique(); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users", (string)null); + + b.HasData( + new + { + UserId = 1, + DisplayName = "System", + Status = "Active", + Username = "system" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b => + { + b.Property("VendorId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("VendorId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasDefaultValue("LKR"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("TaxReg") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Terms") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("VendorId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("vendors", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b => + { + b.Property("QuotationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("QuotationId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RfqId") + .HasColumnType("integer"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.HasKey("QuotationId"); + + b.HasIndex("VendorId"); + + b.HasIndex("RfqId", "VendorId") + .IsUnique(); + + b.ToTable("vendor_quotations", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotationLine", b => + { + b.Property("QuotationLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("QuotationLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LeadDays") + .HasColumnType("integer"); + + b.Property("QuotationId") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.HasKey("QuotationLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("QuotationId"); + + b.ToTable("vendor_quotation_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b => + { + b.Property("WarehouseId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WarehouseId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("WarehouseId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("warehouses", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.WorkShift", b => + { + b.Property("WorkShiftId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WorkShiftId")); + + b.Property("BreakMinutes") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("interval"); + + b.Property("GraceMinutes") + .HasColumnType("integer"); + + b.Property("IsOvernight") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OtMultiplier") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StandardWorkingMinutes") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("interval"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkingDaysMask") + .HasColumnType("integer"); + + b.HasKey("WorkShiftId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_work_shifts", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceRecord", b => + { + b.HasOne("ERPCore.Domain.Entities.AttendanceUploadBatch", "AttendanceUploadBatch") + .WithMany() + .HasForeignKey("AttendanceUploadBatchId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.WorkShift", "WorkShift") + .WithMany() + .HasForeignKey("WorkShiftId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AttendanceUploadBatch"); + + b.Navigation("Employee"); + + b.Navigation("WorkShift"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AuditLog", b => + { + b.HasOne("ERPCore.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b => + { + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany("Bins") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSaleTemplate", "BundleSaleTemplate") + .WithMany() + .HasForeignKey("BundleSaleTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesDayEnd", "DayEnd") + .WithMany("BundleSales") + .HasForeignKey("DayEndId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSaleTemplate"); + + b.Navigation("CashierUser"); + + b.Navigation("Customer"); + + b.Navigation("DayEnd"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleLine", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSale", "BundleSale") + .WithMany("Lines") + .HasForeignKey("BundleSaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSale"); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplateLine", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSaleTemplate", "BundleSaleTemplate") + .WithMany("Lines") + .HasForeignKey("BundleSaleTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSaleTemplate"); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Customer", b => + { + b.HasOne("ERPCore.Domain.Entities.Warehouse", "DefaultWarehouse") + .WithMany() + .HasForeignKey("DefaultWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DefaultWarehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Department", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Employee", "HeadEmployee") + .WithMany() + .HasForeignKey("HeadEmployeeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Department", "ParentDepartment") + .WithMany() + .HasForeignKey("ParentDepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Branch"); + + b.Navigation("HeadEmployee"); + + b.Navigation("ParentDepartment"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Employee", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Designation", "Designation") + .WithMany() + .HasForeignKey("DesignationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.EmploymentType", "EmploymentType") + .WithMany() + .HasForeignKey("EmploymentTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Employee", "ReportingManager") + .WithMany() + .HasForeignKey("ReportingManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("ERPCore.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.WorkShift", "WorkShift") + .WithMany() + .HasForeignKey("WorkShiftId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Department"); + + b.Navigation("Designation"); + + b.Navigation("EmploymentType"); + + b.Navigation("ReportingManager"); + + b.Navigation("User"); + + b.Navigation("WorkShift"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeBankDetail", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.HrDocumentType", "HrDocumentType") + .WithMany() + .HasForeignKey("HrDocumentTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("HrDocumentType"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructureLine", b => + { + b.HasOne("ERPCore.Domain.Entities.EmployeeSalaryStructure", "EmployeeSalaryStructure") + .WithMany("Lines") + .HasForeignKey("EmployeeSalaryStructureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalaryComponent", "SalaryComponent") + .WithMany() + .HasForeignKey("SalaryComponentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EmployeeSalaryStructure"); + + b.Navigation("SalaryComponent"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("PoId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Vendor"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", "Bin") + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Grn", "Grn") + .WithMany("Lines") + .HasForeignKey("GrnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PoLine", "PoLine") + .WithMany() + .HasForeignKey("PoLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Batch"); + + b.Navigation("Bin"); + + b.Navigation("Grn"); + + b.Navigation("Item"); + + b.Navigation("PoLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLineWarrantyNumber", b => + { + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany("WarrantyNumbers") + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GrnLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnPayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Grn", "Grn") + .WithMany("Payments") + .HasForeignKey("GrnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Grn"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => + { + b.HasOne("ERPCore.Domain.Entities.Uom", "BaseUom") + .WithMany() + .HasForeignKey("BaseUomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Brand", "Brand") + .WithMany() + .HasForeignKey("BrandId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "DefaultVendor") + .WithMany() + .HasForeignKey("DefaultVendorId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.SubCategory", "SubCategory") + .WithMany() + .HasForeignKey("SubCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("BaseUom"); + + b.Navigation("Brand"); + + b.Navigation("Category"); + + b.Navigation("DefaultVendor"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany("ReorderSettings") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveBalance", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.LeaveType", "LeaveType") + .WithMany() + .HasForeignKey("LeaveTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("LeaveType"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveRequest", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.LeaveType", "LeaveType") + .WithMany() + .HasForeignKey("LeaveTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("LeaveType"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LoanInstallment", b => + { + b.HasOne("ERPCore.Domain.Entities.EmployeeLoan", "EmployeeLoan") + .WithMany("Installments") + .HasForeignKey("EmployeeLoanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PayrollRun", "PayrollRun") + .WithMany() + .HasForeignKey("PayrollRunId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("EmployeeLoan"); + + b.Navigation("PayrollRun"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PayrollRun", "PayrollRun") + .WithMany("Lines") + .HasForeignKey("PayrollRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("PayrollRun"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLineComponent", b => + { + b.HasOne("ERPCore.Domain.Entities.PayrollLine", "PayrollLine") + .WithMany("Components") + .HasForeignKey("PayrollLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalaryComponent", "SalaryComponent") + .WithMany() + .HasForeignKey("SalaryComponentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("PayrollLine"); + + b.Navigation("SalaryComponent"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollRun", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Payslip", b => + { + b.HasOne("ERPCore.Domain.Entities.PayrollLine", "PayrollLine") + .WithOne() + .HasForeignKey("ERPCore.Domain.Entities.Payslip", "PayrollLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PayrollLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Permission", b => + { + b.HasOne("ERPCore.Domain.Entities.NavItem", "NavItem") + .WithMany() + .HasForeignKey("NavItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ERPCore.Domain.Entities.SubNavItem", "SubNavItem") + .WithMany() + .HasForeignKey("SubNavItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("NavItem"); + + b.Navigation("SubNavItem"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany("Lines") + .HasForeignKey("PoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + 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") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany() + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Requisition"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("ReasonCode"); + + b.Navigation("Vendor"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany() + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseReturn", "Return") + .WithMany("Lines") + .HasForeignKey("ReturnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GrnLine"); + + b.Navigation("Item"); + + b.Navigation("Return"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Requester") + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Requester"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany("Lines") + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Requisition"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany() + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Requisition"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq") + .WithMany("Lines") + .HasForeignKey("RfqId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Rfq"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RolePermission", b => + { + b.HasOne("ERPCore.Domain.Entities.Permission", "Permission") + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + 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.Navigation("FromRunOutput"); + + b.Navigation("Item"); + + b.Navigation("RunStage"); + }); + + 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); + + b.Navigation("Item"); + + b.Navigation("RunStage"); + + b.Navigation("ScrapReason"); + + b.Navigation("Uom"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "ClosedByUser") + .WithMany() + .HasForeignKey("ClosedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CashierUser"); + + b.Navigation("ClosedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatorUserId"); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Customer"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoice", "SalesInvoice") + .WithMany("Lines") + .HasForeignKey("SalesInvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SalesInvoice"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoicePayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoice", "SalesInvoice") + .WithMany("Payments") + .HasForeignKey("SalesInvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("SalesInvoice"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Customer"); + + b.Navigation("ReasonCode"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesReturn", "Return") + .WithMany("Lines") + .HasForeignKey("ReturnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoiceLine", "SalesInvoiceLine") + .WithMany() + .HasForeignKey("SalesInvoiceLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("Return"); + + b.Navigation("SalesInvoiceLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesDayEnd", "DayEnd") + .WithMany("Slips") + .HasForeignKey("DayEndId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CashierUser"); + + b.Navigation("Customer"); + + b.Navigation("DayEnd"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesSlip", "SalesSlip") + .WithMany("Lines") + .HasForeignKey("SalesSlipId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SalesSlip"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.Navigation("FromOutput"); + + b.Navigation("Item"); + + b.Navigation("Stage"); + }); + + 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); + + b.Navigation("Item"); + + b.Navigation("Stage"); + + b.Navigation("Uom"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("ReasonCode"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustmentLine", b => + { + b.HasOne("ERPCore.Domain.Entities.StockAdjustment", "Adjustment") + .WithMany("Lines") + .HasForeignKey("AdjustmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", null) + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Adjustment"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCountLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.StockCount", "Count") + .WithMany("Lines") + .HasForeignKey("CountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Count"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLayer", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany() + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Batch"); + + b.Navigation("GrnLine"); + + b.Navigation("Item"); + + b.Navigation("Serial"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLedger", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", null) + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", null) + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", null) + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "DestWarehouse") + .WithMany() + .HasForeignKey("DestWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "SrcWarehouse") + .WithMany() + .HasForeignKey("SrcWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("DestWarehouse"); + + b.Navigation("SrcWarehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("DestBinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", null) + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("SrcBinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.StockTransfer", "Transfer") + .WithMany("Lines") + .HasForeignKey("TransferId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Transfer"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubCategory", b => + { + b.HasOne("ERPCore.Domain.Entities.Category", "Category") + .WithMany("SubCategories") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubNavItem", b => + { + b.HasOne("ERPCore.Domain.Entities.NavItem", "NavItem") + .WithMany("Children") + .HasForeignKey("NavItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.User", b => + { + b.HasOne("ERPCore.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b => + { + b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq") + .WithMany("Quotations") + .HasForeignKey("RfqId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Rfq"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotationLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.VendorQuotation", "Quotation") + .WithMany("Lines") + .HasForeignKey("QuotationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Quotation"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplate", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Category", b => + { + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.Navigation("Installments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.Navigation("Lines"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.Navigation("WarrantyNumbers"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => + { + b.Navigation("ReorderSettings"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NavItem", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLine", b => + { + b.Navigation("Components"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollRun", b => + { + 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"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.Navigation("Lines"); + + b.Navigation("Quotations"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b => + { + b.Navigation("Events"); + + b.Navigation("Inputs"); + + b.Navigation("Outputs"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.Navigation("BundleSales"); + + b.Navigation("Slips"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.Navigation("Lines"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + 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"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b => + { + b.Navigation("Bins"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/ERPCore/Migrations/20260820083810_itemModel.cs b/Backend/ERPCore/Migrations/20260820083810_itemModel.cs new file mode 100644 index 0000000..bf7be6f --- /dev/null +++ b/Backend/ERPCore/Migrations/20260820083810_itemModel.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ERPCore.Migrations +{ + /// + public partial class itemModel : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "SalePrice", + table: "items", + newName: "SellingMinPrice"); + + migrationBuilder.AddColumn( + name: "SellingFlatPrice", + table: "items", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: true); + + migrationBuilder.AddColumn( + name: "SellingMaxPrice", + table: "items", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SellingFlatPrice", + table: "items"); + + migrationBuilder.DropColumn( + name: "SellingMaxPrice", + table: "items"); + + migrationBuilder.RenameColumn( + name: "SellingMinPrice", + table: "items", + newName: "SalePrice"); + } + } +} diff --git a/Backend/ERPCore/Migrations/20260820120000_UpdateItemSellingPrices.cs b/Backend/ERPCore/Migrations/20260820120000_UpdateItemSellingPrices.cs new file mode 100644 index 0000000..9652733 --- /dev/null +++ b/Backend/ERPCore/Migrations/20260820120000_UpdateItemSellingPrices.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ERPCore.Migrations +{ + public partial class UpdateItemSellingPrices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "SalePrice", + table: "items", + newName: "SellingFlatPrice"); + + migrationBuilder.AddColumn( + name: "SellingMinPrice", + table: "items", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: true); + + migrationBuilder.AddColumn( + name: "SellingMaxPrice", + table: "items", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SellingMinPrice", + table: "items"); + + migrationBuilder.DropColumn( + name: "SellingMaxPrice", + table: "items"); + + migrationBuilder.RenameColumn( + name: "SellingFlatPrice", + table: "items", + newName: "SalePrice"); + } + } +} diff --git a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs index 6db3b9c..ae27b61 100644 --- a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs +++ b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs @@ -1745,7 +1745,15 @@ namespace ERPCore.Migrations .HasColumnType("xid") .HasColumnName("xmin"); - b.Property("SalePrice") + b.Property("SellingFlatPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SellingMaxPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SellingMinPrice") .HasPrecision(18, 4) .HasColumnType("numeric(18,4)"); diff --git a/Backend/ERPCore/Services/BundleSaleService.cs b/Backend/ERPCore/Services/BundleSaleService.cs index 113e423..2cdf7eb 100644 --- a/Backend/ERPCore/Services/BundleSaleService.cs +++ b/Backend/ERPCore/Services/BundleSaleService.cs @@ -85,6 +85,45 @@ public sealed class BundleSaleService : IBundleSaleService x.BundleSaleTemplateLineId, x.ItemId, x.WarehouseId, x.Qty, x.UnitPrice, x.IncludeInBundle, x.SortOrder)).ToList()); } + public async Task CreateTemplateAsync(CreateBundleSaleTemplateRequest request, CancellationToken ct = default) + { + var template = new BundleSaleTemplate + { + TemplateCode = await _numbers.NextAsync(DocumentTypes.BundleSaleTemplate, ct), + TemplateName = request.TemplateName, + Description = request.Description, + Status = EntityStatus.Active, + CreatedAt = DateTime.UtcNow + }; + + foreach (var line in request.Lines) + { + if (!await _items.Query().AsNoTracking().AnyAsync(x => x.ItemId == line.ItemId, ct)) + throw new NotFoundException($"Item {line.ItemId} was not found."); + if (!await _warehouses.Query().AsNoTracking().AnyAsync(x => x.WarehouseId == line.WarehouseId, ct)) + throw new NotFoundException($"Warehouse {line.WarehouseId} was not found."); + + template.Lines.Add(new BundleSaleTemplateLine + { + ItemId = line.ItemId, + WarehouseId = line.WarehouseId, + Qty = line.Qty, + UnitPrice = line.UnitPrice, + IncludeInBundle = line.IncludeInBundle, + SortOrder = line.SortOrder + }); + } + + await _templates.AddAsync(template, ct); + await _uow.SaveChangesAsync(ct); + + return new BundleSaleTemplateDto( + template.BundleSaleTemplateId, template.TemplateCode, template.TemplateName, template.Description, + template.Status, template.CreatedAt, template.UpdatedAt, + template.Lines.OrderBy(x => x.SortOrder).Select(x => new BundleSaleTemplateLineDto( + x.BundleSaleTemplateLineId, x.ItemId, x.WarehouseId, x.Qty, x.UnitPrice, x.IncludeInBundle, x.SortOrder)).ToList()); + } + public async Task> ListAsync(PageQuery query, BundleSaleStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default) { IQueryable q = _bundles.Query().AsNoTracking().Include(x => x.Lines); diff --git a/Backend/ERPCore/Services/Interfaces/IBundleSaleService.cs b/Backend/ERPCore/Services/Interfaces/IBundleSaleService.cs index 2753889..bd18330 100644 --- a/Backend/ERPCore/Services/Interfaces/IBundleSaleService.cs +++ b/Backend/ERPCore/Services/Interfaces/IBundleSaleService.cs @@ -9,6 +9,7 @@ public interface IBundleSaleService { Task> ListTemplatesAsync(PageQuery query, CancellationToken ct = default); Task GetTemplateAsync(int bundleSaleTemplateId, CancellationToken ct = default); + Task CreateTemplateAsync(CreateBundleSaleTemplateRequest request, CancellationToken ct = default); Task> ListAsync(PageQuery query, BundleSaleStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default); Task GetAsync(int bundleSaleId, CancellationToken ct = default); Task CheckPostingAsync(int bundleSaleId, CancellationToken ct = default); diff --git a/Backend/ERPCore/Services/ItemService.cs b/Backend/ERPCore/Services/ItemService.cs index 270a250..edae01b 100644 --- a/Backend/ERPCore/Services/ItemService.cs +++ b/Backend/ERPCore/Services/ItemService.cs @@ -85,7 +85,8 @@ public sealed class ItemService : IItemService .Select(i => new ItemListItemDto( i.ItemId, i.Sku, i.Name, i.CategoryId, i.SubCategoryId, i.BrandId, i.BaseUomId, i.DefaultVendorId, - i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass, i.SalePrice, + i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass, + i.SellingMinPrice, i.SellingMaxPrice, i.SellingFlatPrice, i.ContentQty, i.ContentUnit, i.ContentBaseQty, i.ContentBaseUnit, i.Status)) .ToListAsync(ct); @@ -130,7 +131,9 @@ public sealed class ItemService : IItemService Warranty = request.Warranty, WarrantyPeriodMonths = warrantyPeriodMonths, TaxClass = request.TaxClass, - SalePrice = request.SalePrice, + SellingMinPrice = request.SellingMinPrice, + SellingMaxPrice = request.SellingMaxPrice, + SellingFlatPrice = request.SellingFlatPrice, ContentQty = request.ContentQty, ContentUnit = request.ContentUnit, ContentBaseQty = contentBaseQty, @@ -190,7 +193,9 @@ public sealed class ItemService : IItemService item.Warranty = request.Warranty; item.WarrantyPeriodMonths = warrantyPeriodMonths; item.TaxClass = request.TaxClass; - item.SalePrice = request.SalePrice; + item.SellingMinPrice = request.SellingMinPrice; + item.SellingMaxPrice = request.SellingMaxPrice; + item.SellingFlatPrice = request.SellingFlatPrice; item.ContentQty = request.ContentQty; item.ContentUnit = request.ContentUnit; item.ContentBaseQty = contentBaseQty; @@ -364,7 +369,8 @@ public sealed class ItemService : IItemService private static ItemDetailDto ToDetail(Item i) => new( i.ItemId, i.Sku, i.Name, i.Description, i.CategoryId, i.SubCategoryId, i.BrandId, i.BaseUomId, i.DefaultVendorId, - i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass, i.SalePrice, + i.StockNature, i.TrackingMode, i.Warranty, i.WarrantyPeriodMonths, i.TaxClass, + i.SellingMinPrice, i.SellingMaxPrice, i.SellingFlatPrice, i.ContentQty, i.ContentUnit, i.ContentBaseQty, i.ContentBaseUnit, i.Status, i.ReorderSettings diff --git a/Backend/ERPCore/Services/SalesDayEndService.cs b/Backend/ERPCore/Services/SalesDayEndService.cs index 02a1d92..b2112e3 100644 --- a/Backend/ERPCore/Services/SalesDayEndService.cs +++ b/Backend/ERPCore/Services/SalesDayEndService.cs @@ -34,6 +34,7 @@ public sealed class SalesDayEndService : ISalesDayEndService private readonly IRepository _dayEnds; private readonly IRepository _slips; private readonly IRepository _bundles; + private readonly IRepository _invoices; private readonly IRepository _ledger; private readonly IRepository _items; private readonly INumberSequenceService _numbers; @@ -49,12 +50,13 @@ public sealed class SalesDayEndService : ISalesDayEndService public SalesDayEndService( IRepository dayEnds, IRepository slips, IRepository bundles, - IRepository ledger, IRepository items, INumberSequenceService numbers, + IRepository invoices, IRepository ledger, IRepository items, INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow, IGeneralLedgerService gl, IConfiguration configuration) { _dayEnds = dayEnds; _slips = slips; _bundles = bundles; + _invoices = invoices; _ledger = ledger; _items = items; _numbers = numbers; @@ -71,6 +73,8 @@ public sealed class SalesDayEndService : ISalesDayEndService public async Task PreviewAsync(int cashierUserId, DateOnly businessDate, CancellationToken ct = default) { + var invoiceSummary = await LoadInvoiceSummaryAsync(cashierUserId, businessDate, ct); + var existing = await _dayEnds.Query().AsNoTracking() .FirstOrDefaultAsync(x => x.CashierUserId == cashierUserId && x.BusinessDate == businessDate, ct); if (existing is not null) @@ -89,7 +93,7 @@ public sealed class SalesDayEndService : ISalesDayEndService cashierUserId, businessDate, true, existing.SlipCount, existing.BundleCount, existing.Subtotal, existing.DiscountTotal, existing.TaxTotal, existing.GrandTotal, closedSlipNumbers, closedBundleNumbers, - Array.Empty(), Array.Empty()); + Array.Empty(), Array.Empty(), invoiceSummary); } var openSlips = await LoadOpenSlipsAsync(cashierUserId, businessDate, ct); @@ -111,7 +115,8 @@ public sealed class SalesDayEndService : ISalesDayEndService postedSlips.OrderBy(x => x.SlipNo).Select(x => x.SlipNo).ToList(), postedBundles.OrderBy(x => x.BundleNo).Select(x => x.BundleNo).ToList(), draftSlips.OrderBy(x => x.SlipNo).Select(x => new DraftSlipBlockingCloseDto(x.SalesSlipId, x.SlipNo, x.GrandTotal)).ToList(), - draftBundles.OrderBy(x => x.BundleNo).Select(x => new DraftBundleBlockingCloseDto(x.BundleSaleId, x.BundleNo, x.GrandTotal)).ToList()); + draftBundles.OrderBy(x => x.BundleNo).Select(x => new DraftBundleBlockingCloseDto(x.BundleSaleId, x.BundleNo, x.GrandTotal)).ToList(), + invoiceSummary); } public async Task CloseAsync(CreateSalesDayEndRequest request, CancellationToken ct = default) @@ -265,6 +270,25 @@ public sealed class SalesDayEndService : ISalesDayEndService .ToListAsync(ct); } + // Sales Invoices have no CashierUserId (they aren't a cashier document — SalesInvoiceService + // doesn't take one) so CreatedBy is the closest equivalent: whoever was logged in when they + // made the sale. Matched against Posted invoices only, same as the Slip/Bundle totals above. + private async Task LoadInvoiceSummaryAsync(int cashierUserId, DateOnly businessDate, CancellationToken ct) + { + var (dayStart, dayEnd) = DayRangeUtc(businessDate); + var invoices = await _invoices.Query().AsNoTracking() + .Where(x => x.CreatedBy == cashierUserId && x.Status == SalesInvoiceStatus.Posted + && x.InvoiceDate >= dayStart && x.InvoiceDate < dayEnd) + .ToListAsync(ct); + return new SalesDayEndInvoiceSummaryDto( + invoices.Count, + invoices.Sum(x => x.Subtotal), + invoices.Sum(x => x.DiscountTotal), + invoices.Sum(x => x.TaxTotal), + invoices.Sum(x => x.GrandTotal), + invoices.OrderBy(x => x.InvoiceNo).Select(x => x.InvoiceNo).ToList()); + } + // SlipDate/BundleDate are stored as DateTime.UtcNow (Kind=Utc) throughout — Npgsql requires // a matching Kind=Utc here too, or comparisons against the `timestamptz` column throw. private static (DateTime Start, DateTime End) DayRangeUtc(DateOnly businessDate) @@ -310,10 +334,12 @@ public sealed class SalesDayEndService : ISalesDayEndService .OrderByDescending(x => x.Revenue) .ToList(); + var invoiceSummary = await LoadInvoiceSummaryAsync(entity.CashierUserId, entity.BusinessDate, ct); + return new SalesDayEndDto( entity.SalesDayEndId, entity.DocNo, entity.CashierUserId, entity.BusinessDate, entity.SlipCount, entity.BundleCount, entity.Subtotal, entity.DiscountTotal, entity.TaxTotal, entity.GrandTotal, entity.CostOfGoodsSold, entity.GlJournalNo, entity.GlPostedAt, entity.ClosedBy, entity.ClosedAt, - slipNumbers, bundleNumbers, breakdown); + slipNumbers, bundleNumbers, breakdown, invoiceSummary); } } diff --git a/Backend/ERPCore/Services/SalesPricingService.cs b/Backend/ERPCore/Services/SalesPricingService.cs index b8d09b4..578f9a1 100644 --- a/Backend/ERPCore/Services/SalesPricingService.cs +++ b/Backend/ERPCore/Services/SalesPricingService.cs @@ -31,8 +31,8 @@ public sealed class SalesPricingService : ISalesPricingService { if (!allowManualOverride) { - if (item.SalePrice.HasValue) - return new SalesPriceResolution(item.SalePrice.Value, "SALE_PRICE", item.SalePrice.Value); + if (item.SellingFlatPrice.HasValue) + return new SalesPriceResolution(item.SellingFlatPrice.Value, "SALE_PRICE", item.SellingFlatPrice.Value); var grnPrice = await GetWeightedGrnPriceAsync(itemId, warehouseId, ct); if (grnPrice is not null) @@ -44,8 +44,8 @@ public sealed class SalesPricingService : ISalesPricingService return new SalesPriceResolution(requestedUnitPrice.Value, "MANUAL", requestedUnitPrice.Value); } - if (item.SalePrice.HasValue) - return new SalesPriceResolution(item.SalePrice.Value, "SALE_PRICE", item.SalePrice.Value); + if (item.SellingFlatPrice.HasValue) + return new SalesPriceResolution(item.SellingFlatPrice.Value, "SALE_PRICE", item.SellingFlatPrice.Value); var weighted = await GetWeightedGrnPriceAsync(itemId, warehouseId, ct); if (weighted is not null) diff --git a/Backend/ERPCore/Services/SalesPromotionSuggestionService.cs b/Backend/ERPCore/Services/SalesPromotionSuggestionService.cs index d14ffdd..82c9279 100644 --- a/Backend/ERPCore/Services/SalesPromotionSuggestionService.cs +++ b/Backend/ERPCore/Services/SalesPromotionSuggestionService.cs @@ -46,14 +46,14 @@ public sealed class SalesPromotionSuggestionService : ISalesPromotionSuggestionS var rewardOptions = new List { - new(item.ItemId, item.Sku, item.Name, item.SalePrice) + new(item.ItemId, item.Sku, item.Name, item.SellingFlatPrice) }; var alternates = await _items.Query().AsNoTracking() .Where(x => x.Status == EntityStatus.Active && x.CategoryId == item.CategoryId && x.ItemId != item.ItemId) .OrderBy(x => x.Name) .Take(3) - .Select(x => new SalesFreeIssueRewardOptionDto(x.ItemId, x.Sku, x.Name, x.SalePrice)) + .Select(x => new SalesFreeIssueRewardOptionDto(x.ItemId, x.Sku, x.Name, x.SellingFlatPrice)) .ToListAsync(ct); rewardOptions.AddRange(alternates.Where(x => rewardOptions.All(r => r.ItemId != x.ItemId))); diff --git a/Frontend/erp-system/app/dashboard/products/[id]/page.tsx b/Frontend/erp-system/app/dashboard/products/[id]/page.tsx index be58162..147ea68 100644 --- a/Frontend/erp-system/app/dashboard/products/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/products/[id]/page.tsx @@ -58,6 +58,9 @@ export default function ItemDetailPage() { const [warranty, setWarranty] = useState("NonWarranty") const [warrantyPeriodMonths, setWarrantyPeriodMonths] = useState(null) const [taxClass, setTaxClass] = useState("") + const [sellingMinPrice, setSellingMinPrice] = useState("") + const [sellingMaxPrice, setSellingMaxPrice] = useState("") + const [sellingFlatPrice, setSellingFlatPrice] = useState("") // Raw string: an empty box means "no content size", which is not the same as 0. const [contentQty, setContentQty] = useState("") const [contentUnit, setContentUnit] = useState(null) @@ -86,6 +89,9 @@ export default function ItemDetailPage() { setWarranty(data.warranty) setWarrantyPeriodMonths(data.warrantyPeriodMonths) setTaxClass(data.taxClass ?? "") + setSellingMinPrice(data.sellingMinPrice === null ? "" : String(data.sellingMinPrice)) + setSellingMaxPrice(data.sellingMaxPrice === null ? "" : String(data.sellingMaxPrice)) + setSellingFlatPrice(data.sellingFlatPrice === null ? "" : String(data.sellingFlatPrice)) setContentQty(data.contentQty === null ? "" : String(data.contentQty)) setContentUnit(data.contentUnit) } @@ -130,6 +136,9 @@ export default function ItemDetailPage() { sku, name, description: description || null, categoryId: categoryId as number, subCategoryId, brandId, baseUomId: baseUomId as number, defaultVendorId, stockNature, trackingMode, warranty, warrantyPeriodMonths, taxClass: taxClass || null, + sellingMinPrice: sellingMinPrice.trim() ? Number(sellingMinPrice) : null, + sellingMaxPrice: sellingMaxPrice.trim() ? Number(sellingMaxPrice) : null, + sellingFlatPrice: sellingFlatPrice.trim() ? Number(sellingFlatPrice) : null, contentQty: contentQty.trim() ? Number(contentQty) : null, contentUnit: contentQty.trim() ? contentUnit : null, }, @@ -246,6 +255,48 @@ export default function ItemDetailPage() { setName(e.target.value)} aria-invalid={!!errors.name} className="h-12 text-base" disabled={conflict} /> +
+ + setSellingMinPrice(e.target.value)} + placeholder="Optional" + className="h-12 text-base" + disabled={conflict} + /> +
+
+ + setSellingMaxPrice(e.target.value)} + placeholder="Optional" + className="h-12 text-base" + disabled={conflict} + /> +
+
+ + setSellingFlatPrice(e.target.value)} + placeholder="Optional" + className="h-12 text-base" + disabled={conflict} + /> +
diff --git a/Frontend/erp-system/app/dashboard/products/new/page.tsx b/Frontend/erp-system/app/dashboard/products/new/page.tsx index e8decf8..8221fc0 100644 --- a/Frontend/erp-system/app/dashboard/products/new/page.tsx +++ b/Frontend/erp-system/app/dashboard/products/new/page.tsx @@ -128,13 +128,16 @@ export default function NewItemPage() { // submit, without having to remove and re-add the whole value that produced it. const [removedVariantKeys, setRemovedVariantKeys] = useState>(new Set()) - // Sales pricing (FR-MD-01). "stock" ⇒ salePrice sent as null (sell at FIFO value); + // Sales pricing (FR-MD-01). "stock" ⇒ sellingFlatPrice sent as null (sell at FIFO value); // "fixed" ⇒ every variant must carry a price. `fixValue` is the shared default that // pre-fills rows; a per-key entry in `pricesByKey` overrides it for that one row only. const [priceMode, setPriceMode] = useState<"stock" | "fixed">("stock") const [fixValue, setFixValue] = useState("") const [pricesByKey, setPricesByKey] = useState>({}) const [priceErrors, setPriceErrors] = useState>({}) + const [sellingMinPrice, setSellingMinPrice] = useState("") + const [sellingMaxPrice, setSellingMaxPrice] = useState("") + const [sellingFlatPrice, setSellingFlatPrice] = useState("") // Warranty (FR-MD-01). Applies to every generated variant — there is no per-variant override. const [warranty, setWarranty] = useState("NonWarranty") @@ -401,7 +404,11 @@ export default function NewItemPage() { trackingMode: "None", warranty, warrantyPeriodMonths: warranty === "Warranty" ? warrantyPeriodMonths : null, - salePrice: priceMode === "fixed" ? Number(priceFor(variant.key)) : null, + sellingMinPrice: null, + sellingMaxPrice: null, + sellingMinPrice: sellingMinPrice.trim() ? Number(sellingMinPrice) : null, + sellingMaxPrice: sellingMaxPrice.trim() ? Number(sellingMaxPrice) : null, + sellingFlatPrice: priceMode === "fixed" ? Number(priceFor(variant.key)) : (sellingFlatPrice.trim() ? Number(sellingFlatPrice) : null), // Each variant carries its OWN size when a measurement dimension supplied one; // otherwise the shared form-level pair, which is correct when the varying dimension // isn't size (colour variants of one 500 ml bottle all hold 500 ml). @@ -639,8 +646,58 @@ export default function NewItemPage() {
+
+
+

Selling prices

+

+ Optional values for the item. Leave any field blank if it does not apply. +

+
+
+
+ + setSellingMinPrice(e.target.value)} + placeholder="Optional" + className="h-11 text-base" + /> +
+
+ + setSellingMaxPrice(e.target.value)} + placeholder="Optional" + className="h-11 text-base" + /> +
+
+ + setSellingFlatPrice(e.target.value)} + placeholder="Optional" + className="h-11 text-base" + /> +
+
+
+ {/* Sales pricing (FR-MD-01). The toggle is frontend-only: "stock" sends - salePrice=null (sold at FIFO value); "fixed" requires a price per variant. */} + sellingFlatPrice=null (sold at FIFO value); "fixed" requires a price per variant. */}

Sale price

diff --git a/Frontend/erp-system/app/dashboard/sales/bundles/[id]/page.tsx b/Frontend/erp-system/app/dashboard/sales/bundles/[id]/page.tsx deleted file mode 100644 index 36cad7f..0000000 --- a/Frontend/erp-system/app/dashboard/sales/bundles/[id]/page.tsx +++ /dev/null @@ -1,438 +0,0 @@ -"use client" - -import { useEffect, useMemo, useState } from "react" -import Link from "next/link" -import { useParams, useRouter } from "next/navigation" -import { ArrowLeft, CheckCircle2, Edit, ExternalLink, Minus, Plus, Printer, Save, XCircle } from "lucide-react" - -import { bundleApi } from "@/lib/api/bundles" -import { itemsApi } from "@/lib/api/items" -import { uomsApi } from "@/lib/api/uoms" -import { baseUomLabel } from "@/lib/uom-label" -import { errorMessage } from "@/lib/error-map" -import { Button, buttonVariants } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { cn } from "@/lib/utils" -import { BundleSale, BundleSaleTemplate, BundleSaleTemplateSummary, BundleSaleTemplateLine, UpdateBundleSaleRequest } from "@/types/bundles" -import { customersApi } from "@/lib/api/customers" -import { warehousesApi } from "@/lib/api/warehouses" -import { usersApi } from "@/lib/api/users" -import { Customer } from "@/types/customers" -import { ItemListItem, Uom, Warehouse } from "@/types/master-data" -import { ManagedUser } from "@/types/users" - -type EditableLine = BundleSaleTemplateLine & { key: string } - -const createBlankLine = (templateLine?: BundleSaleTemplateLine): EditableLine => ({ - key: crypto.randomUUID(), - bundleSaleTemplateLineId: templateLine?.bundleSaleTemplateLineId ?? 0, - itemId: templateLine?.itemId ?? 0, - warehouseId: templateLine?.warehouseId ?? 0, - qty: templateLine?.qty ?? 1, - unitPrice: templateLine?.unitPrice ?? 0, - includeInBundle: templateLine?.includeInBundle ?? true, - sortOrder: templateLine?.sortOrder ?? 0, -}) - -function statusClass(status: BundleSale["status"]) { - switch (status) { - case "Draft": - return "border-amber-200 bg-amber-50 text-amber-800" - case "Posted": - return "border-emerald-200 bg-emerald-50 text-emerald-800" - case "Cancelled": - return "border-rose-200 bg-rose-50 text-rose-800" - } -} - -export default function BundleSaleDetailPage() { - const router = useRouter() - const params = useParams<{ id: string }>() - const bundleSaleId = Number(params.id) - const [bundle, setBundle] = useState(null) - const [editing, setEditing] = useState(false) - const [templates, setTemplates] = useState([]) - const [template, setTemplate] = useState(null) - const [customers, setCustomers] = useState([]) - const [items, setItems] = useState([]) - const [uoms, setUoms] = useState([]) - const [warehouses, setWarehouses] = useState([]) - const [users, setUsers] = useState([]) - const [customerId, setCustomerId] = useState(null) - const [warehouseId, setWarehouseId] = useState(null) - const [cashierUserId, setCashierUserId] = useState(null) - const [templateId, setTemplateId] = useState(null) - const [bundleName, setBundleName] = useState("") - const [bundlePrice, setBundlePrice] = useState(0) - const [allowPriceOverride, setAllowPriceOverride] = useState(false) - const [lines, setLines] = useState([]) - const [error, setError] = useState(null) - const [busy, setBusy] = useState(false) - - useEffect(() => { - if (!Number.isFinite(bundleSaleId)) { - setError(`Invalid bundle id '${params.id}'.`) - return - } - Promise.all([ - bundleApi.getBundle(bundleSaleId), - bundleApi.listTemplates({ pageSize: 200 }), - customersApi.list({ pageSize: 200 }), - itemsApi.list({ pageSize: 200 }), - uomsApi.list({ pageSize: 200 }), - warehousesApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), - ]) - .then(([bundleRes, templateRes, custRes, itemRes, uomRes, whRes, userRes]) => { - const data = bundleRes - setBundle(data) - setTemplates(templateRes.items) - setCustomers(custRes.items) - setItems(itemRes.items) - setUoms(uomRes.items) - setWarehouses(whRes.items) - setUsers(userRes.items) - setCustomerId(data.customerId) - setWarehouseId(data.warehouseId) - setCashierUserId(data.cashierUserId) - setTemplateId(data.bundleSaleTemplateId) - setBundleName(data.bundleName) - setBundlePrice(data.bundlePrice) - setLines( - data.lines.map((line) => ({ - key: `${line.bundleSaleLineId}`, - bundleSaleTemplateLineId: line.bundleSaleLineId, - itemId: line.itemId, - warehouseId: line.warehouseId, - qty: line.qty, - unitPrice: line.unitPrice, - includeInBundle: line.includeInBundle, - sortOrder: line.bundleSaleLineId, - })) - ) - }) - .catch((err) => setError(errorMessage(err))) - }, [bundleSaleId, params.id, items]) - - useEffect(() => { - if (!templateId) return - bundleApi.getTemplate(templateId).then(setTemplate).catch((err) => setError(errorMessage(err))) - }, [templateId]) - - const templateLabel = useMemo(() => templates.find((t) => t.bundleSaleTemplateId === templateId)?.templateName ?? "Template", [templates, templateId]) - const componentSubtotal = useMemo(() => lines.reduce((sum, line) => sum + Number(line.qty || 0) * Number(line.unitPrice || 0), 0), [lines]) - const isDraft = bundle?.status === "Draft" - const canEdit = isDraft - - function updateLine(key: string, patch: Partial) { - setLines((prev) => prev.map((line) => (line.key === key ? { ...line, ...patch } : line))) - } - - function addLine() { - const source = template?.lines[0] - if (!source) return - setLines((prev) => [...prev, createBlankLine(source)]) - } - - function removeLine(key: string) { - setLines((prev) => (prev.length === 1 ? prev : prev.filter((line) => line.key !== key))) - } - - async function saveBundle() { - if (!bundle || !customerId || !warehouseId || !cashierUserId || !templateId) return - setBusy(true) - setError(null) - try { - const request: UpdateBundleSaleRequest = { - customerId, - warehouseId, - cashierUserId, - bundleSaleTemplateId: templateId, - bundleName, - bundlePrice, - allowPriceOverride, - lines: lines.map(({ key, ...line }) => line), - } - const res = await bundleApi.updateBundle(bundle.bundleSaleId, request) - setBundle(res) - setEditing(false) - router.refresh() - } catch (err) { - setError(errorMessage(err)) - } finally { - setBusy(false) - } - } - - async function postBundle() { - if (!bundle) return - setBusy(true) - setError(null) - try { - const check = await bundleApi.checkBundlePosting(bundle.bundleSaleId) - if (!check.canPost) { - setError("Resolve stock shortages before posting this bundle.") - return - } - const updated = await bundleApi.postBundle(bundle.bundleSaleId) - setBundle({ ...bundle, ...updated }) - } catch (err) { - setError(errorMessage(err)) - } finally { - setBusy(false) - } - } - - const shortageIssues = bundle && bundle.status === "Draft" - ? bundle.lines - .filter((line) => line.includeInBundle) - .filter((line) => Number(line.qty || 0) > 0) - .map((line) => ({ - bundleSaleLineId: line.bundleSaleLineId, - itemId: line.itemId, - itemSku: items.find((item) => item.itemId === line.itemId)?.sku ?? `#${line.itemId}`, - itemName: items.find((item) => item.itemId === line.itemId)?.name ?? line.itemId.toString(), - warehouseId: line.warehouseId, - requestedQty: line.qty, - })) - : [] - - async function cancelBundle() { - if (!bundle) return - setBusy(true) - setError(null) - try { - const updated = await bundleApi.cancelBundle(bundle.bundleSaleId) - setBundle({ ...bundle, ...updated }) - } catch (err) { - setError(errorMessage(err)) - } finally { - setBusy(false) - } - } - - if (error && !bundle) return
{error}
- if (!bundle) return
Loading bundle sale...
- - const printHref = `/print/sales/bundles/${bundle.bundleSaleId}` - - return ( -
-
-
- - - -
-

{bundle.bundleNo}

-

{bundle.bundleName}

-
-
-
- - - Print - - {canEdit ? ( - editing ? ( - - ) : ( - - ) - ) : null} - {isDraft ? ( - <> - - - - ) : null} -
-
- - {error ?
{error}
: null} - - {bundle.status === "Draft" && shortageIssues.length > 0 ? ( -
-
-
-
Stock shortage detected before posting
-
This bundle cannot be posted until every included component has enough available stock in the selected warehouse.
-
- - - Create GRN - -
-
- - - - - - - - - - {shortageIssues.map((issue) => ( - - - - - - ))} - -
ItemWarehouseRequested
-
{issue.itemSku}
-
{issue.itemName}
-
{issue.warehouseId}{issue.requestedQty.toFixed(0)}
-
-
- ) : null} - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - setBundleName(e.target.value)} disabled={!editing || !isDraft} /> -
-
- - setBundlePrice(Number(e.target.value))} disabled={!editing || !isDraft} /> -
-
-
- -
-
-
-

Component breakdown

-

{editing ? "Edit the component lines and save." : "Read-only until you enter edit mode."}

-
- {editing && isDraft ? : {bundle.status}} -
-
- - - - Item - UOM - Qty - Unit price - Include - {editing && isDraft ? : null} - - - - {lines.map((line) => ( - - - - - - {baseUomLabel(items, uoms, line.itemId)} - - updateLine(line.key, { qty: Number(e.target.value) })} disabled={!editing || !isDraft} className="text-right" /> - updateLine(line.key, { unitPrice: Number(e.target.value) })} disabled={!editing || !isDraft} className="text-right" /> - - {editing && isDraft ? ( - - ) : ( - line.includeInBundle ? "Included" : "Excluded" - )} - - {editing && isDraft ? : null} - - ))} - {lines.length === 0 ? ( - - - No component rows yet. Click Add line to start. - - - ) : null} - -
-
-
- -
-
-
Component subtotal
{componentSubtotal.toFixed(2)}
-
Bundle price
{bundlePrice.toFixed(2)}
-
Margin
{(bundlePrice - componentSubtotal).toFixed(2)}
-
-
-
- ) -} diff --git a/Frontend/erp-system/app/dashboard/sales/bundles/new/page.tsx b/Frontend/erp-system/app/dashboard/sales/bundles/new/page.tsx deleted file mode 100644 index b0c27f8..0000000 --- a/Frontend/erp-system/app/dashboard/sales/bundles/new/page.tsx +++ /dev/null @@ -1,337 +0,0 @@ -"use client" - -import { Suspense, useEffect, useMemo, useState } from "react" -import { useRouter, useSearchParams } from "next/navigation" -import Link from "next/link" -import { ArrowLeft, ExternalLink, Minus, Plus, Save } from "lucide-react" - -import { bundleApi } from "@/lib/api/bundles" -import { customersApi } from "@/lib/api/customers" -import { itemsApi } from "@/lib/api/items" -import { uomsApi } from "@/lib/api/uoms" -import { baseUomLabel } from "@/lib/uom-label" -import { warehousesApi } from "@/lib/api/warehouses" -import { usersApi } from "@/lib/api/users" -import { errorMessage } from "@/lib/error-map" -import { Button, buttonVariants } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Skeleton } from "@/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { cn } from "@/lib/utils" -import { toast } from "@/components/ui/toast" -import { Customer } from "@/types/customers" -import { ItemListItem, Uom, Warehouse } from "@/types/master-data" -import { ManagedUser } from "@/types/users" -import { BundleSaleTemplate, BundleSaleTemplateLine, BundleSaleTemplateSummary, CreateBundleSaleRequest } from "@/types/bundles" - -type EditableLine = BundleSaleTemplateLine & { key: string } - -const createBlankLine = (templateLine?: BundleSaleTemplateLine): EditableLine => ({ - key: crypto.randomUUID(), - bundleSaleTemplateLineId: templateLine?.bundleSaleTemplateLineId ?? 0, - itemId: templateLine?.itemId ?? 0, - warehouseId: templateLine?.warehouseId ?? 0, - qty: templateLine?.qty ?? 1, - unitPrice: templateLine?.unitPrice ?? 0, - includeInBundle: templateLine?.includeInBundle ?? true, - sortOrder: templateLine?.sortOrder ?? 0, -}) - -function NewBundleSaleContent() { - const router = useRouter() - const searchParams = useSearchParams() - const templateFromQuery = searchParams.get("templateId") - const [customers, setCustomers] = useState([]) - const [items, setItems] = useState([]) - const [uoms, setUoms] = useState([]) - const [warehouses, setWarehouses] = useState([]) - const [users, setUsers] = useState([]) - const [templates, setTemplates] = useState([]) - const [template, setTemplate] = useState(null) - const [customerId, setCustomerId] = useState(null) - const [warehouseId, setWarehouseId] = useState(null) - const [cashierUserId, setCashierUserId] = useState(null) - const [templateId, setTemplateId] = useState(templateFromQuery ? Number(templateFromQuery) : null) - const [bundleName, setBundleName] = useState("Demo Bundle") - const [bundlePrice, setBundlePrice] = useState(0) - const [allowPriceOverride, setAllowPriceOverride] = useState(false) - const [lines, setLines] = useState([]) - const [loading, setLoading] = useState(true) - const [saving, setSaving] = useState(false) - const [submitError, setSubmitError] = useState(null) - - useEffect(() => { - Promise.all([ - customersApi.list({ pageSize: 200 }), - itemsApi.list({ pageSize: 200 }), - uomsApi.list({ pageSize: 200 }), - warehousesApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), - bundleApi.listTemplates({ pageSize: 200 }), - ]) - .then(([cust, itemRes, uomRes, whRes, userRes, templateRes]) => { - setCustomers(cust.items) - setItems(itemRes.items) - setUoms(uomRes.items) - setWarehouses(whRes.items) - setUsers(userRes.items) - setTemplates(templateRes.items) - setCustomerId(cust.items[0]?.customerId ?? null) - setWarehouseId(whRes.items[0]?.warehouseId ?? null) - setCashierUserId(userRes.items[0]?.userId ?? null) - setTemplateId((current) => current ?? templateRes.items[0]?.bundleSaleTemplateId ?? null) - }) - .catch((err) => setSubmitError(errorMessage(err))) - .finally(() => setLoading(false)) - }, []) - - useEffect(() => { - if (!templateId) return - bundleApi.getTemplate(templateId).then((res) => { - setTemplate(res) - setLines( - res.lines.length > 0 - ? res.lines.map((line) => { - const item = items.find((candidate) => candidate.itemId === line.itemId) - return createBlankLine({ - ...line, - }) - }) - : [createBlankLine()] - ) - setBundlePrice(res.lines.reduce((sum, line) => sum + line.qty * line.unitPrice, 0)) - }).catch((err) => setSubmitError(errorMessage(err))) - }, [items, templateId]) - - const templateLabel = useMemo(() => template?.templateName ?? "Select template", [template]) - const componentSubtotal = useMemo(() => lines.reduce((sum, line) => sum + Number(line.qty || 0) * Number(line.unitPrice || 0), 0), [lines]) - - function updateLine(key: string, patch: Partial) { - setLines((prev) => prev.map((line) => (line.key === key ? { ...line, ...patch } : line))) - } - - function addLine() { - setLines((prev) => [...prev, createBlankLine(template?.lines[0])]) - } - - function removeLine(key: string) { - setLines((prev) => (prev.length === 1 ? prev : prev.filter((line) => line.key !== key))) - } - - async function submit() { - if (!customerId || !warehouseId || !cashierUserId || !templateId || !template) { - setSubmitError("Select customer, warehouse, cashier, and bundle template.") - return - } - if (lines.length === 0) { - setSubmitError("Add at least one bundle component line.") - return - } - setSaving(true) - setSubmitError(null) - try { - const request: CreateBundleSaleRequest = { - customerId, - warehouseId, - cashierUserId, - bundleSaleTemplateId: templateId, - bundleName, - bundlePrice, - allowPriceOverride, - lines: lines.map(({ key, ...line }) => line), - } - const res = await bundleApi.createBundle(request) - toast.success("Bundle saved", res.bundleNo) - router.push(`/dashboard/sales/bundles/${res.bundleSaleId}`) - } catch (err) { - setSubmitError(errorMessage(err)) - } finally { - setSaving(false) - } - } - - if (loading) return
Loading masters...
- - return ( -
-
- - - -
-

Create bundle sale

-

Create a fixed bundle from a stored template.

-
-
- - {submitError ?
{submitError}
: null} - -
-
-
-
Need stock before saving this bundle?
-
Save the bundle as a draft first. After that, open the draft detail page to check shortages and create a GRN if needed.
-
- - - Create GRN - -
-
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - setBundleName(e.target.value)} /> -
-
- - setBundlePrice(Number(e.target.value))} /> -
-
- - -
-
-
- -
-
-
-

Editable component rows

-

These rows are sent to the backend and stored with the bundle.

-
- -
-
- - - - Item - UOM - Qty - Unit price - Include - - - - - {lines.map((line) => ( - - - - - - {baseUomLabel(items, uoms, line.itemId)} - - updateLine(line.key, { qty: Number(e.target.value) })} className="text-right" /> - updateLine(line.key, { unitPrice: Number(e.target.value) })} className="text-right" /> - - - - - - - - ))} - {lines.length === 0 && ( - - - No component rows yet. Click Add component to start. - - - )} - -
-
-
- -
-
-
Component subtotal
{componentSubtotal.toFixed(2)}
-
Bundle price
{bundlePrice.toFixed(2)}
-
Margin
{(bundlePrice - componentSubtotal).toFixed(2)}
-
-
- -
- Cancel - -
-
- ) -} - -export default function NewBundleSalePage() { - return ( - }> - - - ) -} diff --git a/Frontend/erp-system/app/dashboard/sales/bundles/page.tsx b/Frontend/erp-system/app/dashboard/sales/bundles/page.tsx deleted file mode 100644 index 75e182a..0000000 --- a/Frontend/erp-system/app/dashboard/sales/bundles/page.tsx +++ /dev/null @@ -1,308 +0,0 @@ -"use client" - -import { useEffect, useMemo, useState } from "react" -import Link from "next/link" -import { ChevronLeft, ChevronRight, Eye, Filter, FileText, Plus, Printer } from "lucide-react" - -import { bundleApi } from "@/lib/api/bundles" -import { customersApi } from "@/lib/api/customers" -import { warehousesApi } from "@/lib/api/warehouses" -import { errorMessage } from "@/lib/error-map" -import { Button, buttonVariants } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Input } from "@/components/ui/input" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Skeleton } from "@/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { PaginationMeta } from "@/types/common" -import { Customer } from "@/types/customers" -import { Warehouse } from "@/types/master-data" -import { BundleSaleStatus, BundleSaleSummary } from "@/types/bundles" -import { cn } from "@/lib/utils" - -type StatusFilter = BundleSaleStatus | "All" - -const PAGE_SIZE = 10 -const tabs = ["All", "Draft", "Posted", "Cancelled"] as const - -function statusClass(status: BundleSaleStatus) { - switch (status) { - case "Draft": - return "border-amber-200 bg-amber-50 text-amber-800" - case "Posted": - return "border-emerald-200 bg-emerald-50 text-emerald-800" - case "Cancelled": - return "border-rose-200 bg-rose-50 text-rose-800" - } -} - -export default function BundleSalesPage() { - const [rows, setRows] = useState(null) - const [pagination, setPagination] = useState(null) - const [error, setError] = useState(null) - const [status, setStatus] = useState("All") - const [customerId, setCustomerId] = useState(null) - const [warehouseId, setWarehouseId] = useState(null) - const [searchInput, setSearchInput] = useState("") - const [query, setQuery] = useState("") - const [page, setPage] = useState(1) - const [showFilters, setShowFilters] = useState(false) - const [customers, setCustomers] = useState([]) - const [warehouses, setWarehouses] = useState([]) - - useEffect(() => { - const timeout = setTimeout(() => setQuery(searchInput.trim()), 300) - return () => clearTimeout(timeout) - }, [searchInput]) - - useEffect(() => setPage(1), [query, status, customerId, warehouseId]) - - useEffect(() => { - Promise.all([customersApi.list({ pageSize: 200 }), warehousesApi.list({ pageSize: 200 })]) - .then(([cust, whRes]) => { - setCustomers(cust.items) - setWarehouses(whRes.items) - }) - .catch((err) => setError(errorMessage(err))) - }, []) - - useEffect(() => { - setError(null) - bundleApi - .listBundles({ - page, - pageSize: PAGE_SIZE, - status: status === "All" ? undefined : status, - q: query || undefined, - customerId: customerId ?? undefined, - warehouseId: warehouseId ?? undefined, - }) - .then((res) => { - setRows(res.items) - setPagination(res.pagination) - }) - .catch((err) => setError(errorMessage(err))) - }, [page, status, query, customerId, warehouseId]) - - const visibleRows = useMemo( - () => - rows?.filter((row) => { - const matchesStatus = status === "All" || row.status === status - const matchesQuery = - `${row.bundleNo} ${row.bundleName} ${row.customerSnapshotName} ${row.bundleCode}` - .toLowerCase() - .includes(query.toLowerCase()) - return matchesStatus && matchesQuery - }) ?? [], - [rows, query, status] - ) - const hasFilters = status !== "All" || query.length > 0 || customerId !== null || warehouseId !== null - const bundleTotal = visibleRows.reduce((sum, row) => sum + row.grandTotal, 0) - const printHref = `/print/sales/bundles?status=${encodeURIComponent(status)}&q=${encodeURIComponent(query)}&customerId=${customerId ?? ""}&warehouseId=${warehouseId ?? ""}` - - return ( -
-
-
-

Bundle Sales

-

Fixed bundle register with draft, posted, and cancelled states.

-
-
- - - Print batch - - - - New Bundle - -
-
- -
-
-
- {tabs.map((t) => ( - - ))} -
-
- setSearchInput(e.target.value)} - placeholder="Filter by bundle, code, or customer" - className="h-12 w-full lg:max-w-sm" - /> - -
-
- - {showFilters && ( -
-
-
Customer
- -
-
-
Warehouse
- -
-
- -
-
- )} - - {error &&
{error}
} - - {!error && rows === null && ( -
- {Array.from({ length: 5 }).map((_, i) => ( - - ))} -
- )} - - {!error && rows !== null && visibleRows.length === 0 && ( -
- -

{hasFilters ? "No bundle sales match your filters." : "No bundle sales yet."}

-
- )} - - {!error && rows !== null && visibleRows.length > 0 && ( - <> -
- - - - Bundle - Customer - Date - Price - Grand - Status - View - - - - {visibleRows.map((row) => ( - - - - {row.bundleNo} - - - {row.customerSnapshotName} - {new Date(row.createdAt).toLocaleDateString()} - {row.bundlePrice.toFixed(2)} - {row.grandTotal.toFixed(2)} - - - {row.status} - - - -
- - - - - - -
-
-
- ))} -
-
-
- -
-
-
-
Rows loaded
-
{visibleRows.length}
-
-
-
Grand total
-
{bundleTotal.toFixed(2)}
-
-
-
- - {pagination && pagination.totalPages > 1 && ( -
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}-{Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} of {pagination.totalItems} -

-
- - - Page {pagination.page} of {pagination.totalPages} - - -
-
- )} - - )} -
-
- ) -} diff --git a/Frontend/erp-system/app/dashboard/sales/bundles/register/page.tsx b/Frontend/erp-system/app/dashboard/sales/bundles/register/page.tsx deleted file mode 100644 index ade96a5..0000000 --- a/Frontend/erp-system/app/dashboard/sales/bundles/register/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from "next/navigation" - -export default function BundleRegisterPage() { - redirect("/dashboard/sales/bundles") -} diff --git a/Frontend/erp-system/app/dashboard/sales/bundles/reports/page.tsx b/Frontend/erp-system/app/dashboard/sales/bundles/reports/page.tsx deleted file mode 100644 index b6022c6..0000000 --- a/Frontend/erp-system/app/dashboard/sales/bundles/reports/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client" - -import Link from "next/link" -import { ArrowLeft, FileText } from "lucide-react" - -import { buttonVariants } from "@/components/ui/button" -import { cn } from "@/lib/utils" - -export default function BundleReportsPage() { - return ( -
-
- - - -
-

Bundle Reports

-

Bundle-level reporting will be added after the backend module is wired.

-
-
-
- - This screen is a placeholder for bundle sales reporting. -
-
- ) -} diff --git a/Frontend/erp-system/app/dashboard/sales/bundles/templates/page.tsx b/Frontend/erp-system/app/dashboard/sales/bundles/templates/page.tsx new file mode 100644 index 0000000..1affd4e --- /dev/null +++ b/Frontend/erp-system/app/dashboard/sales/bundles/templates/page.tsx @@ -0,0 +1,318 @@ +"use client" + +import { useEffect, useMemo, useState } from "react" +import Link from "next/link" +import { ArrowLeft, Plus, Save, ShoppingBag, Trash2 } from "lucide-react" + +import { Button, buttonVariants } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Combobox } from "@/components/ui/combobox" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { cn } from "@/lib/utils" +import { errorMessage } from "@/lib/error-map" +import { bundleApi } from "@/lib/api/bundles" +import { itemsApi } from "@/lib/api/items" +import { uomsApi } from "@/lib/api/uoms" +import { baseUomLabel } from "@/lib/uom-label" +import { warehousesApi } from "@/lib/api/warehouses" +import { toast } from "@/components/ui/toast" +import { ItemListItem, Uom, Warehouse } from "@/types/master-data" +import { BundleSaleTemplateSummary, CreateBundleSaleTemplateLineRequest } from "@/types/bundles" + +type Line = CreateBundleSaleTemplateLineRequest & { key: string } + +const blankLine = (key: string, warehouseId: number): Line => ({ + key, + itemId: 0, + warehouseId, + qty: 1, + unitPrice: 0, + includeInBundle: true, + sortOrder: 0, +}) + +export default function BundleTemplatesPage() { + const [items, setItems] = useState([]) + const [uoms, setUoms] = useState([]) + const [warehouses, setWarehouses] = useState([]) + const [warehouseId, setWarehouseId] = useState(null) + const [templateName, setTemplateName] = useState("") + const [description, setDescription] = useState("") + const [lines, setLines] = useState([]) + const [templates, setTemplates] = useState([]) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + async function refreshTemplates() { + const res = await bundleApi.listTemplates({ pageSize: 50 }) + setTemplates(res.items) + } + + useEffect(() => { + Promise.all([ + itemsApi.list({ pageSize: 200 }), + uomsApi.list({ pageSize: 200 }), + warehousesApi.list({ pageSize: 200 }), + ]) + .then(async ([itemRes, uomRes, whRes]) => { + setItems(itemRes.items) + setUoms(uomRes.items) + setWarehouses(whRes.items) + const defaultWarehouseId = whRes.items[0]?.warehouseId ?? 0 + setWarehouseId(defaultWarehouseId) + setLines([blankLine("line-1", defaultWarehouseId)]) + await refreshTemplates() + }) + .catch((err) => setError(errorMessage(err))) + .finally(() => setLoading(false)) + }, []) + + const itemOptions = useMemo( + () => items.map((i) => ({ value: i.itemId, label: `${i.sku} — ${i.name}` })), + [items] + ) + + function updateLine(key: string, patch: Partial) { + setLines((prev) => prev.map((line) => (line.key === key ? { ...line, ...patch } : line))) + } + + function selectItem(key: string, itemId: number) { + const item = items.find((candidate) => candidate.itemId === itemId) + updateLine(key, { itemId, unitPrice: item?.sellingFlatPrice ?? 0 }) + } + + function addLine() { + setLines((prev) => [...prev, blankLine(`line-${Date.now()}`, warehouseId ?? 0)]) + } + + function removeLine(key: string) { + setLines((prev) => (prev.length === 1 ? prev : prev.filter((line) => line.key !== key))) + } + + function resetForm() { + setTemplateName("") + setDescription("") + setLines([blankLine("line-1", warehouseId ?? 0)]) + } + + const componentSubtotal = useMemo( + () => lines.filter((l) => l.includeInBundle).reduce((sum, l) => sum + Number(l.qty || 0) * Number(l.unitPrice || 0), 0), + [lines] + ) + + async function submit() { + if (!templateName.trim()) return setError("Enter a template name.") + if (lines.some((line) => !line.itemId)) return setError("Select an item for every component line.") + + setSaving(true) + setError(null) + try { + const created = await bundleApi.createTemplate({ + templateName: templateName.trim(), + description: description.trim() || null, + lines: lines.map((line, idx) => ({ + itemId: Number(line.itemId), + warehouseId: Number(line.warehouseId || warehouseId || 0), + qty: Number(line.qty), + unitPrice: Number(line.unitPrice), + includeInBundle: line.includeInBundle, + sortOrder: idx, + })), + }) + toast.success("Bundle template saved", `${created.templateCode} is ready to sell.`) + resetForm() + await refreshTemplates() + } catch (err) { + setError(errorMessage(err)) + } finally { + setSaving(false) + } + } + + if (loading) { + return
Loading masters...
+ } + + return ( +
+
+
+ + + +
+

Bundle Templates

+

Define a reusable bundle of items — once saved, it's ready to sell from Create Sale.

+
+
+ +
+ + {error ?
{error}
: null} + +
+

Bundle details

+

Template code is generated automatically when you save.

+
+
+ + setTemplateName(e.target.value)} placeholder="e.g. Back to School Combo" /> +
+
+ + +
+
+ + setDescription(e.target.value)} placeholder="Shown to staff picking a bundle to sell" /> +
+
+
+ +
+
+

Component items

+ +
+
+ + + + Item + UOM + Qty + Unit price + Include + + + + + {lines.map((line) => ( + + + selectItem(line.key, v ?? 0)} + placeholder="Search item…" + /> + + + {baseUomLabel(items, uoms, line.itemId)} + + + updateLine(line.key, { qty: Number(e.target.value) })} className="h-9 w-24 text-right font-mono text-sm tabular-nums" /> + + + updateLine(line.key, { unitPrice: Number(e.target.value) })} className="h-9 w-28 text-right font-mono text-sm tabular-nums" /> + + + + + + + + + ))} + +
+
+
+ Component subtotal + {componentSubtotal.toFixed(2)} +
+
+ +
+
+
+

Bundle templates

+

Every saved bundle shows up here, and as an "Add bundle" option on Create Sale.

+
+ +
+
+ + + + Template + Description + Lines + Status + Actions + + + + {templates.length > 0 ? ( + templates.map((t) => ( + + +
{t.templateName}
+
{t.templateCode}
+
+ {t.description ?? "—"} + {t.lineCount} + {t.status} + + + + Sell + + +
+ )) + ) : ( + + + No bundle templates yet — save one above. + + + )} +
+
+
+
+
+ ) +} diff --git a/Frontend/erp-system/app/dashboard/sales/day-end/page.tsx b/Frontend/erp-system/app/dashboard/sales/day-end/page.tsx index 1b7143b..5e4dc0b 100644 --- a/Frontend/erp-system/app/dashboard/sales/day-end/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/day-end/page.tsx @@ -9,7 +9,7 @@ import { usersApi } from "@/lib/api/users" import { errorMessage } from "@/lib/error-map" import { cn } from "@/lib/utils" import { ManagedUser } from "@/types/users" -import { SalesDayEnd, SalesDayEndPreview } from "@/types/sales" +import { SalesDayEnd, SalesDayEndInvoiceSummary, SalesDayEndPreview } from "@/types/sales" import { Button, buttonVariants } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -150,7 +150,7 @@ export default function SalesDayEndPage() { ))} {preview.draftBundleSalesBlockingClose.map((b) => (
  • - {b.bundleNo} — {b.grandTotal.toFixed(2)} + {b.bundleNo} — {b.grandTotal.toFixed(2)}
  • ))} @@ -159,7 +159,9 @@ export default function SalesDayEndPage() { ) : (

    {preview.slipCount === 0 && preview.bundleCount === 0 - ? "No sales slips or bundle sales for this cashier on this date." + ? preview.invoices.invoiceCount > 0 + ? `No sales slips or bundle sales for this cashier on this date — only ${preview.invoices.invoiceCount} sales invoice(s), which don't close through Day End (see below).` + : "No sales slips or bundle sales for this cashier on this date." : `${preview.slipCount} posted slip(s), ${preview.bundleCount} posted bundle sale(s) ready to close.`}

    )} @@ -171,6 +173,8 @@ export default function SalesDayEndPage() {
    + + {!preview.alreadyClosed && (
    )} + +
    )} ) } +// Sales Invoices aren't part of the cashier close (they post their own GL on Confirm and have +// no CashierUserId/DayEndId) — this is a read-only summary so the screen isn't a wall of zeros +// for cashiers who only use "New Sale", not Sales Slips/Bundle Sales. +function InvoiceSummaryPanel({ invoices }: { invoices: SalesDayEndInvoiceSummary }) { + if (invoices.invoiceCount === 0) return null + return ( +
    +
    +

    Sales invoices (not part of this close)

    +

    + Invoices post their own ledger entry when confirmed — shown here for visibility only. +

    +
    +
    + + + + + +
    +

    {invoices.invoiceNumbers.join(", ")}

    +
    + ) +} + function Stat({ label, value, emphasize, isCount }: { label: string; value: number; emphasize?: boolean; isCount?: boolean }) { return (
    diff --git a/Frontend/erp-system/app/dashboard/sales/invoices/[id]/page.tsx b/Frontend/erp-system/app/dashboard/sales/invoices/[id]/page.tsx index ac76a94..68e8784 100644 --- a/Frontend/erp-system/app/dashboard/sales/invoices/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/invoices/[id]/page.tsx @@ -21,6 +21,7 @@ import { toast } from "@/components/ui/toast" import { Label } from "@/components/ui/label" import { Button } from "@/components/ui/button" import { SalesInvoicePaymentDialog } from "@/components/sales/SalesInvoicePaymentDialog" +import { SalesInvoiceStatusBadge, SalesPaymentStatusBadge } from "@/components/sales/status-badges" type Line = CreateSalesInvoiceLineRequest & { key: string } @@ -47,15 +48,9 @@ const blankLine = (key: string): Line => ({ parentLineId: null, }) -function statusClass(status: SalesInvoiceStatus) { - switch (status) { - case "Draft": - return "border-amber-200 bg-amber-50 text-amber-800" - case "Posted": - return "border-emerald-200 bg-emerald-50 text-emerald-800" - case "Cancelled": - return "border-rose-200 bg-rose-50 text-rose-800" - } +/** "Posted" reads as "approved" everywhere in the UI — matches SalesInvoiceStatusBadge's label. */ +function statusWord(status: SalesInvoiceStatus): string { + return status === "Posted" ? "approved" : status.toLowerCase() } export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ id: string }> }) { @@ -144,11 +139,12 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i salesApi.listInvoicePayments(invoiceId).then(setPayments).catch(() => setPayments([])) }, [invoice, invoiceId]) - function handlePaid(_updated: SalesInvoice, payment: SalesInvoicePayment) { + function handlePaid(payment: SalesInvoicePayment, newStatus: SalesInvoiceStatus) { setInvoice((prev) => prev ? { ...prev, + status: newStatus, totals: { ...prev.totals, paidAmount: prev.totals.paidAmount + payment.amount, balanceAmount: prev.totals.balanceAmount - payment.amount }, } : prev @@ -310,8 +306,10 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i
    Sales Invoice

    {invoice.invoiceNo}

    - Status: - {invoice.status} + + {invoice.status !== "Draft" && ( + + )} Type: {invoice.invoiceType} Date: {new Date(invoice.invoiceDate).toLocaleDateString()}
    @@ -630,7 +628,7 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i ) : (
    - This invoice is {invoice.status.toLowerCase()} and cannot be edited. + This invoice is {statusWord(invoice.status)} and cannot be edited. {invoice.status === "Posted" ? (
    ({ @@ -57,8 +84,13 @@ const lkr = new Intl.NumberFormat("en-LK", { minimumFractionDigits: 2, }) -export default function NewSalesInvoicePage() { +function NewSalesInvoiceContent() { const router = useRouter() + const searchParams = useSearchParams() + const bundleTemplateIdFromQuery = searchParams.get("bundleTemplateId") + const appliedBundleRef = useRef(null) + const bundleGroupSeqRef = useRef(0) + const [customers, setCustomers] = useState([]) const [items, setItems] = useState([]) const [uoms, setUoms] = useState([]) @@ -67,7 +99,8 @@ export default function NewSalesInvoicePage() { const [warehouseId, setWarehouseId] = useState(null) const [invoiceType, setInvoiceType] = useState("B2C") const [lines, setLines] = useState([blankLine("line-1")]) - const [activeFocSchemes, setActiveFocSchemes] = useState([]) + const [freeIssueSchemes, setFreeIssueSchemes] = useState([]) + const [bundleTemplates, setBundleTemplates] = useState([]) const [loading, setLoading] = useState(true) const [submitError, setSubmitError] = useState(null) const [saving, setSaving] = useState(false) @@ -79,12 +112,14 @@ export default function NewSalesInvoicePage() { uomsApi.list({ pageSize: 200 }), warehousesApi.list({ pageSize: 200 }), salesApi.listFreeIssues({ pageSize: 20 }), + bundleApi.listTemplates({ pageSize: 200 }), ]) - .then(([cust, itemRes, uomRes, whRes, freeIssueRes]) => { + .then(([cust, itemRes, uomRes, whRes, freeIssueRes, templateRes]) => { setCustomers(cust.items) setItems(itemRes.items) setUoms(uomRes.items) setWarehouses(whRes.items) + setBundleTemplates(templateRes.items) const defaultWarehouseId = whRes.items[0]?.warehouseId ?? null setCustomerId(cust.items[0]?.customerId ?? null) setWarehouseId(defaultWarehouseId) @@ -96,21 +131,17 @@ export default function NewSalesInvoicePage() { unitPrice: getSuggestedUnitPrice(itemRes.items, itemRes.items[0]?.itemId), }, ]) - setActiveFocSchemes( - freeIssueRes.items.flatMap((issue) => { - if (!issue.itemId) return [] - const item = itemRes.items.find((candidate) => candidate.itemId === issue.itemId) - const warehouse = whRes.items.find((candidate) => candidate.warehouseId === issue.warehouseId) - return [ - { - id: issue.salesSlipId, - slipNo: issue.slipNo, - schemeLabel: issue.schemeLabel, - warehouseName: warehouse?.name ?? `Warehouse ${issue.warehouseId}`, - productLabel: `${item?.sku ?? issue.itemSku} - ${item?.name ?? issue.itemName}`, - }, - ] - }), + // Cancelled Free Issues don't represent a live deal — everything else (Draft/Posted) + // does. First match per item wins (listFreeIssues returns newest first). + setFreeIssueSchemes( + freeIssueRes.items + .filter((issue) => issue.status !== "Cancelled" && issue.itemId && issue.qty > 0 && issue.freeQty > 0) + .map((issue) => ({ + itemId: issue.itemId, + qty: issue.qty, + freeQty: issue.freeQty, + schemeLabel: issue.schemeLabel, + })), ) }) .catch((err) => setSubmitError(errorMessage(err))) @@ -126,14 +157,49 @@ export default function NewSalesInvoicePage() { setLines((prev) => prev.map((line) => ({ ...line, warehouseId: nextWarehouseId ?? line.warehouseId }))) } + function freeIssueSchemeFor(itemId: number): FreeIssueScheme | null { + return freeIssueSchemes.find((s) => s.itemId === itemId) ?? null + } + + // Tiered, not continuous: a "Buy 10 Get 2" scheme only pays out in whole multiples of its + // own qty (10 units -> 2 free, 25 units -> still 2, 30 units -> 4) — matches how the Free + // Issue slip itself frames the deal ("Buy {qty} Get {freeQty}"), not a per-unit ratio. + function suggestedFreeQty(itemId: number, qty: number): number { + const scheme = freeIssueSchemeFor(itemId) + if (!scheme || !qty) return 0 + return Math.floor(qty / scheme.qty) * scheme.freeQty + } + function selectItem(key: string, itemId: number) { - const item = items.find((candidate) => candidate.itemId === itemId) + const qty = Number(lines.find((l) => l.key === key)?.qty ?? 0) updateLine(key, { itemId, unitPrice: getSuggestedUnitPrice(items, itemId), + freeQty: suggestedFreeQty(itemId, qty), }) } + function updateQty(key: string, itemId: number, qty: number) { + updateLine(key, { qty, freeQty: suggestedFreeQty(itemId, qty) }) + } + + // The per-line item search doubles as bundle search: item values are prefixed "item:" and + // bundle template values "bundle:" so one Combobox/handler can tell them apart on select. + const itemOptions = useMemo( + () => items.map((i) => ({ value: `item:${i.itemId}`, label: `${i.sku} — ${i.name}` })), + [items], + ) + + const bundleTemplateOptions = useMemo( + () => bundleTemplates.map((t) => ({ value: `bundle:${t.bundleSaleTemplateId}`, label: `Bundle: ${t.templateCode} — ${t.templateName}` })), + [bundleTemplates], + ) + + const lineComboOptions = useMemo( + () => [...itemOptions, ...bundleTemplateOptions], + [itemOptions, bundleTemplateOptions], + ) + function addLine() { setLines((prev) => [ ...prev, @@ -144,10 +210,114 @@ export default function NewSalesInvoicePage() { ]) } + // A bundle template isn't a stock item itself — it has no itemId of its own — so "adding" one + // expands it into its real component lines (each with its own itemId/qty/price) rather than a + // single pseudo-item row. That's what keeps stock and GL postings correct per component. + // When picked from a specific line's search box (replaceLineKey set), that line is dropped if + // it was still blank — otherwise it's left in place and the bundle's lines are appended after it. + async function addBundle(bundleSaleTemplateId: number, replaceLineKey?: string) { + try { + const template = await bundleApi.getTemplate(bundleSaleTemplateId) + const included = template.lines.filter((l) => l.includeInBundle) + if (included.length === 0) { + toast.error("Bundle has no components", `${template.templateName} has no included items to add.`) + return + } + const groupId = `bundle-${bundleSaleTemplateId}-${bundleGroupSeqRef.current++}` + const newLines = included.map((l, idx) => ({ + ...blankLine(`${groupId}-${l.bundleSaleTemplateLineId || idx}`), + itemId: l.itemId, + warehouseId: warehouseId ?? l.warehouseId, + qty: l.qty, + unitPrice: l.unitPrice, + freeQty: suggestedFreeQty(l.itemId, l.qty), + bundleGroupId: groupId, + bundleName: template.templateName, + bundleQty: 1, + bundleBaseQty: l.qty, + })) + setLines((prev) => [ + ...prev.filter((line) => line.key !== replaceLineKey || line.itemId !== 0), + ...newLines, + ]) + toast.success("Bundle added", `${template.templateName} — ${included.length} line(s) added.`) + } catch (err) { + toast.error("Could not add bundle", errorMessage(err)) + } + } + + // Rescales every component line in the group from its per-bundle base qty (never from the + // already-scaled current qty) so repeated edits stay exact instead of compounding. + function updateBundleQty(groupId: string, rawBundleQty: number) { + const bundleQty = Math.max(1, Math.floor(rawBundleQty) || 1) + setLines((prev) => + prev.map((line) => { + if (line.bundleGroupId !== groupId) return line + const baseQty = line.bundleBaseQty ?? line.qty + const qty = baseQty * bundleQty + return { ...line, bundleQty, qty, freeQty: suggestedFreeQty(line.itemId, qty) } + }), + ) + } + + // Combined handler for each line's item/bundle search box — routes to the right action based + // on the "item:"/"bundle:" value prefix (see lineComboOptions above). + function selectLineOption(key: string, value: string | null) { + if (!value) return + const sep = value.indexOf(":") + const kind = value.slice(0, sep) + const id = Number(value.slice(sep + 1)) + if (kind === "bundle") { + addBundle(id, key) + } else { + selectItem(key, id) + } + } + + // Deep-linked from the Bundle Templates page's "Sell" button (?bundleTemplateId=...) — applies + // once masters have loaded, and only once even if this effect re-runs. + useEffect(() => { + if (loading || !bundleTemplateIdFromQuery) return + if (appliedBundleRef.current === bundleTemplateIdFromQuery) return + appliedBundleRef.current = bundleTemplateIdFromQuery + addBundle(Number(bundleTemplateIdFromQuery)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loading, bundleTemplateIdFromQuery]) + function removeLine(key: string) { setLines((prev) => (prev.length === 1 ? prev : prev.filter((line) => line.key !== key))) } + function removeBundleGroup(groupId: string) { + setLines((prev) => { + const next = prev.filter((line) => line.bundleGroupId !== groupId) + return next.length === 0 ? [{ ...blankLine(`line-${Date.now()}`), warehouseId: warehouseId ?? 0 }] : next + }) + } + + // Groups consecutive component lines added from the same bundle into one display row, so the + // table shows "Bundle: X" once instead of one row per stock item inside it. + const displayRows = useMemo(() => { + const rows: DisplayRow[] = [] + let i = 0 + while (i < lines.length) { + const line = lines[i] + if (line.bundleGroupId) { + const groupId = line.bundleGroupId + const groupLines: Line[] = [] + while (i < lines.length && lines[i].bundleGroupId === groupId) { + groupLines.push(lines[i]) + i++ + } + rows.push({ type: "bundle", groupId, bundleName: groupLines[0].bundleName ?? "Bundle", lines: groupLines }) + } else { + rows.push({ type: "line", line }) + i++ + } + } + return rows + }, [lines]) + const grossTotal = useMemo( () => lines.reduce((sum, line) => sum + Number(line.unitPrice ?? 0) * Number(line.qty || 0), 0), [lines], @@ -218,262 +388,286 @@ export default function NewSalesInvoicePage() { } } - if (loading) { - return
    Loading masters...
    - } - return (
    -
    -
    - - - -
    -

    New Sale

    -

    Select items, set quantity, unit price and line discount.

    -
    -
    -
    - - +
    +
    +

    New Sale

    +

    Select items, set quantity, unit price and line discount.

    - {submitError ?
    {submitError}
    : null} + {submitError && ( +
    {submitError}
    + )} -
    -

    Invoice header

    -
    -
    - - -
    -
    - - -
    -
    - - value={invoiceType} onValueChange={(v) => v && setInvoiceType(v)}> - - - - - B2B - B2C - Cash - Credit - - -
    -
    - -
    - - Draft - - {lkr.format(payableTotal)} + {loading && } + + {!loading && ( + <> +
    +
    + + +
    + +
    + + +
    + +
    + + value={invoiceType} onValueChange={(v) => v && setInvoiceType(v)}> + + + + + B2B + B2C + Cash + Credit + +
    -
    -
    -
    -
    -

    Invoice lines

    - -
    -
    - - - - # - Item - UOM - Qty - Free - Unit price - Discount % - Line total - - - - - {lines.map((line, idx) => { - const lineGross = Number(line.unitPrice ?? 0) * Number(line.qty || 0) - const lineDiscount = - line.discountMode === "Amount" - ? Number(line.discountAmount || 0) - : lineGross * (Number(line.discountPct || 0) / 100) - const lineNet = Math.max(0, lineGross - lineDiscount) +
    +
    +

    Lines

    +
    + +
    +
    - return ( - - {idx + 1} - - - - - {baseUomLabel(items, uoms, line.itemId)} - - - updateLine(line.key, { qty: Number(e.target.value) })} - className="h-9 w-24 text-right font-mono text-sm tabular-nums" - /> - - - updateLine(line.key, { freeQty: Number(e.target.value) })} - className="h-9 w-24 text-right font-mono text-sm tabular-nums" - /> - - - updateLine(line.key, { unitPrice: e.target.value === "" ? null : Number(e.target.value) })} - className="h-9 w-28 text-right font-mono text-sm tabular-nums" - /> -
    - {getSuggestedUnitPrice(items, line.itemId) !== null - ? `Suggested: ${lkr.format(getSuggestedUnitPrice(items, line.itemId) ?? 0)}` - : "No price suggestion available"} -
    -
    - - updateLine(line.key, { discountPct: Number(e.target.value) || 0 })} - className="h-9 w-24 text-right font-mono text-sm tabular-nums" - /> - - {lkr.format(lineNet)} - - - +
    +
    + + + # + Item + UOM + Qty + Free + Unit price + Disc % + Line total + - ) - })} - -
    -
    -
    + + + {displayRows.map((row, idx) => { + if (row.type === "bundle") { + const bundleTotal = row.lines.reduce((sum, l) => sum + lineTotals(l).net, 0) + const bundleFreeQty = row.lines.reduce((sum, l) => sum + Number(l.freeQty || 0), 0) + const bundleQty = row.lines[0]?.bundleQty ?? 1 + return ( + + {idx + 1} + +
    Bundle: {row.bundleName}
    +
      + {row.lines.map((l) => ( +
    • + {items.find((i) => i.itemId === l.itemId)?.name ?? `Item ${l.itemId}`} × {l.qty} +
    • + ))} +
    +
    + — + + updateBundleQty(row.groupId, Number(e.target.value))} + className="h-11 text-right text-sm tabular-nums" + aria-label="Bundle quantity" + /> +

    bundles

    +
    + + {bundleFreeQty || "—"} + + — + — + +
    {lkr.format(bundleTotal)}
    +
    + + + +
    + ) + } -
    - {/*
    -

    Active FOC schemes

    -
      - {activeFocSchemes.length > 0 ? ( - activeFocSchemes.map((scheme) => ( -
    • -
      - {scheme.schemeLabel} - {scheme.slipNo} -
      -
      - {scheme.productLabel} - {scheme.warehouseName} -
      -
    • - )) - ) : ( -
    • - No active free-issue schemes found. -
    • - )} -
    -
    */} + const line = row.line + const { net: lineNet } = lineTotals(line) + const scheme = freeIssueSchemeFor(line.itemId) -
    - {[ - ["Gross", lkr.format(grossTotal)], - ["Discount", `-${lkr.format(discountTotal)}`], - ["Net", lkr.format(netTotal)], - ["Tax", lkr.format(taxTotal)], - ].map(([k, v]) => ( -
    -
    {k}
    -
    {v}
    + return ( + + {idx + 1} + + selectLineOption(line.key, v)} + placeholder="Search item or bundle…" + className="h-11 text-sm" + /> + + +
    + {baseUomLabel(items, uoms, line.itemId)} +
    +
    + + updateQty(line.key, line.itemId, Number(e.target.value))} + className="h-11 text-right text-sm tabular-nums" + /> + + + updateLine(line.key, { freeQty: Number(e.target.value) })} + className="h-11 text-right text-sm tabular-nums" + /> + {scheme && ( +

    + {scheme.schemeLabel || `Buy ${scheme.qty} Get ${scheme.freeQty}`} +

    + )} +
    + + updateLine(line.key, { unitPrice: e.target.value === "" ? null : Number(e.target.value) })} + className="h-11 text-right text-sm tabular-nums" + /> +

    + {getSuggestedUnitPrice(items, line.itemId) !== null + ? `Suggested: ${lkr.format(getSuggestedUnitPrice(items, line.itemId) ?? 0)}` + : "No price suggestion available"} +

    +
    + + updateLine(line.key, { discountPct: Number(e.target.value) || 0 })} + className="h-11 text-right text-sm tabular-nums" + /> + + +
    {lkr.format(lineNet)}
    +
    + + + +
    + ) + })} + +
    - ))} -
    -
    Payable
    -
    {lkr.format(payableTotal)}
    -
    -
    -
    -
    - - Cancel - - -
    +
    +
    + Gross + {lkr.format(grossTotal)} +
    +
    + Discount + -{lkr.format(discountTotal)} +
    +
    + Tax + {lkr.format(taxTotal)} +
    +
    + Payable + {lkr.format(payableTotal)} +
    +
    +
    + +
    + + Cancel + + +
    + + )}
    ) } + +export default function NewSalesInvoicePage() { + return ( + }> + + + ) +} diff --git a/Frontend/erp-system/app/dashboard/sales/invoices/page.tsx b/Frontend/erp-system/app/dashboard/sales/invoices/page.tsx index b6a9d4a..49c8db4 100644 --- a/Frontend/erp-system/app/dashboard/sales/invoices/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/invoices/page.tsx @@ -2,34 +2,30 @@ import { useEffect, useMemo, useState } from "react" import Link from "next/link" -import { ChevronLeft, ChevronRight, Eye, Filter, FileText, Plus, Printer } from "lucide-react" +import { CheckCircle2, ChevronLeft, ChevronRight, CircleDollarSign, Eye, Filter, FileText, Plus, Printer } from "lucide-react" import { salesApi } from "@/lib/api/sales" import { errorMessage } from "@/lib/error-map" import { Button, buttonVariants } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" import { Input } from "@/components/ui/input" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { toast } from "@/components/ui/toast" +import { SalesInvoicePaymentDialog } from "@/components/sales/SalesInvoicePaymentDialog" +import { SalesInvoiceStatusBadge, SalesPaymentStatusBadge } from "@/components/sales/status-badges" import { PaginationMeta } from "@/types/common" -import { SalesInvoiceStatus, SalesInvoiceSummary } from "@/types/sales" +import { SalesInvoicePayment, SalesInvoiceStatus, SalesInvoiceSummary } from "@/types/sales" import { cn } from "@/lib/utils" type StatusFilter = SalesInvoiceStatus | "All" const PAGE_SIZE = 10 -const tabs = ["All", "Draft", "Posted", "Cancelled"] as const - -function statusClass(status: SalesInvoiceStatus) { - switch (status) { - case "Draft": - return "border-amber-200 bg-amber-50 text-amber-800" - case "Posted": - return "border-emerald-200 bg-emerald-50 text-emerald-800" - case "Cancelled": - return "border-rose-200 bg-rose-50 text-rose-800" - } -} +const tabs: { value: SalesInvoiceStatus | "All"; label: string }[] = [ + { value: "All", label: "All" }, + { value: "Draft", label: "Draft" }, + { value: "Posted", label: "Approved" }, + { value: "Cancelled", label: "Cancelled" }, +] export default function SalesInvoicesPage() { const [rows, setRows] = useState(null) @@ -39,6 +35,9 @@ export default function SalesInvoicesPage() { const [searchInput, setSearchInput] = useState("") const [query, setQuery] = useState("") const [page, setPage] = useState(1) + const [confirmingId, setConfirmingId] = useState(null) + const [payDialogInvoice, setPayDialogInvoice] = useState(null) + const [payDialogOpen, setPayDialogOpen] = useState(false) useEffect(() => { const timeout = setTimeout(() => setQuery(searchInput.trim()), 300) @@ -58,6 +57,34 @@ export default function SalesInvoicesPage() { .catch((err) => setError(errorMessage(err))) }, [page, status]) + async function handleConfirm(row: SalesInvoiceSummary) { + setConfirmingId(row.salesInvoiceId) + try { + const posted = await salesApi.postInvoice(row.salesInvoiceId) + setRows((prev) => prev?.map((r) => (r.salesInvoiceId === row.salesInvoiceId ? { ...r, status: posted.status } : r)) ?? prev) + toast.success("Invoice confirmed", posted.invoiceNo) + } catch (err) { + toast.error("Could not confirm invoice", errorMessage(err)) + } finally { + setConfirmingId(null) + } + } + + function handlePaid(payment: SalesInvoicePayment, newStatus: SalesInvoiceStatus) { + setRows( + (prev) => + prev?.map((r) => + r.salesInvoiceId === payment.salesInvoiceId + ? { + ...r, + status: newStatus, + totals: { ...r.totals, paidAmount: r.totals.paidAmount + payment.amount, balanceAmount: r.totals.balanceAmount - payment.amount }, + } + : r + ) ?? prev + ) + } + const visibleRows = useMemo( () => rows?.filter((row) => @@ -100,14 +127,14 @@ export default function SalesInvoicesPage() {
    {tabs.map((t) => ( ))}
    @@ -158,7 +185,7 @@ export default function SalesInvoicesPage() { Discount Net Status - View + Actions @@ -177,18 +204,49 @@ export default function SalesInvoicesPage() { -{row.totals.discountTotal.toFixed(2)} {row.totals.grandTotal.toFixed(2)} - - {row.status} - +
    + + {row.status !== "Draft" && ( + + )} +
    - - - +
    + {row.status === "Draft" && ( + + )} + {row.status !== "Cancelled" && row.totals.balanceAmount > 0 && ( + + )} + + + +
    ))} @@ -234,6 +292,8 @@ export default function SalesInvoicesPage() { )}
    + +
    ) } diff --git a/Frontend/erp-system/app/dashboard/sales/page.tsx b/Frontend/erp-system/app/dashboard/sales/page.tsx index 7a1de31..10deb94 100644 --- a/Frontend/erp-system/app/dashboard/sales/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/page.tsx @@ -1,5 +1,5 @@ import Link from "next/link" -import { FileBarChart, FileText, Lock, PackageX, ReceiptText } from "lucide-react" +import { FileBarChart, FileText, LayoutList, Lock, PackageX, ReceiptText } from "lucide-react" import { buttonVariants } from "@/components/ui/button" import { cn } from "@/lib/utils" @@ -11,6 +11,12 @@ const sections = [ href: "/dashboard/sales/invoices", icon: FileText, }, + { + title: "Bundle Templates", + description: "Define reusable bundles — sell them straight from Create Sale.", + href: "/dashboard/sales/bundles/templates", + icon: LayoutList, + }, { title: "Free Issues", description: "Promotional free-issue slips.", diff --git a/Frontend/erp-system/app/print/sales/bundles/[id]/page.tsx b/Frontend/erp-system/app/print/sales/bundles/[id]/page.tsx deleted file mode 100644 index 1c834ad..0000000 --- a/Frontend/erp-system/app/print/sales/bundles/[id]/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" -import { useParams } from "next/navigation" -import { Printer } from "lucide-react" - -import { bundleApi } from "@/lib/api/bundles" -import { errorMessage } from "@/lib/error-map" -import { Button } from "@/components/ui/button" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { BundleSale } from "@/types/bundles" - -export default function BundlePrintPage() { - const params = useParams<{ id: string }>() - const bundleSaleId = Number(params.id) - const [bundle, setBundle] = useState(null) - const [error, setError] = useState(null) - - useEffect(() => { - if (!Number.isFinite(bundleSaleId)) { - setError(`Invalid bundle id '${params.id}'.`) - return - } - bundleApi.getBundle(bundleSaleId).then((res) => setBundle(res)).catch((err) => setError(errorMessage(err))) - }, [bundleSaleId, params.id]) - - if (error && !bundle) return
    {error}
    - if (!bundle) return
    Loading bundle print...
    - - return ( -
    -
    - -
    -
    -
    Bundle Sales
    -

    {bundle.bundleNo}

    -

    {bundle.bundleName}

    -
    -
    -
    Customer
    {bundle.customerSnapshotName}
    -
    Warehouse
    {bundle.warehouseId}
    -
    Status
    {bundle.status}
    -
    -
    - - - - Item - Description - Qty - Price - Total - - - - {bundle.lines.map((line) => ( - - {line.itemId} - {line.description} - {line.qty.toFixed(2)} - {line.unitPrice.toFixed(2)} - {line.lineTotal.toFixed(2)} - - ))} - -
    -
    -
    - ) -} diff --git a/Frontend/erp-system/app/print/sales/bundles/page.tsx b/Frontend/erp-system/app/print/sales/bundles/page.tsx deleted file mode 100644 index a1ea5cb..0000000 --- a/Frontend/erp-system/app/print/sales/bundles/page.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client" - -import { Suspense, useEffect, useState } from "react" -import { useSearchParams } from "next/navigation" -import { Printer } from "lucide-react" - -import { bundleApi } from "@/lib/api/bundles" -import { errorMessage } from "@/lib/error-map" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Skeleton } from "@/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { BundleSaleSummary } from "@/types/bundles" - -function statusClass(status: BundleSaleSummary["status"]) { - switch (status) { - case "Draft": - return "border-amber-200 bg-amber-50 text-amber-800" - case "Posted": - return "border-emerald-200 bg-emerald-50 text-emerald-800" - case "Cancelled": - return "border-rose-200 bg-rose-50 text-rose-800" - } -} - -function BundleBatchPrintContent() { - const searchParams = useSearchParams() - const status = searchParams.get("status") ?? "All" - const query = searchParams.get("q") ?? "" - const [rows, setRows] = useState(null) - const [error, setError] = useState(null) - - useEffect(() => { - bundleApi.listBundles({ page: 1, pageSize: 200 }).then((res) => setRows(res.items)).catch((err) => setError(errorMessage(err))) - }, []) - - const visibleRows = rows?.filter((row) => { - const matchesStatus = status === "All" || row.status === status - const matchesQuery = `${row.bundleNo} ${row.customerSnapshotName} ${row.bundleName}`.toLowerCase().includes(query.toLowerCase()) - return matchesStatus && matchesQuery - }) - - if (error && !rows) return
    {error}
    - if (!rows) return
    Loading bundle print data...
    - - return ( -
    -
    - -
    -
    -
    Bundle Sales
    -

    Bundle Batch Print

    -

    Printed register snapshot of current bundle sales.

    -
    -
    - - - - Bundle - Customer - Date - Price - Grand - Status - - - - {visibleRows?.map((row) => ( - - {row.bundleNo} - {row.customerSnapshotName} - {new Date(row.createdAt).toLocaleDateString()} - {row.bundlePrice.toFixed(2)} - {row.grandTotal.toFixed(2)} - - - {row.status} - - - - ))} - -
    -
    -
    - ) -} - -export default function BundleBatchPrintPage() { - return ( - }> - - - ) -} diff --git a/Frontend/erp-system/components/Layouts/AppSidebar.tsx b/Frontend/erp-system/components/Layouts/AppSidebar.tsx index 751590f..1bf6f5a 100644 --- a/Frontend/erp-system/components/Layouts/AppSidebar.tsx +++ b/Frontend/erp-system/components/Layouts/AppSidebar.tsx @@ -27,9 +27,11 @@ import { Inbox, Landmark, LayoutGrid, + LayoutList, LayoutTemplate, LineChart, ListTree, + Lock, Menu, Package, PackageCheck, @@ -116,8 +118,9 @@ const navItems: { chevron: true, children: [ { title: "Sales", code: "sales.invoices", href: "/dashboard/sales/invoices", icon: FileText }, - { title: "Bundle Sales", code: "sales.bundle-sales", href: "/dashboard/sales/bundles", icon: Boxes }, + { title: "Bundle Templates", code: "sales.bundle-templates", href: "/dashboard/sales/bundles/templates", icon: LayoutList }, { title: "Free Issues", code: "sales.free-issues", href: "/dashboard/sales/free-issues", icon: PackageX }, + { title: "Day End", code: "sales.day-end", href: "/dashboard/sales/day-end", icon: Lock }, // { title: "Reports", code: "sales.reports", href: "/dashboard/sales/reports", icon: FileBarChart }, ], }, diff --git a/Frontend/erp-system/components/sales/SalesInvoicePaymentDialog.tsx b/Frontend/erp-system/components/sales/SalesInvoicePaymentDialog.tsx index 6dbb47a..90a8857 100644 --- a/Frontend/erp-system/components/sales/SalesInvoicePaymentDialog.tsx +++ b/Frontend/erp-system/components/sales/SalesInvoicePaymentDialog.tsx @@ -7,7 +7,7 @@ import { salesApi } from "@/lib/api/sales" import { errorMessage } from "@/lib/error-map" import { formatAmount } from "@/lib/format" import { CashAndBankAccountDto } from "@/types/general-ledger" -import { SalesInvoice, SalesInvoicePayment } from "@/types/sales" +import { SalesInvoicePayment, SalesInvoiceStatus, SalesInvoiceSummary } from "@/types/sales" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" @@ -17,14 +17,22 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { toast } from "@/components/ui/toast" interface SalesInvoicePaymentDialogProps { - invoice: SalesInvoice | null + // SalesInvoiceSummary rather than the full SalesInvoice — every field this dialog touches + // (id/no/status/balance) is already on the summary, so a list row can open it without a + // separate getInvoice() round trip. SalesInvoice extends SalesInvoiceSummary, so the detail + // page's full invoice still satisfies this as-is. + invoice: SalesInvoiceSummary | null open: boolean onOpenChange: (open: boolean) => void - onPaid: (invoice: SalesInvoice, payment: SalesInvoicePayment) => void + /** `newStatus` is always "Posted" — paying only ever succeeds against a posted invoice, whether + * it started that way or this dialog auto-posted it first. */ + onPaid: (payment: SalesInvoicePayment, newStatus: SalesInvoiceStatus) => void } -/** Record a customer payment against a posted invoice's balance — full or partial, into an - * existing GL cash/bank account. Modeled on GrnPaymentDialog's "pick an account, submit" shape. */ +/** Record a customer payment against an invoice's balance — full or partial, into an existing GL + * cash/bank account. A Draft invoice is posted first (the backend only accepts payments against a + * Posted invoice) so this doubles as "confirm and pay in one step" from a list row; an already-Posted + * invoice skips straight to payment. Modeled on GrnPaymentDialog's "pick an account, submit" shape. */ export function SalesInvoicePaymentDialog({ invoice, open, onOpenChange, onPaid }: SalesInvoicePaymentDialogProps) { const [accounts, setAccounts] = useState(null) const [glBankAccountId, setGlBankAccountId] = useState("") @@ -64,16 +72,32 @@ export function SalesInvoicePaymentDialog({ invoice, open, onOpenChange, onPaid setSubmitting(true) try { + if (invoice.status === "Draft") { + try { + await salesApi.postInvoice(invoice.salesInvoiceId) + } catch (err) { + toast.error("Could not confirm invoice", errorMessage(err)) + return + } + } + const payment = await salesApi.payInvoice(invoice.salesInvoiceId, { amount: amountNum, glBankAccountId: Number(glBankAccountId), reference: reference || undefined, }) - toast.success("Payment recorded", `${formatAmount(amountNum)} posted to the ledger (${payment.glJournalNo ?? "—"}).`) - onPaid(invoice, payment) + const fullyPaid = amountNum >= invoice.totals.balanceAmount + toast.success( + invoice.status === "Draft" ? "Invoice confirmed and paid" : "Payment recorded", + `${formatAmount(amountNum)} posted to the ledger (${payment.glJournalNo ?? "—"}) — ${fullyPaid ? "fully paid" : "partially paid"}.` + ) + onPaid(payment, "Posted") onOpenChange(false) } catch (err) { - toast.error("Could not record payment", errorMessage(err)) + toast.error( + invoice.status === "Draft" ? "Invoice was confirmed, but the payment could not be recorded" : "Could not record payment", + errorMessage(err) + ) } finally { setSubmitting(false) } @@ -83,10 +107,11 @@ export function SalesInvoicePaymentDialog({ invoice, open, onOpenChange, onPaid - Pay invoice {invoice.invoiceNo} + {invoice.status === "Draft" ? "Confirm & pay" : "Pay"} invoice {invoice.invoiceNo}

    + {invoice.status === "Draft" && "This invoice is still a draft — submitting will confirm it, then record the payment. "} Balance due: {formatAmount(invoice.totals.balanceAmount)}

    @@ -134,7 +159,11 @@ export function SalesInvoicePaymentDialog({ invoice, open, onOpenChange, onPaid Cancel
    diff --git a/Frontend/erp-system/components/sales/status-badges.tsx b/Frontend/erp-system/components/sales/status-badges.tsx new file mode 100644 index 0000000..f5cb95a --- /dev/null +++ b/Frontend/erp-system/components/sales/status-badges.tsx @@ -0,0 +1,57 @@ +import { Badge } from "@/components/ui/badge" +import { SalesInvoiceStatus } from "@/types/sales" + +// Same fixed-width convention as components/receiving/status-badges.tsx's GRN badges — every +// pill is the same size regardless of label length ("Draft" vs "Approved" vs "Partial"). +const badgeSize = "h-6 w-28 justify-center px-2.5 py-1 text-sm" + +function statusClass(status: SalesInvoiceStatus) { + if (status === "Draft") return "bg-warning/10 text-warning border-transparent" + if (status === "Posted") return "bg-success/10 text-success border-transparent" + return "bg-destructive/10 text-destructive border-transparent" // Cancelled +} + +// "Posted" reads as "Approved" everywhere in the UI — the underlying SalesInvoiceStatus value +// stays "Posted" (that's what the API sends/expects), only the displayed label changes. +const statusLabel: Record = { + Draft: "Draft", + Posted: "Approved", + Cancelled: "Cancelled", +} + +export function SalesInvoiceStatusBadge({ status }: { status: SalesInvoiceStatus }) { + return ( + + {statusLabel[status]} + + ) +} + +export type SalesPaymentStatus = "Unpaid" | "PartiallyPaid" | "Paid" + +export function salesPaymentStatus(paidAmount: number, balanceAmount: number): SalesPaymentStatus { + if (balanceAmount <= 0) return "Paid" + if (paidAmount > 0) return "PartiallyPaid" + return "Unpaid" +} + +function paymentClass(status: SalesPaymentStatus) { + if (status === "Paid") return "bg-success/10 text-success border-transparent" + if (status === "PartiallyPaid") return "bg-warning/10 text-warning border-transparent" + return "bg-muted text-muted-foreground border-transparent" // Unpaid +} + +const paymentLabel: Record = { + Unpaid: "Unpaid", + PartiallyPaid: "Partially paid", + Paid: "Paid", +} + +export function SalesPaymentStatusBadge({ paidAmount, balanceAmount }: { paidAmount: number; balanceAmount: number }) { + const status = salesPaymentStatus(paidAmount, balanceAmount) + return ( + + {paymentLabel[status]} + + ) +} diff --git a/Frontend/erp-system/lib/api-client.ts b/Frontend/erp-system/lib/api-client.ts index 7f569f5..59fb123 100644 --- a/Frontend/erp-system/lib/api-client.ts +++ b/Frontend/erp-system/lib/api-client.ts @@ -47,7 +47,36 @@ export function readCsrfToken(): string | null { return match ? decodeURIComponent(match[1]) : null } -async function rawRequest(path: string, options: RequestOptions = {}): Promise { +// erp_at is short-lived; erp_rt (a separate, refresh-route-scoped cookie) outlives it. Before +// this, an expired erp_at surfaced as a bare 401 on whatever the user happened to click next +// (confirming an invoice, recording a payment, ...) with no attempt to renew the session — this +// dedupes concurrent 401s onto one /auth/refresh-token call so every caller gets one retry. +let refreshPromise: Promise | null = null + +function refreshSession(): Promise { + if (!refreshPromise) { + refreshPromise = fetch(`${API_BASE}/auth/refresh-token`, { + method: "POST", + credentials: "include", + headers: { Accept: "application/json", "Content-Type": "application/json" }, + body: "{}", + }) + .then((res) => res.ok) + .catch(() => false) + .finally(() => { + refreshPromise = null + }) + } + return refreshPromise +} + +function redirectToLogin() { + if (typeof window === "undefined") return + const next = `${window.location.pathname}${window.location.search}` + window.location.href = `/login?next=${encodeURIComponent(next)}` +} + +async function rawRequest(path: string, options: RequestOptions = {}, isRetry = false): Promise { const { body, ifMatch, idempotencyKey, csrf, headers, ...rest } = options const csrfToken = csrf ? readCsrfToken() : null // Multipart uploads (attendance files, staff documents) pass a FormData body — the @@ -70,6 +99,12 @@ async function rawRequest(path: string, options: RequestOptions = {}): Promise { return apiRequest(`/bundle-sales/templates/${bundleSaleTemplateId}`) }, + + createTemplate(request: CreateBundleSaleTemplateRequest): Promise { + return apiRequest("/bundle-sales/templates", { method: "POST", body: request }) + }, } diff --git a/Frontend/erp-system/lib/sales-line-utils.ts b/Frontend/erp-system/lib/sales-line-utils.ts index 5037f43..0758161 100644 --- a/Frontend/erp-system/lib/sales-line-utils.ts +++ b/Frontend/erp-system/lib/sales-line-utils.ts @@ -2,5 +2,5 @@ import { ItemListItem } from "@/types/master-data" export function getSuggestedUnitPrice(items: ItemListItem[], itemId: number | null | undefined): number | null { if (!itemId) return null - return items.find((item) => item.itemId === itemId)?.salePrice ?? null -} \ No newline at end of file + return items.find((item) => item.itemId === itemId)?.sellingFlatPrice ?? null +} diff --git a/Frontend/erp-system/types/bundles.ts b/Frontend/erp-system/types/bundles.ts index 6e243e1..7719658 100644 --- a/Frontend/erp-system/types/bundles.ts +++ b/Frontend/erp-system/types/bundles.ts @@ -34,6 +34,21 @@ export interface BundleSaleTemplate { lines: BundleSaleTemplateLine[] } +export interface CreateBundleSaleTemplateLineRequest { + itemId: number + warehouseId: number + qty: number + unitPrice: number + includeInBundle: boolean + sortOrder: number +} + +export interface CreateBundleSaleTemplateRequest { + templateName: string + description?: string | null + lines: CreateBundleSaleTemplateLineRequest[] +} + export interface BundleSaleLine { bundleSaleLineId: number itemId: number diff --git a/Frontend/erp-system/types/master-data.ts b/Frontend/erp-system/types/master-data.ts index 426c048..44effcd 100644 --- a/Frontend/erp-system/types/master-data.ts +++ b/Frontend/erp-system/types/master-data.ts @@ -50,7 +50,9 @@ export interface ItemListItem extends ItemContent { warrantyPeriodMonths: WarrantyPeriodMonths | null taxClass: string | null /** Fixed sale price (Sales only); null ⇒ sell at stock/FIFO value. */ - salePrice: number | null + sellingMinPrice: number | null + sellingMaxPrice: number | null + sellingFlatPrice: number | null status: EntityStatus } @@ -85,7 +87,9 @@ export interface Item extends ItemContent { warrantyPeriodMonths: WarrantyPeriodMonths | null taxClass: string | null /** Fixed sale price (Sales only); null ⇒ sell at stock/FIFO value. */ - salePrice: number | null + sellingMinPrice: number | null + sellingMaxPrice: number | null + sellingFlatPrice: number | null status: EntityStatus reorder: ItemReorderSetting[] createdAt: string @@ -112,7 +116,9 @@ export interface CreateItemRequest { warrantyPeriodMonths?: WarrantyPeriodMonths | null taxClass?: string | null /** Optional fixed sale price (Sales only). Null/omitted ⇒ sell at stock/FIFO value. */ - salePrice?: number | null + sellingMinPrice?: number | null + sellingMaxPrice?: number | null + sellingFlatPrice?: number | null /** * Content per pack. Send both or neither — a half-filled pair is a 422. The base pair is * derived server-side and is deliberately not accepted here. diff --git a/Frontend/erp-system/types/sales.ts b/Frontend/erp-system/types/sales.ts index 4977b85..9e5b9c8 100644 --- a/Frontend/erp-system/types/sales.ts +++ b/Frontend/erp-system/types/sales.ts @@ -299,7 +299,7 @@ export interface SalesFreeIssueRewardOption { itemId: number sku: string name: string - salePrice: number | null + sellingFlatPrice: number | null } export interface SalesFreeIssueSuggestionLine { @@ -399,6 +399,19 @@ export interface SalesDayEndItemLine { revenue: number } +/** Read-only visibility into that cashier's Sales Invoices for the date — informational only. + * Invoices post their own GL entry on Confirm and have no DayEndId, so they're never locked + * against a close: this is computed live on every read, unlike the frozen Slip/Bundle totals + * alongside it. */ +export interface SalesDayEndInvoiceSummary { + invoiceCount: number + invoiceSubtotal: number + invoiceDiscountTotal: number + invoiceTaxTotal: number + invoiceGrandTotal: number + invoiceNumbers: string[] +} + export interface SalesDayEnd { salesDayEndId: number docNo: string @@ -418,6 +431,7 @@ export interface SalesDayEnd { slipNumbers: string[] bundleNumbers: string[] itemBreakdown: SalesDayEndItemLine[] + invoices: SalesDayEndInvoiceSummary } export interface SalesDayEndSummary { @@ -458,6 +472,7 @@ export interface SalesDayEndPreview { bundleNumbers: string[] draftSlipsBlockingClose: DraftSlipBlockingClose[] draftBundleSalesBlockingClose: DraftBundleBlockingClose[] + invoices: SalesDayEndInvoiceSummary } export interface CreateSalesDayEndRequest {