From ae6a87022df0eb8ad1b196ad3b8ffee54469cf3b Mon Sep 17 00:00:00 2001 From: Dhananjaya99 <152056742+ashanruu@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:43:01 +0530 Subject: [PATCH] make account service with grn --- Backend/ERPCore/Controllers/GrnsController.cs | 26 +- Backend/ERPCore/Domain/Entities/Grn.cs | 10 + Backend/ERPCore/Domain/Entities/GrnPayment.cs | 31 + Backend/ERPCore/Dtos/Grn/GrnDtos.cs | 19 +- .../Configurations/GrnConfiguration.cs | 24 + .../ERPCore/Infra/Persistence/ErpDbContext.cs | 1 + ...117_AddGrnGlPostingAndPayments.Designer.cs | 7188 +++++++++++++++++ ...260812091117_AddGrnGlPostingAndPayments.cs | 113 + .../Migrations/ErpDbContextModelSnapshot.cs | 237 + Backend/ERPCore/Program.cs | 1 + .../ERPCore/Services/GeneralLedgerService.cs | 95 + Backend/ERPCore/Services/Gl/GlModels.cs | 27 + Backend/ERPCore/Services/GrnPaymentService.cs | 120 + Backend/ERPCore/Services/GrnService.cs | 51 +- .../Interfaces/IGeneralLedgerService.cs | 17 +- .../Services/Interfaces/IGrnPaymentService.cs | 10 + Backend/ERPCore/System/Errors/ErrorCodes.cs | 7 + Backend/ERPCore/appsettings.json | 5 + .../app/dashboard/receiving/grn/[id]/page.tsx | 109 +- .../app/dashboard/receiving/grn/page.tsx | 15 +- .../components/receiving/GrnPaymentDialog.tsx | 145 + .../components/receiving/status-badges.tsx | 29 + Frontend/erp-system/lib/api/grns.ts | 12 + Frontend/erp-system/types/general-ledger.ts | 2 + Frontend/erp-system/types/grn.ts | 28 + 25 files changed, 8298 insertions(+), 24 deletions(-) create mode 100644 Backend/ERPCore/Domain/Entities/GrnPayment.cs create mode 100644 Backend/ERPCore/Migrations/20260812091117_AddGrnGlPostingAndPayments.Designer.cs create mode 100644 Backend/ERPCore/Migrations/20260812091117_AddGrnGlPostingAndPayments.cs create mode 100644 Backend/ERPCore/Services/Gl/GlModels.cs create mode 100644 Backend/ERPCore/Services/GrnPaymentService.cs create mode 100644 Backend/ERPCore/Services/Interfaces/IGrnPaymentService.cs create mode 100644 Frontend/erp-system/components/receiving/GrnPaymentDialog.tsx diff --git a/Backend/ERPCore/Controllers/GrnsController.cs b/Backend/ERPCore/Controllers/GrnsController.cs index 3ad1f5a..78337b9 100644 --- a/Backend/ERPCore/Controllers/GrnsController.cs +++ b/Backend/ERPCore/Controllers/GrnsController.cs @@ -11,8 +11,13 @@ namespace ERPCore.Controllers; public sealed class GrnsController : ApiControllerBase { private readonly IGrnService _grns; + private readonly IGrnPaymentService _payments; - public GrnsController(IGrnService grns) => _grns = grns; + public GrnsController(IGrnService grns, IGrnPaymentService payments) + { + _grns = grns; + _payments = payments; + } /// List GRNs, newest first. [HttpGet] @@ -59,4 +64,23 @@ public sealed class GrnsController : ApiControllerBase public async Task> Release( int grnId, int grnLineId, [FromBody] ReleaseLineRequest request, CancellationToken ct) => Ok(await _grns.ReleaseLineAsync(grnId, grnLineId, request.Action, ct)); + + /// Pay the vendor against this GRN's balance, in full or in installments; posts a real GL journal entry. + [HttpPost("{grnId:int}/payments")] + [ProducesResponseType(typeof(GrnPaymentDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> Pay(int grnId, [FromBody] CreateGrnPaymentRequest request, CancellationToken ct) + { + var dto = await _payments.PayAsync(grnId, request, ct); + return Created($"/api/v1/grns/{grnId}/payments/{dto.GrnPaymentId}", dto); + } + + /// Payment history for this GRN, newest first. + [HttpGet("{grnId:int}/payments")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> ListPayments(int grnId, CancellationToken ct) + => Ok(await _payments.ListAsync(grnId, ct)); } diff --git a/Backend/ERPCore/Domain/Entities/Grn.cs b/Backend/ERPCore/Domain/Entities/Grn.cs index 5642f1b..508a184 100644 --- a/Backend/ERPCore/Domain/Entities/Grn.cs +++ b/Backend/ERPCore/Domain/Entities/Grn.cs @@ -30,8 +30,18 @@ public class Grn public DateTime CreatedAt { get; set; } public DateTime? PostedAt { get; set; } + /// Journal number of the real GL journal entry posted for this GRN's receipt (set on confirm). + public string? GlJournalNo { get; set; } + public DateTime? GlPostedAt { get; set; } + + /// Sum of vendor payments made against this GRN's total payable (). + public decimal PaidAmount { get; set; } + /// Total payable minus ; installments accrue against this until it reaches zero. + public decimal BalanceAmount { get; set; } + /// PostgreSQL xmin-backed optimistic concurrency token. public uint RowVersion { get; set; } public ICollection Lines { get; set; } = new List(); + public ICollection Payments { get; set; } = new List(); } diff --git a/Backend/ERPCore/Domain/Entities/GrnPayment.cs b/Backend/ERPCore/Domain/Entities/GrnPayment.cs new file mode 100644 index 0000000..161f71a --- /dev/null +++ b/Backend/ERPCore/Domain/Entities/GrnPayment.cs @@ -0,0 +1,31 @@ +namespace ERPCore.Domain.Entities; + +/// +/// A vendor payment made against a confirmed GRN's balance (installments allowed — +/// see GrnPaymentService.PayAsync). Posts its own real GL journal entry (Debit GRN +/// Clearing / Credit the selected bank-or-cash account) before being recorded here. +/// +public class GrnPayment +{ + public int GrnPaymentId { get; set; } + + public int GrnId { get; set; } + public Grn? Grn { get; set; } + + public decimal Amount { get; set; } + public DateTime PaymentDate { get; set; } + + /// GL's numeric id for the bank/cash account the payment was made from. + public long GlBankAccountId { get; set; } + /// Snapshot of the account's display name at payment time (GL account lists have no local FK). + public string BankAccountName { get; set; } = string.Empty; + + public string? Reference { get; set; } + + /// Journal number of the real GL journal entry this payment posted. + public string? GlJournalNo { get; set; } + + public int CreatedBy { get; set; } + public User? Creator { get; set; } + public DateTime CreatedAt { get; set; } +} diff --git a/Backend/ERPCore/Dtos/Grn/GrnDtos.cs b/Backend/ERPCore/Dtos/Grn/GrnDtos.cs index 83f2f1c..f9120bf 100644 --- a/Backend/ERPCore/Dtos/Grn/GrnDtos.cs +++ b/Backend/ERPCore/Dtos/Grn/GrnDtos.cs @@ -16,23 +16,36 @@ public sealed record GrnLineDto( public sealed record GrnDto( int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status, - int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, IReadOnlyList Lines); + int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, string? GlJournalNo, + decimal PaidAmount, decimal BalanceAmount, IReadOnlyList Lines); /// Row shape for GET /grns — line count instead of the lines themselves. public sealed record GrnSummaryDto( int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status, - int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount); + int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount, + decimal PaidAmount, decimal BalanceAmount); public sealed record CreatedLayerDto( int LayerId, int ItemId, int WarehouseId, int? BatchId, decimal QtyReceived, decimal QtyRemaining, decimal UnitCost, DateTime ReceiptDate); public sealed record GrnConfirmResultDto( - int GrnId, GrnStatus Status, DateTime PostedAt, + int GrnId, GrnStatus Status, DateTime PostedAt, string GlJournalNo, decimal BalanceAmount, IReadOnlyList CreatedLayers, IReadOnlyList LedgerRefs, PurchaseOrderStatus? PoStatus); public sealed record ReleaseLineResultDto(int GrnLineId, HoldStatus HoldStatus); +public sealed record GrnPaymentDto( + int GrnPaymentId, int GrnId, decimal Amount, DateTime PaymentDate, + long GlBankAccountId, string BankAccountName, string? Reference, string? GlJournalNo, DateTime CreatedAt); + +public sealed class CreateGrnPaymentRequest +{ + [Range(0.01, double.MaxValue)] public decimal Amount { get; set; } + [Required] public long GlBankAccountId { get; set; } + [StringLength(100)] public string? Reference { get; set; } +} + // Requests ---------------------------------------------------------------------- public sealed class BatchInput diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/GrnConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/GrnConfiguration.cs index b003f20..5901e10 100644 --- a/Backend/ERPCore/Infra/Persistence/Configurations/GrnConfiguration.cs +++ b/Backend/ERPCore/Infra/Persistence/Configurations/GrnConfiguration.cs @@ -16,6 +16,9 @@ public sealed class GrnConfiguration : IEntityTypeConfiguration builder.Property(g => g.Status).HasConversion().HasMaxLength(20).IsRequired(); builder.Property(g => g.CreatedAt).IsRequired(); + builder.Property(g => g.GlJournalNo).HasMaxLength(30); + builder.Property(g => g.PaidAmount).HasPrecision(18, 4); + builder.Property(g => g.BalanceAmount).HasPrecision(18, 4); builder.Property(g => g.RowVersion).IsRowVersion(); builder.HasOne(g => g.PurchaseOrder).WithMany().HasForeignKey(g => g.PoId).OnDelete(DeleteBehavior.Restrict); @@ -28,6 +31,27 @@ public sealed class GrnConfiguration : IEntityTypeConfiguration } } +public sealed class GrnPaymentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("grn_payments"); + builder.HasKey(p => p.GrnPaymentId); + + builder.Property(p => p.Amount).HasPrecision(18, 4); + builder.Property(p => p.PaymentDate).IsRequired(); + builder.Property(p => p.BankAccountName).IsRequired().HasMaxLength(200); + builder.Property(p => p.Reference).HasMaxLength(100); + builder.Property(p => p.GlJournalNo).HasMaxLength(30); + builder.Property(p => p.CreatedAt).IsRequired(); + + builder.HasOne(p => p.Grn).WithMany(g => g.Payments).HasForeignKey(p => p.GrnId).OnDelete(DeleteBehavior.Restrict); + builder.HasOne(p => p.Creator).WithMany().HasForeignKey(p => p.CreatedBy).OnDelete(DeleteBehavior.Restrict); + + builder.HasIndex(p => p.GrnId); + } +} + public sealed class GrnLineConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) diff --git a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs index 47e402c..17a2ba8 100644 --- a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs +++ b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs @@ -63,6 +63,7 @@ public class ErpDbContext : DbContext // --- Goods Receipt (docs/10 Part C.3) --- public DbSet Grns => Set(); public DbSet GrnLines => Set(); + public DbSet GrnPayments => Set(); // --- Batch / Serial (docs/10 Part C.4) --- public DbSet Batches => Set(); diff --git a/Backend/ERPCore/Migrations/20260812091117_AddGrnGlPostingAndPayments.Designer.cs b/Backend/ERPCore/Migrations/20260812091117_AddGrnGlPostingAndPayments.Designer.cs new file mode 100644 index 0000000..07170f5 --- /dev/null +++ b/Backend/ERPCore/Migrations/20260812091117_AddGrnGlPostingAndPayments.Designer.cs @@ -0,0 +1,7188 @@ +// +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("20260812091117_AddGrnGlPostingAndPayments")] + partial class AddGrnGlPostingAndPayments + { + /// + 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("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("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.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("SalePrice") + .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.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("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.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("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.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("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("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("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("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.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSaleTemplate"); + + b.Navigation("CashierUser"); + + b.Navigation("Customer"); + + 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.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.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.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.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CashierUser"); + + b.Navigation("Customer"); + + 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.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.SalesInvoice", b => + { + b.Navigation("Lines"); + }); + + 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/20260812091117_AddGrnGlPostingAndPayments.cs b/Backend/ERPCore/Migrations/20260812091117_AddGrnGlPostingAndPayments.cs new file mode 100644 index 0000000..7c2cdd0 --- /dev/null +++ b/Backend/ERPCore/Migrations/20260812091117_AddGrnGlPostingAndPayments.cs @@ -0,0 +1,113 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ERPCore.Migrations +{ + /// + public partial class AddGrnGlPostingAndPayments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BalanceAmount", + table: "grns", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "GlJournalNo", + table: "grns", + type: "character varying(30)", + maxLength: 30, + nullable: true); + + migrationBuilder.AddColumn( + name: "GlPostedAt", + table: "grns", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "PaidAmount", + table: "grns", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: false, + defaultValue: 0m); + + migrationBuilder.CreateTable( + name: "grn_payments", + columns: table => new + { + GrnPaymentId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + GrnId = table.Column(type: "integer", nullable: false), + Amount = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + PaymentDate = table.Column(type: "timestamp with time zone", nullable: false), + GlBankAccountId = table.Column(type: "bigint", nullable: false), + BankAccountName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Reference = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + GlJournalNo = table.Column(type: "character varying(30)", maxLength: 30, nullable: true), + CreatedBy = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_grn_payments", x => x.GrnPaymentId); + table.ForeignKey( + name: "FK_grn_payments_grns_GrnId", + column: x => x.GrnId, + principalTable: "grns", + principalColumn: "GrnId", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_grn_payments_users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_grn_payments_CreatedBy", + table: "grn_payments", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_grn_payments_GrnId", + table: "grn_payments", + column: "GrnId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "grn_payments"); + + migrationBuilder.DropColumn( + name: "BalanceAmount", + table: "grns"); + + migrationBuilder.DropColumn( + name: "GlJournalNo", + table: "grns"); + + migrationBuilder.DropColumn( + name: "GlPostedAt", + table: "grns"); + + migrationBuilder.DropColumn( + name: "PaidAmount", + table: "grns"); + } + } +} diff --git a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs index d08c575..f77eff4 100644 --- a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs +++ b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs @@ -1406,6 +1406,10 @@ namespace ERPCore.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnId")); + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -1417,6 +1421,17 @@ namespace ERPCore.Migrations .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"); @@ -1537,6 +1552,55 @@ namespace ERPCore.Migrations b.ToTable("grn_lines", (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.GrnLineWarrantyNumber", b => { b.Property("GrnLineWarrantyNumberId") @@ -3999,6 +4063,87 @@ namespace ERPCore.Migrations b.ToTable("sales_return_lines", (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("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") @@ -5831,6 +5976,25 @@ namespace ERPCore.Migrations b.Navigation("PoLine"); }); + 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.GrnLineWarrantyNumber", b => { b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") @@ -6528,6 +6692,67 @@ namespace ERPCore.Migrations b.Navigation("SalesInvoiceLine"); }); + 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") @@ -7025,6 +7250,13 @@ namespace ERPCore.Migrations 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.GrnLine", b => @@ -7111,6 +7343,11 @@ namespace ERPCore.Migrations b.Navigation("Lines"); }); + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.Navigation("Lines"); + }); + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => { b.Navigation("Lines"); diff --git a/Backend/ERPCore/Program.cs b/Backend/ERPCore/Program.cs index 83099de..cefd914 100644 --- a/Backend/ERPCore/Program.cs +++ b/Backend/ERPCore/Program.cs @@ -103,6 +103,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Sales (Phase 1) builder.Services.AddScoped(); diff --git a/Backend/ERPCore/Services/GeneralLedgerService.cs b/Backend/ERPCore/Services/GeneralLedgerService.cs index 4b70e23..a3e7333 100644 --- a/Backend/ERPCore/Services/GeneralLedgerService.cs +++ b/Backend/ERPCore/Services/GeneralLedgerService.cs @@ -1,11 +1,21 @@ +using System.Text; +using System.Text.Json; using ERPCore.Infra.Gl; +using ERPCore.Services.Gl; using ERPCore.Services.Interfaces; +using ERPCore.System.Errors; namespace ERPCore.Services; /// public sealed class GeneralLedgerService : IGeneralLedgerService { + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + private readonly IGeneralLedgerClient _client; public GeneralLedgerService(IGeneralLedgerClient client) => _client = client; @@ -13,4 +23,89 @@ public sealed class GeneralLedgerService : IGeneralLedgerService public Task ForwardAsync( HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct) => _client.SendAsync(method, path, queryString, contentType, body, ct); + + public async Task PostJournalEntryAsync(GlJournalEntryRequest request, CancellationToken ct) + { + using var body = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(request, JsonOptions)); + var response = await _client.SendAsync(HttpMethod.Post, "journal-entries", null, "application/json", body, ct); + var data = ParseSuccess(response); + return new GlJournalEntryResult(data.JournalNo, data.IsPosted); + } + + public async Task GetPeriodByDateAsync(DateOnly date, CancellationToken ct) + { + var response = await _client.SendAsync( + HttpMethod.Get, "fiscal-years/periods/by-date", $"?date={date:yyyy-MM-dd}", null, null, ct); + var data = ParseSuccess(response); + return new GlPeriod(data.PeriodId, data.FiscalYearId); + } + + public async Task> ListBankAccountsAsync(CancellationToken ct) + { + var response = await _client.SendAsync(HttpMethod.Get, "bank-accounts", "?accountType=Both", null, null, ct); + var data = ParseSuccess>(response); + return data.Select(a => new GlBankAccount( + a.AccountType, a.AccountId, a.AccountName, a.BankName, + a.CashAccountTypeName, a.AccountNumber, a.GlAccountId, a.GlAccountCode, a.CurrencyCode)).ToList(); + } + + /// + /// Parses GL's ApiResponse envelope (case-insensitively, since GL's success bodies are + /// camelCase and its error bodies are PascalCase — docs/12 §4) and throws + /// if the call didn't succeed. + /// + private static T ParseSuccess(GeneralLedgerResponse response) + { + GlEnvelope? envelope; + try + { + envelope = JsonSerializer.Deserialize>(response.Body, JsonOptions); + } + catch (JsonException) + { + envelope = null; + } + + if (response.StatusCode is < 200 or >= 300 || envelope is null || !envelope.Success || envelope.Data is null) + { + var message = envelope?.Message ?? "The General Ledger service rejected the request."; + var status = response.StatusCode is >= 400 and < 500 ? response.StatusCode : 502; + throw new DomainException(ErrorCodes.GlRequestFailed, message, status); + } + + return envelope.Data; + } + + private sealed class GlEnvelope + { + public int StatusCode { get; set; } + public bool Success { get; set; } + public string? Message { get; set; } + public T? Data { get; set; } + } + + private sealed class GlJournalEntryData + { + public string JournalNo { get; set; } = string.Empty; + public bool IsPosted { get; set; } + } + + private sealed class GlPeriodData + { + public int PeriodId { get; set; } + public int FiscalYearId { get; set; } + } + + private sealed class GlBankAccountData + { + public string AccountType { get; set; } = string.Empty; + public long AccountId { get; set; } + public string AccountName { get; set; } = string.Empty; + public string? BankName { get; set; } + public string? CashAccountTypeName { get; set; } + public string? AccountNumber { get; set; } + public long GlAccountId { get; set; } + public string GlAccountCode { get; set; } = string.Empty; + public string CurrencyCode { get; set; } = string.Empty; + } } diff --git a/Backend/ERPCore/Services/Gl/GlModels.cs b/Backend/ERPCore/Services/Gl/GlModels.cs new file mode 100644 index 0000000..7876d0e --- /dev/null +++ b/Backend/ERPCore/Services/Gl/GlModels.cs @@ -0,0 +1,27 @@ +namespace ERPCore.Services.Gl; + +/// +/// Purpose-built request/response shapes for the specific GL endpoints ERPCore's GRN +/// module calls directly (journal-entries, period lookup, bank-account listing) — not +/// a full model of GL's contract (docs/12-GENERAL-LEDGER-INTEGRATION.md §5/§6: typed +/// DTOs are added only for the flow that actually needs them). +/// +public sealed record GlJournalEntryLineRequest(string AccountCode, decimal DebitAmount, decimal CreditAmount, string? Memo = null); + +public sealed class GlJournalEntryRequest +{ + public int PeriodId { get; set; } + public DateOnly EntryDate { get; set; } + public string? SourceModule { get; set; } + public string? Reference { get; set; } + public string? Narration { get; set; } + public List Lines { get; set; } = new(); +} + +public sealed record GlJournalEntryResult(string JournalNo, bool IsPosted); + +public sealed record GlPeriod(int PeriodId, int FiscalYearId); + +public sealed record GlBankAccount( + string AccountType, long AccountId, string AccountName, string? BankName, + string? CashAccountTypeName, string? AccountNumber, long GlAccountId, string GlAccountCode, string CurrencyCode); diff --git a/Backend/ERPCore/Services/GrnPaymentService.cs b/Backend/ERPCore/Services/GrnPaymentService.cs new file mode 100644 index 0000000..bfec736 --- /dev/null +++ b/Backend/ERPCore/Services/GrnPaymentService.cs @@ -0,0 +1,120 @@ +using ERPCore.Domain.Entities; +using ERPCore.Domain.Enums; +using ERPCore.Dtos.Grn; +using ERPCore.Infra.Auth; +using ERPCore.Infra.UoW; +using ERPCore.Repositories.Interfaces; +using ERPCore.Services.Gl; +using ERPCore.Services.Interfaces; +using ERPCore.System.Errors; +using Microsoft.EntityFrameworkCore; + +namespace ERPCore.Services; + +/// +/// Vendor payments against a confirmed GRN. Multiple installments are allowed until +/// reaches zero. Each payment posts its own real GL +/// journal entry (Debit GRN Clearing / Credit the selected bank-or-cash account) before +/// being recorded, using the same call-GL-before-commit pattern as +/// so a rejected/unreachable GL post rolls back the whole payment atomically. +/// +public sealed class GrnPaymentService : IGrnPaymentService +{ + private readonly IRepository _grns; + private readonly IRepository _payments; + private readonly IGeneralLedgerService _gl; + private readonly ICurrentUser _currentUser; + private readonly IUnitOfWork _uow; + private readonly string _glClearingAccountCode; + + public GrnPaymentService( + IRepository grns, IRepository payments, IGeneralLedgerService gl, + ICurrentUser currentUser, IUnitOfWork uow, IConfiguration configuration) + { + _grns = grns; + _payments = payments; + _gl = gl; + _currentUser = currentUser; + _uow = uow; + _glClearingAccountCode = configuration["Grn:GlClearingAccountCode"] ?? string.Empty; + } + + public async Task PayAsync(int grnId, CreateGrnPaymentRequest request, CancellationToken ct = default) + { + var grn = await _grns.Query().FirstOrDefaultAsync(g => g.GrnId == grnId, ct) + ?? throw new NotFoundException($"GRN {grnId} was not found."); + + if (grn.Status == GrnStatus.Draft) + throw new DomainException(ErrorCodes.GrnNotPayable, $"GRN {grnId} must be confirmed before it can be paid.", 409); + if (request.Amount > grn.BalanceAmount) + throw new DomainException(ErrorCodes.GrnPaymentExceedsBalance, + $"Payment amount {request.Amount} exceeds the remaining balance {grn.BalanceAmount}.", 400); + + var accounts = await _gl.ListBankAccountsAsync(ct); + var account = accounts.FirstOrDefault(a => a.AccountId == request.GlBankAccountId) + ?? throw new DomainException(ErrorCodes.GrnBankAccountNotFound, $"Bank/cash account {request.GlBankAccountId} was not found.", 404); + + var actor = _currentUser.AuditUserId; + var now = DateTime.UtcNow; + + var payment = await _uow.ExecuteInTransactionAsync(async token => + { + // Same atomicity approach as GrnService.ConfirmAsync: post to GL first, inside + // this transaction, before anything is committed — a GL rejection/timeout rolls + // the whole payment back with no partial local state. + var period = await _gl.GetPeriodByDateAsync(DateOnly.FromDateTime(now), token); + var posted = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest + { + PeriodId = period.PeriodId, + EntryDate = DateOnly.FromDateTime(now), + SourceModule = "GRN_PAYMENT", + Reference = grn.DocNo, + Narration = $"Payment against GRN {grn.DocNo}", + Lines = + [ + new GlJournalEntryLineRequest(_glClearingAccountCode, request.Amount, 0m, $"Payment against GRN {grn.DocNo}"), + new GlJournalEntryLineRequest(account.GlAccountCode, 0m, request.Amount, $"Payment against GRN {grn.DocNo}") + ] + }, token); + + var entity = new GrnPayment + { + GrnId = grn.GrnId, + Amount = request.Amount, + PaymentDate = now, + GlBankAccountId = account.AccountId, + BankAccountName = account.AccountName, + Reference = request.Reference, + GlJournalNo = posted.JournalNo, + CreatedBy = actor, + CreatedAt = now + }; + await _payments.AddAsync(entity, token); + + grn.PaidAmount += request.Amount; + grn.BalanceAmount -= request.Amount; + + return entity; + }, ct); + + return Map(payment); + } + + public async Task> ListAsync(int grnId, CancellationToken ct = default) + { + if (!await _grns.Query().AnyAsync(g => g.GrnId == grnId, ct)) + throw new NotFoundException($"GRN {grnId} was not found."); + + return await _payments.Query().AsNoTracking() + .Where(p => p.GrnId == grnId) + .OrderByDescending(p => p.GrnPaymentId) + .Select(p => new GrnPaymentDto( + p.GrnPaymentId, p.GrnId, p.Amount, p.PaymentDate, + p.GlBankAccountId, p.BankAccountName, p.Reference, p.GlJournalNo, p.CreatedAt)) + .ToListAsync(ct); + } + + private static GrnPaymentDto Map(GrnPayment p) => new( + p.GrnPaymentId, p.GrnId, p.Amount, p.PaymentDate, + p.GlBankAccountId, p.BankAccountName, p.Reference, p.GlJournalNo, p.CreatedAt); +} diff --git a/Backend/ERPCore/Services/GrnService.cs b/Backend/ERPCore/Services/GrnService.cs index 26ec228..dd4fe54 100644 --- a/Backend/ERPCore/Services/GrnService.cs +++ b/Backend/ERPCore/Services/GrnService.cs @@ -6,6 +6,7 @@ using ERPCore.Dtos.Grn; using ERPCore.Infra.Auth; using ERPCore.Infra.UoW; using ERPCore.Repositories.Interfaces; +using ERPCore.Services.Gl; using ERPCore.Services.Interfaces; using ERPCore.System.Errors; using Microsoft.EntityFrameworkCore; @@ -38,13 +39,18 @@ public sealed class GrnService : IGrnService private readonly INumberSequenceService _numbers; private readonly ICurrentUser _currentUser; private readonly IUnitOfWork _uow; + private readonly IGeneralLedgerService _gl; + private readonly string _glInventoryAccountCode; + private readonly string _glVatRecoverableAccountCode; + private readonly string _glClearingAccountCode; public GrnService( IRepository grns, IRepository pos, IRepository poLines, IRepository items, IRepository warehouses, IRepository bins, IRepository vendors, IRepository batches, IRepository layers, IRepository ledger, - IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow) + IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow, + IGeneralLedgerService gl, IConfiguration configuration) { _grns = grns; _pos = pos; @@ -60,6 +66,10 @@ public sealed class GrnService : IGrnService _numbers = numbers; _currentUser = currentUser; _uow = uow; + _gl = gl; + _glInventoryAccountCode = configuration["Grn:GlInventoryAccountCode"] ?? string.Empty; + _glVatRecoverableAccountCode = configuration["Grn:GlVatRecoverableAccountCode"] ?? string.Empty; + _glClearingAccountCode = configuration["Grn:GlClearingAccountCode"] ?? string.Empty; } public async Task> ListAsync( @@ -82,7 +92,7 @@ public sealed class GrnService : IGrnService .Skip(query.Skip).Take(query.PageSize) .Select(g => new GrnSummaryDto( g.GrnId, g.DocNo, g.PoId, g.VendorId, g.WarehouseId, g.Status, - g.CreatedBy, g.CreatedAt, g.PostedAt, g.Lines.Count)) + g.CreatedBy, g.CreatedAt, g.PostedAt, g.Lines.Count, g.PaidAmount, g.BalanceAmount)) .ToListAsync(ct); return PagedResponse.Create(rows, query.Page, query.PageSize, total); @@ -260,15 +270,47 @@ public sealed class GrnService : IGrnService } } + // Post the real GL journal entry for this receipt before committing — if GL + // rejects it or is unreachable, the exception propagates out of this callback + // and the whole transaction (FIFO layers, stock ledger, PO accrual) rolls back + // with it, so inventory and the ledger never diverge (user-approved "fails + // atomically" behavior; residual risk if GL posts but the local commit still + // fails afterward is accepted for this phase, see the integration plan). + var period = await _gl.GetPeriodByDateAsync(DateOnly.FromDateTime(now), token); + var glLines = new List(); + decimal totalPayable = 0m; + foreach (var line in grn.Lines.OrderBy(l => l.GrnLineId)) + { + glLines.Add(new GlJournalEntryLineRequest(_glInventoryAccountCode, line.ReceivedValue, 0m, $"GRN {grn.DocNo} line {line.GrnLineId}")); + if (line.VatAmount > 0) + glLines.Add(new GlJournalEntryLineRequest(_glVatRecoverableAccountCode, line.VatAmount, 0m, $"GRN {grn.DocNo} line {line.GrnLineId} VAT")); + totalPayable += line.LineTotal; + } + glLines.Add(new GlJournalEntryLineRequest(_glClearingAccountCode, 0m, totalPayable, $"GRN {grn.DocNo} received")); + + var posted = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest + { + PeriodId = period.PeriodId, + EntryDate = DateOnly.FromDateTime(now), + SourceModule = "GRN", + Reference = grn.DocNo, + Narration = $"Goods received - GRN {grn.DocNo}", + Lines = glLines + }, token); + grn.Status = GrnStatus.Confirmed; grn.PostedAt = now; + grn.GlJournalNo = posted.JournalNo; + grn.GlPostedAt = now; + grn.PaidAmount = 0m; + grn.BalanceAmount = totalPayable; await UpdatePoStatusAsync(grn.PoId, token); return 0; }, ct); return new GrnConfirmResultDto( - grn.GrnId, grn.Status, now, + grn.GrnId, grn.Status, now, grn.GlJournalNo ?? string.Empty, grn.BalanceAmount, createdLayers.Select(ToCreatedLayer).ToList(), ledgerRefs.Select(l => l.LedgerId).ToList(), await GetPoStatusAsync(grn.PoId, ct)); @@ -395,7 +437,7 @@ public sealed class GrnService : IGrnService .Select(l => l.LedgerId).ToListAsync(ct); return new GrnConfirmResultDto( - grn.GrnId, grn.Status, grn.PostedAt ?? grn.CreatedAt, + grn.GrnId, grn.Status, grn.PostedAt ?? grn.CreatedAt, grn.GlJournalNo ?? string.Empty, grn.BalanceAmount, layers.Select(ToCreatedLayer).ToList(), ledgerRefs, await GetPoStatusAsync(grn.PoId, ct)); } @@ -404,6 +446,7 @@ public sealed class GrnService : IGrnService private static GrnDto Map(Grn g) => new( g.GrnId, g.DocNo, g.PoId, g.VendorId, g.WarehouseId, g.Status, g.CreatedBy, g.CreatedAt, g.PostedAt, + g.GlJournalNo, g.PaidAmount, g.BalanceAmount, g.Lines.OrderBy(l => l.GrnLineId).Select(l => new GrnLineDto( l.GrnLineId, l.PoLineId, l.ItemId, l.BinId, l.Qty, l.UnitCost, l.PoUnitPrice, l.DiscountPct, l.NetUnitCost, l.VatPct, l.VatAmount, l.ReceivedValue, l.LineTotal, diff --git a/Backend/ERPCore/Services/Interfaces/IGeneralLedgerService.cs b/Backend/ERPCore/Services/Interfaces/IGeneralLedgerService.cs index eeff07b..55f6024 100644 --- a/Backend/ERPCore/Services/Interfaces/IGeneralLedgerService.cs +++ b/Backend/ERPCore/Services/Interfaces/IGeneralLedgerService.cs @@ -1,16 +1,27 @@ using ERPCore.Infra.Gl; +using ERPCore.Services.Gl; namespace ERPCore.Services.Interfaces; /// /// Single entry point into the external General Ledger service — the one function /// used both by (frontend -/// requests, forwarded verbatim) and, once wired, other ERPCore services that need to -/// post directly to GL (e.g. GRN confirm, adjustments — docs/12-GENERAL-LEDGER-INTEGRATION.md). -/// No business logic lives here yet; this pass only connects the transport. +/// requests, forwarded verbatim) and by other ERPCore services that post directly to +/// GL (docs/12-GENERAL-LEDGER-INTEGRATION.md §5/§6). stays a +/// byte-for-byte passthrough; the three typed methods below are the first internal +/// callers (GRN receipt + payment posting) and model only what those flows need. /// public interface IGeneralLedgerService { Task ForwardAsync( HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct); + + /// Creates and posts a balanced journal entry. Throws on rejection/unreachability. + Task PostJournalEntryAsync(GlJournalEntryRequest request, CancellationToken ct); + + /// Resolves the accounting period covering . + Task GetPeriodByDateAsync(DateOnly date, CancellationToken ct); + + /// Lists GL's cash and bank accounts (default: both types). + Task> ListBankAccountsAsync(CancellationToken ct); } diff --git a/Backend/ERPCore/Services/Interfaces/IGrnPaymentService.cs b/Backend/ERPCore/Services/Interfaces/IGrnPaymentService.cs new file mode 100644 index 0000000..12a4456 --- /dev/null +++ b/Backend/ERPCore/Services/Interfaces/IGrnPaymentService.cs @@ -0,0 +1,10 @@ +using ERPCore.Dtos.Grn; + +namespace ERPCore.Services.Interfaces; + +/// Vendor payments against a confirmed GRN's balance (installments allowed). +public interface IGrnPaymentService +{ + Task PayAsync(int grnId, CreateGrnPaymentRequest request, CancellationToken ct = default); + Task> ListAsync(int grnId, CancellationToken ct = default); +} diff --git a/Backend/ERPCore/System/Errors/ErrorCodes.cs b/Backend/ERPCore/System/Errors/ErrorCodes.cs index 27a4de1..891e1fb 100644 --- a/Backend/ERPCore/System/Errors/ErrorCodes.cs +++ b/Backend/ERPCore/System/Errors/ErrorCodes.cs @@ -70,4 +70,11 @@ public static class ErrorCodes // General Ledger service proxy (GeneralLedgerController → external GL service, docs/12) public const string GlServiceUnavailable = "GL_SERVICE_UNAVAILABLE"; + // General Ledger typed internal calls (GRN receipt/payment posting, docs/12 §5's deferred work) + public const string GlRequestFailed = "GL_REQUEST_FAILED"; + + // GRN payments + public const string GrnNotPayable = "GRN_NOT_PAYABLE"; + public const string GrnPaymentExceedsBalance = "GRN_PAYMENT_EXCEEDS_BALANCE"; + public const string GrnBankAccountNotFound = "GRN_BANK_ACCOUNT_NOT_FOUND"; } diff --git a/Backend/ERPCore/appsettings.json b/Backend/ERPCore/appsettings.json index 9d07a93..1b05d32 100644 --- a/Backend/ERPCore/appsettings.json +++ b/Backend/ERPCore/appsettings.json @@ -26,5 +26,10 @@ "BaseUrl": "https://localhost:7024/api/v1/", "ApiKey": "1A5FE0E389C029B1FBCD2A94650CEBF0656083BF06FF48E5E11987040ABA262D" }, + "Grn": { + "GlInventoryAccountCode": "1100", + "GlVatRecoverableAccountCode": "1200", + "GlClearingAccountCode": "2000" + }, "AllowedHosts": "*" } diff --git a/Frontend/erp-system/app/dashboard/receiving/grn/[id]/page.tsx b/Frontend/erp-system/app/dashboard/receiving/grn/[id]/page.tsx index b75d4a3..e207d21 100644 --- a/Frontend/erp-system/app/dashboard/receiving/grn/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/receiving/grn/[id]/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react" import { useParams } from "next/navigation" import Link from "next/link" -import { CheckCircle2, PackageCheck, PackageX, ShieldAlert, XCircle } from "lucide-react" +import { BookOpen, CheckCircle2, CircleDollarSign, PackageCheck, PackageX, ShieldAlert, XCircle } from "lucide-react" import { grnsApi } from "@/lib/api/grns" import { warehousesApi } from "@/lib/api/warehouses" @@ -11,8 +11,9 @@ 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 { formatAmount } from "@/lib/format" import { cn } from "@/lib/utils" -import { ConfirmGrnResponse, Grn } from "@/types/grn" +import { ConfirmGrnResponse, Grn, GrnPayment } from "@/types/grn" import { Bin, ItemListItem, Uom } from "@/types/master-data" import { Badge } from "@/components/ui/badge" @@ -20,7 +21,8 @@ import { Button, buttonVariants } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" -import { GrnStatusBadge, HoldStatusBadge } from "@/components/receiving/status-badges" +import { GrnPaymentStatusBadge, GrnStatusBadge, HoldStatusBadge } from "@/components/receiving/status-badges" +import { GrnPaymentDialog } from "@/components/receiving/GrnPaymentDialog" export default function GrnDetailPage() { const params = useParams<{ id: string }>() @@ -36,6 +38,9 @@ export default function GrnDetailPage() { const [confirmResult, setConfirmResult] = useState(null) const [releasingLineId, setReleasingLineId] = useState(null) + const [payments, setPayments] = useState([]) + const [payDialogOpen, setPayDialogOpen] = useState(false) + // Stable per detail-page-session key so a retried confirm click doesn't double-post. const idempotencyKey = useRef(crypto.randomUUID()) @@ -55,6 +60,18 @@ export default function GrnDetailPage() { warehousesApi.listBins(grn.warehouseId).then(setBins).catch(() => setBins([])) }, [grn?.warehouseId]) + useEffect(() => { + if (!grn || grn.status === "Draft") return + grnsApi.listPayments(grn.grnId).then(setPayments).catch(() => setPayments([])) + }, [grn?.grnId, grn?.status]) + + function handlePaid(_updated: Grn, payment: GrnPayment) { + setGrn((prev) => + prev ? { ...prev, paidAmount: prev.paidAmount + payment.amount, balanceAmount: prev.balanceAmount - payment.amount } : prev + ) + setPayments((prev) => [payment, ...prev]) + } + function itemFor(itemId: number) { return items.find((i) => i.itemId === itemId) } @@ -70,8 +87,10 @@ export default function GrnDetailPage() { try { const result = await grnsApi.confirm(grn.grnId, idempotencyKey.current) setConfirmResult(result) - setGrn((prev) => (prev ? { ...prev, status: result.status } : prev)) - toast.success("GRN confirmed", `${result.createdLayers.length} layer(s) posted to stock.`) + setGrn((prev) => + prev ? { ...prev, status: result.status, glJournalNo: result.glJournalNo, balanceAmount: result.balanceAmount } : prev + ) + toast.success("GRN confirmed", `${result.createdLayers.length} layer(s) posted to stock. Journal ${result.glJournalNo}.`) } catch (err) { setError(errorMessage(err)) toast.error("Could not confirm GRN", errorMessage(err)) @@ -121,6 +140,7 @@ export default function GrnDetailPage() {

{grn.docNo}

+ {grn.status !== "Draft" && }

{grn.poId ? `Against PO #${grn.poId}` : "Direct receipt"} — Vendor #{grn.vendorId} — Warehouse #{grn.warehouseId} @@ -128,14 +148,36 @@ export default function GrnDetailPage() { - {grn.status === "Draft" && ( - - )} +

+ {grn.status !== "Draft" && ( + <> + + + View in Ledger + + {grn.balanceAmount > 0 && ( + + )} + + )} + {grn.status === "Draft" && ( + + )} +
+ + {error && (
{error}
)} @@ -150,6 +192,7 @@ export default function GrnDetailPage() { Layers: {confirmResult.createdLayers.map((l) => `#${l.layerId} (${l.qtyReceived} @ ${l.unitCost})`).join(", ")}
Ledger refs: {confirmResult.ledgerRefs.join(", ")}
+
GL journal entry: {confirmResult.glJournalNo}
{confirmResult.poStatus &&
PO status: {confirmResult.poStatus}
} )} @@ -258,7 +301,7 @@ export default function GrnDetailPage() { -
+
Stock value (excl. VAT) {grn.lines.reduce((s, l) => s + l.receivedValue, 0).toFixed(2)} @@ -271,7 +314,49 @@ export default function GrnDetailPage() { Document total {grn.lines.reduce((s, l) => s + l.lineTotal, 0).toFixed(2)}
+ {grn.status !== "Draft" && ( + <> +
+ Amount paid + {formatAmount(grn.paidAmount)} +
+
+ Balance due + {formatAmount(grn.balanceAmount)} +
+ + )}
+ + {grn.status !== "Draft" && payments.length > 0 && ( +
+

Payment History

+
+ + + + Date + Amount + Account + Reference + GL Journal + + + + {payments.map((p) => ( + + {new Date(p.paymentDate).toLocaleString()} + {formatAmount(p.amount)} + {p.bankAccountName} + {p.reference ?? "—"} + {p.glJournalNo ?? "—"} + + ))} + +
+
+
+ )}
) } diff --git a/Frontend/erp-system/app/dashboard/receiving/grn/page.tsx b/Frontend/erp-system/app/dashboard/receiving/grn/page.tsx index c7eb8ef..1d48f76 100644 --- a/Frontend/erp-system/app/dashboard/receiving/grn/page.tsx +++ b/Frontend/erp-system/app/dashboard/receiving/grn/page.tsx @@ -6,6 +6,7 @@ import { ChevronLeft, ChevronRight, Eye, PackageSearch, Plus, Search } from "luc import { grnsApi } from "@/lib/api/grns" import { errorMessage } from "@/lib/error-map" +import { formatAmount } from "@/lib/format" import { GrnStatus, GrnSummary } from "@/types/grn" import { PaginationMeta } from "@/types/common" import { cn } from "@/lib/utils" @@ -22,7 +23,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { GrnStatusBadge } from "@/components/receiving/status-badges" +import { GrnPaymentStatusBadge, GrnStatusBadge } from "@/components/receiving/status-badges" type StatusFilter = GrnStatus | "All" @@ -174,6 +175,8 @@ export default function GrnListPage() { Vendor Warehouse Status + Balance + Payment Created Actions @@ -196,6 +199,16 @@ export default function GrnListPage() { + + {grn.status === "Draft" ? "—" : formatAmount(grn.balanceAmount)} + + + {grn.status === "Draft" ? ( + + ) : ( + + )} + {new Date(grn.createdAt).toLocaleString()}
diff --git a/Frontend/erp-system/components/receiving/GrnPaymentDialog.tsx b/Frontend/erp-system/components/receiving/GrnPaymentDialog.tsx new file mode 100644 index 0000000..4da408c --- /dev/null +++ b/Frontend/erp-system/components/receiving/GrnPaymentDialog.tsx @@ -0,0 +1,145 @@ +"use client" + +import { useEffect, useState } from "react" + +import { bankAccountsApi } from "@/lib/api/general-ledger" +import { grnsApi } from "@/lib/api/grns" +import { errorMessage } from "@/lib/error-map" +import { formatAmount } from "@/lib/format" +import { CashAndBankAccountDto } from "@/types/general-ledger" +import { Grn, GrnPayment } from "@/types/grn" + +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { toast } from "@/components/ui/toast" + +interface GrnPaymentDialogProps { + grn: Grn | null + open: boolean + onOpenChange: (open: boolean) => void + onPaid: (grn: Grn, payment: GrnPayment) => void +} + +/** Pay the vendor against a confirmed GRN's balance — full or partial, from an existing + * GL cash/bank account. Modeled on ReceivedChequeDialog's "pick an account, submit" shape. */ +export function GrnPaymentDialog({ grn, open, onOpenChange, onPaid }: GrnPaymentDialogProps) { + const [accounts, setAccounts] = useState(null) + const [glBankAccountId, setGlBankAccountId] = useState("") + const [amount, setAmount] = useState("") + const [reference, setReference] = useState("") + const [errors, setErrors] = useState>({}) + const [submitting, setSubmitting] = useState(false) + + // Reset the form fields for a fresh open (or a different GRN) during render, React's own + // sanctioned "adjust state while rendering" pattern (see the General Ledger report page's + // periodKey comment) — not inside the effect below, which would be a synchronous + // setState-in-effect (react-hooks/set-state-in-effect). + const openKey = open && grn ? `${grn.grnId}` : null + const [resetFor, setResetFor] = useState(null) + if (openKey !== null && resetFor !== openKey) { + setResetFor(openKey) + setAmount(grn!.balanceAmount.toFixed(2)) + setGlBankAccountId("") + setReference("") + setErrors({}) + } + + useEffect(() => { + if (!open || accounts !== null) return + bankAccountsApi.list("Both").then(setAccounts).catch(() => setAccounts([])) + }, [open, accounts]) + + if (!grn) return null + + async function submit() { + if (!grn) return + const nextErrors: Record = {} + const amountNum = Number(amount) + if (!glBankAccountId) nextErrors.glBankAccountId = "Select an account to pay from" + if (!amount || Number.isNaN(amountNum) || amountNum <= 0) nextErrors.amount = "Enter a valid amount" + else if (amountNum > grn.balanceAmount) nextErrors.amount = `Cannot exceed the balance (${formatAmount(grn.balanceAmount)})` + setErrors(nextErrors) + if (Object.keys(nextErrors).length > 0) return + + setSubmitting(true) + try { + const payment = await grnsApi.pay(grn.grnId, { + amount: amountNum, + glBankAccountId: Number(glBankAccountId), + reference: reference || undefined, + }) + toast.success("Payment recorded", `${formatAmount(amountNum)} posted to the ledger (${payment.glJournalNo ?? "—"}).`) + onPaid(grn, payment) + onOpenChange(false) + } catch (err) { + toast.error("Could not record payment", errorMessage(err)) + } finally { + setSubmitting(false) + } + } + + return ( + + + + Pay GRN {grn.docNo} + + +

+ Balance due: {formatAmount(grn.balanceAmount)} +

+ + + + Pay from + value={glBankAccountId} onValueChange={(v) => setGlBankAccountId(v ?? "")}> + + + + + {(accounts ?? []).map((a) => ( + + {a.accountName} ({a.accountType}) + + ))} + + + + + + + Amount + setAmount(e.target.value)} + aria-invalid={!!errors.amount} + /> + + + + + Reference (optional) + setReference(e.target.value)} /> + + + + + + + +
+
+ ) +} diff --git a/Frontend/erp-system/components/receiving/status-badges.tsx b/Frontend/erp-system/components/receiving/status-badges.tsx index 24eed3d..11409a4 100644 --- a/Frontend/erp-system/components/receiving/status-badges.tsx +++ b/Frontend/erp-system/components/receiving/status-badges.tsx @@ -32,3 +32,32 @@ export function HoldStatusBadge({ status }: { status: HoldStatus }) { ) } + +export type GrnPaymentStatus = "Unpaid" | "PartiallyPaid" | "Paid" + +export function grnPaymentStatus(paidAmount: number, balanceAmount: number): GrnPaymentStatus { + if (balanceAmount <= 0) return "Paid" + if (paidAmount > 0) return "PartiallyPaid" + return "Unpaid" +} + +function paymentClass(status: GrnPaymentStatus) { + 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: "Partial", + Paid: "Paid", +} + +export function GrnPaymentStatusBadge({ paidAmount, balanceAmount }: { paidAmount: number; balanceAmount: number }) { + const status = grnPaymentStatus(paidAmount, balanceAmount) + return ( + + {paymentLabel[status]} + + ) +} diff --git a/Frontend/erp-system/lib/api/grns.ts b/Frontend/erp-system/lib/api/grns.ts index 9a7cba0..5954cb6 100644 --- a/Frontend/erp-system/lib/api/grns.ts +++ b/Frontend/erp-system/lib/api/grns.ts @@ -10,8 +10,10 @@ import { apiRequest, buildQuery } from "@/lib/api-client" import { PagedResponse } from "@/types/common" import { ConfirmGrnResponse, + CreateGrnPaymentRequest, CreateGrnRequest, Grn, + GrnPayment, GrnStatus, GrnSummary, ReleaseAction, @@ -58,4 +60,14 @@ export const grnsApi = { body: { action }, }) }, + + /** Pay the vendor against the GRN's balance, in full or in installments. Posts a real GL journal entry. */ + pay(grnId: number, request: CreateGrnPaymentRequest): Promise { + return apiRequest(`/grns/${grnId}/payments`, { method: "POST", body: request }) + }, + + /** Payment history for a GRN, newest first. */ + listPayments(grnId: number): Promise { + return apiRequest(`/grns/${grnId}/payments`) + }, } diff --git a/Frontend/erp-system/types/general-ledger.ts b/Frontend/erp-system/types/general-ledger.ts index 9e7e0af..a148d6f 100644 --- a/Frontend/erp-system/types/general-ledger.ts +++ b/Frontend/erp-system/types/general-ledger.ts @@ -288,6 +288,8 @@ export interface CashAndBankAccountDto { cashAccountTypeName: string | null accountNumber: string | null glAccountId: number + /** The underlying GL account's business code (added GL Phase 38, 2026-08-12). */ + glAccountCode: string currencyCode: string createdAt: string } diff --git a/Frontend/erp-system/types/grn.ts b/Frontend/erp-system/types/grn.ts index 5348837..d92342c 100644 --- a/Frontend/erp-system/types/grn.ts +++ b/Frontend/erp-system/types/grn.ts @@ -100,6 +100,10 @@ export interface Grn { createdBy: number createdAt: string postedAt: string | null + /** Journal number of the real GL journal entry posted for this GRN's receipt (set on confirm). */ + glJournalNo: string | null + paidAmount: number + balanceAmount: number lines: GrnLine[] } @@ -114,6 +118,8 @@ export interface GrnSummary { createdAt: string postedAt: string | null lineCount: number + paidAmount: number + balanceAmount: number } /** docs/11 §4.2 confirm response. */ @@ -132,6 +138,8 @@ export interface ConfirmGrnResponse { grnId: number status: GrnStatus postedAt: string + glJournalNo: string + balanceAmount: number createdLayers: CreatedLayer[] ledgerRefs: number[] poStatus: PurchaseOrderStatus | null @@ -147,3 +155,23 @@ export interface ReleaseGrnLineResponse { grnLineId: number holdStatus: HoldStatus } + +/** A vendor payment against a confirmed GRN's balance (installments allowed). */ +export interface GrnPayment { + grnPaymentId: number + grnId: number + amount: number + paymentDate: string + glBankAccountId: number + bankAccountName: string + reference: string | null + /** Journal number of the real GL journal entry this payment posted. */ + glJournalNo: string | null + createdAt: string +} + +export interface CreateGrnPaymentRequest { + amount: number + glBankAccountId: number + reference?: string | null +}