From bebec10eb379ddb499388d6f493fc0ae75b799d1 Mon Sep 17 00:00:00 2001 From: Sasanka20 Date: Tue, 1 Sep 2026 15:25:32 +0530 Subject: [PATCH] feat: add purchase invoice batch print page and related components - Implemented PurchaseInvoiceBatchPrintPage for printing purchase invoices. - Created InvoicePrintHeader and InvoicePrintFooter components for invoice layout. - Added invoice layout presets for different styles (Classic, Compact, Modern). - Developed PurchaseInvoicePaymentDialog for handling payments against invoices. - Introduced API mocks for company profile and invoice template configuration. - Added validation functions for company profile and purchase invoices. - Defined types for company profile, invoice template configuration, and purchase invoices. --- Backend/ERPCore/Controllers/GrnsController.cs | 23 +- .../Controllers/PurchaseInvoicesController.cs | 102 + Backend/ERPCore/Domain/DocumentTypes.cs | 1 + Backend/ERPCore/Domain/Entities/Grn.cs | 10 +- .../Domain/Entities/PurchaseInvoice.cs | 50 + .../Domain/Entities/PurchaseInvoiceLine.cs | 21 + ...rnPayment.cs => PurchaseInvoicePayment.cs} | 15 +- .../Domain/Entities/SalesInvoicePayment.cs | 2 +- .../Domain/Enums/PurchaseInvoiceStatus.cs | 8 + Backend/ERPCore/Dtos/Grn/GrnDtos.cs | 20 +- .../Dtos/Procurement/PurchaseInvoiceDtos.cs | 50 + .../Configurations/GrnConfiguration.cs | 24 +- .../Configurations/PermissionConfiguration.cs | 5 +- .../PurchaseInvoiceConfiguration.cs | 97 + .../Configurations/SubNavItemConfiguration.cs | 7 +- .../ERPCore/Infra/Persistence/ErpDbContext.cs | 6 +- ...nTemplatesSubNavItemIdConflict.Designer.cs | 24 + ...ProductionTemplatesSubNavItemIdConflict.cs | 46 + ...0831060800_AddPurchaseInvoices.Designer.cs | 7860 +++++++++++++++++ .../20260831060800_AddPurchaseInvoices.cs | 303 + .../Migrations/ErpDbContextModelSnapshot.cs | 357 +- Backend/ERPCore/Program.cs | 5 +- Backend/ERPCore/Services/GrnPaymentService.cs | 120 - Backend/ERPCore/Services/GrnService.cs | 12 +- .../Services/Interfaces/IGrnPaymentService.cs | 10 - .../IPurchaseInvoicePaymentService.cs | 10 + .../Interfaces/IPurchaseInvoiceService.cs | 20 + .../Services/PurchaseInvoicePaymentService.cs | 119 + .../Services/PurchaseInvoiceService.cs | 303 + .../Services/SalesInvoicePaymentService.cs | 4 +- Backend/ERPCore/System/Errors/ErrorCodes.cs | 15 +- Backend/ERPCore/appsettings.json | 4 + .../components/procurement/status-badges.tsx | 96 + .../purchase-invoices/[id]/page.tsx | 298 + .../purchase-invoices/new/page.tsx | 337 + .../procurement/purchase-invoices/page.tsx | 299 + .../app/dashboard/receiving/grn/[id]/page.tsx | 149 +- .../app/dashboard/receiving/grn/new/page.tsx | 172 +- .../app/dashboard/receiving/grn/page.tsx | 17 +- .../dashboard/sales/invoices/[id]/page.tsx | 70 +- .../settings/company-profile/page.tsx | 294 +- .../dashboard/settings/configuration/page.tsx | 54 + .../settings/invoice-template/page.tsx | 275 + .../app/dashboard/settings/page.tsx | 14 +- Frontend/erp-system/app/globals.css | 4 + .../purchase-invoices/[id]/page.tsx | 155 + .../procurement/purchase-invoices/page.tsx | 137 + .../app/print/sales/invoices/[id]/page.tsx | 112 +- .../app/print/sales/invoices/page.tsx | 43 +- .../components/Layouts/AppSidebar.tsx | 2 +- .../components/Layouts/Breadcrumbs.tsx | 1 + .../invoice-template/InvoicePrintFooter.tsx | 52 + .../invoice-template/InvoicePrintHeader.tsx | 53 + .../invoice-template/invoiceLayoutPresets.ts | 97 + .../PurchaseInvoicePaymentDialog.tsx} | 90 +- .../components/procurement/status-badges.tsx | 51 + .../components/receiving/status-badges.tsx | 29 - .../sales/SalesInvoicePaymentDialog.tsx | 5 +- .../erp-system/components/ui/textarea.tsx | 18 + .../erp-system/lib/api/company-profile.ts | 94 + Frontend/erp-system/lib/api/grns.ts | 12 - .../lib/api/invoice-template-config.ts | 62 + .../erp-system/lib/api/purchase-invoices.ts | 67 + Frontend/erp-system/lib/error-map.ts | 6 + .../lib/validations/company-profile.ts | 22 + .../lib/validations/purchase-invoices.ts | 21 + Frontend/erp-system/types/company-profile.ts | 27 + Frontend/erp-system/types/grn.ts | 32 +- Frontend/erp-system/types/invoice-template.ts | 21 + .../erp-system/types/purchase-invoices.ts | 94 + 70 files changed, 12446 insertions(+), 589 deletions(-) create mode 100644 Backend/ERPCore/Controllers/PurchaseInvoicesController.cs create mode 100644 Backend/ERPCore/Domain/Entities/PurchaseInvoice.cs create mode 100644 Backend/ERPCore/Domain/Entities/PurchaseInvoiceLine.cs rename Backend/ERPCore/Domain/Entities/{GrnPayment.cs => PurchaseInvoicePayment.cs} (62%) create mode 100644 Backend/ERPCore/Domain/Enums/PurchaseInvoiceStatus.cs create mode 100644 Backend/ERPCore/Dtos/Procurement/PurchaseInvoiceDtos.cs create mode 100644 Backend/ERPCore/Infra/Persistence/Configurations/PurchaseInvoiceConfiguration.cs create mode 100644 Backend/ERPCore/Migrations/20260825000000_FixProductionTemplatesSubNavItemIdConflict.Designer.cs create mode 100644 Backend/ERPCore/Migrations/20260825000000_FixProductionTemplatesSubNavItemIdConflict.cs create mode 100644 Backend/ERPCore/Migrations/20260831060800_AddPurchaseInvoices.Designer.cs create mode 100644 Backend/ERPCore/Migrations/20260831060800_AddPurchaseInvoices.cs delete mode 100644 Backend/ERPCore/Services/GrnPaymentService.cs delete mode 100644 Backend/ERPCore/Services/Interfaces/IGrnPaymentService.cs create mode 100644 Backend/ERPCore/Services/Interfaces/IPurchaseInvoicePaymentService.cs create mode 100644 Backend/ERPCore/Services/Interfaces/IPurchaseInvoiceService.cs create mode 100644 Backend/ERPCore/Services/PurchaseInvoicePaymentService.cs create mode 100644 Backend/ERPCore/Services/PurchaseInvoiceService.cs create mode 100644 Frontend/erp-system/Frontend/erp-system/components/procurement/status-badges.tsx create mode 100644 Frontend/erp-system/app/dashboard/procurement/purchase-invoices/[id]/page.tsx create mode 100644 Frontend/erp-system/app/dashboard/procurement/purchase-invoices/new/page.tsx create mode 100644 Frontend/erp-system/app/dashboard/procurement/purchase-invoices/page.tsx create mode 100644 Frontend/erp-system/app/dashboard/settings/configuration/page.tsx create mode 100644 Frontend/erp-system/app/dashboard/settings/invoice-template/page.tsx create mode 100644 Frontend/erp-system/app/print/procurement/purchase-invoices/[id]/page.tsx create mode 100644 Frontend/erp-system/app/print/procurement/purchase-invoices/page.tsx create mode 100644 Frontend/erp-system/components/invoice-template/InvoicePrintFooter.tsx create mode 100644 Frontend/erp-system/components/invoice-template/InvoicePrintHeader.tsx create mode 100644 Frontend/erp-system/components/invoice-template/invoiceLayoutPresets.ts rename Frontend/erp-system/components/{receiving/GrnPaymentDialog.tsx => procurement/PurchaseInvoicePaymentDialog.tsx} (51%) create mode 100644 Frontend/erp-system/components/ui/textarea.tsx create mode 100644 Frontend/erp-system/lib/api/company-profile.ts create mode 100644 Frontend/erp-system/lib/api/invoice-template-config.ts create mode 100644 Frontend/erp-system/lib/api/purchase-invoices.ts create mode 100644 Frontend/erp-system/lib/validations/company-profile.ts create mode 100644 Frontend/erp-system/lib/validations/purchase-invoices.ts create mode 100644 Frontend/erp-system/types/company-profile.ts create mode 100644 Frontend/erp-system/types/invoice-template.ts create mode 100644 Frontend/erp-system/types/purchase-invoices.ts diff --git a/Backend/ERPCore/Controllers/GrnsController.cs b/Backend/ERPCore/Controllers/GrnsController.cs index 78337b9..bb5e8c2 100644 --- a/Backend/ERPCore/Controllers/GrnsController.cs +++ b/Backend/ERPCore/Controllers/GrnsController.cs @@ -11,12 +11,10 @@ namespace ERPCore.Controllers; public sealed class GrnsController : ApiControllerBase { private readonly IGrnService _grns; - private readonly IGrnPaymentService _payments; - public GrnsController(IGrnService grns, IGrnPaymentService payments) + public GrnsController(IGrnService grns) { _grns = grns; - _payments = payments; } /// List GRNs, newest first. @@ -64,23 +62,4 @@ 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/Controllers/PurchaseInvoicesController.cs b/Backend/ERPCore/Controllers/PurchaseInvoicesController.cs new file mode 100644 index 0000000..35a8696 --- /dev/null +++ b/Backend/ERPCore/Controllers/PurchaseInvoicesController.cs @@ -0,0 +1,102 @@ +using ERPCore.Domain.Enums; +using ERPCore.Dtos.Common; +using ERPCore.Dtos.Grn; +using ERPCore.Dtos.Procurement; +using ERPCore.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace ERPCore.Controllers; + +/// Purchase invoice endpoints — vendor invoices consolidating Confirmed GRNs. +[Route("api/v1/purchase-invoices")] +public sealed class PurchaseInvoicesController : ApiControllerBase +{ + private readonly IPurchaseInvoiceService _invoices; + private readonly IPurchaseInvoicePaymentService _payments; + + public PurchaseInvoicesController(IPurchaseInvoiceService invoices, IPurchaseInvoicePaymentService payments) + { + _invoices = invoices; + _payments = payments; + } + + [HttpGet] + [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)] + public async Task>> List( + [FromQuery] PageQuery query, [FromQuery] PurchaseInvoiceStatus? status, [FromQuery] int? vendorId, CancellationToken ct) + => Ok(await _invoices.ListAsync(query, status, vendorId, ct)); + + /// Confirmed, not-yet-invoiced GRNs for a vendor — candidates for a new/edited invoice. + [HttpGet("eligible-grns")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task>> EligibleGrns([FromQuery] int vendorId, CancellationToken ct) + => Ok(await _invoices.ListEligibleGrnsAsync(vendorId, ct)); + + [HttpGet("{purchaseInvoiceId:int}")] + [ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetById(int purchaseInvoiceId, CancellationToken ct) + { + var result = await _invoices.GetAsync(purchaseInvoiceId, ct); + if (result is null) return NotFound(); + SetETag(result.RowVersion); + return Ok(result.Value); + } + + [HttpPost] + [ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> Create([FromBody] CreatePurchaseInvoiceRequest request, CancellationToken ct) + { + var result = await _invoices.CreateAsync(request, ct); + SetETag(result.RowVersion); + return Created($"/api/v1/purchase-invoices/{result.Value.PurchaseInvoiceId}", result.Value); + } + + [HttpPut("{purchaseInvoiceId:int}")] + [ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status412PreconditionFailed)] + public async Task> Update(int purchaseInvoiceId, [FromBody] UpdatePurchaseInvoiceRequest request, CancellationToken ct) + { + var expected = RequireIfMatch(); + var result = await _invoices.UpdateAsync(purchaseInvoiceId, request, expected, ct); + SetETag(result.RowVersion); + return Ok(result.Value); + } + + [HttpPost("{purchaseInvoiceId:int}/post")] + [ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> Post(int purchaseInvoiceId, CancellationToken ct) + => Ok(await _invoices.PostAsync(purchaseInvoiceId, ct)); + + [HttpPost("{purchaseInvoiceId:int}/cancel")] + [ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> Cancel(int purchaseInvoiceId, CancellationToken ct) + => Ok(await _invoices.CancelAsync(purchaseInvoiceId, ct)); + + /// Pay the vendor against this invoice's balance, in full or in installments; posts a real GL journal entry. + [HttpPost("{purchaseInvoiceId:int}/payments")] + [ProducesResponseType(typeof(PurchaseInvoicePaymentDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> Pay(int purchaseInvoiceId, [FromBody] CreatePurchaseInvoicePaymentRequest request, CancellationToken ct) + { + var dto = await _payments.PayAsync(purchaseInvoiceId, request, ct); + return Created($"/api/v1/purchase-invoices/{purchaseInvoiceId}/payments/{dto.PurchaseInvoicePaymentId}", dto); + } + + /// Payment history for this invoice, newest first. + [HttpGet("{purchaseInvoiceId:int}/payments")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> ListPayments(int purchaseInvoiceId, CancellationToken ct) + => Ok(await _payments.ListAsync(purchaseInvoiceId, ct)); +} diff --git a/Backend/ERPCore/Domain/DocumentTypes.cs b/Backend/ERPCore/Domain/DocumentTypes.cs index 6f39a9b..f00ad99 100644 --- a/Backend/ERPCore/Domain/DocumentTypes.cs +++ b/Backend/ERPCore/Domain/DocumentTypes.cs @@ -10,6 +10,7 @@ public static class DocumentTypes public const string Rfq = "RFQ"; public const string PurchaseOrder = "PO"; public const string Grn = "GRN"; + public const string PurchaseInvoice = "PINV"; public const string Transfer = "TRF"; public const string Adjustment = "ADJ"; public const string Count = "CNT"; diff --git a/Backend/ERPCore/Domain/Entities/Grn.cs b/Backend/ERPCore/Domain/Entities/Grn.cs index 508a184..920d3d6 100644 --- a/Backend/ERPCore/Domain/Entities/Grn.cs +++ b/Backend/ERPCore/Domain/Entities/Grn.cs @@ -34,14 +34,14 @@ public class Grn 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; } + /// The Purchase Invoice this GRN has been consolidated into, if any (null until + /// invoiced). Vendor settlement is tracked on the invoice, not per-GRN — see + /// . + public int? PurchaseInvoiceId { get; set; } + public PurchaseInvoice? PurchaseInvoice { 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/PurchaseInvoice.cs b/Backend/ERPCore/Domain/Entities/PurchaseInvoice.cs new file mode 100644 index 0000000..2e73d7f --- /dev/null +++ b/Backend/ERPCore/Domain/Entities/PurchaseInvoice.cs @@ -0,0 +1,50 @@ +using ERPCore.Domain.Enums; + +namespace ERPCore.Domain.Entities; + +/// +/// Vendor invoice consolidating one or more Confirmed GRNs for the same vendor +/// (each linked GRN becomes one , not re-itemized). +/// Draft is editable (GRNs can be re-selected); Posted reclassifies the GRN clearing +/// balance to Accounts Payable (see PurchaseInvoiceService.PostAsync); vendor payments +/// are then recorded against this aggregate rather than against the individual GRNs. +/// +public class PurchaseInvoice +{ + public int PurchaseInvoiceId { get; set; } + public string DocNo { get; set; } = string.Empty; + + public string VendorInvoiceNo { get; set; } = string.Empty; + public DateTime VendorInvoiceDate { get; set; } + + public int VendorId { get; set; } + public Vendor? Vendor { get; set; } + + public string VendorSnapshotName { get; set; } = string.Empty; + public string? VendorSnapshotTaxNo { get; set; } + + public PurchaseInvoiceStatus Status { get; set; } = PurchaseInvoiceStatus.Draft; + + public decimal Subtotal { get; set; } + public decimal VatTotal { get; set; } + public decimal GrandTotal { get; set; } + public decimal PaidAmount { get; set; } + public decimal BalanceAmount { get; set; } + + /// Journal number of the AP-recognition GL journal entry posted when this + /// invoice is Posted (Debit GRN Clearing / Credit Accounts Payable) — see + /// PurchaseInvoiceService.PostAsync. + public string? GlJournalNo { get; set; } + public DateTime? GlPostedAt { get; set; } + + public int CreatedBy { get; set; } + public User? Creator { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + + 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/PurchaseInvoiceLine.cs b/Backend/ERPCore/Domain/Entities/PurchaseInvoiceLine.cs new file mode 100644 index 0000000..a652660 --- /dev/null +++ b/Backend/ERPCore/Domain/Entities/PurchaseInvoiceLine.cs @@ -0,0 +1,21 @@ +namespace ERPCore.Domain.Entities; + +/// +/// One row per consolidated GRN on a — a straight +/// consolidation of that GRN's line totals, not a re-itemization (docs: Purchase +/// Invoice consolidates GRNs, it does not re-enter item lines). +/// +public class PurchaseInvoiceLine +{ + public int PurchaseInvoiceLineId { get; set; } + + public int PurchaseInvoiceId { get; set; } + public PurchaseInvoice? PurchaseInvoice { get; set; } + + public int GrnId { get; set; } + public Grn? Grn { get; set; } + + public decimal Subtotal { get; set; } + public decimal VatTotal { get; set; } + public decimal LineTotal { get; set; } +} diff --git a/Backend/ERPCore/Domain/Entities/GrnPayment.cs b/Backend/ERPCore/Domain/Entities/PurchaseInvoicePayment.cs similarity index 62% rename from Backend/ERPCore/Domain/Entities/GrnPayment.cs rename to Backend/ERPCore/Domain/Entities/PurchaseInvoicePayment.cs index 161f71a..1a65cac 100644 --- a/Backend/ERPCore/Domain/Entities/GrnPayment.cs +++ b/Backend/ERPCore/Domain/Entities/PurchaseInvoicePayment.cs @@ -1,16 +1,17 @@ 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. +/// A vendor payment made against a Posted purchase invoice's balance (installments +/// allowed — see PurchaseInvoicePaymentService.PayAsync). Posts its own real GL +/// journal entry (Debit Accounts Payable / Credit the selected bank-or-cash account) +/// before being recorded here. /// -public class GrnPayment +public class PurchaseInvoicePayment { - public int GrnPaymentId { get; set; } + public int PurchaseInvoicePaymentId { get; set; } - public int GrnId { get; set; } - public Grn? Grn { get; set; } + public int PurchaseInvoiceId { get; set; } + public PurchaseInvoice? PurchaseInvoice { get; set; } public decimal Amount { get; set; } public DateTime PaymentDate { get; set; } diff --git a/Backend/ERPCore/Domain/Entities/SalesInvoicePayment.cs b/Backend/ERPCore/Domain/Entities/SalesInvoicePayment.cs index adb2386..27705b3 100644 --- a/Backend/ERPCore/Domain/Entities/SalesInvoicePayment.cs +++ b/Backend/ERPCore/Domain/Entities/SalesInvoicePayment.cs @@ -4,7 +4,7 @@ namespace ERPCore.Domain.Entities; /// A customer payment made against a Posted sales invoice's balance (installments /// allowed — see SalesInvoicePaymentService.PayAsync). Posts its own real GL journal /// entry (Debit the selected bank-or-cash account / Credit Accounts Receivable) before -/// being recorded here — the mirror image of . +/// being recorded here — the mirror image of . /// public class SalesInvoicePayment { diff --git a/Backend/ERPCore/Domain/Enums/PurchaseInvoiceStatus.cs b/Backend/ERPCore/Domain/Enums/PurchaseInvoiceStatus.cs new file mode 100644 index 0000000..ee85b40 --- /dev/null +++ b/Backend/ERPCore/Domain/Enums/PurchaseInvoiceStatus.cs @@ -0,0 +1,8 @@ +namespace ERPCore.Domain.Enums; + +public enum PurchaseInvoiceStatus +{ + Draft, + Posted, + Cancelled +} diff --git a/Backend/ERPCore/Dtos/Grn/GrnDtos.cs b/Backend/ERPCore/Dtos/Grn/GrnDtos.cs index f9120bf..9d6c7e8 100644 --- a/Backend/ERPCore/Dtos/Grn/GrnDtos.cs +++ b/Backend/ERPCore/Dtos/Grn/GrnDtos.cs @@ -17,34 +17,28 @@ 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, string? GlJournalNo, - decimal PaidAmount, decimal BalanceAmount, IReadOnlyList Lines); + int? PurchaseInvoiceId, string? PurchaseInvoiceNo, 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, - decimal PaidAmount, decimal BalanceAmount); + int? PurchaseInvoiceId, string? PurchaseInvoiceNo); 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, string GlJournalNo, decimal BalanceAmount, + int GrnId, GrnStatus Status, DateTime PostedAt, string GlJournalNo, 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; } -} +/// A Confirmed, not-yet-invoiced GRN eligible to be consolidated onto a Purchase Invoice. +public sealed record GrnEligibleForInvoiceDto( + int GrnId, string DocNo, DateTime PostedAt, int WarehouseId, + int LineCount, decimal Subtotal, decimal VatTotal, decimal GrandTotal); // Requests ---------------------------------------------------------------------- diff --git a/Backend/ERPCore/Dtos/Procurement/PurchaseInvoiceDtos.cs b/Backend/ERPCore/Dtos/Procurement/PurchaseInvoiceDtos.cs new file mode 100644 index 0000000..d53b078 --- /dev/null +++ b/Backend/ERPCore/Dtos/Procurement/PurchaseInvoiceDtos.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; +using ERPCore.Domain.Enums; + +namespace ERPCore.Dtos.Procurement; + +public sealed record PurchaseInvoiceLineDto( + int PurchaseInvoiceLineId, int GrnId, string GrnDocNo, DateTime GrnPostedAt, + decimal Subtotal, decimal VatTotal, decimal LineTotal); + +public sealed record PurchaseInvoiceTotalsDto( + decimal Subtotal, decimal VatTotal, decimal GrandTotal, decimal PaidAmount, decimal BalanceAmount); + +public sealed record PurchaseInvoiceDto( + int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate, + int VendorId, string VendorSnapshotName, string? VendorSnapshotTaxNo, + PurchaseInvoiceStatus Status, int GrnCount, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt, + string? GlJournalNo, DateTime? GlPostedAt, + PurchaseInvoiceTotalsDto Totals, IReadOnlyList Lines); + +public sealed record PurchaseInvoiceSummaryDto( + int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate, + int VendorId, string VendorSnapshotName, PurchaseInvoiceStatus Status, int GrnCount, + PurchaseInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo); + +public sealed record PurchaseInvoicePaymentDto( + int PurchaseInvoicePaymentId, int PurchaseInvoiceId, decimal Amount, DateTime PaymentDate, + long GlBankAccountId, string BankAccountName, string? Reference, string? GlJournalNo, DateTime CreatedAt); + +public sealed class CreatePurchaseInvoicePaymentRequest +{ + [Range(0.01, double.MaxValue)] public decimal Amount { get; set; } + [Required] public long GlBankAccountId { get; set; } + [StringLength(100)] public string? Reference { get; set; } +} + +public sealed class CreatePurchaseInvoiceRequest +{ + [Required] public int VendorId { get; set; } + [Required, StringLength(50)] public string VendorInvoiceNo { get; set; } = string.Empty; + [Required] public DateTime VendorInvoiceDate { get; set; } + [Required, MinLength(1)] public List GrnIds { get; set; } = new(); +} + +public sealed class UpdatePurchaseInvoiceRequest +{ + [Required] public int VendorId { get; set; } + [Required, StringLength(50)] public string VendorInvoiceNo { get; set; } = string.Empty; + [Required] public DateTime VendorInvoiceDate { get; set; } + [Required, MinLength(1)] public List GrnIds { get; set; } = new(); +} diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/GrnConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/GrnConfiguration.cs index 5901e10..537922d 100644 --- a/Backend/ERPCore/Infra/Persistence/Configurations/GrnConfiguration.cs +++ b/Backend/ERPCore/Infra/Persistence/Configurations/GrnConfiguration.cs @@ -17,41 +17,19 @@ 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); builder.HasOne(g => g.Vendor).WithMany().HasForeignKey(g => g.VendorId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(g => g.Warehouse).WithMany().HasForeignKey(g => g.WarehouseId).OnDelete(DeleteBehavior.Restrict); builder.HasOne(g => g.Creator).WithMany().HasForeignKey(g => g.CreatedBy).OnDelete(DeleteBehavior.Restrict); + builder.HasOne(g => g.PurchaseInvoice).WithMany().HasForeignKey(g => g.PurchaseInvoiceId).OnDelete(DeleteBehavior.Restrict); builder.HasIndex(g => g.Status); builder.HasIndex(g => g.PoId); } } -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/Configurations/PermissionConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs index b5198bb..7395561 100644 --- a/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs +++ b/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs @@ -63,9 +63,10 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("purchase_invoices"); + builder.HasKey(x => x.PurchaseInvoiceId); + + builder.Property(x => x.DocNo).IsRequired().HasMaxLength(30); + builder.HasIndex(x => x.DocNo).IsUnique(); + + builder.Property(x => x.VendorInvoiceNo).IsRequired().HasMaxLength(50); + builder.Property(x => x.VendorInvoiceDate).IsRequired(); + // A vendor invoice number is only meaningful per-vendor; the service also excludes + // Cancelled invoices from the duplicate check, so this composite index is a coarse + // backstop (it does not distinguish Cancelled rows) rather than the sole enforcement. + builder.HasIndex(x => new { x.VendorId, x.VendorInvoiceNo }); + + builder.HasOne(x => x.Vendor) + .WithMany() + .HasForeignKey(x => x.VendorId) + .OnDelete(DeleteBehavior.Restrict); + + builder.Property(x => x.VendorSnapshotName).IsRequired().HasMaxLength(200); + builder.Property(x => x.VendorSnapshotTaxNo).HasMaxLength(50); + + builder.Property(x => x.Status) + .HasConversion().HasMaxLength(20).IsRequired() + .HasDefaultValue(PurchaseInvoiceStatus.Draft); + + foreach (var p in new[] { nameof(PurchaseInvoice.Subtotal), nameof(PurchaseInvoice.VatTotal), nameof(PurchaseInvoice.GrandTotal), nameof(PurchaseInvoice.PaidAmount), nameof(PurchaseInvoice.BalanceAmount) }) + builder.Property(p).HasPrecision(18, 4); + builder.Property(x => x.GlJournalNo).HasMaxLength(30); + + builder.HasOne(x => x.Creator).WithMany().HasForeignKey(x => x.CreatedBy).OnDelete(DeleteBehavior.Restrict); + builder.Property(x => x.CreatedAt).IsRequired(); + builder.Property(x => x.RowVersion).IsRowVersion(); + + builder.HasIndex(x => x.Status); + builder.HasIndex(x => x.VendorId); + + builder.HasMany(x => x.Lines) + .WithOne(x => x.PurchaseInvoice) + .HasForeignKey(x => x.PurchaseInvoiceId) + .OnDelete(DeleteBehavior.Cascade); + } +} + +public sealed class PurchaseInvoiceLineConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("purchase_invoice_lines"); + builder.HasKey(x => x.PurchaseInvoiceLineId); + + builder.Property(x => x.Subtotal).HasPrecision(18, 4); + builder.Property(x => x.VatTotal).HasPrecision(18, 4); + builder.Property(x => x.LineTotal).HasPrecision(18, 4); + + builder.HasOne(x => x.Grn) + .WithMany() + .HasForeignKey(x => x.GrnId) + .OnDelete(DeleteBehavior.Restrict); + + // A GRN can be consolidated onto at most one active line at a time — cancelling the + // invoice removes its lines and frees the GRN (PurchaseInvoiceService.CancelAsync), so + // this stays enforceable rather than blocking a later re-invoice of the same GRN. + builder.HasIndex(x => x.GrnId).IsUnique(); + } +} + +public sealed class PurchaseInvoicePaymentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("purchase_invoice_payments"); + builder.HasKey(p => p.PurchaseInvoicePaymentId); + + 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.PurchaseInvoice).WithMany(x => x.Payments).HasForeignKey(p => p.PurchaseInvoiceId).OnDelete(DeleteBehavior.Restrict); + builder.HasOne(p => p.Creator).WithMany().HasForeignKey(p => p.CreatedBy).OnDelete(DeleteBehavior.Restrict); + + builder.HasIndex(p => p.PurchaseInvoiceId); + } +} diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/SubNavItemConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/SubNavItemConfiguration.cs index 37dac63..b1e35ba 100644 --- a/Backend/ERPCore/Infra/Persistence/Configurations/SubNavItemConfiguration.cs +++ b/Backend/ERPCore/Infra/Persistence/Configurations/SubNavItemConfiguration.cs @@ -42,6 +42,7 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration Grns => Set(); public DbSet GrnLines => Set(); - public DbSet GrnPayments => Set(); + + // --- Purchase Invoices (vendor invoices consolidating Confirmed GRNs) --- + public DbSet PurchaseInvoices => Set(); + public DbSet PurchaseInvoiceLines => Set(); + public DbSet PurchaseInvoicePayments => Set(); // --- Batch / Serial (docs/10 Part C.4) --- public DbSet Batches => Set(); diff --git a/Backend/ERPCore/Migrations/20260825000000_FixProductionTemplatesSubNavItemIdConflict.Designer.cs b/Backend/ERPCore/Migrations/20260825000000_FixProductionTemplatesSubNavItemIdConflict.Designer.cs new file mode 100644 index 0000000..51431e7 --- /dev/null +++ b/Backend/ERPCore/Migrations/20260825000000_FixProductionTemplatesSubNavItemIdConflict.Designer.cs @@ -0,0 +1,24 @@ +// +using ERPCore.Infra.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ERPCore.Migrations +{ + [DbContext(typeof(ErpDbContext))] + [Migration("20260825000000_FixProductionTemplatesSubNavItemIdConflict")] + partial class FixProductionTemplatesSubNavItemIdConflict + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + // This migration only repairs seed row data via raw SQL — no entity/column shape + // changes — so it does not redeclare the full model here. The single source of + // truth for scaffolding future migrations is ErpDbContextModelSnapshot.cs, which + // already reflects this fix (production.templates seeded at SubNavItemId 27). + } + } +} diff --git a/Backend/ERPCore/Migrations/20260825000000_FixProductionTemplatesSubNavItemIdConflict.cs b/Backend/ERPCore/Migrations/20260825000000_FixProductionTemplatesSubNavItemIdConflict.cs new file mode 100644 index 0000000..1e16d06 --- /dev/null +++ b/Backend/ERPCore/Migrations/20260825000000_FixProductionTemplatesSubNavItemIdConflict.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ERPCore.Migrations +{ + /// + /// Repairs a collision baked into migration history: 20260818090139_AddProductionNavPermissions + /// inserted "production.templates" as sub_nav_items row 24, then + /// 20260820100345_AddNotificationConfigModulePreferences reused the same literal id 24 for + /// "settings.notification-config" via a raw-SQL INSERT ... ON CONFLICT ("SubNavItemId") DO + /// UPDATE, silently overwriting the production.templates row instead of inserting a new + /// one. Row 24 now correctly holds notification-config; this migration re-creates the lost + /// production.templates row under a fresh id (27, matching the corrected seed data in + /// SubNavItemConfiguration/PermissionConfiguration) and repoints the NAV:production.templates + /// permission (id 36) at it. + /// + public partial class FixProductionTemplatesSubNavItemIdConflict : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + INSERT INTO sub_nav_items ("SubNavItemId", "NavItemId", "Code", "Href", "Icon", "Label", "SortOrder") + VALUES (27, 14, 'production.templates', '/dashboard/production/templates', NULL, 'Templates', 1) + ON CONFLICT ("SubNavItemId") DO NOTHING; + """); + + migrationBuilder.Sql(""" + UPDATE permissions SET "SubNavItemId" = 27 WHERE "PermissionId" = 36; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + UPDATE permissions SET "SubNavItemId" = 24 WHERE "PermissionId" = 36; + """); + + migrationBuilder.Sql(""" + DELETE FROM sub_nav_items WHERE "SubNavItemId" = 27; + """); + } + } +} diff --git a/Backend/ERPCore/Migrations/20260831060800_AddPurchaseInvoices.Designer.cs b/Backend/ERPCore/Migrations/20260831060800_AddPurchaseInvoices.Designer.cs new file mode 100644 index 0000000..54830fd --- /dev/null +++ b/Backend/ERPCore/Migrations/20260831060800_AddPurchaseInvoices.Designer.cs @@ -0,0 +1,7860 @@ +// +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("20260831060800_AddPurchaseInvoices")] + partial class AddPurchaseInvoices + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceRecord", b => + { + b.Property("AttendanceRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AttendanceRecordId")); + + b.Property("AttendanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("AttendanceStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("AttendanceUploadBatchId") + .HasColumnType("integer"); + + b.Property("CheckIn") + .HasColumnType("interval"); + + b.Property("CheckOut") + .HasColumnType("interval"); + + b.Property("DuplicateOfAttendanceRecordId") + .HasColumnType("integer"); + + b.Property("EarlyLeaveMinutes") + .HasColumnType("integer"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EditedBy") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("IsManualOverride") + .HasColumnType("boolean"); + + b.Property("LateMinutes") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("OvertimeMinutes") + .HasColumnType("integer"); + + b.Property("RowValidationStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WorkShiftId") + .HasColumnType("integer"); + + b.Property("WorkingMinutes") + .HasColumnType("integer"); + + b.HasKey("AttendanceRecordId"); + + b.HasIndex("AttendanceUploadBatchId"); + + b.HasIndex("WorkShiftId"); + + b.HasIndex("EmployeeId", "AttendanceDate"); + + b.ToTable("hr_attendance_records", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceUploadBatch", b => + { + b.Property("AttendanceUploadBatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AttendanceUploadBatchId")); + + b.Property("ConfirmedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConfirmedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OriginalFileName") + .HasMaxLength(260) + .HasColumnType("character varying(260)"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("RowCountDuplicate") + .HasColumnType("integer"); + + b.Property("RowCountError") + .HasColumnType("integer"); + + b.Property("RowCountTotal") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedBy") + .HasColumnType("integer"); + + b.HasKey("AttendanceUploadBatchId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("hr_attendance_upload_batches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AuditLog", b => + { + b.Property("AuditId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AuditId")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("ChangeSet") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .HasColumnType("integer"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("AuditId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UserId"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("audit_logs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b => + { + b.Property("BatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BatchId")); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ExpiryDate") + .HasColumnType("date"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.HasKey("BatchId"); + + b.HasIndex("ItemId", "BatchNo") + .IsUnique(); + + b.ToTable("batches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b => + { + b.Property("BinId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BinId")); + + b.Property("BinType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("BinId"); + + b.HasIndex("WarehouseId", "Code") + .IsUnique(); + + b.ToTable("bins", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Branch", b => + { + b.Property("BranchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BranchId")); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BranchId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_branches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Brand", b => + { + b.Property("BrandId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BrandId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BrandId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("brands", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.Property("BundleSaleId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleId")); + + b.Property("BundleCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BundleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("BundleName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BundleNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BundlePrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("BundleSaleTemplateId") + .HasColumnType("integer"); + + b.Property("CashierUserId") + .HasColumnType("integer"); + + b.Property("ComponentSubtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DayEndId") + .HasColumnType("integer"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("MarginAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("BundleSaleId"); + + b.HasIndex("BundleNo") + .IsUnique(); + + b.HasIndex("BundleSaleTemplateId"); + + b.HasIndex("CashierUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DayEndId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sales", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleLine", b => + { + b.Property("BundleSaleLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleLineId")); + + b.Property("BundleSaleId") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IncludeInBundle") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsComponent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("BundleSaleLineId"); + + b.HasIndex("BundleSaleId"); + + b.HasIndex("ItemId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sale_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplate", b => + { + b.Property("BundleSaleTemplateId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleTemplateId")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TemplateName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BundleSaleTemplateId"); + + b.HasIndex("TemplateCode") + .IsUnique(); + + b.ToTable("bundle_sale_templates", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplateLine", b => + { + b.Property("BundleSaleTemplateLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleTemplateLineId")); + + b.Property("BundleSaleTemplateId") + .HasColumnType("integer"); + + b.Property("IncludeInBundle") + .HasColumnType("boolean"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("BundleSaleTemplateLineId"); + + b.HasIndex("BundleSaleTemplateId"); + + b.HasIndex("ItemId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sale_template_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CategoryId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CategoryId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Customer", b => + { + b.Property("CustomerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CustomerId")); + + b.Property("AddressLine1") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("AddressLine2") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Country") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditDays") + .HasColumnType("integer"); + + b.Property("CreditLimit") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CustomerCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CustomerType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("B2C"); + + b.Property("DefaultWarehouseId") + .HasColumnType("integer"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("TaxRegistrationNo") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CustomerId"); + + b.HasIndex("CustomerCode") + .IsUnique(); + + b.HasIndex("CustomerType"); + + b.HasIndex("DefaultWarehouseId"); + + b.HasIndex("Status"); + + b.ToTable("customers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Department", b => + { + b.Property("DepartmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DepartmentId")); + + b.Property("BranchId") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HeadEmployeeId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParentDepartmentId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DepartmentId"); + + b.HasIndex("BranchId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("HeadEmployeeId"); + + b.HasIndex("ParentDepartmentId"); + + b.HasIndex("Status"); + + b.ToTable("hr_departments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Designation", b => + { + b.Property("DesignationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DesignationId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DesignationId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_designations", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Employee", b => + { + b.Property("EmployeeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeId")); + + b.Property("AddressLine1") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AddressLine2") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BranchId") + .HasColumnType("integer"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ConfirmationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Country") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DateOfBirth") + .HasColumnType("timestamp with time zone"); + + b.Property("DepartmentId") + .HasColumnType("integer"); + + b.Property("DesignationId") + .HasColumnType("integer"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("EmergencyContactName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EmergencyContactRelationship") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmployeeCode") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EmploymentTypeId") + .HasColumnType("integer"); + + b.Property("EpfNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EtfNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Gender") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastWorkingDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Nationality") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Nic") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PersonalMobile") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PostalCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ProfilePhotoPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReportingManagerId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("TaxIdentificationNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WorkShiftId") + .HasColumnType("integer"); + + b.HasKey("EmployeeId"); + + b.HasIndex("BranchId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("DesignationId"); + + b.HasIndex("Email"); + + b.HasIndex("EmployeeCode") + .IsUnique(); + + b.HasIndex("EmploymentTypeId"); + + b.HasIndex("ReportingManagerId"); + + b.HasIndex("Status"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("WorkShiftId"); + + b.ToTable("hr_employees", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeBankDetail", b => + { + b.Property("EmployeeBankDetailId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeBankDetailId")); + + b.Property("AccountHolderName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AccountNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BankName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BranchName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("SwiftCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("EmployeeBankDetailId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("hr_employee_bank_details", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeDocument", b => + { + b.Property("EmployeeDocumentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeDocumentId")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HrDocumentTypeId") + .HasColumnType("integer"); + + b.Property("IssueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("character varying(260)"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("StoredFileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("character varying(260)"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedBy") + .HasColumnType("integer"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedBy") + .HasColumnType("integer"); + + b.HasKey("EmployeeDocumentId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ExpiryDate"); + + b.HasIndex("HrDocumentTypeId"); + + b.ToTable("hr_employee_documents", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.Property("EmployeeLoanId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeLoanId")); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("InstallmentAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("InterestRate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)"); + + b.Property("LoanKind") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("NumberOfInstallments") + .HasColumnType("integer"); + + b.Property("OutstandingBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PrincipalAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartMonth") + .HasColumnType("integer"); + + b.Property("StartYear") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("EmployeeLoanId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("EmployeeId"); + + b.HasIndex("Status"); + + b.ToTable("hr_employee_loans", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.Property("EmployeeSalaryStructureId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeSalaryStructureId")); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("integer"); + + b.Property("BasicSalary") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("EmployeeSalaryStructureId"); + + b.HasIndex("EmployeeId", "EffectiveTo"); + + b.ToTable("hr_employee_salary_structures", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructureLine", b => + { + b.Property("EmployeeSalaryStructureLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeSalaryStructureLineId")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeSalaryStructureId") + .HasColumnType("integer"); + + b.Property("SalaryComponentId") + .HasColumnType("integer"); + + b.HasKey("EmployeeSalaryStructureLineId"); + + b.HasIndex("EmployeeSalaryStructureId"); + + b.HasIndex("SalaryComponentId"); + + b.ToTable("hr_employee_salary_structure_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmploymentType", b => + { + b.Property("EmploymentTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmploymentTypeId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("EmploymentTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_employment_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.Property("GrnId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnId")); + + b.Property("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("PoId") + .HasColumnType("integer"); + + b.Property("PostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PurchaseInvoiceId") + .HasColumnType("integer"); + + 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("PurchaseInvoiceId"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("grns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.Property("GrnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnLineId")); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("BinId") + .HasColumnType("integer"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("GrnId") + .HasColumnType("integer"); + + b.Property("HoldStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("NetUnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("PoLineId") + .HasColumnType("integer"); + + b.Property("PoUnitPrice") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReceivedValue") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("VatAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("VatPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.HasKey("GrnLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("GrnId"); + + b.HasIndex("ItemId"); + + b.HasIndex("PoLineId"); + + b.ToTable("grn_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLineWarrantyNumber", b => + { + b.Property("GrnLineWarrantyNumberId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnLineWarrantyNumberId")); + + b.Property("GrnLineId") + .HasColumnType("integer"); + + b.Property("WarrantyNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("WarrantyPeriodMonths") + .HasColumnType("integer"); + + b.HasKey("GrnLineWarrantyNumberId"); + + b.HasIndex("GrnLineId", "WarrantyNo") + .IsUnique(); + + b.ToTable("grn_line_warranty_numbers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.HrDocumentType", b => + { + b.Property("HrDocumentTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HrDocumentTypeId")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiryTracked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiredAtOnboarding") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("HrDocumentTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_document_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => + { + b.Property("ItemId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ItemId")); + + b.Property("BaseUomId") + .HasColumnType("integer"); + + b.Property("BrandId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ContentBaseQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ContentBaseUnit") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ContentQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ContentUnit") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultVendorId") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SellingFlatPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SellingMaxPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SellingMinPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("StockNature") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubCategoryId") + .HasColumnType("integer"); + + b.Property("TaxClass") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TrackingMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Warranty") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("NonWarranty"); + + b.Property("WarrantyPeriodMonths") + .HasColumnType("integer"); + + b.HasKey("ItemId"); + + b.HasIndex("BaseUomId"); + + b.HasIndex("BrandId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DefaultVendorId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("SubCategoryId"); + + b.ToTable("items", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b => + { + b.Property("ReorderId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReorderId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("ReorderPoint") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReorderQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("ReorderId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ItemId", "WarehouseId") + .IsUnique(); + + b.ToTable("item_reorders", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemType", b => + { + b.Property("ItemTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ItemTypeId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsMeasurable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ItemTypeId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("item_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.JournalEntryStub", b => + { + b.Property("JournalId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("JournalId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CreditAccount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("DebitAccount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SourceDocId") + .HasColumnType("integer"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("JournalId"); + + b.HasIndex("SourceDocType", "SourceDocId"); + + b.ToTable("journal_entry_stubs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveBalance", b => + { + b.Property("LeaveBalanceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LeaveBalanceId")); + + b.Property("AdjustmentDays") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("CarriedForwardDays") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("EntitledDays") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("LeaveTypeId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TakenDays") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("LeaveBalanceId"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("EmployeeId", "LeaveTypeId", "Year") + .IsUnique(); + + b.ToTable("hr_leave_balances", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveRequest", b => + { + b.Property("LeaveRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LeaveRequestId")); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DaysCount") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("EmployeeId") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LeaveTypeId") + .HasColumnType("integer"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RejectionReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("LeaveRequestId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("EmployeeId"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("Status"); + + b.HasIndex("StartDate", "EndDate"); + + b.ToTable("hr_leave_requests", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveType", b => + { + b.Property("LeaveTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LeaveTypeId")); + + b.Property("AccrualPerYear") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("CarryForwardAllowed") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CountsAsNoPay") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MaxCarryForwardDays") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiresApproval") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("LeaveTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_leave_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LoanInstallment", b => + { + b.Property("LoanInstallmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LoanInstallmentId")); + + b.Property("DueMonth") + .HasColumnType("integer"); + + b.Property("DueYear") + .HasColumnType("integer"); + + b.Property("EmployeeLoanId") + .HasColumnType("integer"); + + b.Property("InstallmentNumber") + .HasColumnType("integer"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PayrollRunId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ScheduledAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("LoanInstallmentId"); + + b.HasIndex("EmployeeLoanId"); + + b.HasIndex("PayrollRunId"); + + b.HasIndex("DueYear", "DueMonth"); + + b.ToTable("hr_loan_installments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NavItem", b => + { + b.Property("NavItemId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NavItemId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Href") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.HasKey("NavItemId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("nav_items", (string)null); + + b.HasData( + new + { + NavItemId = 1, + Code = "dashboard", + Href = "/dashboard", + Label = "Dashboard", + SortOrder = 1, + Status = "Active" + }, + new + { + NavItemId = 2, + Code = "products", + Href = "/dashboard/products", + Label = "Products", + SortOrder = 2, + Status = "Active" + }, + new + { + NavItemId = 3, + Code = "vendors", + Href = "/dashboard/vendors", + Label = "Vendors", + SortOrder = 3, + Status = "Active" + }, + new + { + NavItemId = 4, + Code = "procurement", + Href = "/dashboard/procurement", + Label = "Procurement", + SortOrder = 4, + Status = "Active" + }, + new + { + NavItemId = 5, + Code = "receiving", + Href = "/dashboard/receiving/grn", + Label = "Receiving", + SortOrder = 5, + Status = "Active" + }, + new + { + NavItemId = 6, + Code = "stock", + Href = "/dashboard/stock", + Label = "Stock", + SortOrder = 6, + Status = "Active" + }, + new + { + NavItemId = 7, + Code = "warehouses", + Href = "/dashboard/warehouse", + Label = "Warehouses", + SortOrder = 7, + Status = "Active" + }, + new + { + NavItemId = 8, + Code = "orders", + Href = "/dashboard/orders", + Label = "Orders", + SortOrder = 8, + Status = "Active" + }, + new + { + NavItemId = 9, + Code = "settings", + Href = "/dashboard/settings", + Label = "Settings", + SortOrder = 9, + Status = "Active" + }, + new + { + NavItemId = 10, + Code = "help", + Href = "/dashboard/help", + Label = "Help", + SortOrder = 10, + Status = "Active" + }, + new + { + NavItemId = 11, + Code = "ledgers", + Href = "/dashboard/ledgers", + Label = "Ledgers", + SortOrder = 11, + Status = "Active" + }, + new + { + NavItemId = 12, + Code = "accounts", + Href = "/dashboard/accounts", + Label = "Accounts", + SortOrder = 12, + Status = "Active" + }, + new + { + NavItemId = 13, + Code = "sales", + Href = "/dashboard/sales", + Label = "Sales", + SortOrder = 13, + Status = "Active" + }, + new + { + NavItemId = 14, + Code = "production", + Href = "/dashboard/production", + Label = "Production", + SortOrder = 14, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfig", b => + { + b.Property("NotificationConfigId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("integer"); + + b.HasKey("NotificationConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("notification_config", null, t => + { + t.HasCheckConstraint("ck_notification_config_singleton", "\"NotificationConfigId\" = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfigModule", b => + { + b.Property("NotificationConfigModuleId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NotificationConfigModuleId")); + + b.Property("EmailEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("InAppEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ModuleKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NotificationConfigId") + .HasColumnType("integer"); + + b.Property("SmsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.HasKey("NotificationConfigModuleId"); + + b.HasIndex("ModuleKey") + .IsUnique(); + + b.HasIndex("NotificationConfigId"); + + b.ToTable("notification_config_modules", (string)null); + }); + + 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 + }, + new + { + PermissionId = 35, + Code = "NAV:production", + NavItemId = 14 + }, + new + { + PermissionId = 36, + Code = "NAV:production.templates", + SubNavItemId = 27 + }, + new + { + PermissionId = 37, + Code = "NAV:production.runs", + SubNavItemId = 25 + }, + new + { + PermissionId = 38, + Code = "NAV:production.produced", + SubNavItemId = 26 + }, + new + { + PermissionId = 39, + Code = "NAV:procurement.purchase-invoices", + SubNavItemId = 28 + }); + }); + + 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("BatchListsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + 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("SellingPricesEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("SerialNumbersEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("SubcategoriesEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("integer"); + + b.Property("WarrantyPeriodsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + 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.PurchaseInvoice", b => + { + b.Property("PurchaseInvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PurchaseInvoiceId")); + + 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("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("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VatTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.Property("VendorInvoiceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("VendorInvoiceNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("VendorSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("VendorSnapshotTaxNo") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("PurchaseInvoiceId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.HasIndex("VendorId", "VendorInvoiceNo"); + + b.ToTable("purchase_invoices", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoiceLine", b => + { + b.Property("PurchaseInvoiceLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PurchaseInvoiceLineId")); + + b.Property("GrnId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PurchaseInvoiceId") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("VatTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.HasKey("PurchaseInvoiceLineId"); + + b.HasIndex("GrnId") + .IsUnique(); + + b.HasIndex("PurchaseInvoiceId"); + + b.ToTable("purchase_invoice_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoicePayment", b => + { + b.Property("PurchaseInvoicePaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PurchaseInvoicePaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PaymentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PurchaseInvoiceId") + .HasColumnType("integer"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("PurchaseInvoicePaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("PurchaseInvoiceId"); + + b.ToTable("purchase_invoice_payments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => + { + b.Property("PoId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PoId")); + + b.Property("ApprovalRequired") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RequisitionId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.HasKey("PoId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequisitionId"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.ToTable("purchase_orders", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.Property("ReturnId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReasonCodeId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("ReturnId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("VendorId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("purchase_returns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturnLine", b => + { + b.Property("ReturnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnLineId")); + + b.Property("GrnLineId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReturnId") + .HasColumnType("integer"); + + b.HasKey("ReturnLineId"); + + b.HasIndex("GrnLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReturnId"); + + b.ToTable("purchase_return_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ReasonCode", b => + { + b.Property("ReasonCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReasonCodeId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Context") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("ReasonCodeId"); + + b.HasIndex("Context", "Code") + .IsUnique(); + + b.ToTable("reason_codes", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.Property("RequisitionId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RequisitionId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RequestedBy") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("RequisitionId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequestedBy"); + + b.HasIndex("Status"); + + b.ToTable("requisitions", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b => + { + b.Property("ReqLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReqLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RequiredBy") + .HasColumnType("date"); + + b.Property("RequisitionId") + .HasColumnType("integer"); + + b.HasKey("ReqLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RequisitionId"); + + b.ToTable("requisition_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.Property("RfqId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RfqId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RequisitionId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("RfqId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequisitionId"); + + b.ToTable("rfqs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b => + { + b.Property("RfqLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RfqLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RfqId") + .HasColumnType("integer"); + + b.HasKey("RfqLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RfqId"); + + b.ToTable("rfq_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Role", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RoleId")); + + b.Property("AuthRoleId") + .HasColumnType("uuid") + .HasColumnName("auth_role_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsSystemRole") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("AuthRoleId") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("roles", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("PermissionId") + .HasColumnType("integer"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.ToTable("role_permissions", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunEdge", b => + { + b.Property("RunEdgeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunEdgeId")); + + b.Property("ChildRunStageId") + .HasColumnType("integer"); + + b.Property("ParentRunStageId") + .HasColumnType("integer"); + + b.Property("RunId") + .HasColumnType("integer"); + + b.HasKey("RunEdgeId"); + + b.HasIndex("ChildRunStageId"); + + b.HasIndex("RunId"); + + b.HasIndex("ParentRunStageId", "ChildRunStageId") + .IsUnique(); + + b.ToTable("run_edges", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b => + { + b.Property("RunStageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunStageId")); + + b.Property("ActualEndAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ActualStartAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedMinutes") + .HasColumnType("integer"); + + b.Property("FieldDefs") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("FieldValues") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("PosX") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PosY") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RoleLabel") + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("RunId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TemplateStageId") + .HasColumnType("integer"); + + b.HasKey("RunStageId"); + + b.HasIndex("TemplateStageId"); + + b.HasIndex("RunId", "Status"); + + b.ToTable("run_stages", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageEvent", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EventId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("RunId") + .HasColumnType("integer"); + + b.Property("RunStageId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("EventId"); + + b.HasIndex("RunStageId"); + + b.HasIndex("UserId"); + + b.HasIndex("RunId", "EventId"); + + b.ToTable("run_stage_events", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageInput", b => + { + b.Property("RunInputId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunInputId")); + + b.Property("ConsumedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ConsumedValue") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DeliveredQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("FromRunOutputId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("PlannedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ReturnedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReturnedValue") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RunStageId") + .HasColumnType("integer"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("RunInputId"); + + b.HasIndex("FromRunOutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RunStageId"); + + b.ToTable("run_stage_inputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageOutput", b => + { + b.Property("RunOutputId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RunOutputId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("PlannedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ProducedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RunStageId") + .HasColumnType("integer"); + + b.Property("ScrapReasonCodeId") + .HasColumnType("integer"); + + b.Property("ScrappedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TransferredQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UomId") + .HasColumnType("integer"); + + b.HasKey("RunOutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RunStageId"); + + b.HasIndex("ScrapReasonCodeId"); + + b.HasIndex("UomId"); + + b.ToTable("run_stage_outputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalaryComponent", b => + { + b.Property("SalaryComponentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalaryComponentId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ComponentType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEpfEtfApplicable") + .HasColumnType("boolean"); + + b.Property("IsTaxable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("SalaryComponentId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_salary_components", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.Property("SalesDayEndId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesDayEndId")); + + b.Property("BundleCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("BusinessDate") + .HasColumnType("date"); + + b.Property("CashierUserId") + .HasColumnType("integer"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedBy") + .HasColumnType("integer"); + + b.Property("CostOfGoodsSold") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SlipCount") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.HasKey("SalesDayEndId"); + + b.HasIndex("ClosedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("CashierUserId", "BusinessDate") + .IsUnique(); + + b.ToTable("sales_day_ends", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.Property("SalesInvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesInvoiceId")); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("CreatorUserId") + .HasColumnType("integer"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CustomerSnapshotTaxNo") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("InvoiceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("B2C"); + + b.Property("NetPayable") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RoundOff") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("SalesInvoiceId"); + + b.HasIndex("CreatorUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("InvoiceDate"); + + b.HasIndex("InvoiceNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_invoices", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLine", b => + { + b.Property("SalesInvoiceLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesInvoiceLineId")); + + b.Property("BaseCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DiscountAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DiscountMode") + .HasColumnType("integer"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("FreeQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("IsFreeIssue") + .HasColumnType("boolean"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("NetUnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("integer"); + + b.Property("PriceSource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SalesInvoiceId") + .HasColumnType("integer"); + + b.Property("TaxAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("SalesInvoiceLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SalesInvoiceId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_invoice_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoicePayment", b => + { + b.Property("SalesInvoicePaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesInvoicePaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PaymentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SalesInvoiceId") + .HasColumnType("integer"); + + b.HasKey("SalesInvoicePaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("SalesInvoiceId"); + + b.ToTable("sales_invoice_payments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.Property("ReturnId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReasonCodeId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("ReturnId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_returns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLine", b => + { + b.Property("ReturnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReturnId") + .HasColumnType("integer"); + + b.Property("SalesInvoiceLineId") + .HasColumnType("integer"); + + b.HasKey("ReturnLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReturnId"); + + b.HasIndex("SalesInvoiceLineId"); + + b.ToTable("sales_return_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.Property("SalesSlipId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesSlipId")); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CashierUserId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DayEndId") + .HasColumnType("integer"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SlipDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SlipNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("SalesSlipId"); + + b.HasIndex("CashierUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DayEndId"); + + b.HasIndex("SlipDate"); + + b.HasIndex("SlipNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_slips", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLine", b => + { + b.Property("SalesSlipLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SalesSlipLineId")); + + b.Property("BaseCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DiscountAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("DiscountMode") + .HasColumnType("integer"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("FreeQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("IsFreeIssue") + .HasColumnType("boolean"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("NetUnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("integer"); + + b.Property("PriceSource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SalesSlipId") + .HasColumnType("integer"); + + b.Property("TaxAmount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("TaxPct") + .HasPrecision(9, 4) + .HasColumnType("numeric(9,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("SalesSlipLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SalesSlipId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_slip_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b => + { + b.Property("SerialId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SerialId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("SerialNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("SerialId"); + + b.HasIndex("ItemId", "SerialNo") + .IsUnique(); + + b.ToTable("serials", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageEdge", b => + { + b.Property("EdgeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EdgeId")); + + b.Property("ChildStageId") + .HasColumnType("integer"); + + b.Property("ParentStageId") + .HasColumnType("integer"); + + b.Property("TemplateId") + .HasColumnType("integer"); + + b.HasKey("EdgeId"); + + b.HasIndex("ChildStageId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("ParentStageId", "ChildStageId") + .IsUnique(); + + b.ToTable("stage_edges", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageInput", b => + { + b.Property("InputId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InputId")); + + b.Property("FromOutputId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("QtyPerBatch") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StageId") + .HasColumnType("integer"); + + b.HasKey("InputId"); + + b.HasIndex("FromOutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("StageId"); + + b.ToTable("stage_inputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageOutput", b => + { + b.Property("OutputId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("OutputId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("QtyPerBatch") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("StageId") + .HasColumnType("integer"); + + b.Property("UomId") + .HasColumnType("integer"); + + b.HasKey("OutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("StageId"); + + b.HasIndex("UomId"); + + b.ToTable("stage_outputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => + { + b.Property("AdjustmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AdjustmentId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("GlPostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReasonCodeId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("AdjustmentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("stock_adjustments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustmentLine", b => + { + b.Property("AdjLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AdjLineId")); + + b.Property("AdjustmentId") + .HasColumnType("integer"); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("BinId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("QtyDelta") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SerialId") + .HasColumnType("integer"); + + b.HasKey("AdjLineId"); + + b.HasIndex("AdjustmentId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SerialId"); + + b.ToTable("stock_adjustment_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.Property("CountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CountId")); + + b.Property("CountType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("CountId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("stock_counts", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCountLine", b => + { + b.Property("CountLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CountLineId")); + + b.Property("BinId") + .HasColumnType("integer"); + + b.Property("CountId") + .HasColumnType("integer"); + + b.Property("CountedQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("SystemQty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Variance") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.HasKey("CountLineId"); + + b.HasIndex("BinId"); + + b.HasIndex("CountId"); + + b.HasIndex("ItemId"); + + b.ToTable("stock_count_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLayer", b => + { + b.Property("LayerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LayerId")); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("GrnLineId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("QtyReceived") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyRemaining") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReceiptDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SerialId") + .HasColumnType("integer"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("LayerId"); + + b.HasIndex("BatchId"); + + b.HasIndex("GrnLineId"); + + b.HasIndex("SerialId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ItemId", "WarehouseId", "ReceiptDate", "LayerId"); + + b.ToTable("stock_layers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLedger", b => + { + b.Property("LedgerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LedgerId")); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("BinId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("QtyBase") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RunningBalance") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SerialId") + .HasColumnType("integer"); + + b.Property("SourceDocId") + .HasColumnType("integer"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("integer"); + + b.HasKey("LedgerId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("SerialId"); + + b.HasIndex("UserId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("SourceDocType", "SourceDocId"); + + b.HasIndex("ItemId", "WarehouseId", "CreatedAt"); + + b.HasIndex("ItemId", "WarehouseId", "LedgerId"); + + b.ToTable("stock_ledger", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + b.Property("TransferId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TransferId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("DestWarehouseId") + .HasColumnType("integer"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SrcWarehouseId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("TransferId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DestWarehouseId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("SrcWarehouseId"); + + b.HasIndex("Status"); + + b.ToTable("stock_transfers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLine", b => + { + b.Property("TransferLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TransferLineId")); + + b.Property("BatchId") + .HasColumnType("integer"); + + b.Property("DestBinId") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QtyReceived") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("SerialId") + .HasColumnType("integer"); + + b.Property("SrcBinId") + .HasColumnType("integer"); + + b.Property("TransferId") + .HasColumnType("integer"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.HasKey("TransferLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("DestBinId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SerialId"); + + b.HasIndex("SrcBinId"); + + b.HasIndex("TransferId"); + + b.ToTable("stock_transfer_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubCategory", b => + { + b.Property("SubCategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SubCategoryId")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("SubCategoryId"); + + b.HasIndex("Status"); + + b.HasIndex("CategoryId", "Name") + .IsUnique(); + + b.ToTable("subcategories", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubNavItem", b => + { + b.Property("SubNavItemId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SubNavItemId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Href") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NavItemId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.HasKey("SubNavItemId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("NavItemId"); + + b.ToTable("sub_nav_items", (string)null); + + b.HasData( + new + { + SubNavItemId = 1, + Code = "products.item", + Href = "/dashboard/products", + Label = "Item", + NavItemId = 2, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 2, + Code = "products.category", + Href = "/dashboard/products/categories", + Label = "Category", + NavItemId = 2, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 3, + Code = "products.brand", + Href = "/dashboard/products/brands", + Label = "Brand", + NavItemId = 2, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 4, + Code = "products.item-type", + Href = "/dashboard/products/item-types", + Label = "Item Type", + NavItemId = 2, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 5, + Code = "products.uom", + Href = "/dashboard/products/uoms", + Label = "UOM", + NavItemId = 2, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 6, + Code = "products.configuration", + Href = "/dashboard/products/settings", + Label = "Configuration", + NavItemId = 2, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 7, + Code = "settings.roles", + Href = "/dashboard/settings/roles", + Label = "Roles", + NavItemId = 9, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 8, + Code = "settings.users", + Href = "/dashboard/settings/users", + Label = "Users", + NavItemId = 9, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 24, + Code = "settings.notification-config", + Href = "/dashboard/settings/notification-config", + Label = "Notification Config", + NavItemId = 9, + SortOrder = 3, + 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 = 28, + Code = "procurement.purchase-invoices", + Href = "/dashboard/procurement/purchase-invoices", + Label = "Purchase Invoices", + NavItemId = 4, + SortOrder = 5, + 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" + }, + new + { + SubNavItemId = 27, + Code = "production.templates", + Href = "/dashboard/production/templates", + Label = "Templates", + NavItemId = 14, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 25, + Code = "production.runs", + Href = "/dashboard/production/runs", + Label = "Runs", + NavItemId = 14, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 26, + Code = "production.produced", + Href = "/dashboard/production/produced", + Label = "Produced", + NavItemId = 14, + SortOrder = 3, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TaxSlab", b => + { + b.Property("TaxSlabId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TaxSlabId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("LowerBound") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Rate") + .HasPrecision(6, 4) + .HasColumnType("numeric(6,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpperBound") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("TaxSlabId"); + + b.HasIndex("EffectiveFrom"); + + b.ToTable("hr_tax_slabs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b => + { + b.Property("StageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StageId")); + + b.Property("EstimatedMinutes") + .HasColumnType("integer"); + + b.Property("FieldDefs") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("PosX") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PosY") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RoleLabel") + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("TemplateId") + .HasColumnType("integer"); + + b.HasKey("StageId"); + + b.HasIndex("TemplateId"); + + b.ToTable("template_stages", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b => + { + b.Property("UomId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UomId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("UomId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("uoms", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId")); + + b.Property("AuthUserId") + .HasColumnType("uuid") + .HasColumnName("auth_user_id"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("UserId"); + + b.HasIndex("AuthUserId") + .IsUnique(); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users", (string)null); + + b.HasData( + new + { + UserId = 1, + DisplayName = "System", + Status = "Active", + Username = "system" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b => + { + b.Property("VendorId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("VendorId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasDefaultValue("LKR"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("TaxReg") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Terms") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("VendorId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("vendors", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b => + { + b.Property("QuotationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("QuotationId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RfqId") + .HasColumnType("integer"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.HasKey("QuotationId"); + + b.HasIndex("VendorId"); + + b.HasIndex("RfqId", "VendorId") + .IsUnique(); + + b.ToTable("vendor_quotations", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotationLine", b => + { + b.Property("QuotationLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("QuotationLineId")); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("LeadDays") + .HasColumnType("integer"); + + b.Property("QuotationId") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.HasKey("QuotationLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("QuotationId"); + + b.ToTable("vendor_quotation_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b => + { + b.Property("WarehouseId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WarehouseId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("WarehouseId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("warehouses", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.WorkShift", b => + { + b.Property("WorkShiftId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WorkShiftId")); + + b.Property("BreakMinutes") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("interval"); + + b.Property("GraceMinutes") + .HasColumnType("integer"); + + b.Property("IsOvernight") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OtMultiplier") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StandardWorkingMinutes") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("interval"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkingDaysMask") + .HasColumnType("integer"); + + b.HasKey("WorkShiftId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_work_shifts", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceRecord", b => + { + b.HasOne("ERPCore.Domain.Entities.AttendanceUploadBatch", "AttendanceUploadBatch") + .WithMany() + .HasForeignKey("AttendanceUploadBatchId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.WorkShift", "WorkShift") + .WithMany() + .HasForeignKey("WorkShiftId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AttendanceUploadBatch"); + + b.Navigation("Employee"); + + b.Navigation("WorkShift"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AuditLog", b => + { + b.HasOne("ERPCore.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b => + { + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany("Bins") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSaleTemplate", "BundleSaleTemplate") + .WithMany() + .HasForeignKey("BundleSaleTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesDayEnd", "DayEnd") + .WithMany("BundleSales") + .HasForeignKey("DayEndId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSaleTemplate"); + + b.Navigation("CashierUser"); + + b.Navigation("Customer"); + + b.Navigation("DayEnd"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleLine", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSale", "BundleSale") + .WithMany("Lines") + .HasForeignKey("BundleSaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSale"); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplateLine", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSaleTemplate", "BundleSaleTemplate") + .WithMany("Lines") + .HasForeignKey("BundleSaleTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSaleTemplate"); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Customer", b => + { + b.HasOne("ERPCore.Domain.Entities.Warehouse", "DefaultWarehouse") + .WithMany() + .HasForeignKey("DefaultWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DefaultWarehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Department", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Employee", "HeadEmployee") + .WithMany() + .HasForeignKey("HeadEmployeeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Department", "ParentDepartment") + .WithMany() + .HasForeignKey("ParentDepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Branch"); + + b.Navigation("HeadEmployee"); + + b.Navigation("ParentDepartment"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Employee", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Designation", "Designation") + .WithMany() + .HasForeignKey("DesignationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.EmploymentType", "EmploymentType") + .WithMany() + .HasForeignKey("EmploymentTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Employee", "ReportingManager") + .WithMany() + .HasForeignKey("ReportingManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("ERPCore.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.WorkShift", "WorkShift") + .WithMany() + .HasForeignKey("WorkShiftId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Department"); + + b.Navigation("Designation"); + + b.Navigation("EmploymentType"); + + b.Navigation("ReportingManager"); + + b.Navigation("User"); + + b.Navigation("WorkShift"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeBankDetail", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.HrDocumentType", "HrDocumentType") + .WithMany() + .HasForeignKey("HrDocumentTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("HrDocumentType"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructureLine", b => + { + b.HasOne("ERPCore.Domain.Entities.EmployeeSalaryStructure", "EmployeeSalaryStructure") + .WithMany("Lines") + .HasForeignKey("EmployeeSalaryStructureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalaryComponent", "SalaryComponent") + .WithMany() + .HasForeignKey("SalaryComponentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EmployeeSalaryStructure"); + + b.Navigation("SalaryComponent"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("PoId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany() + .HasForeignKey("PurchaseInvoiceId") + .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("PurchaseInvoice"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Vendor"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", "Bin") + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Grn", "Grn") + .WithMany("Lines") + .HasForeignKey("GrnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PoLine", "PoLine") + .WithMany() + .HasForeignKey("PoLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Batch"); + + b.Navigation("Bin"); + + b.Navigation("Grn"); + + b.Navigation("Item"); + + b.Navigation("PoLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLineWarrantyNumber", b => + { + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany("WarrantyNumbers") + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GrnLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.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.NotificationConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfigModule", b => + { + b.HasOne("ERPCore.Domain.Entities.NotificationConfig", "NotificationConfig") + .WithMany("Modules") + .HasForeignKey("NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationConfig"); + }); + + 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.PurchaseInvoice", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoiceLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Grn", "Grn") + .WithMany() + .HasForeignKey("GrnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany("Lines") + .HasForeignKey("PurchaseInvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grn"); + + b.Navigation("PurchaseInvoice"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoicePayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany("Payments") + .HasForeignKey("PurchaseInvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("PurchaseInvoice"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany() + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Requisition"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("ReasonCode"); + + b.Navigation("Vendor"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany() + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseReturn", "Return") + .WithMany("Lines") + .HasForeignKey("ReturnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GrnLine"); + + b.Navigation("Item"); + + b.Navigation("Return"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Requester") + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Requester"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany("Lines") + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Requisition"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany() + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Requisition"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq") + .WithMany("Lines") + .HasForeignKey("RfqId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Rfq"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RolePermission", b => + { + b.HasOne("ERPCore.Domain.Entities.Permission", "Permission") + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunEdge", b => + { + b.HasOne("ERPCore.Domain.Entities.RunStage", "ChildRunStage") + .WithMany() + .HasForeignKey("ChildRunStageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.RunStage", "ParentRunStage") + .WithMany() + .HasForeignKey("ParentRunStageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run") + .WithMany("Edges") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChildRunStage"); + + b.Navigation("ParentRunStage"); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b => + { + b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run") + .WithMany("Stages") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "TemplateStage") + .WithMany() + .HasForeignKey("TemplateStageId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Run"); + + b.Navigation("TemplateStage"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageEvent", b => + { + b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run") + .WithMany("Events") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage") + .WithMany("Events") + .HasForeignKey("RunStageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ERPCore.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Run"); + + b.Navigation("RunStage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageInput", b => + { + b.HasOne("ERPCore.Domain.Entities.RunStageOutput", "FromRunOutput") + .WithMany() + .HasForeignKey("FromRunOutputId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage") + .WithMany("Inputs") + .HasForeignKey("RunStageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FromRunOutput"); + + b.Navigation("Item"); + + b.Navigation("RunStage"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageOutput", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage") + .WithMany("Outputs") + .HasForeignKey("RunStageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ScrapReason") + .WithMany() + .HasForeignKey("ScrapReasonCodeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Uom", "Uom") + .WithMany() + .HasForeignKey("UomId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("RunStage"); + + b.Navigation("ScrapReason"); + + b.Navigation("Uom"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "ClosedByUser") + .WithMany() + .HasForeignKey("ClosedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CashierUser"); + + b.Navigation("ClosedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatorUserId"); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Customer"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoice", "SalesInvoice") + .WithMany("Lines") + .HasForeignKey("SalesInvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SalesInvoice"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoicePayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoice", "SalesInvoice") + .WithMany("Payments") + .HasForeignKey("SalesInvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("SalesInvoice"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Customer"); + + b.Navigation("ReasonCode"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesReturn", "Return") + .WithMany("Lines") + .HasForeignKey("ReturnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoiceLine", "SalesInvoiceLine") + .WithMany() + .HasForeignKey("SalesInvoiceLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("Return"); + + b.Navigation("SalesInvoiceLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesDayEnd", "DayEnd") + .WithMany("Slips") + .HasForeignKey("DayEndId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CashierUser"); + + b.Navigation("Customer"); + + b.Navigation("DayEnd"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesSlip", "SalesSlip") + .WithMany("Lines") + .HasForeignKey("SalesSlipId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SalesSlip"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageEdge", b => + { + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "ChildStage") + .WithMany() + .HasForeignKey("ChildStageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "ParentStage") + .WithMany() + .HasForeignKey("ParentStageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ProductionTemplate", "Template") + .WithMany("Edges") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChildStage"); + + b.Navigation("ParentStage"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageInput", b => + { + b.HasOne("ERPCore.Domain.Entities.StageOutput", "FromOutput") + .WithMany() + .HasForeignKey("FromOutputId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "Stage") + .WithMany("Inputs") + .HasForeignKey("StageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FromOutput"); + + b.Navigation("Item"); + + b.Navigation("Stage"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageOutput", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "Stage") + .WithMany("Outputs") + .HasForeignKey("StageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Uom", "Uom") + .WithMany() + .HasForeignKey("UomId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("Stage"); + + b.Navigation("Uom"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("ReasonCode"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustmentLine", b => + { + b.HasOne("ERPCore.Domain.Entities.StockAdjustment", "Adjustment") + .WithMany("Lines") + .HasForeignKey("AdjustmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", null) + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Adjustment"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCountLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.StockCount", "Count") + .WithMany("Lines") + .HasForeignKey("CountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Count"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLayer", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany() + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Batch"); + + b.Navigation("GrnLine"); + + b.Navigation("Item"); + + b.Navigation("Serial"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLedger", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", null) + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", null) + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", null) + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "DestWarehouse") + .WithMany() + .HasForeignKey("DestWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "SrcWarehouse") + .WithMany() + .HasForeignKey("SrcWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("DestWarehouse"); + + b.Navigation("SrcWarehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("DestBinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", null) + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("SrcBinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.StockTransfer", "Transfer") + .WithMany("Lines") + .HasForeignKey("TransferId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Transfer"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubCategory", b => + { + b.HasOne("ERPCore.Domain.Entities.Category", "Category") + .WithMany("SubCategories") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubNavItem", b => + { + b.HasOne("ERPCore.Domain.Entities.NavItem", "NavItem") + .WithMany("Children") + .HasForeignKey("NavItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NavItem"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b => + { + b.HasOne("ERPCore.Domain.Entities.ProductionTemplate", "Template") + .WithMany("Stages") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.User", b => + { + b.HasOne("ERPCore.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b => + { + b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq") + .WithMany("Quotations") + .HasForeignKey("RfqId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Rfq"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotationLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.VendorQuotation", "Quotation") + .WithMany("Lines") + .HasForeignKey("QuotationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Quotation"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplate", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Category", b => + { + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.Navigation("Installments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.Navigation("WarrantyNumbers"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => + { + b.Navigation("ReorderSettings"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NavItem", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfig", b => + { + b.Navigation("Modules"); + }); + + 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.PurchaseInvoice", b => + { + b.Navigation("Lines"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.Navigation("Lines"); + + b.Navigation("Quotations"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b => + { + b.Navigation("Events"); + + b.Navigation("Inputs"); + + b.Navigation("Outputs"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.Navigation("BundleSales"); + + b.Navigation("Slips"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.Navigation("Lines"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b => + { + b.Navigation("Inputs"); + + b.Navigation("Outputs"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b => + { + b.Navigation("Bins"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/ERPCore/Migrations/20260831060800_AddPurchaseInvoices.cs b/Backend/ERPCore/Migrations/20260831060800_AddPurchaseInvoices.cs new file mode 100644 index 0000000..768f560 --- /dev/null +++ b/Backend/ERPCore/Migrations/20260831060800_AddPurchaseInvoices.cs @@ -0,0 +1,303 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ERPCore.Migrations +{ + /// + public partial class AddPurchaseInvoices : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "grn_payments"); + + migrationBuilder.DropColumn( + name: "BalanceAmount", + table: "grns"); + + migrationBuilder.DropColumn( + name: "PaidAmount", + table: "grns"); + + migrationBuilder.AddColumn( + name: "PurchaseInvoiceId", + table: "grns", + type: "integer", + nullable: true); + + migrationBuilder.CreateTable( + name: "purchase_invoices", + columns: table => new + { + PurchaseInvoiceId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DocNo = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + VendorInvoiceNo = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + VendorInvoiceDate = table.Column(type: "timestamp with time zone", nullable: false), + VendorId = table.Column(type: "integer", nullable: false), + VendorSnapshotName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + VendorSnapshotTaxNo = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Draft"), + Subtotal = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + VatTotal = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + GrandTotal = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + PaidAmount = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + BalanceAmount = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + GlJournalNo = table.Column(type: "character varying(30)", maxLength: 30, nullable: true), + GlPostedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + xmin = table.Column(type: "xid", rowVersion: true, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_purchase_invoices", x => x.PurchaseInvoiceId); + table.ForeignKey( + name: "FK_purchase_invoices_users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_purchase_invoices_vendors_VendorId", + column: x => x.VendorId, + principalTable: "vendors", + principalColumn: "VendorId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "purchase_invoice_lines", + columns: table => new + { + PurchaseInvoiceLineId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PurchaseInvoiceId = table.Column(type: "integer", nullable: false), + GrnId = table.Column(type: "integer", nullable: false), + Subtotal = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + VatTotal = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + LineTotal = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_purchase_invoice_lines", x => x.PurchaseInvoiceLineId); + table.ForeignKey( + name: "FK_purchase_invoice_lines_grns_GrnId", + column: x => x.GrnId, + principalTable: "grns", + principalColumn: "GrnId", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_purchase_invoice_lines_purchase_invoices_PurchaseInvoiceId", + column: x => x.PurchaseInvoiceId, + principalTable: "purchase_invoices", + principalColumn: "PurchaseInvoiceId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "purchase_invoice_payments", + columns: table => new + { + PurchaseInvoicePaymentId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PurchaseInvoiceId = 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_purchase_invoice_payments", x => x.PurchaseInvoicePaymentId); + table.ForeignKey( + name: "FK_purchase_invoice_payments_purchase_invoices_PurchaseInvoice~", + column: x => x.PurchaseInvoiceId, + principalTable: "purchase_invoices", + principalColumn: "PurchaseInvoiceId", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_purchase_invoice_payments_users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.InsertData( + table: "sub_nav_items", + columns: new[] { "SubNavItemId", "Code", "Href", "Icon", "Label", "NavItemId", "SortOrder" }, + values: new object[] { 28, "procurement.purchase-invoices", "/dashboard/procurement/purchase-invoices", null, "Purchase Invoices", 4, 5 }); + + migrationBuilder.InsertData( + table: "permissions", + columns: new[] { "PermissionId", "Code", "NavItemId", "SubNavItemId" }, + values: new object[] { 39, "NAV:procurement.purchase-invoices", null, 28 }); + + migrationBuilder.CreateIndex( + name: "IX_grns_PurchaseInvoiceId", + table: "grns", + column: "PurchaseInvoiceId"); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoice_lines_GrnId", + table: "purchase_invoice_lines", + column: "GrnId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoice_lines_PurchaseInvoiceId", + table: "purchase_invoice_lines", + column: "PurchaseInvoiceId"); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoice_payments_CreatedBy", + table: "purchase_invoice_payments", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoice_payments_PurchaseInvoiceId", + table: "purchase_invoice_payments", + column: "PurchaseInvoiceId"); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoices_CreatedBy", + table: "purchase_invoices", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoices_DocNo", + table: "purchase_invoices", + column: "DocNo", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoices_Status", + table: "purchase_invoices", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoices_VendorId", + table: "purchase_invoices", + column: "VendorId"); + + migrationBuilder.CreateIndex( + name: "IX_purchase_invoices_VendorId_VendorInvoiceNo", + table: "purchase_invoices", + columns: new[] { "VendorId", "VendorInvoiceNo" }); + + migrationBuilder.AddForeignKey( + name: "FK_grns_purchase_invoices_PurchaseInvoiceId", + table: "grns", + column: "PurchaseInvoiceId", + principalTable: "purchase_invoices", + principalColumn: "PurchaseInvoiceId", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_grns_purchase_invoices_PurchaseInvoiceId", + table: "grns"); + + migrationBuilder.DropTable( + name: "purchase_invoice_lines"); + + migrationBuilder.DropTable( + name: "purchase_invoice_payments"); + + migrationBuilder.DropTable( + name: "purchase_invoices"); + + migrationBuilder.DropIndex( + name: "IX_grns_PurchaseInvoiceId", + table: "grns"); + + migrationBuilder.DeleteData( + table: "permissions", + keyColumn: "PermissionId", + keyValue: 39); + + migrationBuilder.DeleteData( + table: "sub_nav_items", + keyColumn: "SubNavItemId", + keyValue: 28); + + migrationBuilder.DropColumn( + name: "PurchaseInvoiceId", + table: "grns"); + + migrationBuilder.AddColumn( + name: "BalanceAmount", + table: "grns", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: false, + defaultValue: 0m); + + 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), + CreatedBy = table.Column(type: "integer", nullable: false), + GrnId = table.Column(type: "integer", nullable: false), + Amount = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + BankAccountName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + GlBankAccountId = table.Column(type: "bigint", nullable: false), + GlJournalNo = table.Column(type: "character varying(30)", maxLength: 30, nullable: true), + PaymentDate = table.Column(type: "timestamp with time zone", nullable: false), + Reference = table.Column(type: "character varying(100)", maxLength: 100, nullable: true) + }, + 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"); + } + } +} diff --git a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs index 9944183..9b62e94 100644 --- a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs +++ b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs @@ -1411,10 +1411,6 @@ 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"); @@ -1433,16 +1429,15 @@ namespace ERPCore.Migrations 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("PurchaseInvoiceId") + .HasColumnType("integer"); + b.Property("RowVersion") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -1469,6 +1464,8 @@ namespace ERPCore.Migrations b.HasIndex("PoId"); + b.HasIndex("PurchaseInvoiceId"); + b.HasIndex("Status"); b.HasIndex("VendorId"); @@ -1584,55 +1581,6 @@ namespace ERPCore.Migrations b.ToTable("grn_line_warranty_numbers", (string)null); }); - modelBuilder.Entity("ERPCore.Domain.Entities.GrnPayment", b => - { - b.Property("GrnPaymentId") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnPaymentId")); - - b.Property("Amount") - .HasPrecision(18, 4) - .HasColumnType("numeric(18,4)"); - - b.Property("BankAccountName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("integer"); - - b.Property("GlBankAccountId") - .HasColumnType("bigint"); - - b.Property("GlJournalNo") - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("GrnId") - .HasColumnType("integer"); - - b.Property("PaymentDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Reference") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("GrnPaymentId"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("GrnId"); - - b.ToTable("grn_payments", (string)null); - }); - modelBuilder.Entity("ERPCore.Domain.Entities.HrDocumentType", b => { b.Property("HrDocumentTypeId") @@ -2976,7 +2924,7 @@ namespace ERPCore.Migrations { PermissionId = 36, Code = "NAV:production.templates", - SubNavItemId = 24 + SubNavItemId = 27 }, new { @@ -2989,6 +2937,12 @@ namespace ERPCore.Migrations PermissionId = 38, Code = "NAV:production.produced", SubNavItemId = 26 + }, + new + { + PermissionId = 39, + Code = "NAV:procurement.purchase-invoices", + SubNavItemId = 28 }); }); @@ -3228,6 +3182,189 @@ namespace ERPCore.Migrations b.ToTable("production_templates", (string)null); }); + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoice", b => + { + b.Property("PurchaseInvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PurchaseInvoiceId")); + + 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("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("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VatTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("VendorId") + .HasColumnType("integer"); + + b.Property("VendorInvoiceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("VendorInvoiceNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("VendorSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("VendorSnapshotTaxNo") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("PurchaseInvoiceId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.HasIndex("VendorId", "VendorInvoiceNo"); + + b.ToTable("purchase_invoices", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoiceLine", b => + { + b.Property("PurchaseInvoiceLineId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PurchaseInvoiceLineId")); + + b.Property("GrnId") + .HasColumnType("integer"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PurchaseInvoiceId") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("VatTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.HasKey("PurchaseInvoiceLineId"); + + b.HasIndex("GrnId") + .IsUnique(); + + b.HasIndex("PurchaseInvoiceId"); + + b.ToTable("purchase_invoice_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoicePayment", b => + { + b.Property("PurchaseInvoicePaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PurchaseInvoicePaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("integer"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PaymentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PurchaseInvoiceId") + .HasColumnType("integer"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("PurchaseInvoicePaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("PurchaseInvoiceId"); + + b.ToTable("purchase_invoice_payments", (string)null); + }); + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => { b.Property("PoId") @@ -5337,6 +5474,16 @@ namespace ERPCore.Migrations Status = "Active" }, new + { + SubNavItemId = 28, + Code = "procurement.purchase-invoices", + Href = "/dashboard/procurement/purchase-invoices", + Label = "Purchase Invoices", + NavItemId = 4, + SortOrder = 5, + Status = "Active" + }, + new { SubNavItemId = 9, Code = "ledgers.trial-balance", @@ -5438,7 +5585,7 @@ namespace ERPCore.Migrations }, new { - SubNavItemId = 24, + SubNavItemId = 27, Code = "production.templates", Href = "/dashboard/production/templates", Label = "Templates", @@ -6176,6 +6323,11 @@ namespace ERPCore.Migrations .HasForeignKey("PoId") .OnDelete(DeleteBehavior.Restrict); + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany() + .HasForeignKey("PurchaseInvoiceId") + .OnDelete(DeleteBehavior.Restrict); + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") .WithMany() .HasForeignKey("VendorId") @@ -6190,6 +6342,8 @@ namespace ERPCore.Migrations b.Navigation("Creator"); + b.Navigation("PurchaseInvoice"); + b.Navigation("PurchaseOrder"); b.Navigation("Vendor"); @@ -6248,25 +6402,6 @@ namespace ERPCore.Migrations b.Navigation("GrnLine"); }); - modelBuilder.Entity("ERPCore.Domain.Entities.GrnPayment", b => - { - b.HasOne("ERPCore.Domain.Entities.User", "Creator") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("ERPCore.Domain.Entities.Grn", "Grn") - .WithMany("Payments") - .HasForeignKey("GrnId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Creator"); - - b.Navigation("Grn"); - }); - modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => { b.HasOne("ERPCore.Domain.Entities.Uom", "BaseUom") @@ -6567,6 +6702,63 @@ namespace ERPCore.Migrations b.Navigation("Creator"); }); + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoice", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoiceLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Grn", "Grn") + .WithMany() + .HasForeignKey("GrnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany("Lines") + .HasForeignKey("PurchaseInvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grn"); + + b.Navigation("PurchaseInvoice"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoicePayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany("Payments") + .HasForeignKey("PurchaseInvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("PurchaseInvoice"); + }); + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => { b.HasOne("ERPCore.Domain.Entities.User", "Creator") @@ -7516,8 +7708,6 @@ namespace ERPCore.Migrations modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => { b.Navigation("Lines"); - - b.Navigation("Payments"); }); modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => @@ -7568,6 +7758,13 @@ namespace ERPCore.Migrations b.Navigation("Stages"); }); + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoice", b => + { + b.Navigation("Lines"); + + b.Navigation("Payments"); + }); + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => { b.Navigation("Lines"); diff --git a/Backend/ERPCore/Program.cs b/Backend/ERPCore/Program.cs index 8434f0f..37de271 100644 --- a/Backend/ERPCore/Program.cs +++ b/Backend/ERPCore/Program.cs @@ -105,7 +105,10 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); + +// Purchase invoices (vendor invoices consolidating Confirmed GRNs) +builder.Services.AddScoped(); +builder.Services.AddScoped(); // Sales (Phase 1) builder.Services.AddScoped(); diff --git a/Backend/ERPCore/Services/GrnPaymentService.cs b/Backend/ERPCore/Services/GrnPaymentService.cs deleted file mode 100644 index bfec736..0000000 --- a/Backend/ERPCore/Services/GrnPaymentService.cs +++ /dev/null @@ -1,120 +0,0 @@ -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 dd4fe54..07e1639 100644 --- a/Backend/ERPCore/Services/GrnService.cs +++ b/Backend/ERPCore/Services/GrnService.cs @@ -92,7 +92,8 @@ 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.PaidAmount, g.BalanceAmount)) + g.CreatedBy, g.CreatedAt, g.PostedAt, g.Lines.Count, + g.PurchaseInvoiceId, g.PurchaseInvoice != null ? g.PurchaseInvoice.DocNo : null)) .ToListAsync(ct); return PagedResponse.Create(rows, query.Page, query.PageSize, total); @@ -102,6 +103,7 @@ public sealed class GrnService : IGrnService { var grn = await _grns.Query().AsNoTracking() .Include(g => g.Lines).ThenInclude(l => l.WarrantyNumbers) + .Include(g => g.PurchaseInvoice) .FirstOrDefaultAsync(g => g.GrnId == grnId, ct); return grn is null ? null : Map(grn); } @@ -302,15 +304,13 @@ public sealed class GrnService : IGrnService 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.GlJournalNo ?? string.Empty, grn.BalanceAmount, + grn.GrnId, grn.Status, now, grn.GlJournalNo ?? string.Empty, createdLayers.Select(ToCreatedLayer).ToList(), ledgerRefs.Select(l => l.LedgerId).ToList(), await GetPoStatusAsync(grn.PoId, ct)); @@ -437,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.GlJournalNo ?? string.Empty, grn.BalanceAmount, + grn.GrnId, grn.Status, grn.PostedAt ?? grn.CreatedAt, grn.GlJournalNo ?? string.Empty, layers.Select(ToCreatedLayer).ToList(), ledgerRefs, await GetPoStatusAsync(grn.PoId, ct)); } @@ -446,7 +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.GlJournalNo, g.PurchaseInvoiceId, g.PurchaseInvoice?.DocNo, 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/IGrnPaymentService.cs b/Backend/ERPCore/Services/Interfaces/IGrnPaymentService.cs deleted file mode 100644 index 12a4456..0000000 --- a/Backend/ERPCore/Services/Interfaces/IGrnPaymentService.cs +++ /dev/null @@ -1,10 +0,0 @@ -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/Services/Interfaces/IPurchaseInvoicePaymentService.cs b/Backend/ERPCore/Services/Interfaces/IPurchaseInvoicePaymentService.cs new file mode 100644 index 0000000..3e62145 --- /dev/null +++ b/Backend/ERPCore/Services/Interfaces/IPurchaseInvoicePaymentService.cs @@ -0,0 +1,10 @@ +using ERPCore.Dtos.Procurement; + +namespace ERPCore.Services.Interfaces; + +/// Vendor payments against a Posted purchase invoice's balance (installments allowed). +public interface IPurchaseInvoicePaymentService +{ + Task PayAsync(int purchaseInvoiceId, CreatePurchaseInvoicePaymentRequest request, CancellationToken ct = default); + Task> ListAsync(int purchaseInvoiceId, CancellationToken ct = default); +} diff --git a/Backend/ERPCore/Services/Interfaces/IPurchaseInvoiceService.cs b/Backend/ERPCore/Services/Interfaces/IPurchaseInvoiceService.cs new file mode 100644 index 0000000..b672a3b --- /dev/null +++ b/Backend/ERPCore/Services/Interfaces/IPurchaseInvoiceService.cs @@ -0,0 +1,20 @@ +using ERPCore.Common.Http; +using ERPCore.Domain.Enums; +using ERPCore.Dtos.Common; +using ERPCore.Dtos.Grn; +using ERPCore.Dtos.Procurement; + +namespace ERPCore.Services.Interfaces; + +public interface IPurchaseInvoiceService +{ + Task> ListAsync(PageQuery query, PurchaseInvoiceStatus? status, int? vendorId, CancellationToken ct = default); + Task?> GetAsync(int purchaseInvoiceId, CancellationToken ct = default); + Task> CreateAsync(CreatePurchaseInvoiceRequest request, CancellationToken ct = default); + Task> UpdateAsync(int purchaseInvoiceId, UpdatePurchaseInvoiceRequest request, uint expectedRowVersion, CancellationToken ct = default); + Task PostAsync(int purchaseInvoiceId, CancellationToken ct = default); + Task CancelAsync(int purchaseInvoiceId, CancellationToken ct = default); + + /// Confirmed, not-yet-invoiced GRNs for a vendor — candidates for a new/edited invoice. + Task> ListEligibleGrnsAsync(int vendorId, CancellationToken ct = default); +} diff --git a/Backend/ERPCore/Services/PurchaseInvoicePaymentService.cs b/Backend/ERPCore/Services/PurchaseInvoicePaymentService.cs new file mode 100644 index 0000000..cd5c1b6 --- /dev/null +++ b/Backend/ERPCore/Services/PurchaseInvoicePaymentService.cs @@ -0,0 +1,119 @@ +using ERPCore.Domain.Entities; +using ERPCore.Domain.Enums; +using ERPCore.Dtos.Procurement; +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 Posted purchase invoice. Multiple installments are allowed +/// until reaches zero. Each payment posts its +/// own real GL journal entry (Debit Accounts Payable / Credit the selected bank-or-cash +/// account) — the mirror image of (that one +/// credits Accounts Receivable when the customer pays; this one debits Accounts Payable +/// when the vendor is paid) — using the same call-GL-before-commit pattern so a +/// rejected/unreachable GL post rolls back the whole payment atomically. +/// +public sealed class PurchaseInvoicePaymentService : IPurchaseInvoicePaymentService +{ + private readonly IRepository _invoices; + private readonly IRepository _payments; + private readonly IGeneralLedgerService _gl; + private readonly ICurrentUser _currentUser; + private readonly IUnitOfWork _uow; + private readonly string _glAccountsPayableCode; + + public PurchaseInvoicePaymentService( + IRepository invoices, IRepository payments, IGeneralLedgerService gl, + ICurrentUser currentUser, IUnitOfWork uow, IConfiguration configuration) + { + _invoices = invoices; + _payments = payments; + _gl = gl; + _currentUser = currentUser; + _uow = uow; + _glAccountsPayableCode = configuration["PurchaseInvoice:GlAccountsPayableCode"] ?? string.Empty; + } + + public async Task PayAsync(int purchaseInvoiceId, CreatePurchaseInvoicePaymentRequest request, CancellationToken ct = default) + { + var invoice = await _invoices.Query().FirstOrDefaultAsync(i => i.PurchaseInvoiceId == purchaseInvoiceId, ct) + ?? throw new NotFoundException($"Purchase invoice {purchaseInvoiceId} was not found."); + + if (invoice.Status != PurchaseInvoiceStatus.Posted) + throw new DomainException(ErrorCodes.PurchaseInvoiceNotPayable, $"Purchase invoice {purchaseInvoiceId} must be posted before it can be paid.", 409); + if (request.Amount > invoice.BalanceAmount) + throw new DomainException(ErrorCodes.PurchaseInvoicePaymentExceedsBalance, + $"Payment amount {request.Amount} exceeds the remaining balance {invoice.BalanceAmount}.", 400); + + var accounts = await _gl.ListBankAccountsAsync(ct); + var account = accounts.FirstOrDefault(a => a.AccountId == request.GlBankAccountId) + ?? throw new DomainException(ErrorCodes.PurchaseInvoiceBankAccountNotFound, $"Bank/cash account {request.GlBankAccountId} was not found.", 404); + + var actor = _currentUser.AuditUserId; + var now = DateTime.UtcNow; + + var payment = await _uow.ExecuteInTransactionAsync(async token => + { + var period = await _gl.GetPeriodByDateAsync(DateOnly.FromDateTime(now), token); + var posted = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest + { + PeriodId = period.PeriodId, + EntryDate = DateOnly.FromDateTime(now), + SourceModule = "PURCHASE_INVOICE_PAYMENT", + Reference = invoice.DocNo, + Narration = $"Payment against invoice {invoice.DocNo}", + Lines = + [ + new GlJournalEntryLineRequest(_glAccountsPayableCode, request.Amount, 0m, $"Payment against invoice {invoice.DocNo}"), + new GlJournalEntryLineRequest(account.GlAccountCode, 0m, request.Amount, $"Payment against invoice {invoice.DocNo}") + ] + }, token); + + var entity = new PurchaseInvoicePayment + { + PurchaseInvoiceId = invoice.PurchaseInvoiceId, + 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); + + invoice.PaidAmount += request.Amount; + invoice.BalanceAmount -= request.Amount; + + return entity; + }, ct); + + return Map(payment); + } + + public async Task> ListAsync(int purchaseInvoiceId, CancellationToken ct = default) + { + if (!await _invoices.Query().AnyAsync(i => i.PurchaseInvoiceId == purchaseInvoiceId, ct)) + throw new NotFoundException($"Purchase invoice {purchaseInvoiceId} was not found."); + + return await _payments.Query().AsNoTracking() + .Where(p => p.PurchaseInvoiceId == purchaseInvoiceId) + .OrderByDescending(p => p.PurchaseInvoicePaymentId) + .Select(p => new PurchaseInvoicePaymentDto( + p.PurchaseInvoicePaymentId, p.PurchaseInvoiceId, p.Amount, p.PaymentDate, + p.GlBankAccountId, p.BankAccountName, p.Reference, p.GlJournalNo, p.CreatedAt)) + .ToListAsync(ct); + } + + private static PurchaseInvoicePaymentDto Map(PurchaseInvoicePayment p) => new( + p.PurchaseInvoicePaymentId, p.PurchaseInvoiceId, p.Amount, p.PaymentDate, + p.GlBankAccountId, p.BankAccountName, p.Reference, p.GlJournalNo, p.CreatedAt); +} diff --git a/Backend/ERPCore/Services/PurchaseInvoiceService.cs b/Backend/ERPCore/Services/PurchaseInvoiceService.cs new file mode 100644 index 0000000..946db05 --- /dev/null +++ b/Backend/ERPCore/Services/PurchaseInvoiceService.cs @@ -0,0 +1,303 @@ +using ERPCore.Common.Http; +using ERPCore.Domain; +using ERPCore.Domain.Entities; +using ERPCore.Domain.Enums; +using ERPCore.Dtos.Common; +using ERPCore.Dtos.Grn; +using ERPCore.Dtos.Procurement; +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; + +/// +/// Purchase invoice service. A purchase invoice consolidates one or more Confirmed +/// GRNs for the same vendor into a single vendor-facing document — each linked GRN +/// becomes one , a straight sum of that GRN's line +/// totals (no re-itemization). Draft invoices are fully editable (GRNs can be +/// re-selected); Post reclassifies the GRN clearing balance to Accounts Payable via a +/// real GL journal entry (mirrors 's AR recognition); +/// vendor payments are then recorded against this aggregate (see +/// ) rather than per-GRN. +/// +public sealed class PurchaseInvoiceService : IPurchaseInvoiceService +{ + private readonly IRepository _invoices; + private readonly IRepository _vendors; + private readonly IRepository _grns; + private readonly ICurrentUser _currentUser; + private readonly INumberSequenceService _numbers; + private readonly IUnitOfWork _uow; + private readonly IGeneralLedgerService _gl; + private readonly string _glClearingAccountCode; + private readonly string _glAccountsPayableCode; + + public PurchaseInvoiceService( + IRepository invoices, IRepository vendors, IRepository grns, + ICurrentUser currentUser, INumberSequenceService numbers, IUnitOfWork uow, + IGeneralLedgerService gl, IConfiguration configuration) + { + _invoices = invoices; + _vendors = vendors; + _grns = grns; + _currentUser = currentUser; + _numbers = numbers; + _uow = uow; + _gl = gl; + _glClearingAccountCode = configuration["PurchaseInvoice:GlClearingAccountCode"] ?? string.Empty; + _glAccountsPayableCode = configuration["PurchaseInvoice:GlAccountsPayableCode"] ?? string.Empty; + } + + public async Task> ListAsync(PageQuery query, PurchaseInvoiceStatus? status, int? vendorId, CancellationToken ct = default) + { + IQueryable q = _invoices.Query().AsNoTracking().Include(x => x.Lines); + if (!string.IsNullOrWhiteSpace(query.Q)) + { + var term = query.Q.Trim(); + q = q.Where(x => EF.Functions.ILike(x.DocNo, $"%{term}%") + || EF.Functions.ILike(x.VendorInvoiceNo, $"%{term}%") + || EF.Functions.ILike(x.VendorSnapshotName, $"%{term}%")); + } + if (status is not null) q = q.Where(x => x.Status == status); + if (vendorId is not null) q = q.Where(x => x.VendorId == vendorId); + + var total = await q.CountAsync(ct); + var rows = await q.OrderByDescending(x => x.PurchaseInvoiceId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct); + + return PagedResponse.Create(rows.Select(MapSummary).ToList(), query.Page, query.PageSize, total); + } + + public async Task?> GetAsync(int purchaseInvoiceId, CancellationToken ct = default) + { + var invoice = await _invoices.Query().AsNoTracking() + .Include(x => x.Lines).ThenInclude(l => l.Grn) + .FirstOrDefaultAsync(x => x.PurchaseInvoiceId == purchaseInvoiceId, ct); + return invoice is null ? null : new ETagged(MapInvoice(invoice), invoice.RowVersion); + } + + public async Task> CreateAsync(CreatePurchaseInvoiceRequest request, CancellationToken ct = default) + { + var vendor = await _vendors.Query().AsNoTracking().FirstOrDefaultAsync(v => v.VendorId == request.VendorId, ct) + ?? throw new DomainException(ErrorCodes.Validation, $"Vendor {request.VendorId} does not exist.", 422); + + var vendorInvoiceNo = request.VendorInvoiceNo.Trim(); + await EnsureVendorInvoiceNoIsUniqueAsync(request.VendorId, vendorInvoiceNo, excludingPurchaseInvoiceId: null, ct); + + var grns = await LoadAndValidateGrnsAsync(request.VendorId, request.GrnIds, excludingPurchaseInvoiceId: null, ct); + + var invoice = await _uow.ExecuteInTransactionAsync(async token => + { + var entity = new PurchaseInvoice + { + DocNo = await _numbers.NextAsync(DocumentTypes.PurchaseInvoice, token), + VendorInvoiceNo = vendorInvoiceNo, + VendorInvoiceDate = request.VendorInvoiceDate, + VendorId = request.VendorId, + VendorSnapshotName = vendor.Name, + VendorSnapshotTaxNo = vendor.TaxReg, + Status = PurchaseInvoiceStatus.Draft, + CreatedBy = _currentUser.AuditUserId, + CreatedAt = DateTime.UtcNow, + Lines = grns.Select(BuildLine).ToList() + }; + Recalculate(entity); + await _invoices.AddAsync(entity, token); + + foreach (var grn in grns) grn.PurchaseInvoice = entity; + + return entity; + }, ct); + + return new ETagged(MapInvoice(invoice), invoice.RowVersion); + } + + public async Task> UpdateAsync(int purchaseInvoiceId, UpdatePurchaseInvoiceRequest request, uint expectedRowVersion, CancellationToken ct = default) + { + var invoice = await _invoices.Query() + .Include(x => x.Lines).ThenInclude(l => l.Grn) + .FirstOrDefaultAsync(x => x.PurchaseInvoiceId == purchaseInvoiceId, ct) + ?? throw new NotFoundException($"Purchase invoice {purchaseInvoiceId} was not found."); + + if (invoice.RowVersion != expectedRowVersion) + throw new DomainException(ErrorCodes.ConcurrencyConflict, "The purchase invoice was modified by another request.", 412); + if (invoice.Status != PurchaseInvoiceStatus.Draft) + throw new DomainException(ErrorCodes.PurchaseInvoiceNotEditable, $"Purchase invoice {purchaseInvoiceId} is {invoice.Status} and cannot be edited.", 409); + + var vendor = await _vendors.Query().AsNoTracking().FirstOrDefaultAsync(v => v.VendorId == request.VendorId, ct) + ?? throw new DomainException(ErrorCodes.Validation, $"Vendor {request.VendorId} does not exist.", 422); + + var vendorInvoiceNo = request.VendorInvoiceNo.Trim(); + await EnsureVendorInvoiceNoIsUniqueAsync(request.VendorId, vendorInvoiceNo, excludingPurchaseInvoiceId: purchaseInvoiceId, ct); + + var grns = await LoadAndValidateGrnsAsync(request.VendorId, request.GrnIds, excludingPurchaseInvoiceId: purchaseInvoiceId, ct); + + // Free GRNs that were linked to this invoice but are no longer selected. + var newGrnIds = grns.Select(g => g.GrnId).ToHashSet(); + foreach (var line in invoice.Lines) + if (line.Grn is not null && !newGrnIds.Contains(line.Grn.GrnId)) line.Grn.PurchaseInvoiceId = null; + + invoice.VendorId = request.VendorId; + invoice.VendorInvoiceNo = vendorInvoiceNo; + invoice.VendorInvoiceDate = request.VendorInvoiceDate; + invoice.VendorSnapshotName = vendor.Name; + invoice.VendorSnapshotTaxNo = vendor.TaxReg; + invoice.Lines.Clear(); + foreach (var line in grns.Select(BuildLine)) invoice.Lines.Add(line); + foreach (var grn in grns) grn.PurchaseInvoice = invoice; + Recalculate(invoice); + invoice.UpdatedAt = DateTime.UtcNow; + + await _uow.SaveChangesAsync(ct); + return new ETagged(MapInvoice(invoice), invoice.RowVersion); + } + + public async Task PostAsync(int purchaseInvoiceId, CancellationToken ct = default) + { + var invoice = await _invoices.Query() + .Include(x => x.Lines).ThenInclude(l => l.Grn) + .FirstOrDefaultAsync(x => x.PurchaseInvoiceId == purchaseInvoiceId, ct) + ?? throw new NotFoundException($"Purchase invoice {purchaseInvoiceId} was not found."); + + if (invoice.Status != PurchaseInvoiceStatus.Draft) + throw new ConflictException($"Purchase invoice {purchaseInvoiceId} is {invoice.Status} and cannot be posted."); + + var now = DateTime.UtcNow; + + await _uow.ExecuteInTransactionAsync(async token => + { + // Reclassify the GRN clearing balance to Accounts Payable — the mirror image of + // SalesInvoicePaymentService's AR credit — before committing, so a GL rejection + // rolls the whole post back (same atomicity approach as GrnService.ConfirmAsync). + var period = await _gl.GetPeriodByDateAsync(DateOnly.FromDateTime(now), token); + var posted = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest + { + PeriodId = period.PeriodId, + EntryDate = DateOnly.FromDateTime(now), + SourceModule = "PURCHASE_INVOICE", + Reference = invoice.DocNo, + Narration = $"Vendor invoice recognized - {invoice.DocNo}", + Lines = + [ + new GlJournalEntryLineRequest(_glClearingAccountCode, invoice.GrandTotal, 0m, $"Purchase invoice {invoice.DocNo}"), + new GlJournalEntryLineRequest(_glAccountsPayableCode, 0m, invoice.GrandTotal, $"Purchase invoice {invoice.DocNo}") + ] + }, token); + + invoice.Status = PurchaseInvoiceStatus.Posted; + invoice.GlJournalNo = posted.JournalNo; + invoice.GlPostedAt = now; + invoice.UpdatedAt = now; + return 0; + }, ct); + + return MapInvoice(invoice); + } + + public async Task CancelAsync(int purchaseInvoiceId, CancellationToken ct = default) + { + var invoice = await _invoices.Query() + .Include(x => x.Lines).ThenInclude(l => l.Grn) + .FirstOrDefaultAsync(x => x.PurchaseInvoiceId == purchaseInvoiceId, ct) + ?? throw new NotFoundException($"Purchase invoice {purchaseInvoiceId} was not found."); + + if (invoice.Status != PurchaseInvoiceStatus.Draft) + throw new ConflictException($"Purchase invoice {purchaseInvoiceId} is {invoice.Status} and cannot be cancelled."); + + var dto = MapInvoice(invoice); + + // Cancelling frees the consolidated GRNs so they can be picked up by a later invoice — + // the link rows are removed (the unique index on GrnId only tracks the active link), + // while the Cancelled header row itself remains for audit history. + foreach (var line in invoice.Lines) + if (line.Grn is not null) line.Grn.PurchaseInvoiceId = null; + invoice.Lines.Clear(); + invoice.Status = PurchaseInvoiceStatus.Cancelled; + invoice.UpdatedAt = DateTime.UtcNow; + await _uow.SaveChangesAsync(ct); + + return dto with { Status = PurchaseInvoiceStatus.Cancelled, UpdatedAt = invoice.UpdatedAt }; + } + + public async Task> ListEligibleGrnsAsync(int vendorId, CancellationToken ct = default) + { + return await _grns.Query().AsNoTracking() + .Where(g => g.VendorId == vendorId && g.Status == GrnStatus.Confirmed && g.PurchaseInvoiceId == null) + .OrderBy(g => g.PostedAt) + .Select(g => new GrnEligibleForInvoiceDto( + g.GrnId, g.DocNo, g.PostedAt ?? g.CreatedAt, g.WarehouseId, g.Lines.Count, + g.Lines.Sum(l => l.ReceivedValue), g.Lines.Sum(l => l.VatAmount), g.Lines.Sum(l => l.LineTotal))) + .ToListAsync(ct); + } + + private async Task EnsureVendorInvoiceNoIsUniqueAsync(int vendorId, string vendorInvoiceNo, int? excludingPurchaseInvoiceId, CancellationToken ct) + { + var duplicate = await _invoices.Query().AsNoTracking().AnyAsync(x => + x.VendorId == vendorId + && x.Status != PurchaseInvoiceStatus.Cancelled + && x.VendorInvoiceNo == vendorInvoiceNo + && (excludingPurchaseInvoiceId == null || x.PurchaseInvoiceId != excludingPurchaseInvoiceId), ct); + if (duplicate) + throw new DomainException(ErrorCodes.DuplicateVendorInvoiceNo, + $"Vendor invoice number '{vendorInvoiceNo}' already exists for this vendor.", 409); + } + + private async Task> LoadAndValidateGrnsAsync(int vendorId, List grnIds, int? excludingPurchaseInvoiceId, CancellationToken ct) + { + var distinctIds = grnIds.Distinct().ToList(); + var grns = await _grns.Query().Include(g => g.Lines) + .Where(g => distinctIds.Contains(g.GrnId)).ToListAsync(ct); + + if (grns.Count != distinctIds.Count) + throw new DomainException(ErrorCodes.Validation, "One or more selected GRNs do not exist.", 422); + + foreach (var grn in grns) + { + if (grn.Status != GrnStatus.Confirmed) + throw new DomainException(ErrorCodes.GrnNotConfirmed, $"GRN {grn.DocNo} must be confirmed before it can be invoiced.", 409); + if (grn.VendorId != vendorId) + throw new DomainException(ErrorCodes.GrnVendorMismatch, $"GRN {grn.DocNo} belongs to a different vendor.", 409); + if (grn.PurchaseInvoiceId is not null && grn.PurchaseInvoiceId != excludingPurchaseInvoiceId) + throw new DomainException(ErrorCodes.GrnAlreadyInvoiced, $"GRN {grn.DocNo} is already linked to another purchase invoice.", 409); + } + + return grns; + } + + private static PurchaseInvoiceLine BuildLine(Grn grn) => new() + { + Grn = grn, + GrnId = grn.GrnId, + Subtotal = grn.Lines.Sum(l => l.ReceivedValue), + VatTotal = grn.Lines.Sum(l => l.VatAmount), + LineTotal = grn.Lines.Sum(l => l.LineTotal) + }; + + private static void Recalculate(PurchaseInvoice invoice) + { + invoice.Subtotal = invoice.Lines.Sum(x => x.Subtotal); + invoice.VatTotal = invoice.Lines.Sum(x => x.VatTotal); + invoice.GrandTotal = invoice.Lines.Sum(x => x.LineTotal); + invoice.PaidAmount = 0m; + invoice.BalanceAmount = invoice.GrandTotal; + } + + private static PurchaseInvoiceTotalsDto MapTotals(PurchaseInvoice x) => new( + x.Subtotal, x.VatTotal, x.GrandTotal, x.PaidAmount, x.BalanceAmount); + + private static PurchaseInvoiceSummaryDto MapSummary(PurchaseInvoice x) => new( + x.PurchaseInvoiceId, x.DocNo, x.VendorInvoiceNo, x.VendorInvoiceDate, x.VendorId, + x.VendorSnapshotName, x.Status, x.Lines.Count, MapTotals(x), x.CreatedAt, x.GlJournalNo); + + private static PurchaseInvoiceDto MapInvoice(PurchaseInvoice x) => new( + x.PurchaseInvoiceId, x.DocNo, x.VendorInvoiceNo, x.VendorInvoiceDate, x.VendorId, + x.VendorSnapshotName, x.VendorSnapshotTaxNo, x.Status, x.Lines.Count, x.CreatedBy, x.CreatedAt, x.UpdatedAt, + x.GlJournalNo, x.GlPostedAt, MapTotals(x), + x.Lines.OrderBy(l => l.PurchaseInvoiceLineId).Select(l => new PurchaseInvoiceLineDto( + l.PurchaseInvoiceLineId, l.GrnId, l.Grn?.DocNo ?? string.Empty, l.Grn?.PostedAt ?? default, + l.Subtotal, l.VatTotal, l.LineTotal)).ToList()); +} diff --git a/Backend/ERPCore/Services/SalesInvoicePaymentService.cs b/Backend/ERPCore/Services/SalesInvoicePaymentService.cs index 5d8ca28..1121d11 100644 --- a/Backend/ERPCore/Services/SalesInvoicePaymentService.cs +++ b/Backend/ERPCore/Services/SalesInvoicePaymentService.cs @@ -16,8 +16,8 @@ namespace ERPCore.Services; /// until reaches zero. Each payment posts its /// own real GL journal entry (Debit the selected bank-or-cash account / Credit Accounts /// Receivable) before being recorded, using the same call-GL-before-commit pattern as -/// — the mirror image of it (that one credits a payable -/// clearing account when vendor-paid; this one credits the receivable asset when +/// — the mirror image of it (that one debits a +/// payable account when the vendor is paid; this one credits the receivable asset when /// customer-paid) — so a rejected/unreachable GL post rolls back the whole payment atomically. /// public sealed class SalesInvoicePaymentService : ISalesInvoicePaymentService diff --git a/Backend/ERPCore/System/Errors/ErrorCodes.cs b/Backend/ERPCore/System/Errors/ErrorCodes.cs index 4b53fd4..e1d5746 100644 --- a/Backend/ERPCore/System/Errors/ErrorCodes.cs +++ b/Backend/ERPCore/System/Errors/ErrorCodes.cs @@ -73,13 +73,18 @@ public static class ErrorCodes // 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"; - // Sales invoice payments public const string SalesInvoiceNotPayable = "SALES_INVOICE_NOT_PAYABLE"; public const string SalesInvoicePaymentExceedsBalance = "SALES_INVOICE_PAYMENT_EXCEEDS_BALANCE"; public const string SalesInvoiceBankAccountNotFound = "SALES_INVOICE_BANK_ACCOUNT_NOT_FOUND"; + + // Purchase invoices (consolidate Confirmed GRNs; vendor payments now tracked here, not on the GRN) + public const string GrnAlreadyInvoiced = "GRN_ALREADY_INVOICED"; + public const string GrnVendorMismatch = "GRN_VENDOR_MISMATCH"; + public const string GrnNotConfirmed = "GRN_NOT_CONFIRMED"; + public const string PurchaseInvoiceNotEditable = "PURCHASE_INVOICE_NOT_EDITABLE"; + public const string DuplicateVendorInvoiceNo = "DUPLICATE_VENDOR_INVOICE_NO"; + public const string PurchaseInvoiceNotPayable = "PURCHASE_INVOICE_NOT_PAYABLE"; + public const string PurchaseInvoicePaymentExceedsBalance = "PURCHASE_INVOICE_PAYMENT_EXCEEDS_BALANCE"; + public const string PurchaseInvoiceBankAccountNotFound = "PURCHASE_INVOICE_BANK_ACCOUNT_NOT_FOUND"; } diff --git a/Backend/ERPCore/appsettings.json b/Backend/ERPCore/appsettings.json index 240e74f..ed05dc3 100644 --- a/Backend/ERPCore/appsettings.json +++ b/Backend/ERPCore/appsettings.json @@ -47,5 +47,9 @@ "SalesInvoice": { "GlAccountsReceivableCode": "1300" }, + "PurchaseInvoice": { + "GlClearingAccountCode": "2000", + "GlAccountsPayableCode": "2200" + }, "AllowedHosts": "*" } diff --git a/Frontend/erp-system/Frontend/erp-system/components/procurement/status-badges.tsx b/Frontend/erp-system/Frontend/erp-system/components/procurement/status-badges.tsx new file mode 100644 index 0000000..c404463 --- /dev/null +++ b/Frontend/erp-system/Frontend/erp-system/components/procurement/status-badges.tsx @@ -0,0 +1,96 @@ +import { Badge } from "@/components/ui/badge" +import { PurchaseOrderStatus, RequisitionStatus, RfqStatus } from "@/types/procurement" +import { PurchaseInvoiceStatus } from "@/types/purchase-invoices" + +// Fixed width (not w-fit) so every status pill is the same size regardless of +// label length, same convention as components/receiving/status-badges.tsx. +const badgeSize = "h-6 w-32 justify-center px-2.5 py-1 text-sm" + +function requisitionClass(status: RequisitionStatus) { + return status === "Draft" ? "bg-warning/10 text-warning border-transparent" : "bg-success/10 text-success border-transparent" +} + +export function RequisitionStatusBadge({ status }: { status: RequisitionStatus }) { + return ( + + {status} + + ) +} + +function rfqClass(status: RfqStatus) { + return status === "Open" ? "bg-success/10 text-success border-transparent" : "bg-muted text-muted-foreground border-transparent" +} + +export function RfqStatusBadge({ status }: { status: RfqStatus }) { + return ( + + {status} + + ) +} + +function poClass(status: PurchaseOrderStatus) { + if (status === "Draft" || status === "PendingApproval") return "bg-warning/10 text-warning border-transparent" + if (status === "Cancelled") return "bg-destructive/10 text-destructive border-transparent" + if (status === "Closed") return "bg-muted text-muted-foreground border-transparent" + return "bg-success/10 text-success border-transparent" // Approved / PartiallyReceived / FullyReceived +} + +export function PoStatusBadge({ status }: { status: PurchaseOrderStatus }) { + return ( + + {status} + + ) +} + +function purchaseInvoiceClass(status: PurchaseInvoiceStatus) { + if (status === "Draft") return "bg-warning/10 text-warning border-transparent" + if (status === "Posted") return "bg-success/10 text-success border-transparent" + return "bg-destructive/10 text-destructive border-transparent" // Cancelled +} + +// "Posted" reads as "Approved" everywhere in the UI, matching SalesInvoiceStatusBadge. +const purchaseInvoiceStatusLabel: Record = { + Draft: "Draft", + Posted: "Approved", + Cancelled: "Cancelled", +} + +export function PurchaseInvoiceStatusBadge({ status }: { status: PurchaseInvoiceStatus }) { + return ( + + {purchaseInvoiceStatusLabel[status]} + + ) +} + +export type PurchaseInvoicePaymentStatus = "Unpaid" | "PartiallyPaid" | "Paid" + +export function purchaseInvoicePaymentStatus(paidAmount: number, balanceAmount: number): PurchaseInvoicePaymentStatus { + if (balanceAmount <= 0) return "Paid" + if (paidAmount > 0) return "PartiallyPaid" + return "Unpaid" +} + +function purchaseInvoicePaymentClass(status: PurchaseInvoicePaymentStatus) { + 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 purchaseInvoicePaymentLabel: Record = { + Unpaid: "Unpaid", + PartiallyPaid: "Partially paid", + Paid: "Paid", +} + +export function PurchaseInvoicePaymentStatusBadge({ paidAmount, balanceAmount }: { paidAmount: number; balanceAmount: number }) { + const status = purchaseInvoicePaymentStatus(paidAmount, balanceAmount) + return ( + + {purchaseInvoicePaymentLabel[status]} + + ) +} diff --git a/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/[id]/page.tsx b/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/[id]/page.tsx new file mode 100644 index 0000000..6f6bba1 --- /dev/null +++ b/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/[id]/page.tsx @@ -0,0 +1,298 @@ +"use client" + +import { use, useEffect, useMemo, useRef, useState } from "react" +import Link from "next/link" +import { useRouter } from "next/navigation" +import { ArrowLeft, BookOpen, Printer } from "lucide-react" + +import { purchaseInvoicesApi } from "@/lib/api/purchase-invoices" +import { vendorsApi } from "@/lib/api/vendors" +import { companyProfileApi } from "@/lib/api/company-profile" +import { invoiceTemplateConfigApi } from "@/lib/api/invoice-template-config" +import { errorMessage } from "@/lib/error-map" +import { formatAmount } from "@/lib/format" +import { cn } from "@/lib/utils" +import { Vendor } from "@/types/master-data" +import { CompanyProfile } from "@/types/company-profile" +import { InvoiceTemplateConfig } from "@/types/invoice-template" +import { PurchaseInvoice, PurchaseInvoicePayment, PurchaseInvoiceStatus } from "@/types/purchase-invoices" + +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 { PurchaseInvoicePaymentDialog } from "@/components/procurement/PurchaseInvoicePaymentDialog" +import { PurchaseInvoicePaymentStatusBadge, PurchaseInvoiceStatusBadge } from "@/components/procurement/status-badges" +import { InvoicePrintHeader } from "@/components/invoice-template/InvoicePrintHeader" +import { InvoicePrintFooter } from "@/components/invoice-template/InvoicePrintFooter" +import { getInvoiceLayoutClasses } from "@/components/invoice-template/invoiceLayoutPresets" + +/** "Posted" reads as "approved" everywhere in the UI — matches PurchaseInvoiceStatusBadge's label. */ +function statusWord(status: PurchaseInvoiceStatus): string { + return status === "Posted" ? "approved" : status.toLowerCase() +} + +export default function PurchaseInvoiceDetailPage({ params }: { params: Promise<{ id: string }> }) { + const router = useRouter() + const resolvedParams = use(params) + const purchaseInvoiceId = Number(resolvedParams.id) + + const [invoice, setInvoice] = useState(null) + const [vendors, setVendors] = useState([]) + const [company, setCompany] = useState(null) + const [template, setTemplate] = useState(null) + const [error, setError] = useState(null) + const [posting, setPosting] = useState(false) + + const [payments, setPayments] = useState([]) + const [payDialogOpen, setPayDialogOpen] = useState(false) + + useEffect(() => { + if (!Number.isFinite(purchaseInvoiceId)) { + setError(`Invalid purchase invoice id '${resolvedParams.id}'.`) + return + } + Promise.all([ + vendorsApi.list({ pageSize: 200 }), + purchaseInvoicesApi.get(purchaseInvoiceId), + companyProfileApi.get().catch(() => ({ data: null, etag: null })), + invoiceTemplateConfigApi.get().catch(() => ({ data: null, etag: null })), + ]) + .then(([vendorRes, doc, companyRes, templateRes]) => { + setVendors(vendorRes.items) + setInvoice(doc.data) + setCompany(companyRes.data) + setTemplate(templateRes.data) + }) + .catch((err) => setError(errorMessage(err))) + }, [purchaseInvoiceId, resolvedParams.id]) + + // No manual Save/Post/Cancel step: a Draft landed on here is posted immediately and + // automatically — this page is only ever meant to be seen in its Posted (read-only) state. + const autoPostedRef = useRef(false) + useEffect(() => { + if (!invoice || invoice.status !== "Draft" || autoPostedRef.current) return + autoPostedRef.current = true + setPosting(true) + setError(null) + purchaseInvoicesApi + .post(purchaseInvoiceId) + .then((posted) => { + setInvoice(posted) + toast.success("Purchase invoice posted", posted.docNo) + router.refresh() + }) + .catch((err) => setError(errorMessage(err))) + .finally(() => setPosting(false)) + }, [invoice, purchaseInvoiceId, router]) + + useEffect(() => { + if (!invoice || invoice.status !== "Posted") { + setPayments([]) + return + } + purchaseInvoicesApi.listPayments(purchaseInvoiceId).then(setPayments).catch(() => setPayments([])) + }, [invoice?.status, purchaseInvoiceId]) + + function handlePaid(payment: PurchaseInvoicePayment, newStatus: PurchaseInvoiceStatus) { + setInvoice((prev) => + prev + ? { + ...prev, + status: newStatus, + totals: { ...prev.totals, paidAmount: prev.totals.paidAmount + payment.amount, balanceAmount: prev.totals.balanceAmount - payment.amount }, + } + : prev + ) + setPayments((prev) => [payment, ...prev]) + } + + const vendor = useMemo(() => vendors.find((v) => v.vendorId === invoice?.vendorId), [vendors, invoice?.vendorId]) + + if (error && !invoice) { + return
{error}
+ } + + if (!invoice) { + return ( +
+ + +
+ ) + } + + const layout = getInvoiceLayoutClasses(template?.templateStyle) + + return ( +
+
+
+ + + +
+

Purchase Invoice Details

+

{invoice.docNo}

+
+
+
+ {invoice.status !== "Draft" && ( + + + View in Ledger + + )} + + + Print + +
+
+ + {error &&
{error}
} + +
+
+
+
+
Purchase Invoice
+

{invoice.docNo}

+
+ + {invoice.status !== "Draft" && ( + + )} +
+
Vendor invoice: {invoice.vendorInvoiceNo}
+
Date: {new Date(invoice.vendorInvoiceDate).toLocaleDateString()}
+
+ +
+
+ + {layout.arrangement === "highlight" && ( +
+ Grand total + {formatAmount(invoice.totals.grandTotal)} +
+ )} + +
+
+
Vendor
+
{invoice.vendorSnapshotName}
+ {vendor?.code &&
Code: {vendor.code}
} + {invoice.vendorSnapshotTaxNo ?
Tax No: {invoice.vendorSnapshotTaxNo}
: null} +
+ {layout.arrangement !== "highlight" && ( +
+
Totals
+
+
Subtotal
+
{formatAmount(invoice.totals.subtotal)}
+
VAT
+
{formatAmount(invoice.totals.vatTotal)}
+
Grand total
+
{formatAmount(invoice.totals.grandTotal)}
+
+
+ )} +
+ +
+ + + + GRN + Posted + Subtotal + VAT + Total + + + + {invoice.lines.map((line) => ( + + + + {line.grnDocNo} + + + {new Date(line.grnPostedAt).toLocaleDateString()} + {formatAmount(line.subtotal)} + {formatAmount(line.vatTotal)} + {formatAmount(line.lineTotal)} + + ))} + +
+
+ + + +
+ {invoice.status === "Draft" ? ( +
+ {posting ? "Posting invoice…" : "Waiting to post…"} +
+ ) : ( +
+ This invoice is {statusWord(invoice.status)}. + {invoice.status === "Posted" ? ( + + ) : null} +
+ )} + + {invoice.status === "Posted" && ( +
+
+ Payment status + + Paid {formatAmount(invoice.totals.paidAmount)} of {formatAmount(invoice.totals.grandTotal)} — balance {formatAmount(invoice.totals.balanceAmount)} + {invoice.glJournalNo ? ` · GL journal ${invoice.glJournalNo}` : ""} + +
+ {payments.length > 0 && ( +
+ + + + Date + Account + Reference + GL journal + Amount + + + + {payments.map((p) => ( + + {new Date(p.paymentDate).toLocaleString()} + {p.bankAccountName} + {p.reference ?? "—"} + {p.glJournalNo ?? "—"} + {formatAmount(p.amount)} + + ))} + +
+
+ )} +
+ )} +
+
+ + +
+ ) +} diff --git a/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/new/page.tsx b/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/new/page.tsx new file mode 100644 index 0000000..7ae4efb --- /dev/null +++ b/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/new/page.tsx @@ -0,0 +1,337 @@ +"use client" + +import { Suspense, useEffect, useMemo, useRef, useState } from "react" +import { useRouter, useSearchParams } from "next/navigation" +import Link from "next/link" + +import { Button, buttonVariants } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Combobox } from "@/components/ui/combobox" +import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Skeleton } from "@/components/ui/skeleton" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { toast } from "@/components/ui/toast" +import { cn } from "@/lib/utils" +import { errorMessage } from "@/lib/error-map" +import { purchaseInvoicesApi } from "@/lib/api/purchase-invoices" +import { vendorsApi } from "@/lib/api/vendors" +import { validatePurchaseInvoiceHeader } from "@/lib/validations/purchase-invoices" +import { Vendor } from "@/types/master-data" +import { GrnEligibleForInvoice } from "@/types/purchase-invoices" + +const lkr = new Intl.NumberFormat("en-LK", { + style: "currency", + currency: "LKR", + minimumFractionDigits: 2, +}) + +function todayIso(): string { + return new Date().toISOString().slice(0, 10) +} + +function NewPurchaseInvoiceContent() { + const router = useRouter() + const searchParams = useSearchParams() + const vendorIdFromQuery = searchParams.get("vendorId") + const grnIdFromQuery = searchParams.get("grnId") + const appliedDeepLinkRef = useRef(false) + + const [vendors, setVendors] = useState([]) + const [vendorId, setVendorId] = useState(null) + const [vendorInvoiceNo, setVendorInvoiceNo] = useState("") + const [vendorInvoiceDate, setVendorInvoiceDate] = useState(todayIso()) + + const [eligibleGrns, setEligibleGrns] = useState(null) + const [selectedGrnIds, setSelectedGrnIds] = useState>(new Set()) + const [grnsError, setGrnsError] = useState(null) + + const [loading, setLoading] = useState(true) + const [submitError, setSubmitError] = useState(null) + const [errors, setErrors] = useState>({}) + const [saving, setSaving] = useState(false) + + useEffect(() => { + vendorsApi + .list({ pageSize: 200 }) + .then((res) => { + setVendors(res.items) + if (vendorIdFromQuery) { + setVendorId(Number(vendorIdFromQuery)) + } else { + setVendorId(res.items[0]?.vendorId ?? null) + } + }) + .catch((err) => setSubmitError(errorMessage(err))) + .finally(() => setLoading(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // Bumped to force a refetch when the browser restores this page from its back-forward + // cache (e.g. Back button after create() redirected away) — bfcache restores the frozen + // JS/React state verbatim without re-running effects, so without this the picker would + // keep showing the GRN we just invoiced as still eligible. + const [refetchTick, setRefetchTick] = useState(0) + useEffect(() => { + function onPageShow(e: PageTransitionEvent) { + if (e.persisted) setRefetchTick((t) => t + 1) + } + window.addEventListener("pageshow", onPageShow) + return () => window.removeEventListener("pageshow", onPageShow) + }, []) + + useEffect(() => { + if (!vendorId) { + setEligibleGrns(null) + setSelectedGrnIds(new Set()) + return + } + setGrnsError(null) + setEligibleGrns(null) + purchaseInvoicesApi + .listEligibleGrns(vendorId) + .then((grns) => { + setEligibleGrns(grns) + // Deep-linked from a GRN detail page's "Create Purchase Invoice" prompt — pre-check + // that GRN once its vendor's eligible list has loaded, but only once per page load + // (a bfcache-triggered refetch should re-apply it the same way, since the GRN may + // still be eligible if the user came Back without ever submitting). + if (grnIdFromQuery && (!appliedDeepLinkRef.current || refetchTick > 0)) { + appliedDeepLinkRef.current = true + const id = Number(grnIdFromQuery) + setSelectedGrnIds(grns.some((g) => g.grnId === id) ? new Set([id]) : new Set()) + } else if (!grnIdFromQuery) { + setSelectedGrnIds(new Set()) + } + }) + .catch((err) => setGrnsError(errorMessage(err))) + }, [vendorId, grnIdFromQuery, refetchTick]) + + const vendorOptions = useMemo( + () => vendors.map((v) => ({ value: v.vendorId, label: `${v.code} — ${v.name}` })), + [vendors] + ) + + function toggleGrn(grnId: number, checked: boolean) { + setSelectedGrnIds((prev) => { + const next = new Set(prev) + if (checked) next.add(grnId) + else next.delete(grnId) + return next + }) + } + + function toggleAll(checked: boolean) { + setSelectedGrnIds(checked ? new Set(visibleGrns.map((g) => g.grnId)) : new Set()) + } + + // Deep-linked from a GRN detail page's "Create Purchase Invoice" prompt — that GRN is the + // whole reason this page was opened, so show only it rather than the vendor's full eligible + // list (which could pull in unrelated GRNs the user never meant to consolidate here). + const deepLinkedGrnId = grnIdFromQuery ? Number(grnIdFromQuery) : null + const visibleGrns = useMemo( + () => (deepLinkedGrnId === null ? (eligibleGrns ?? []) : (eligibleGrns ?? []).filter((g) => g.grnId === deepLinkedGrnId)), + [eligibleGrns, deepLinkedGrnId] + ) + + const selectedGrns = useMemo( + () => (eligibleGrns ?? []).filter((g) => selectedGrnIds.has(g.grnId)), + [eligibleGrns, selectedGrnIds] + ) + const subtotal = selectedGrns.reduce((sum, g) => sum + g.subtotal, 0) + const vatTotal = selectedGrns.reduce((sum, g) => sum + g.vatTotal, 0) + const grandTotal = selectedGrns.reduce((sum, g) => sum + g.grandTotal, 0) + + const allSelected = visibleGrns.length > 0 && visibleGrns.every((g) => selectedGrnIds.has(g.grnId)) + + async function submit() { + const grnIds = Array.from(selectedGrnIds) + const nextErrors = validatePurchaseInvoiceHeader({ vendorId, vendorInvoiceNo, vendorInvoiceDate, grnIds }) + setErrors(nextErrors) + if (Object.keys(nextErrors).length > 0) return + + setSaving(true) + setSubmitError(null) + try { + const created = await purchaseInvoicesApi.create({ + vendorId: vendorId!, + vendorInvoiceNo: vendorInvoiceNo.trim(), + vendorInvoiceDate, + grnIds, + }) + + // Deep-linked from a specific GRN — the GRN is already known and confirmed, so there's + // nothing left to review before posting; skip the separate Draft step and post right away. + if (deepLinkedGrnId !== null) { + try { + const posted = await purchaseInvoicesApi.post(created.data.purchaseInvoiceId) + toast.success("Purchase invoice created and posted", posted.docNo) + } catch (postErr) { + toast.error("Invoice created, but could not be posted", errorMessage(postErr)) + } + } else { + toast.success("Purchase invoice created", created.data.docNo) + } + + router.push(`/dashboard/procurement/purchase-invoices/${created.data.purchaseInvoiceId}`) + } catch (err) { + setSubmitError(errorMessage(err)) + } finally { + setSaving(false) + } + } + + return ( +
+
+

New Purchase Invoice

+

Pick a vendor, then consolidate one or more of its confirmed, un-invoiced GRNs.

+
+ + {submitError && ( +
{submitError}
+ )} + + {loading && } + + {!loading && ( + <> + + + Vendor + + + + + + Vendor invoice no. + setVendorInvoiceNo(e.target.value)} + placeholder="e.g. INV-84213" + aria-invalid={!!errors.vendorInvoiceNo} + /> + + + + + Vendor invoice date + setVendorInvoiceDate(e.target.value)} + aria-invalid={!!errors.vendorInvoiceDate} + /> + + + + +
+
+

Eligible GRNs

+ {errors.grnIds && {errors.grnIds}} +
+ + {!vendorId && ( +

Select a vendor to see its confirmed, un-invoiced GRNs.

+ )} + + {vendorId && grnsError && ( +
{grnsError}
+ )} + + {vendorId && !grnsError && eligibleGrns === null && } + + {vendorId && !grnsError && eligibleGrns !== null && visibleGrns.length === 0 && ( +

+ {deepLinkedGrnId !== null + ? "That GRN is no longer eligible to invoice (already invoiced, or not confirmed)." + : "No confirmed, un-invoiced GRNs for this vendor."} +

+ )} + + {vendorId && !grnsError && eligibleGrns !== null && visibleGrns.length > 0 && ( +
+ + + + + toggleAll(!!v)} aria-label="Select all" /> + + GRN + Posted + Lines + Subtotal + VAT + Total + + + + {visibleGrns.map((g) => ( + + + toggleGrn(g.grnId, !!v)} + aria-label={`Select ${g.docNo}`} + /> + + {g.docNo} + {new Date(g.postedAt).toLocaleDateString()} + {g.lineCount} + {lkr.format(g.subtotal)} + {lkr.format(g.vatTotal)} + {lkr.format(g.grandTotal)} + + ))} + +
+
+ )} + +
+
+ Subtotal + {lkr.format(subtotal)} +
+
+ VAT + {lkr.format(vatTotal)} +
+
+ Grand total + {lkr.format(grandTotal)} +
+
+
+ +
+ + Cancel + + +
+ + )} +
+ ) +} + +export default function NewPurchaseInvoicePage() { + return ( + }> + + + ) +} diff --git a/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/page.tsx b/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/page.tsx new file mode 100644 index 0000000..38125f2 --- /dev/null +++ b/Frontend/erp-system/app/dashboard/procurement/purchase-invoices/page.tsx @@ -0,0 +1,299 @@ +"use client" + +import { useEffect, useMemo, useState } from "react" +import Link from "next/link" +import { CheckCircle2, ChevronLeft, ChevronRight, CircleDollarSign, Eye, Filter, Plus, Printer, ReceiptText } from "lucide-react" + +import { purchaseInvoicesApi } from "@/lib/api/purchase-invoices" +import { errorMessage } from "@/lib/error-map" +import { Button, buttonVariants } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Skeleton } from "@/components/ui/skeleton" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { toast } from "@/components/ui/toast" +import { PurchaseInvoicePaymentDialog } from "@/components/procurement/PurchaseInvoicePaymentDialog" +import { PurchaseInvoicePaymentStatusBadge, PurchaseInvoiceStatusBadge } from "@/components/procurement/status-badges" +import { PaginationMeta } from "@/types/common" +import { PurchaseInvoicePayment, PurchaseInvoiceStatus, PurchaseInvoiceSummary } from "@/types/purchase-invoices" +import { cn } from "@/lib/utils" + +type StatusFilter = PurchaseInvoiceStatus | "All" + +const PAGE_SIZE = 10 +const tabs: { value: StatusFilter; label: string }[] = [ + { value: "All", label: "All" }, + { value: "Draft", label: "Draft" }, + { value: "Posted", label: "Approved" }, + { value: "Cancelled", label: "Cancelled" }, +] + +export default function PurchaseInvoicesPage() { + const [rows, setRows] = useState(null) + const [pagination, setPagination] = useState(null) + const [error, setError] = useState(null) + const [status, setStatus] = useState("All") + const [searchInput, setSearchInput] = useState("") + const [query, setQuery] = useState("") + const [page, setPage] = useState(1) + const [confirmingId, setConfirmingId] = useState(null) + const [payDialogInvoice, setPayDialogInvoice] = useState(null) + const [payDialogOpen, setPayDialogOpen] = useState(false) + + useEffect(() => { + const timeout = setTimeout(() => setQuery(searchInput.trim()), 300) + return () => clearTimeout(timeout) + }, [searchInput]) + + useEffect(() => setPage(1), [query, status]) + + useEffect(() => { + setError(null) + purchaseInvoicesApi + .list({ page, pageSize: PAGE_SIZE, status: status === "All" ? undefined : status }) + .then((res) => { + setRows(res.items) + setPagination(res.pagination) + }) + .catch((err) => setError(errorMessage(err))) + }, [page, status]) + + async function handleConfirm(row: PurchaseInvoiceSummary) { + setConfirmingId(row.purchaseInvoiceId) + try { + const posted = await purchaseInvoicesApi.post(row.purchaseInvoiceId) + setRows((prev) => prev?.map((r) => (r.purchaseInvoiceId === row.purchaseInvoiceId ? { ...r, status: posted.status } : r)) ?? prev) + toast.success("Invoice confirmed", posted.docNo) + } catch (err) { + toast.error("Could not confirm invoice", errorMessage(err)) + } finally { + setConfirmingId(null) + } + } + + function handlePaid(payment: PurchaseInvoicePayment, newStatus: PurchaseInvoiceStatus) { + setRows( + (prev) => + prev?.map((r) => + r.purchaseInvoiceId === payment.purchaseInvoiceId + ? { + ...r, + status: newStatus, + totals: { ...r.totals, paidAmount: r.totals.paidAmount + payment.amount, balanceAmount: r.totals.balanceAmount - payment.amount }, + } + : r + ) ?? prev + ) + } + + const visibleRows = useMemo( + () => + rows?.filter((row) => + `${row.docNo} ${row.vendorInvoiceNo} ${row.vendorSnapshotName}`.toLowerCase().includes(query.toLowerCase()) + ) ?? [], + [rows, query] + ) + const hasFilters = status !== "All" || query.length > 0 + const grossTotal = visibleRows.reduce((sum, row) => sum + row.totals.subtotal, 0) + const vatTotal = visibleRows.reduce((sum, row) => sum + row.totals.vatTotal, 0) + const netTotal = visibleRows.reduce((sum, row) => sum + row.totals.grandTotal, 0) + const printHref = `/print/procurement/purchase-invoices?status=${encodeURIComponent(status)}&q=${encodeURIComponent(query)}` + + return ( +
+
+
+

Purchase Invoices

+

Vendor bills consolidating confirmed GRNs, with posting and settlement tracking.

+
+
+ + + Print batch + + + + New Purchase Invoice + +
+
+ +
+
+
+ {tabs.map((t) => ( + + ))} +
+ +
+ setSearchInput(e.target.value)} + placeholder="Filter by vendor, invoice no. or doc no." + className="h-12 w-full lg:max-w-sm" + /> + +
+
+ + {error &&
{error}
} + + {!error && rows === null && ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ )} + + {!error && rows !== null && visibleRows.length === 0 && ( +
+ +

{hasFilters ? "No purchase invoices match your filters." : "No purchase invoices yet."}

+
+ )} + + {!error && rows !== null && visibleRows.length > 0 && ( + <> +
+ + + + Invoice + Vendor invoice no. + Vendor + Date + GRNs + Subtotal + VAT + Total + Status + Actions + + + + {visibleRows.map((row) => ( + + + + {row.docNo} + + + {row.vendorInvoiceNo} + {row.vendorSnapshotName} + {new Date(row.vendorInvoiceDate).toLocaleDateString()} + {row.grnCount} + {row.totals.subtotal.toFixed(2)} + {row.totals.vatTotal.toFixed(2)} + {row.totals.grandTotal.toFixed(2)} + +
+ + {row.status !== "Draft" && ( + + )} +
+
+ +
+ {row.status === "Draft" && ( + + )} + {row.status !== "Cancelled" && row.totals.balanceAmount > 0 && ( + + )} + + + +
+
+
+ ))} +
+
+
+ +
+
+
+
Subtotal
+
{grossTotal.toFixed(2)}
+
+
+
VAT
+
{vatTotal.toFixed(2)}
+
+
+
Grand total
+
{netTotal.toFixed(2)}
+
+
+
+ + {pagination && pagination.totalPages > 1 && ( +
+

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

+
+ + Page {pagination.page} of {pagination.totalPages} + +
+
+ )} + + )} +
+ + +
+ ) +} diff --git a/Frontend/erp-system/app/dashboard/receiving/grn/[id]/page.tsx b/Frontend/erp-system/app/dashboard/receiving/grn/[id]/page.tsx index e207d21..a8cca59 100644 --- a/Frontend/erp-system/app/dashboard/receiving/grn/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/receiving/grn/[id]/page.tsx @@ -3,26 +3,27 @@ import { useEffect, useRef, useState } from "react" import { useParams } from "next/navigation" import Link from "next/link" -import { BookOpen, CheckCircle2, CircleDollarSign, PackageCheck, PackageX, ShieldAlert, XCircle } from "lucide-react" +import { BookOpen, CheckCircle2, FileText, PackageCheck, PackageX, ShieldAlert, XCircle } from "lucide-react" import { grnsApi } from "@/lib/api/grns" import { warehousesApi } from "@/lib/api/warehouses" import { itemsApi } from "@/lib/api/items" import { uomsApi } from "@/lib/api/uoms" +import { purchaseInvoicesApi } from "@/lib/api/purchase-invoices" 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, GrnPayment } from "@/types/grn" +import { ConfirmGrnResponse, Grn } from "@/types/grn" import { Bin, ItemListItem, Uom } from "@/types/master-data" +import { PurchaseInvoice } from "@/types/purchase-invoices" import { Badge } from "@/components/ui/badge" 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 { GrnPaymentStatusBadge, GrnStatusBadge, HoldStatusBadge } from "@/components/receiving/status-badges" -import { GrnPaymentDialog } from "@/components/receiving/GrnPaymentDialog" +import { GrnStatusBadge, HoldStatusBadge } from "@/components/receiving/status-badges" +import { PurchaseInvoiceStatusBadge } from "@/components/procurement/status-badges" export default function GrnDetailPage() { const params = useParams<{ id: string }>() @@ -38,12 +39,24 @@ export default function GrnDetailPage() { const [confirmResult, setConfirmResult] = useState(null) const [releasingLineId, setReleasingLineId] = useState(null) - const [payments, setPayments] = useState([]) - const [payDialogOpen, setPayDialogOpen] = useState(false) + const [linkedInvoice, setLinkedInvoice] = useState(null) // Stable per detail-page-session key so a retried confirm click doesn't double-post. const idempotencyKey = useRef(crypto.randomUUID()) + // Bumped to force a refetch when the browser restores this page from its back-forward + // cache (e.g. Back button after "Create Purchase Invoice" redirected away) — bfcache + // restores the frozen JS/React state verbatim without re-running effects, so without this + // the page would keep showing the pre-invoice GRN state (no linked invoice) after Back. + const [refetchTick, setRefetchTick] = useState(0) + useEffect(() => { + function onPageShow(e: PageTransitionEvent) { + if (e.persisted) setRefetchTick((t) => t + 1) + } + window.addEventListener("pageshow", onPageShow) + return () => window.removeEventListener("pageshow", onPageShow) + }, []) + useEffect(() => { if (!Number.isFinite(grnId)) return grnsApi.get(grnId).then(setGrn).catch((err) => setError(errorMessage(err))) @@ -53,7 +66,7 @@ export default function GrnDetailPage() { setUoms(uo.items) }) .catch(() => {}) - }, [grnId]) + }, [grnId, refetchTick]) useEffect(() => { if (!grn) return @@ -61,16 +74,12 @@ export default function GrnDetailPage() { }, [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]) - } + if (!grn?.purchaseInvoiceId) return + purchaseInvoicesApi + .get(grn.purchaseInvoiceId) + .then((res) => setLinkedInvoice(res.data)) + .catch(() => setLinkedInvoice(null)) + }, [grn?.purchaseInvoiceId]) function itemFor(itemId: number) { return items.find((i) => i.itemId === itemId) @@ -87,9 +96,7 @@ export default function GrnDetailPage() { try { const result = await grnsApi.confirm(grn.grnId, idempotencyKey.current) setConfirmResult(result) - setGrn((prev) => - prev ? { ...prev, status: result.status, glJournalNo: result.glJournalNo, balanceAmount: result.balanceAmount } : prev - ) + setGrn((prev) => (prev ? { ...prev, status: result.status, glJournalNo: result.glJournalNo } : prev)) toast.success("GRN confirmed", `${result.createdLayers.length} layer(s) posted to stock. Journal ${result.glJournalNo}.`) } catch (err) { setError(errorMessage(err)) @@ -140,7 +147,6 @@ export default function GrnDetailPage() {

{grn.docNo}

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

{grn.poId ? `Against PO #${grn.poId}` : "Direct receipt"} — Vendor #{grn.vendorId} — Warehouse #{grn.warehouseId} @@ -150,22 +156,14 @@ export default function GrnDetailPage() {

{grn.status !== "Draft" && ( - <> - - - View in Ledger - - {grn.balanceAmount > 0 && ( - - )} - + + + View in Ledger + )} {grn.status === "Draft" && (
- - {error && (
{error}
)} @@ -314,47 +310,42 @@ 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 ?? "—"} - - ))} - -
-
+ {grn.status === "Confirmed" && ( +
+

Purchase Invoice

+ {grn.purchaseInvoiceId ? ( +
+
+ {grn.purchaseInvoiceNo ?? `#${grn.purchaseInvoiceId}`} + {linkedInvoice && } + {linkedInvoice && linkedInvoice.status !== "Draft" && ( + + Balance due: {linkedInvoice.totals.balanceAmount.toFixed(2)} + + )} +
+ + + View invoice + +
+ ) : ( +
+ This GRN hasn't been added to a purchase invoice yet. + + + Create Purchase Invoice + +
+ )}
)}
diff --git a/Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx b/Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx index 903e3a2..d6a8763 100644 --- a/Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx +++ b/Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx @@ -98,6 +98,64 @@ function newKey() { return `gline-${keySeq}` } +let warrantySeq = 0 +/** Auto-generated warranty number, pre-filled into the input so the user can accept or edit it. */ +function generateWarrantyNumber(): string { + warrantySeq += 1 + const stamp = Date.now().toString(36).toUpperCase() + const seq = warrantySeq.toString(36).toUpperCase().padStart(3, "0") + return `WTY-${stamp}-${seq}` +} + +/** Splits a warranty number into its non-numeric prefix and trailing digit run, e.g. "WTY-000101" → { prefix: "WTY-", num: 101, width: 6 }. */ +function parseTrailingNumber(s: string): { prefix: string; num: number; width: number } | null { + const m = s.trim().match(/^(.*?)(\d+)$/) + if (!m) return null + return { prefix: m[1], num: parseInt(m[2], 10), width: m[2].length } +} + +/** + * Splices an "adding number" onto the end of the current number: it keeps everything except + * the trailing characters, replacing as many of them as `adding` is long, e.g. current + * "WTY-000101" + adding "105" (3 chars) → keep "WTY-000", replace the last 3 with "105" → + * "WTY-000105". + */ +function applyAddingNumber(current: string, adding: string): string { + const tail = adding.trim() + if (!tail) return current + if (tail.length >= current.length) return tail + return current.slice(0, current.length - tail.length) + tail +} + +/** + * Builds the sequential run of warranty numbers spanning `from` to `to` (inclusive) — both + * must share the same prefix and `to` must not come before `from`. Returns null otherwise + * so the caller can surface a validation message. + */ +function buildWarrantyRange(from: string, to: string): string[] | null { + const f = parseTrailingNumber(from) + const t = parseTrailingNumber(to) + if (!f || !t || f.prefix !== t.prefix) return null + const span = t.num - f.num + 1 + if (span <= 0) return null + return Array.from({ length: span }, (_, i) => `${f.prefix}${String(f.num + i).padStart(f.width, "0")}`) +} + +/** + * Resizes a line's warrantyNumbers to match its unit count and auto-fills any blank slot + * with a generated number — existing (manually edited) values are left untouched. + */ +function syncWarrantyNumbers(l: DraftLine, item: ItemListItem | null): string[] { + if (item?.warranty !== "Warranty") return l.warrantyNumbers + const units = unitCount(l.qty) + const next = l.warrantyNumbers.slice(0, units) + while (next.length < units) next.push("") + for (let i = 0; i < units; i++) { + if (!next[i] || next[i].trim() === "") next[i] = generateWarrantyNumber() + } + return next +} + function emptyLine(): DraftLine { return { key: newKey(), @@ -218,8 +276,8 @@ export default function NewGrnPage() { } setWarehouseId((prev) => prev ?? openLines[0].warehouseId) setLines( - openLines.map( - (l): DraftLine => ({ + openLines.map((l): DraftLine => { + const draft: DraftLine = { key: newKey(), poLineId: l.poLineId, itemId: l.itemId, @@ -231,8 +289,9 @@ export default function NewGrnPage() { vatPct: headerVatPct.trim() || "0", holdStatus: "Available", warrantyNumbers: [], - }) - ) + } + return { ...draft, warrantyNumbers: syncWarrantyNumbers(draft, itemFor(draft.itemId)) } + }) ) } catch (err) { setHeaderError(errorMessage(err)) @@ -257,7 +316,13 @@ export default function NewGrnPage() { } function updateLine(key: string, patch: Partial) { - setLines((prev) => prev.map((l) => (l.key === key ? { ...l, ...patch } : l))) + setLines((prev) => + prev.map((l) => { + if (l.key !== key) return l + const merged = { ...l, ...patch } + return { ...merged, warrantyNumbers: syncWarrantyNumbers(merged, itemFor(merged.itemId)) } + }) + ) } function removeLine(key: string) { @@ -280,6 +345,62 @@ export default function NewGrnPage() { ) } + // Typed "From" number plus an "Adding number" per line — "Replace numbers" keeps From as-is + // and splices Adding number onto its tail (see applyAddingNumber) to get To, then fills as + // many units as that range spans (from unit 1), leaving any remaining units' numbers untouched. + const [warrantyRangeInputs, setWarrantyRangeInputs] = useState>({}) + const [warrantyAddErrors, setWarrantyAddErrors] = useState>({}) + + function updateWarrantyRangeInput(lineKey: string, field: "from" | "adding", value: string) { + setWarrantyRangeInputs((prev) => { + const current = prev[lineKey] ?? { from: "", adding: "" } + return { ...prev, [lineKey]: { ...current, [field]: value } } + }) + } + + // Fills units 1..(To-From+1) with the typed sequence — only as many units as the range + // actually spans. Anything beyond that (the other units' numbers) is left exactly as it + // was: nothing gets wiped just because the range doesn't cover the whole line. + function replaceWarrantyNumbers(lineKey: string, units: number) { + const range = warrantyRangeInputs[lineKey] + if (!range?.from.trim() || !range?.adding.trim()) { + setWarrantyAddErrors((prev) => ({ ...prev, [lineKey]: "Enter both a from number and an adding number" })) + return + } + // "From" can be typed short too (e.g. "101") — it splices onto the line's current unit-1 + // number the same way "adding" splices onto "From", so you never have to retype the prefix. + const currentFirst = lines.find((l) => l.key === lineKey)?.warrantyNumbers[0]?.trim() + const from = currentFirst ? applyAddingNumber(currentFirst, range.from) : range.from + const to = applyAddingNumber(from, range.adding) + const seq = buildWarrantyRange(from, to) + if (!seq) { + setWarrantyAddErrors((prev) => ({ ...prev, [lineKey]: "The computed number must share From's prefix and not come before it" })) + return + } + if (seq.length > units) { + setWarrantyAddErrors((prev) => ({ + ...prev, + [lineKey]: `From/adding number spans ${seq.length} number(s) but this line only has ${units} unit(s)`, + })) + return + } + setWarrantyAddErrors((prev) => { + const next = { ...prev } + delete next[lineKey] + return next + }) + setLines((prev) => + prev.map((l) => { + if (l.key !== lineKey) return l + const next = [...l.warrantyNumbers] + while (next.length < units) next.push("") + for (let i = 0; i < seq.length; i++) next[i] = seq[i] + return { ...l, warrantyNumbers: next } + }) + ) + // From/To are left exactly as typed — not cleared — so a typo is easy to fix in place. + } + // Lines whose item is warranty-tracked and carry at least one unit — these are the rows // the "Warranty numbers" tab needs to capture, one input per unit. const warrantyLines = lines @@ -806,17 +927,56 @@ export default function NewGrnPage() {
{warrantyLines.map(({ line, item, units }) => { const errors = lineErrors[line.key] ?? {} + const range = warrantyRangeInputs[line.key] ?? { from: "", adding: "" } + const addError = warrantyAddErrors[line.key] return (
{item ? `${item.sku} — ${item.name}` : `Item #${line.itemId}`}
+
+
+ + updateWarrantyRangeInput(line.key, "from", e.target.value)} + placeholder="e.g. 101 or WTY-000101" + className="h-9 w-40 text-sm" + /> +
+
+ + updateWarrantyRangeInput(line.key, "adding", e.target.value)} + placeholder="e.g. 120" + className="h-9 w-40 text-sm" + /> +
+ + + Type just the trailing digits — From and Adding number each replace that many + characters off the end of unit 1's current number. Fills starting from unit 1 + — units beyond the range keep their number. + + {addError && } +
Unit - Warranty number + + Warranty number + (auto-generated — edit if needed) + diff --git a/Frontend/erp-system/app/dashboard/receiving/grn/page.tsx b/Frontend/erp-system/app/dashboard/receiving/grn/page.tsx index 1d48f76..72f9a89 100644 --- a/Frontend/erp-system/app/dashboard/receiving/grn/page.tsx +++ b/Frontend/erp-system/app/dashboard/receiving/grn/page.tsx @@ -6,7 +6,6 @@ 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" @@ -23,7 +22,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { GrnPaymentStatusBadge, GrnStatusBadge } from "@/components/receiving/status-badges" +import { GrnStatusBadge } from "@/components/receiving/status-badges" type StatusFilter = GrnStatus | "All" @@ -175,8 +174,7 @@ export default function GrnListPage() { Vendor Warehouse Status - Balance - Payment + Invoice Created Actions @@ -199,14 +197,15 @@ export default function GrnListPage() { - - {grn.status === "Draft" ? "—" : formatAmount(grn.balanceAmount)} - - {grn.status === "Draft" ? ( + {grn.status !== "Confirmed" ? ( — + ) : grn.purchaseInvoiceId ? ( + + {grn.purchaseInvoiceNo ?? `#${grn.purchaseInvoiceId}`} + ) : ( - + Not invoiced )} {new Date(grn.createdAt).toLocaleString()} diff --git a/Frontend/erp-system/app/dashboard/sales/invoices/[id]/page.tsx b/Frontend/erp-system/app/dashboard/sales/invoices/[id]/page.tsx index 68e8784..54851a1 100644 --- a/Frontend/erp-system/app/dashboard/sales/invoices/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/invoices/[id]/page.tsx @@ -11,17 +11,24 @@ import { itemsApi } from "@/lib/api/items" import { uomsApi } from "@/lib/api/uoms" import { baseUomLabel } from "@/lib/uom-label" import { warehousesApi } from "@/lib/api/warehouses" +import { companyProfileApi } from "@/lib/api/company-profile" +import { invoiceTemplateConfigApi } from "@/lib/api/invoice-template-config" import { errorMessage } from "@/lib/error-map" import { cn } from "@/lib/utils" import { getSuggestedUnitPrice } from "@/lib/sales-line-utils" import { Customer } from "@/types/customers" import { ItemListItem, Uom, Warehouse } from "@/types/master-data" +import { CompanyProfile } from "@/types/company-profile" +import { InvoiceTemplateConfig } from "@/types/invoice-template" import { CreateSalesInvoiceLineRequest, SalesInvoice, SalesInvoicePayment, SalesInvoicePostingCheck, SalesInvoiceStatus, SalesInvoiceType } from "@/types/sales" import { toast } from "@/components/ui/toast" import { Label } from "@/components/ui/label" import { Button } from "@/components/ui/button" import { SalesInvoicePaymentDialog } from "@/components/sales/SalesInvoicePaymentDialog" import { SalesInvoiceStatusBadge, SalesPaymentStatusBadge } from "@/components/sales/status-badges" +import { InvoicePrintHeader } from "@/components/invoice-template/InvoicePrintHeader" +import { InvoicePrintFooter } from "@/components/invoice-template/InvoicePrintFooter" +import { getInvoiceLayoutClasses } from "@/components/invoice-template/invoiceLayoutPresets" type Line = CreateSalesInvoiceLineRequest & { key: string } @@ -63,6 +70,8 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i const [items, setItems] = useState([]) const [uoms, setUoms] = useState([]) const [warehouses, setWarehouses] = useState([]) + const [company, setCompany] = useState(null) + const [template, setTemplate] = useState(null) const [lines, setLines] = useState([blankLine("line-1")]) const [customerId, setCustomerId] = useState(null) const [warehouseId, setWarehouseId] = useState(null) @@ -87,13 +96,17 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i uomsApi.list({ pageSize: 200 }), warehousesApi.list({ pageSize: 200 }), salesApi.getInvoice(invoiceId), + companyProfileApi.get().catch(() => ({ data: null, etag: null })), + invoiceTemplateConfigApi.get().catch(() => ({ data: null, etag: null })), ]) - .then(([customerRes, itemRes, uomRes, warehouseRes, doc]) => { + .then(([customerRes, itemRes, uomRes, warehouseRes, doc, companyRes, templateRes]) => { setCustomers(customerRes.items) setItems(itemRes.items) setUoms(uomRes.items) setWarehouses(warehouseRes.items) setInvoice(doc.data) + setCompany(companyRes.data) + setTemplate(templateRes.data) setEtag(doc.etag) setCustomerId(doc.data.customerId) setWarehouseId(doc.data.warehouseId) @@ -274,6 +287,8 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i return
{error ?? "Invoice data is loading or unavailable."}
} + const layout = getInvoiceLayoutClasses(template?.templateStyle) + return (
@@ -301,7 +316,7 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i
-
+
Sales Invoice

{invoice.invoiceNo}

@@ -314,14 +329,18 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i Date: {new Date(invoice.invoiceDate).toLocaleDateString()}
-
-
ERP Core Trading
-
Company details are not configured for this invoice view.
-
+
-
+ {layout.arrangement === "highlight" && ( +
+ Net payable + {money.format(invoice.totals.netPayable)} +
+ )} + +
Bill To
{invoice.customerSnapshotName}
@@ -334,26 +353,28 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i
{warehouse?.name ?? `#${invoice.warehouseId}`}
Code: {warehouse?.code ?? invoice.warehouseId}
-
-
Totals
-
-
Subtotal
-
{money.format(invoice.totals.subtotal)}
-
Discount
-
{money.format(invoice.totals.discountTotal)}
-
Free qty
-
{freeQtyTotal.toFixed(0)}
-
Tax
-
{money.format(invoice.totals.taxTotal)}
-
Net payable
-
{money.format(invoice.totals.netPayable)}
+ {layout.arrangement !== "highlight" && ( +
+
Totals
+
+
Subtotal
+
{money.format(invoice.totals.subtotal)}
+
Discount
+
{money.format(invoice.totals.discountTotal)}
+
Free qty
+
{freeQtyTotal.toFixed(0)}
+
Tax
+
{money.format(invoice.totals.taxTotal)}
+
Net payable
+
{money.format(invoice.totals.netPayable)}
+
-
+ )}
- + @@ -446,10 +467,7 @@ export default function SalesInvoiceDetailPage({ params }: { params: Promise<{ i ) : null} -
-
Notes
-

Standard invoice template view.

-
+
diff --git a/Frontend/erp-system/app/dashboard/settings/company-profile/page.tsx b/Frontend/erp-system/app/dashboard/settings/company-profile/page.tsx index accc8c6..2787c5d 100644 --- a/Frontend/erp-system/app/dashboard/settings/company-profile/page.tsx +++ b/Frontend/erp-system/app/dashboard/settings/company-profile/page.tsx @@ -1,22 +1,290 @@ -import Link from "next/link" -import { Building2 } from "lucide-react" +"use client" -import { buttonVariants } from "@/components/ui/button" -import { cn } from "@/lib/utils" +import { useEffect, useRef, useState } from "react" +import { Building2, Landmark, Trash2, Upload } from "lucide-react" + +import { companyProfileApi } from "@/lib/api/company-profile" +import { errorMessage } from "@/lib/error-map" +import { validateCompanyProfile } from "@/lib/validations/company-profile" +import { CompanyProfile } from "@/types/company-profile" + +import { Button } from "@/components/ui/button" +import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Skeleton } from "@/components/ui/skeleton" +import { toast } from "@/components/ui/toast" + +const blank = { + legalName: "", + tradeName: "", + taxRegistrationNo: "", + vatRegistrationNo: "", + addressLine1: "", + addressLine2: "", + city: "", + country: "", + phone: "", + email: "", + bankName: "", + bankBranch: "", + accountName: "", + accountNumber: "", + swiftCode: "", +} export default function CompanyProfilePage() { + const [profile, setProfile] = useState(null) + const [etag, setEtag] = useState(null) + const [error, setError] = useState(null) + const [saving, setSaving] = useState(false) + const [uploadingLogo, setUploadingLogo] = useState(false) + const [errors, setErrors] = useState>({}) + const [form, setForm] = useState(blank) + const fileInputRef = useRef(null) + + function load() { + setError(null) + companyProfileApi + .get() + .then((res) => { + setProfile(res.data) + setEtag(res.etag) + setForm({ + legalName: res.data.legalName, + tradeName: res.data.tradeName, + taxRegistrationNo: res.data.taxRegistrationNo ?? "", + vatRegistrationNo: res.data.vatRegistrationNo ?? "", + addressLine1: res.data.addressLine1, + addressLine2: res.data.addressLine2 ?? "", + city: res.data.city, + country: res.data.country, + phone: res.data.phone, + email: res.data.email, + bankName: res.data.bankName ?? "", + bankBranch: res.data.bankBranch ?? "", + accountName: res.data.accountName ?? "", + accountNumber: res.data.accountNumber ?? "", + swiftCode: res.data.swiftCode ?? "", + }) + }) + .catch((err) => setError(errorMessage(err))) + } + + useEffect(load, []) + + function set(field: K, value: string) { + setForm((prev) => ({ ...prev, [field]: value })) + } + + async function save() { + if (!etag) return + const nextErrors = validateCompanyProfile(form) + setErrors(nextErrors) + if (Object.keys(nextErrors).length > 0) return + + setSaving(true) + try { + const res = await companyProfileApi.update( + { + legalName: form.legalName.trim(), + tradeName: form.tradeName.trim(), + taxRegistrationNo: form.taxRegistrationNo.trim() || null, + vatRegistrationNo: form.vatRegistrationNo.trim() || null, + addressLine1: form.addressLine1.trim(), + addressLine2: form.addressLine2.trim() || null, + city: form.city.trim(), + country: form.country.trim(), + phone: form.phone.trim(), + email: form.email.trim(), + bankName: form.bankName.trim() || null, + bankBranch: form.bankBranch.trim() || null, + accountName: form.accountName.trim() || null, + accountNumber: form.accountNumber.trim() || null, + swiftCode: form.swiftCode.trim() || null, + }, + etag + ) + setProfile(res.data) + setEtag(res.etag) + toast.success("Company profile saved") + } catch (err) { + toast.error("Could not save company profile", errorMessage(err)) + load() + } finally { + setSaving(false) + } + } + + async function handleLogoPick(e: React.ChangeEvent) { + const file = e.target.files?.[0] + e.target.value = "" + if (!file) return + setUploadingLogo(true) + try { + const res = await companyProfileApi.uploadLogo(file) + setProfile(res.data) + setEtag(res.etag) + toast.success("Logo uploaded") + } catch (err) { + toast.error("Could not upload logo", errorMessage(err)) + } finally { + setUploadingLogo(false) + } + } + + async function removeLogo() { + setUploadingLogo(true) + try { + const res = await companyProfileApi.deleteLogo() + setProfile(res.data) + setEtag(res.etag) + toast.success("Logo removed") + } catch (err) { + toast.error("Could not remove logo", errorMessage(err)) + } finally { + setUploadingLogo(false) + } + } + return (
-
- -
-

Company Profile

-

This feature is not available yet.

-
- - Back to Settings - +
+

Company Profile

+

Legal identity, contact, and bank details used on printed invoices.

+ + {error && ( +
{error}
+ )} + + {!error && !profile && } + + {!error && profile && ( +
+
+ +

Identity & contact

+
+
+ + + + Legal name + set("legalName", e.target.value)} aria-invalid={!!errors.legalName} /> + + + + Trade name + set("tradeName", e.target.value)} placeholder="Shown on invoices if set" /> + + + Tax registration no. + set("taxRegistrationNo", e.target.value)} /> + + + VAT registration no. + set("vatRegistrationNo", e.target.value)} /> + + + Address line 1 + set("addressLine1", e.target.value)} aria-invalid={!!errors.addressLine1} /> + + + + Address line 2 + set("addressLine2", e.target.value)} /> + + + City + set("city", e.target.value)} aria-invalid={!!errors.city} /> + + + + Country + set("country", e.target.value)} aria-invalid={!!errors.country} /> + + + + Phone + set("phone", e.target.value)} aria-invalid={!!errors.phone} /> + + + + Email + set("email", e.target.value)} aria-invalid={!!errors.email} /> + + + + +
+ +

Bank details

+
+
+ + + + Bank name + set("bankName", e.target.value)} /> + + + Branch + set("bankBranch", e.target.value)} /> + + + Account name + set("accountName", e.target.value)} /> + + + Account number + set("accountNumber", e.target.value)} /> + + + SWIFT code + set("swiftCode", e.target.value)} /> + + + +
+

Logo

+
+
+ +
+
+ {profile.logoUrl ? ( + // eslint-disable-next-line @next/next/no-img-element -- small settings preview, not worth next/image config + {profile.legalName} + ) : ( + No logo + )} +
+ + + {profile.logoUrl && ( + + )} +
+ +
+ {profile.updatedAt && ( +

+ Last changed {new Date(profile.updatedAt).toLocaleString()} + {profile.updatedBy ? ` by user #${profile.updatedBy}` : ""}. +

+ )} + +
+
+ )}
) } diff --git a/Frontend/erp-system/app/dashboard/settings/configuration/page.tsx b/Frontend/erp-system/app/dashboard/settings/configuration/page.tsx new file mode 100644 index 0000000..71995b0 --- /dev/null +++ b/Frontend/erp-system/app/dashboard/settings/configuration/page.tsx @@ -0,0 +1,54 @@ +import Link from "next/link" +import { Building2, LayoutTemplate, SlidersHorizontal } from "lucide-react" + +const cards = [ + { + title: "Product Configuration", + href: "/dashboard/products/settings", + icon: SlidersHorizontal, + description: "Configure product and master-data options", + }, + { + title: "Invoice Template", + href: "/dashboard/settings/invoice-template", + icon: LayoutTemplate, + description: "Logo, footer, terms, and template style for printed invoices", + }, + { + title: "Company Profile", + href: "/dashboard/settings/company-profile", + icon: Building2, + description: "Invoice header, tax, and bank details", + }, +] + +export default function ConfigurationPage() { + return ( +
+
+

Configuration

+

+ Product options, invoice presentation, and company details used across the ERP. +

+
+ +
+ {cards.map((card) => ( + +
+ +
+
+

{card.title}

+

{card.description}

+
+ + ))} +
+
+ ) +} diff --git a/Frontend/erp-system/app/dashboard/settings/invoice-template/page.tsx b/Frontend/erp-system/app/dashboard/settings/invoice-template/page.tsx new file mode 100644 index 0000000..9ae997f --- /dev/null +++ b/Frontend/erp-system/app/dashboard/settings/invoice-template/page.tsx @@ -0,0 +1,275 @@ +"use client" + +import { useEffect, useState } from "react" +import { LayoutTemplate } from "lucide-react" + +import { invoiceTemplateConfigApi } from "@/lib/api/invoice-template-config" +import { companyProfileApi } from "@/lib/api/company-profile" +import { errorMessage } from "@/lib/error-map" +import { InvoiceTemplateConfig, InvoiceTemplateStyle, LogoPlacement } from "@/types/invoice-template" +import { CompanyProfile } from "@/types/company-profile" + +import { Button } from "@/components/ui/button" +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Skeleton } from "@/components/ui/skeleton" +import { Switch } from "@/components/ui/switch" +import { Textarea } from "@/components/ui/textarea" +import { toast } from "@/components/ui/toast" +import { InvoicePrintHeader } from "@/components/invoice-template/InvoicePrintHeader" +import { InvoicePrintFooter } from "@/components/invoice-template/InvoicePrintFooter" +import { getInvoiceLayoutClasses } from "@/components/invoice-template/invoiceLayoutPresets" + +/** + * Invoice Template Config — a singleton settings row shared by both the Sales Invoice and + * Purchase Invoice print pages. Unlike Product Configuration's per-toggle autosave, this page + * holds two free-text fields (footer note, terms & conditions) that must not PUT on every + * keystroke, so it's an explicit-Save form instead. + */ +export default function InvoiceTemplatePage() { + const [config, setConfig] = useState(null) + const [etag, setEtag] = useState(null) + const [company, setCompany] = useState(null) + const [error, setError] = useState(null) + const [saving, setSaving] = useState(false) + + const [templateStyle, setTemplateStyle] = useState("Classic") + const [logoPlacement, setLogoPlacement] = useState("Left") + const [showBankDetails, setShowBankDetails] = useState(true) + const [showTaxRegNo, setShowTaxRegNo] = useState(true) + const [footerNote, setFooterNote] = useState("") + const [termsAndConditions, setTermsAndConditions] = useState("") + + function load() { + setError(null) + Promise.all([invoiceTemplateConfigApi.get(), companyProfileApi.get().catch(() => ({ data: null, etag: null }))]) + .then(([configRes, companyRes]) => { + setConfig(configRes.data) + setEtag(configRes.etag) + setCompany(companyRes.data) + setTemplateStyle(configRes.data.templateStyle) + setLogoPlacement(configRes.data.logoPlacement) + setShowBankDetails(configRes.data.showBankDetails) + setShowTaxRegNo(configRes.data.showTaxRegNo) + setFooterNote(configRes.data.footerNote) + setTermsAndConditions(configRes.data.termsAndConditions) + }) + .catch((err) => setError(errorMessage(err))) + } + + useEffect(load, []) + + async function save() { + if (!etag) return + setSaving(true) + try { + const res = await invoiceTemplateConfigApi.update( + { templateStyle, logoPlacement, showBankDetails, showTaxRegNo, footerNote, termsAndConditions }, + etag + ) + setConfig(res.data) + setEtag(res.etag) + toast.success("Invoice template saved") + } catch (err) { + toast.error("Could not save invoice template", errorMessage(err)) + load() + } finally { + setSaving(false) + } + } + + const previewConfig: InvoiceTemplateConfig = { + templateStyle, + logoPlacement, + showBankDetails, + showTaxRegNo, + footerNote, + termsAndConditions, + updatedAt: config?.updatedAt ?? null, + updatedBy: config?.updatedBy ?? null, + } + + return ( +
+
+

Invoice Template

+

+ Logo placement, footer, terms, and template style used by both Sales and Purchase invoice print views. +

+
+ + {error && ( +
{error}
+ )} + + {!error && !config && } + + {!error && config && ( + <> +
+
+ +

Layout

+
+
+ + + + Template style + value={templateStyle} onValueChange={(v) => v && setTemplateStyle(v)}> + + + + + Classic + Compact + Modern + + + + + + Logo placement + value={logoPlacement} onValueChange={(v) => v && setLogoPlacement(v)}> + + + + + Left + Center + Right + None + + + + + +
+
+ +
+ Show bank details + Prints the company's bank name, account and SWIFT code on invoices. +
+
+
+ +
+ Show tax registration no. + Prints the company's tax registration number in the invoice header. +
+
+
+ + + + Footer note +
Item UOM