diff --git a/Backend/ERPCore/Controllers/PurchaseOrdersController.cs b/Backend/ERPCore/Controllers/PurchaseOrdersController.cs
new file mode 100644
index 0000000..03c9524
--- /dev/null
+++ b/Backend/ERPCore/Controllers/PurchaseOrdersController.cs
@@ -0,0 +1,74 @@
+using ERPCore.Domain.Enums;
+using ERPCore.Dtos.Common;
+using ERPCore.Dtos.Procurement;
+using ERPCore.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+/// Purchase-order endpoints (docs/11 §3.3).
+[Route("api/v1/purchase-orders")]
+public sealed class PurchaseOrdersController : ApiControllerBase
+{
+ private readonly IPurchaseOrderService _pos;
+
+ public PurchaseOrdersController(IPurchaseOrderService pos) => _pos = pos;
+
+ [HttpGet]
+ [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
+ public async Task>> List(
+ [FromQuery] PageQuery query, [FromQuery] PurchaseOrderStatus? status, [FromQuery] long? vendorId, CancellationToken ct)
+ => Ok(await _pos.ListAsync(query, status, vendorId, ct));
+
+ [HttpGet("{poId:long}")]
+ [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(long poId, CancellationToken ct)
+ {
+ var result = await _pos.GetAsync(poId, ct);
+ if (result is null) return NotFound();
+
+ SetETag(result.RowVersion);
+ return Ok(result.Value);
+ }
+
+ /// Create a PO — auto-approved on creation in Phase 1 (FR-PROC-04). Totals computed server-side.
+ [HttpPost]
+ [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+ public async Task> Create([FromBody] CreatePurchaseOrderRequest request, CancellationToken ct)
+ {
+ var result = await _pos.CreateAsync(request, ct);
+ SetETag(result.RowVersion);
+ return Created($"/api/v1/purchase-orders/{result.Value.PoId}", result.Value);
+ }
+
+ /// Edit while open (FR-PROC-05); requires If-Match. 409 PO_NOT_EDITABLE if closed.
+ [HttpPut("{poId:long}")]
+ [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ [ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
+ public async Task> Update(long poId, [FromBody] UpdatePurchaseOrderRequest request, CancellationToken ct)
+ {
+ var expected = RequireIfMatch();
+ var result = await _pos.UpdateAsync(poId, request, expected, ct);
+ SetETag(result.RowVersion);
+ return Ok(result.Value);
+ }
+
+ /// Approve — no-op in Phase 1 (POs auto-approve); transitions PendingApproval→Approved when enabled.
+ [HttpPost("{poId:long}/approve")]
+ [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Approve(long poId, CancellationToken ct)
+ => Ok(await _pos.ApproveAsync(poId, ct));
+
+ /// Cancel — 409 if any goods have been received against the PO.
+ [HttpPost("{poId:long}/cancel")]
+ [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task> Cancel(long poId, [FromBody] CancelPurchaseOrderRequest request, CancellationToken ct)
+ => Ok(await _pos.CancelAsync(poId, request.Reason, ct));
+}
diff --git a/Backend/ERPCore/Controllers/RequisitionsController.cs b/Backend/ERPCore/Controllers/RequisitionsController.cs
new file mode 100644
index 0000000..10d4c08
--- /dev/null
+++ b/Backend/ERPCore/Controllers/RequisitionsController.cs
@@ -0,0 +1,44 @@
+using ERPCore.Dtos.Common;
+using ERPCore.Dtos.Procurement;
+using ERPCore.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+/// Purchase-requisition endpoints (docs/11 §3.1).
+[Route("api/v1/requisitions")]
+public sealed class RequisitionsController : ApiControllerBase
+{
+ private readonly IRequisitionService _requisitions;
+
+ public RequisitionsController(IRequisitionService requisitions) => _requisitions = requisitions;
+
+ [HttpGet]
+ [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
+ public async Task>> List([FromQuery] PageQuery query, CancellationToken ct)
+ => Ok(await _requisitions.ListAsync(query, ct));
+
+ [HttpGet("{requisitionId:long}")]
+ [ProducesResponseType(typeof(RequisitionDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(long requisitionId, CancellationToken ct)
+ {
+ var dto = await _requisitions.GetAsync(requisitionId, ct);
+ return dto is null ? NotFound() : Ok(dto);
+ }
+
+ [HttpPost]
+ [ProducesResponseType(typeof(RequisitionDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+ public async Task> Create([FromBody] CreateRequisitionRequest request, CancellationToken ct)
+ {
+ var dto = await _requisitions.CreateAsync(request, ct);
+ return Created($"/api/v1/requisitions/{dto.RequisitionId}", dto);
+ }
+
+ [HttpPost("{requisitionId:long}/submit")]
+ [ProducesResponseType(typeof(RequisitionDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Submit(long requisitionId, CancellationToken ct)
+ => Ok(await _requisitions.SubmitAsync(requisitionId, ct));
+}
diff --git a/Backend/ERPCore/Controllers/RfqsController.cs b/Backend/ERPCore/Controllers/RfqsController.cs
new file mode 100644
index 0000000..736ddbc
--- /dev/null
+++ b/Backend/ERPCore/Controllers/RfqsController.cs
@@ -0,0 +1,49 @@
+using ERPCore.Dtos.Procurement;
+using ERPCore.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+/// RFQ & quotation endpoints (docs/11 §3.2).
+[Route("api/v1/rfqs")]
+public sealed class RfqsController : ApiControllerBase
+{
+ private readonly IRfqService _rfqs;
+
+ public RfqsController(IRfqService rfqs) => _rfqs = rfqs;
+
+ [HttpGet("{rfqId:long}")]
+ [ProducesResponseType(typeof(RfqDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(long rfqId, CancellationToken ct)
+ {
+ var dto = await _rfqs.GetAsync(rfqId, ct);
+ return dto is null ? NotFound() : Ok(dto);
+ }
+
+ [HttpPost]
+ [ProducesResponseType(typeof(RfqDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+ public async Task> Create([FromBody] CreateRfqRequest request, CancellationToken ct)
+ {
+ var dto = await _rfqs.CreateAsync(request, ct);
+ return Created($"/api/v1/rfqs/{dto.RfqId}", dto);
+ }
+
+ [HttpPost("{rfqId:long}/quotations")]
+ [ProducesResponseType(typeof(VendorQuotationDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+ public async Task> AddQuotation(long rfqId, [FromBody] CreateQuotationRequest request, CancellationToken ct)
+ {
+ var dto = await _rfqs.AddQuotationAsync(rfqId, request, ct);
+ return Created($"/api/v1/rfqs/{rfqId}/quotations/{dto.QuotationId}", dto);
+ }
+
+ [HttpGet("{rfqId:long}/comparison")]
+ [ProducesResponseType(typeof(RfqComparisonDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Comparison(long rfqId, CancellationToken ct)
+ => Ok(await _rfqs.GetComparisonAsync(rfqId, ct));
+}
diff --git a/Backend/ERPCore/Domain/DocumentTypes.cs b/Backend/ERPCore/Domain/DocumentTypes.cs
new file mode 100644
index 0000000..0a84d94
--- /dev/null
+++ b/Backend/ERPCore/Domain/DocumentTypes.cs
@@ -0,0 +1,17 @@
+namespace ERPCore.Domain;
+
+///
+/// Document-type prefixes for and the
+/// generated document numbers (docs/10 §B.8.2). One prefix per numbered document.
+///
+public static class DocumentTypes
+{
+ public const string Requisition = "PR";
+ public const string Rfq = "RFQ";
+ public const string PurchaseOrder = "PO";
+ public const string Grn = "GRN";
+ public const string Transfer = "TRF";
+ public const string Adjustment = "ADJ";
+ public const string Count = "CNT";
+ public const string PurchaseReturn = "PRET";
+}
diff --git a/Backend/ERPCore/Domain/Entities/NumberSequence.cs b/Backend/ERPCore/Domain/Entities/NumberSequence.cs
new file mode 100644
index 0000000..6db8de6
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/NumberSequence.cs
@@ -0,0 +1,15 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// Per-document-type, per-year running counter behind human document numbers
+/// (FR-X-03): PR-2026-00001, PO-2026-00042, … Numbers are issued
+/// inside the document's transaction so they are unique and gap-controlled.
+/// Model: docs/10 Part C.7.
+///
+public class NumberSequence
+{
+ public long SequenceId { get; set; }
+ public string DocType { get; set; } = string.Empty;
+ public int Year { get; set; }
+ public long LastNumber { get; set; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/PoLine.cs b/Backend/ERPCore/Domain/Entities/PoLine.cs
new file mode 100644
index 0000000..b564499
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/PoLine.cs
@@ -0,0 +1,28 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// Purchase-order line (FR-PROC-03). is the line tax rate
+/// (e.g. 0.18); accrues as GRNs confirm (FR-PROC-07).
+/// Model: docs/10 Part C.2.
+///
+public class PoLine
+{
+ public long PoLineId { get; set; }
+
+ public long PoId { get; set; }
+ public PurchaseOrder? PurchaseOrder { get; set; }
+
+ public long ItemId { get; set; }
+ public Item? Item { get; set; }
+
+ public long UomId { get; set; }
+ public Uom? Uom { get; set; }
+
+ public long WarehouseId { get; set; }
+ public Warehouse? Warehouse { get; set; }
+
+ public decimal Qty { get; set; }
+ public decimal UnitPrice { get; set; }
+ public decimal Tax { get; set; }
+ public decimal QtyReceived { get; set; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/PurchaseOrder.cs b/Backend/ERPCore/Domain/Entities/PurchaseOrder.cs
new file mode 100644
index 0000000..257f67d
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/PurchaseOrder.cs
@@ -0,0 +1,36 @@
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Domain.Entities;
+
+///
+/// Purchase order header (FR-PROC-03..06). Mutable aggregate with a
+/// ETag token; editable while open (FR-PROC-05).
+/// Phase 1 auto-approves on creation; is retained
+/// for the future approval workflow. Totals are computed server-side from lines
+/// (not stored). Model: docs/10 Part C.2.
+///
+public class PurchaseOrder
+{
+ public long PoId { get; set; }
+ public string DocNo { get; set; } = string.Empty;
+
+ public long VendorId { get; set; }
+ public Vendor? Vendor { get; set; }
+
+ public long? RequisitionId { get; set; }
+ public Requisition? Requisition { get; set; }
+
+ public PurchaseOrderStatus Status { get; set; } = PurchaseOrderStatus.Draft;
+ public bool ApprovalRequired { get; set; }
+
+ public long CreatedBy { get; set; }
+ public User? Creator { get; set; }
+
+ public DateTime CreatedAt { get; set; }
+ public DateTime? UpdatedAt { get; set; }
+
+ /// PostgreSQL xmin-backed optimistic concurrency token (ETag source).
+ public uint RowVersion { get; set; }
+
+ public ICollection Lines { get; set; } = new List();
+}
diff --git a/Backend/ERPCore/Domain/Entities/Requisition.cs b/Backend/ERPCore/Domain/Entities/Requisition.cs
new file mode 100644
index 0000000..e6cfda6
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/Requisition.cs
@@ -0,0 +1,21 @@
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Domain.Entities;
+
+///
+/// Purchase requisition header (FR-PROC-01). is the audit
+/// actor from the token (never the body). Model: docs/10 Part C.2.
+///
+public class Requisition
+{
+ public long RequisitionId { get; set; }
+ public string DocNo { get; set; } = string.Empty;
+
+ public long RequestedBy { get; set; }
+ public User? Requester { get; set; }
+
+ public RequisitionStatus Status { get; set; } = RequisitionStatus.Draft;
+ public DateTime CreatedAt { get; set; }
+
+ public ICollection Lines { get; set; } = new List();
+}
diff --git a/Backend/ERPCore/Domain/Entities/RequisitionLine.cs b/Backend/ERPCore/Domain/Entities/RequisitionLine.cs
new file mode 100644
index 0000000..13afc08
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/RequisitionLine.cs
@@ -0,0 +1,16 @@
+namespace ERPCore.Domain.Entities;
+
+/// Requisition line (FR-PROC-01). Model: docs/10 Part C.2.
+public class RequisitionLine
+{
+ public long ReqLineId { get; set; }
+
+ public long RequisitionId { get; set; }
+ public Requisition? Requisition { get; set; }
+
+ public long ItemId { get; set; }
+ public Item? Item { get; set; }
+
+ public decimal Qty { get; set; }
+ public DateOnly? RequiredBy { get; set; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/Rfq.cs b/Backend/ERPCore/Domain/Entities/Rfq.cs
new file mode 100644
index 0000000..e7177fd
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/Rfq.cs
@@ -0,0 +1,22 @@
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Domain.Entities;
+
+///
+/// Request for Quotation header (FR-PROC-02) raised from a requisition. Vendor
+/// quotations attach for comparison. Model: docs/10 Part C.2.
+///
+public class Rfq
+{
+ public long RfqId { get; set; }
+ public string DocNo { get; set; } = string.Empty;
+
+ public long RequisitionId { get; set; }
+ public Requisition? Requisition { get; set; }
+
+ public RfqStatus Status { get; set; } = RfqStatus.Open;
+ public DateTime CreatedAt { get; set; }
+
+ public ICollection Lines { get; set; } = new List();
+ public ICollection Quotations { get; set; } = new List();
+}
diff --git a/Backend/ERPCore/Domain/Entities/RfqLine.cs b/Backend/ERPCore/Domain/Entities/RfqLine.cs
new file mode 100644
index 0000000..a91191b
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/RfqLine.cs
@@ -0,0 +1,15 @@
+namespace ERPCore.Domain.Entities;
+
+/// RFQ line — an item + quantity being quoted (FR-PROC-02). Model: docs/10 Part C.2.
+public class RfqLine
+{
+ public long RfqLineId { get; set; }
+
+ public long RfqId { get; set; }
+ public Rfq? Rfq { get; set; }
+
+ public long ItemId { get; set; }
+ public Item? Item { get; set; }
+
+ public decimal Qty { get; set; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/User.cs b/Backend/ERPCore/Domain/Entities/User.cs
new file mode 100644
index 0000000..24b58db
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/User.cs
@@ -0,0 +1,20 @@
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Domain.Entities;
+
+///
+/// Application user (FR-X-01). In Phase 1 authentication/RBAC are deferred; this
+/// table exists so mutations can be stamped with an audit actor and documents can
+/// carry a `createdBy`/`requestedBy` FK. A seeded system user (id 1) is the
+/// fallback actor until `/auth/login` lands (§6). Model: docs/10 Part C.7.
+///
+public class User
+{
+ /// Seeded fallback actor used while auth is deferred.
+ public const long SystemUserId = 1;
+
+ public long UserId { get; set; }
+ public string Username { get; set; } = string.Empty;
+ public string DisplayName { get; set; } = string.Empty;
+ public EntityStatus Status { get; set; } = EntityStatus.Active;
+}
diff --git a/Backend/ERPCore/Domain/Entities/VendorQuotation.cs b/Backend/ERPCore/Domain/Entities/VendorQuotation.cs
new file mode 100644
index 0000000..5eab1f4
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/VendorQuotation.cs
@@ -0,0 +1,26 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// A vendor's quotation against an RFQ (FR-PROC-02). Per-item pricing lives in
+/// .
+///
+/// Deviation note: docs/10 Part C.2 models VENDOR_QUOTATION with scalar
+/// unit_price/lead_days and no item reference, which cannot represent
+/// the per-line pricing the API contract requires (docs/11 §3.2). This header +
+/// split follows the authoritative API shape.
+///
+///
+public class VendorQuotation
+{
+ public long QuotationId { get; set; }
+
+ public long RfqId { get; set; }
+ public Rfq? Rfq { get; set; }
+
+ public long VendorId { get; set; }
+ public Vendor? Vendor { get; set; }
+
+ public DateTime CreatedAt { get; set; }
+
+ public ICollection Lines { get; set; } = new List();
+}
diff --git a/Backend/ERPCore/Domain/Entities/VendorQuotationLine.cs b/Backend/ERPCore/Domain/Entities/VendorQuotationLine.cs
new file mode 100644
index 0000000..6880524
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/VendorQuotationLine.cs
@@ -0,0 +1,16 @@
+namespace ERPCore.Domain.Entities;
+
+/// Per-item quoted price and lead time within a (docs/11 §3.2).
+public class VendorQuotationLine
+{
+ public long QuotationLineId { get; set; }
+
+ public long QuotationId { get; set; }
+ public VendorQuotation? Quotation { get; set; }
+
+ public long ItemId { get; set; }
+ public Item? Item { get; set; }
+
+ public decimal UnitPrice { get; set; }
+ public int LeadDays { get; set; }
+}
diff --git a/Backend/ERPCore/Domain/Enums/PurchaseOrderStatus.cs b/Backend/ERPCore/Domain/Enums/PurchaseOrderStatus.cs
new file mode 100644
index 0000000..9cb7c49
--- /dev/null
+++ b/Backend/ERPCore/Domain/Enums/PurchaseOrderStatus.cs
@@ -0,0 +1,17 @@
+namespace ERPCore.Domain.Enums;
+
+///
+/// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Phase 1 auto-approves on
+/// creation, so is reserved (not entered) until the
+/// approval workflow is enabled (FR-PROC-04). Stored as a string.
+///
+public enum PurchaseOrderStatus
+{
+ Draft,
+ PendingApproval,
+ Approved,
+ PartiallyReceived,
+ FullyReceived,
+ Closed,
+ Cancelled
+}
diff --git a/Backend/ERPCore/Domain/Enums/RequisitionStatus.cs b/Backend/ERPCore/Domain/Enums/RequisitionStatus.cs
new file mode 100644
index 0000000..8e9d65e
--- /dev/null
+++ b/Backend/ERPCore/Domain/Enums/RequisitionStatus.cs
@@ -0,0 +1,8 @@
+namespace ERPCore.Domain.Enums;
+
+/// Purchase-requisition lifecycle (docs/11 §3.1; docs/10 §B.8.1). Stored as a string.
+public enum RequisitionStatus
+{
+ Draft,
+ Submitted
+}
diff --git a/Backend/ERPCore/Domain/Enums/RfqStatus.cs b/Backend/ERPCore/Domain/Enums/RfqStatus.cs
new file mode 100644
index 0000000..89c85b8
--- /dev/null
+++ b/Backend/ERPCore/Domain/Enums/RfqStatus.cs
@@ -0,0 +1,8 @@
+namespace ERPCore.Domain.Enums;
+
+/// RFQ lifecycle (docs/11 §3.2). Stored as a string.
+public enum RfqStatus
+{
+ Open,
+ Closed
+}
diff --git a/Backend/ERPCore/Dtos/Procurement/PurchaseOrderDtos.cs b/Backend/ERPCore/Dtos/Procurement/PurchaseOrderDtos.cs
new file mode 100644
index 0000000..2c372c8
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Procurement/PurchaseOrderDtos.cs
@@ -0,0 +1,52 @@
+using System.ComponentModel.DataAnnotations;
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Dtos.Procurement;
+
+// Responses (docs/11 §3.3) ------------------------------------------------------
+
+public sealed record PoLineDto(
+ long PoLineId, long ItemId, long UomId, long WarehouseId,
+ decimal Qty, decimal UnitPrice, decimal Tax, decimal QtyReceived);
+
+public sealed record PoTotalsDto(decimal SubTotal, decimal Tax, decimal GrandTotal, string Currency);
+
+public sealed record PurchaseOrderDto(
+ long PoId, string DocNo, long VendorId, long? RequisitionId, PurchaseOrderStatus Status,
+ bool ApprovalRequired, long CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt,
+ PoTotalsDto Totals, IReadOnlyList Lines);
+
+public sealed record PurchaseOrderSummaryDto(
+ long PoId, string DocNo, long VendorId, PurchaseOrderStatus Status,
+ bool ApprovalRequired, DateTime CreatedAt, PoTotalsDto Totals);
+
+// Requests — server sets docNo, status, createdBy, timestamps, qtyReceived, totals
+
+public sealed class CreatePoLineInput
+{
+ [Required] public long ItemId { get; set; }
+ [Required] public long UomId { get; set; }
+ [Required] public long WarehouseId { get; set; }
+ [Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
+ [Range(0, double.MaxValue)] public decimal UnitPrice { get; set; }
+ [Range(0, 1)] public decimal Tax { get; set; }
+}
+
+public sealed class CreatePurchaseOrderRequest
+{
+ [Required] public long VendorId { get; set; }
+ public long? RequisitionId { get; set; }
+ [Required, MinLength(1)] public List Lines { get; set; } = new();
+}
+
+public sealed class UpdatePurchaseOrderRequest
+{
+ [Required] public long VendorId { get; set; }
+ public long? RequisitionId { get; set; }
+ [Required, MinLength(1)] public List Lines { get; set; } = new();
+}
+
+public sealed class CancelPurchaseOrderRequest
+{
+ [StringLength(500)] public string? Reason { get; set; }
+}
diff --git a/Backend/ERPCore/Dtos/Procurement/RequisitionDtos.cs b/Backend/ERPCore/Dtos/Procurement/RequisitionDtos.cs
new file mode 100644
index 0000000..d9af44f
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Procurement/RequisitionDtos.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Dtos.Procurement;
+
+// Responses (docs/11 §3.1) ------------------------------------------------------
+
+public sealed record RequisitionLineDto(long ReqLineId, long ItemId, decimal Qty, DateOnly? RequiredBy);
+
+public sealed record RequisitionDto(
+ long RequisitionId, string DocNo, RequisitionStatus Status, long RequestedBy,
+ DateTime CreatedAt, IReadOnlyList Lines);
+
+public sealed record RequisitionSummaryDto(
+ long RequisitionId, string DocNo, RequisitionStatus Status, long RequestedBy, DateTime CreatedAt);
+
+// Requests — server sets docNo, status, requestedBy (audit actor), timestamps ----
+
+public sealed class CreateRequisitionLineInput
+{
+ [Required] public long ItemId { get; set; }
+ [Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
+ public DateOnly? RequiredBy { get; set; }
+}
+
+public sealed class CreateRequisitionRequest
+{
+ [Required, MinLength(1)] public List Lines { get; set; } = new();
+}
diff --git a/Backend/ERPCore/Dtos/Procurement/RfqDtos.cs b/Backend/ERPCore/Dtos/Procurement/RfqDtos.cs
new file mode 100644
index 0000000..8109941
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Procurement/RfqDtos.cs
@@ -0,0 +1,50 @@
+using System.ComponentModel.DataAnnotations;
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Dtos.Procurement;
+
+// Responses (docs/11 §3.2) ------------------------------------------------------
+
+public sealed record RfqLineDto(long RfqLineId, long ItemId, decimal Qty);
+
+public sealed record RfqDto(
+ long RfqId, string DocNo, long RequisitionId, RfqStatus Status, IReadOnlyList Lines);
+
+public sealed record QuotationLineDto(long ItemId, decimal UnitPrice, int LeadDays);
+
+public sealed record VendorQuotationDto(
+ long QuotationId, long RfqId, long VendorId, IReadOnlyList Lines);
+
+/// Per-item, per-vendor price matrix for GET /rfqs/{id}/comparison.
+public sealed record RfqComparisonCellDto(long VendorId, long QuotationId, decimal UnitPrice, int LeadDays);
+public sealed record RfqComparisonRowDto(long ItemId, decimal Qty, IReadOnlyList Quotes);
+public sealed record RfqComparisonDto(long RfqId, IReadOnlyList VendorIds, IReadOnlyList Rows);
+
+// Requests ----------------------------------------------------------------------
+
+public sealed class CreateRfqLineInput
+{
+ [Required] public long ItemId { get; set; }
+ [Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
+}
+
+public sealed class CreateRfqRequest
+{
+ [Required] public long RequisitionId { get; set; }
+ /// Vendors the RFQ is issued to (validated for existence; quotations reference them).
+ public List VendorIds { get; set; } = new();
+ [Required, MinLength(1)] public List Lines { get; set; } = new();
+}
+
+public sealed class CreateQuotationLineInput
+{
+ [Required] public long ItemId { get; set; }
+ [Range(0, double.MaxValue)] public decimal UnitPrice { get; set; }
+ [Range(0, int.MaxValue)] public int LeadDays { get; set; }
+}
+
+public sealed class CreateQuotationRequest
+{
+ [Required] public long VendorId { get; set; }
+ [Required, MinLength(1)] public List Lines { get; set; } = new();
+}
diff --git a/Backend/ERPCore/Infra/Auth/CurrentUser.cs b/Backend/ERPCore/Infra/Auth/CurrentUser.cs
index 780f200..06ff7ba 100644
--- a/Backend/ERPCore/Infra/Auth/CurrentUser.cs
+++ b/Backend/ERPCore/Infra/Auth/CurrentUser.cs
@@ -1,4 +1,5 @@
using System.Security.Claims;
+using ERPCore.Domain.Entities;
namespace ERPCore.Infra.Auth;
@@ -28,4 +29,6 @@ public sealed class CurrentUser : ICurrentUser
return string.IsNullOrWhiteSpace(sub) ? SystemActor : sub;
}
}
+
+ public long AuditUserId => long.TryParse(UserId, out var id) ? id : User.SystemUserId;
}
diff --git a/Backend/ERPCore/Infra/Auth/ICurrentUser.cs b/Backend/ERPCore/Infra/Auth/ICurrentUser.cs
index ad7860d..be7317f 100644
--- a/Backend/ERPCore/Infra/Auth/ICurrentUser.cs
+++ b/Backend/ERPCore/Infra/Auth/ICurrentUser.cs
@@ -10,6 +10,13 @@ public interface ICurrentUser
/// The audit actor identity (token `sub`), or "system" when unauthenticated.
string UserId { get; }
+ ///
+ /// Numeric audit actor for stamping document createdBy/requestedBy FKs.
+ /// Resolves the token sub to a user id; falls back to the seeded system
+ /// user () while auth is deferred (§6).
+ ///
+ long AuditUserId { get; }
+
/// True when the request carries an authenticated principal.
bool IsAuthenticated { get; }
}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/NumberSequenceConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/NumberSequenceConfiguration.cs
new file mode 100644
index 0000000..45282a2
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/NumberSequenceConfiguration.cs
@@ -0,0 +1,21 @@
+using ERPCore.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ERPCore.Infra.Persistence.Configurations;
+
+public sealed class NumberSequenceConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("number_sequences");
+ builder.HasKey(s => s.SequenceId);
+
+ builder.Property(s => s.DocType).HasColumnName("doc_type").IsRequired().HasMaxLength(10);
+ builder.Property(s => s.Year).HasColumnName("year").IsRequired();
+ builder.Property(s => s.LastNumber).HasColumnName("last_number").IsRequired();
+
+ // One counter per (doc type, year); also the ON CONFLICT target for atomic issue.
+ builder.HasIndex(s => new { s.DocType, s.Year }).IsUnique();
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/PurchaseOrderConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/PurchaseOrderConfiguration.cs
new file mode 100644
index 0000000..8d452e0
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/PurchaseOrderConfiguration.cs
@@ -0,0 +1,76 @@
+using ERPCore.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ERPCore.Infra.Persistence.Configurations;
+
+public sealed class PurchaseOrderConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("purchase_orders");
+ builder.HasKey(p => p.PoId);
+
+ builder.Property(p => p.DocNo).IsRequired().HasMaxLength(30);
+ builder.HasIndex(p => p.DocNo).IsUnique();
+
+ builder.Property(p => p.Status)
+ .HasConversion().HasMaxLength(20).IsRequired();
+ builder.Property(p => p.CreatedAt).IsRequired();
+
+ // PostgreSQL xmin system column as the optimistic concurrency token (ETag).
+ builder.Property(p => p.RowVersion).IsRowVersion();
+
+ builder.HasOne(p => p.Vendor)
+ .WithMany()
+ .HasForeignKey(p => p.VendorId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasOne(p => p.Requisition)
+ .WithMany()
+ .HasForeignKey(p => p.RequisitionId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasOne(p => p.Creator)
+ .WithMany()
+ .HasForeignKey(p => p.CreatedBy)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasIndex(p => p.Status);
+ builder.HasIndex(p => p.VendorId);
+ }
+}
+
+public sealed class PoLineConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("po_lines");
+ builder.HasKey(l => l.PoLineId);
+
+ builder.Property(l => l.Qty).HasPrecision(18, 4);
+ builder.Property(l => l.UnitPrice).HasPrecision(18, 4);
+ builder.Property(l => l.Tax).HasPrecision(9, 4);
+ builder.Property(l => l.QtyReceived).HasPrecision(18, 4);
+
+ builder.HasOne(l => l.PurchaseOrder)
+ .WithMany(p => p.Lines)
+ .HasForeignKey(l => l.PoId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(l => l.Item)
+ .WithMany()
+ .HasForeignKey(l => l.ItemId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasOne(l => l.Uom)
+ .WithMany()
+ .HasForeignKey(l => l.UomId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasOne(l => l.Warehouse)
+ .WithMany()
+ .HasForeignKey(l => l.WarehouseId)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/RequisitionConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/RequisitionConfiguration.cs
new file mode 100644
index 0000000..492d786
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/RequisitionConfiguration.cs
@@ -0,0 +1,49 @@
+using ERPCore.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ERPCore.Infra.Persistence.Configurations;
+
+public sealed class RequisitionConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("requisitions");
+ builder.HasKey(r => r.RequisitionId);
+
+ builder.Property(r => r.DocNo).IsRequired().HasMaxLength(30);
+ builder.HasIndex(r => r.DocNo).IsUnique();
+
+ builder.Property(r => r.Status)
+ .HasConversion().HasMaxLength(20).IsRequired();
+ builder.Property(r => r.CreatedAt).IsRequired();
+
+ builder.HasOne(r => r.Requester)
+ .WithMany()
+ .HasForeignKey(r => r.RequestedBy)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasIndex(r => r.Status);
+ }
+}
+
+public sealed class RequisitionLineConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("requisition_lines");
+ builder.HasKey(l => l.ReqLineId);
+
+ builder.Property(l => l.Qty).HasPrecision(18, 4);
+
+ builder.HasOne(l => l.Requisition)
+ .WithMany(r => r.Lines)
+ .HasForeignKey(l => l.RequisitionId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(l => l.Item)
+ .WithMany()
+ .HasForeignKey(l => l.ItemId)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/RfqConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/RfqConfiguration.cs
new file mode 100644
index 0000000..6a95ef3
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/RfqConfiguration.cs
@@ -0,0 +1,92 @@
+using ERPCore.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ERPCore.Infra.Persistence.Configurations;
+
+public sealed class RfqConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("rfqs");
+ builder.HasKey(r => r.RfqId);
+
+ builder.Property(r => r.DocNo).IsRequired().HasMaxLength(30);
+ builder.HasIndex(r => r.DocNo).IsUnique();
+
+ builder.Property(r => r.Status)
+ .HasConversion().HasMaxLength(20).IsRequired();
+ builder.Property(r => r.CreatedAt).IsRequired();
+
+ builder.HasOne(r => r.Requisition)
+ .WithMany()
+ .HasForeignKey(r => r.RequisitionId)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
+
+public sealed class RfqLineConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("rfq_lines");
+ builder.HasKey(l => l.RfqLineId);
+
+ builder.Property(l => l.Qty).HasPrecision(18, 4);
+
+ builder.HasOne(l => l.Rfq)
+ .WithMany(r => r.Lines)
+ .HasForeignKey(l => l.RfqId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(l => l.Item)
+ .WithMany()
+ .HasForeignKey(l => l.ItemId)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
+
+public sealed class VendorQuotationConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("vendor_quotations");
+ builder.HasKey(q => q.QuotationId);
+
+ builder.Property(q => q.CreatedAt).IsRequired();
+
+ builder.HasOne(q => q.Rfq)
+ .WithMany(r => r.Quotations)
+ .HasForeignKey(q => q.RfqId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(q => q.Vendor)
+ .WithMany()
+ .HasForeignKey(q => q.VendorId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ // One quotation per vendor per RFQ.
+ builder.HasIndex(q => new { q.RfqId, q.VendorId }).IsUnique();
+ }
+}
+
+public sealed class VendorQuotationLineConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("vendor_quotation_lines");
+ builder.HasKey(l => l.QuotationLineId);
+
+ builder.Property(l => l.UnitPrice).HasPrecision(18, 4);
+
+ builder.HasOne(l => l.Quotation)
+ .WithMany(q => q.Lines)
+ .HasForeignKey(l => l.QuotationId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(l => l.Item)
+ .WithMany()
+ .HasForeignKey(l => l.ItemId)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/UserConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/UserConfiguration.cs
new file mode 100644
index 0000000..7a1c48b
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/UserConfiguration.cs
@@ -0,0 +1,30 @@
+using ERPCore.Domain.Entities;
+using ERPCore.Domain.Enums;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ERPCore.Infra.Persistence.Configurations;
+
+public sealed class UserConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("users");
+ builder.HasKey(u => u.UserId);
+
+ builder.Property(u => u.Username).IsRequired().HasMaxLength(100);
+ builder.HasIndex(u => u.Username).IsUnique();
+ builder.Property(u => u.DisplayName).IsRequired().HasMaxLength(200);
+ builder.Property(u => u.Status)
+ .HasConversion().HasMaxLength(20).IsRequired();
+
+ // Seeded fallback audit actor while auth is deferred (§6).
+ builder.HasData(new User
+ {
+ UserId = User.SystemUserId,
+ Username = "system",
+ DisplayName = "System",
+ Status = EntityStatus.Active
+ });
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
index e55b00f..d8bf5d5 100644
--- a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
+++ b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
@@ -25,6 +25,20 @@ public class ErpDbContext : DbContext
public DbSet Warehouses => Set();
public DbSet Bins => Set();
+ // --- Cross-cutting (docs/10 Part C.7) ---
+ public DbSet Users => Set();
+ public DbSet NumberSequences => Set();
+
+ // --- Procurement (docs/10 Part C.2) ---
+ public DbSet Requisitions => Set();
+ public DbSet RequisitionLines => Set();
+ public DbSet Rfqs => Set();
+ public DbSet RfqLines => Set();
+ public DbSet VendorQuotations => Set();
+ public DbSet VendorQuotationLines => Set();
+ public DbSet PurchaseOrders => Set();
+ public DbSet PoLines => Set();
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
diff --git a/Backend/ERPCore/Infra/Persistence/Migrations/20260710090753_AddProcurement.Designer.cs b/Backend/ERPCore/Infra/Persistence/Migrations/20260710090753_AddProcurement.Designer.cs
new file mode 100644
index 0000000..9ce865a
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Migrations/20260710090753_AddProcurement.Designer.cs
@@ -0,0 +1,987 @@
+//
+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.Infra.Persistence.Migrations
+{
+ [DbContext(typeof(ErpDbContext))]
+ [Migration("20260710090753_AddProcurement")]
+ partial class AddProcurement
+ {
+ ///
+ 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.Bin", b =>
+ {
+ b.Property("BinId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ 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("bigint");
+
+ b.HasKey("BinId");
+
+ b.HasIndex("WarehouseId", "Code")
+ .IsUnique();
+
+ b.ToTable("bins", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
+ {
+ b.Property("CategoryId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CategoryId"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("ParentId")
+ .HasColumnType("bigint");
+
+ b.HasKey("CategoryId");
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("categories", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
+ {
+ b.Property("ItemId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ItemId"));
+
+ b.Property("BaseUomId")
+ .HasColumnType("bigint");
+
+ b.Property("CategoryId")
+ .HasColumnType("bigint");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DefaultVendorId")
+ .HasColumnType("bigint");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("ItemType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ 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("TaxClass")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("TrackingMode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("ItemId");
+
+ b.HasIndex("BaseUomId");
+
+ b.HasIndex("CategoryId");
+
+ b.HasIndex("DefaultVendorId");
+
+ b.HasIndex("Sku")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("items", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
+ {
+ b.Property("ReorderId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReorderId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("ReorderPoint")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ReorderQty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("ReorderId");
+
+ b.HasIndex("WarehouseId");
+
+ b.HasIndex("ItemId", "WarehouseId")
+ .IsUnique();
+
+ b.ToTable("item_reorders", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.NumberSequence", b =>
+ {
+ b.Property("SequenceId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceId"));
+
+ b.Property("DocType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasColumnName("doc_type");
+
+ b.Property("LastNumber")
+ .HasColumnType("bigint")
+ .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.PoLine", b =>
+ {
+ b.Property("PoLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PoLineId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("PoId")
+ .HasColumnType("bigint");
+
+ 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("UomId")
+ .HasColumnType("bigint");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("PoLineId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("PoId");
+
+ b.HasIndex("UomId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("po_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
+ {
+ b.Property("PoId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PoId"));
+
+ b.Property("ApprovalRequired")
+ .HasColumnType("boolean");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("RequisitionId")
+ .HasColumnType("bigint");
+
+ 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("bigint");
+
+ 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.Requisition", b =>
+ {
+ b.Property("RequisitionId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ 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("bigint");
+
+ 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("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReqLineId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("RequiredBy")
+ .HasColumnType("date");
+
+ b.Property("RequisitionId")
+ .HasColumnType("bigint");
+
+ 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("bigint");
+
+ 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("bigint");
+
+ 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("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RfqLineId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("RfqId")
+ .HasColumnType("bigint");
+
+ b.HasKey("RfqLineId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("RfqId");
+
+ b.ToTable("rfq_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b =>
+ {
+ b.Property("UomId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ 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.UomConversion", b =>
+ {
+ b.Property("ConversionId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ConversionId"));
+
+ b.Property("Factor")
+ .HasPrecision(18, 6)
+ .HasColumnType("numeric(18,6)");
+
+ b.Property("FromUomId")
+ .HasColumnType("bigint");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("ToUomId")
+ .HasColumnType("bigint");
+
+ b.HasKey("ConversionId");
+
+ b.HasIndex("FromUomId");
+
+ b.HasIndex("ToUomId");
+
+ b.HasIndex("ItemId", "FromUomId", "ToUomId")
+ .IsUnique();
+
+ b.ToTable("uom_conversions", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.User", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId"));
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ 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("Username")
+ .IsUnique();
+
+ b.ToTable("users", (string)null);
+
+ b.HasData(
+ new
+ {
+ UserId = 1L,
+ DisplayName = "System",
+ Status = "Active",
+ Username = "system"
+ });
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b =>
+ {
+ b.Property("VendorId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ 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("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("QuotationId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RfqId")
+ .HasColumnType("bigint");
+
+ b.Property("VendorId")
+ .HasColumnType("bigint");
+
+ 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("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("QuotationLineId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("LeadDays")
+ .HasColumnType("integer");
+
+ b.Property("QuotationId")
+ .HasColumnType("bigint");
+
+ 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("bigint");
+
+ 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.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.Category", b =>
+ {
+ b.HasOne("ERPCore.Domain.Entities.Category", "Parent")
+ .WithMany("Children")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Parent");
+ });
+
+ 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.Category", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ERPCore.Domain.Entities.Vendor", "DefaultVendor")
+ .WithMany()
+ .HasForeignKey("DefaultVendorId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("BaseUom");
+
+ b.Navigation("Category");
+
+ b.Navigation("DefaultVendor");
+ });
+
+ 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.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.Uom", "Uom")
+ .WithMany()
+ .HasForeignKey("UomId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
+ .WithMany()
+ .HasForeignKey("WarehouseId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("PurchaseOrder");
+
+ b.Navigation("Uom");
+
+ b.Navigation("Warehouse");
+ });
+
+ 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.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.UomConversion", b =>
+ {
+ b.HasOne("ERPCore.Domain.Entities.Uom", "FromUom")
+ .WithMany()
+ .HasForeignKey("FromUomId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ERPCore.Domain.Entities.Item", "Item")
+ .WithMany("UomConversions")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("ERPCore.Domain.Entities.Uom", "ToUom")
+ .WithMany()
+ .HasForeignKey("ToUomId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("FromUom");
+
+ b.Navigation("Item");
+
+ b.Navigation("ToUom");
+ });
+
+ 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.Category", b =>
+ {
+ b.Navigation("Children");
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
+ {
+ b.Navigation("ReorderSettings");
+
+ b.Navigation("UomConversions");
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", 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.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/Infra/Persistence/Migrations/20260710090753_AddProcurement.cs b/Backend/ERPCore/Infra/Persistence/Migrations/20260710090753_AddProcurement.cs
new file mode 100644
index 0000000..ee2d903
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Migrations/20260710090753_AddProcurement.cs
@@ -0,0 +1,448 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ERPCore.Infra.Persistence.Migrations
+{
+ ///
+ public partial class AddProcurement : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "number_sequences",
+ columns: table => new
+ {
+ SequenceId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ doc_type = table.Column(type: "character varying(10)", maxLength: 10, nullable: false),
+ year = table.Column(type: "integer", nullable: false),
+ last_number = table.Column(type: "bigint", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_number_sequences", x => x.SequenceId);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "users",
+ columns: table => new
+ {
+ UserId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Username = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ DisplayName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_users", x => x.UserId);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "requisitions",
+ columns: table => new
+ {
+ RequisitionId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ DocNo = table.Column(type: "character varying(30)", maxLength: 30, nullable: false),
+ RequestedBy = table.Column(type: "bigint", nullable: false),
+ Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_requisitions", x => x.RequisitionId);
+ table.ForeignKey(
+ name: "FK_requisitions_users_RequestedBy",
+ column: x => x.RequestedBy,
+ principalTable: "users",
+ principalColumn: "UserId",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "purchase_orders",
+ columns: table => new
+ {
+ PoId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ DocNo = table.Column(type: "character varying(30)", maxLength: 30, nullable: false),
+ VendorId = table.Column(type: "bigint", nullable: false),
+ RequisitionId = table.Column(type: "bigint", nullable: true),
+ Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ ApprovalRequired = table.Column(type: "boolean", nullable: false),
+ CreatedBy = table.Column(type: "bigint", 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_orders", x => x.PoId);
+ table.ForeignKey(
+ name: "FK_purchase_orders_requisitions_RequisitionId",
+ column: x => x.RequisitionId,
+ principalTable: "requisitions",
+ principalColumn: "RequisitionId",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_purchase_orders_users_CreatedBy",
+ column: x => x.CreatedBy,
+ principalTable: "users",
+ principalColumn: "UserId",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_purchase_orders_vendors_VendorId",
+ column: x => x.VendorId,
+ principalTable: "vendors",
+ principalColumn: "VendorId",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "requisition_lines",
+ columns: table => new
+ {
+ ReqLineId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ RequisitionId = table.Column(type: "bigint", nullable: false),
+ ItemId = table.Column(type: "bigint", nullable: false),
+ Qty = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
+ RequiredBy = table.Column(type: "date", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_requisition_lines", x => x.ReqLineId);
+ table.ForeignKey(
+ name: "FK_requisition_lines_items_ItemId",
+ column: x => x.ItemId,
+ principalTable: "items",
+ principalColumn: "ItemId",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_requisition_lines_requisitions_RequisitionId",
+ column: x => x.RequisitionId,
+ principalTable: "requisitions",
+ principalColumn: "RequisitionId",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "rfqs",
+ columns: table => new
+ {
+ RfqId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ DocNo = table.Column(type: "character varying(30)", maxLength: 30, nullable: false),
+ RequisitionId = table.Column(type: "bigint", nullable: false),
+ Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_rfqs", x => x.RfqId);
+ table.ForeignKey(
+ name: "FK_rfqs_requisitions_RequisitionId",
+ column: x => x.RequisitionId,
+ principalTable: "requisitions",
+ principalColumn: "RequisitionId",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "po_lines",
+ columns: table => new
+ {
+ PoLineId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ PoId = table.Column(type: "bigint", nullable: false),
+ ItemId = table.Column(type: "bigint", nullable: false),
+ UomId = table.Column(type: "bigint", nullable: false),
+ WarehouseId = table.Column(type: "bigint", nullable: false),
+ Qty = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
+ UnitPrice = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
+ Tax = table.Column(type: "numeric(9,4)", precision: 9, scale: 4, nullable: false),
+ QtyReceived = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_po_lines", x => x.PoLineId);
+ table.ForeignKey(
+ name: "FK_po_lines_items_ItemId",
+ column: x => x.ItemId,
+ principalTable: "items",
+ principalColumn: "ItemId",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_po_lines_purchase_orders_PoId",
+ column: x => x.PoId,
+ principalTable: "purchase_orders",
+ principalColumn: "PoId",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_po_lines_uoms_UomId",
+ column: x => x.UomId,
+ principalTable: "uoms",
+ principalColumn: "UomId",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_po_lines_warehouses_WarehouseId",
+ column: x => x.WarehouseId,
+ principalTable: "warehouses",
+ principalColumn: "WarehouseId",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "rfq_lines",
+ columns: table => new
+ {
+ RfqLineId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ RfqId = table.Column(type: "bigint", nullable: false),
+ ItemId = table.Column(type: "bigint", nullable: false),
+ Qty = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_rfq_lines", x => x.RfqLineId);
+ table.ForeignKey(
+ name: "FK_rfq_lines_items_ItemId",
+ column: x => x.ItemId,
+ principalTable: "items",
+ principalColumn: "ItemId",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_rfq_lines_rfqs_RfqId",
+ column: x => x.RfqId,
+ principalTable: "rfqs",
+ principalColumn: "RfqId",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "vendor_quotations",
+ columns: table => new
+ {
+ QuotationId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ RfqId = table.Column(type: "bigint", nullable: false),
+ VendorId = table.Column