feat: Implement procurement services and number sequence management
- Added INumberSequenceService interface for generating document numbers. - Created NumberSequenceService to handle atomic document number issuance. - Introduced IPurchaseOrderService interface and implemented PurchaseOrderService for managing purchase orders. - Added IRequisitionService interface and implemented RequisitionService for handling requisitions. - Created IRfqService interface and implemented RfqService for managing RFQs and vendor quotations. - Defined necessary DTOs and domain entities for procurement processes. - Ensured proper validation and error handling across services.
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>Purchase-order endpoints (docs/11 §3.3).</summary>
|
||||
[Route("api/v1/purchase-orders")]
|
||||
public sealed class PurchaseOrdersController : ApiControllerBase
|
||||
{
|
||||
private readonly IPurchaseOrderService _pos;
|
||||
|
||||
public PurchaseOrdersController(IPurchaseOrderService pos) => _pos = pos;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<PurchaseOrderSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<PurchaseOrderSummaryDto>>> 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<ActionResult<PurchaseOrderDto>> 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);
|
||||
}
|
||||
|
||||
/// <summary>Create a PO — auto-approved on creation in Phase 1 (FR-PROC-04). Totals computed server-side.</summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> 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);
|
||||
}
|
||||
|
||||
/// <summary>Edit while open (FR-PROC-05); requires <c>If-Match</c>. 409 PO_NOT_EDITABLE if closed.</summary>
|
||||
[HttpPut("{poId:long}")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> 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);
|
||||
}
|
||||
|
||||
/// <summary>Approve — no-op in Phase 1 (POs auto-approve); transitions PendingApproval→Approved when enabled.</summary>
|
||||
[HttpPost("{poId:long}/approve")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Approve(long poId, CancellationToken ct)
|
||||
=> Ok(await _pos.ApproveAsync(poId, ct));
|
||||
|
||||
/// <summary>Cancel — 409 if any goods have been received against the PO.</summary>
|
||||
[HttpPost("{poId:long}/cancel")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Cancel(long poId, [FromBody] CancelPurchaseOrderRequest request, CancellationToken ct)
|
||||
=> Ok(await _pos.CancelAsync(poId, request.Reason, ct));
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Purchase-requisition endpoints (docs/11 §3.1).</summary>
|
||||
[Route("api/v1/requisitions")]
|
||||
public sealed class RequisitionsController : ApiControllerBase
|
||||
{
|
||||
private readonly IRequisitionService _requisitions;
|
||||
|
||||
public RequisitionsController(IRequisitionService requisitions) => _requisitions = requisitions;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<RequisitionSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<RequisitionSummaryDto>>> 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<ActionResult<RequisitionDto>> 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<ActionResult<RequisitionDto>> 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<ActionResult<RequisitionDto>> Submit(long requisitionId, CancellationToken ct)
|
||||
=> Ok(await _requisitions.SubmitAsync(requisitionId, ct));
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>RFQ & quotation endpoints (docs/11 §3.2).</summary>
|
||||
[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<ActionResult<RfqDto>> 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<ActionResult<RfqDto>> 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<ActionResult<VendorQuotationDto>> 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<ActionResult<RfqComparisonDto>> Comparison(long rfqId, CancellationToken ct)
|
||||
=> Ok(await _rfqs.GetComparisonAsync(rfqId, ct));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ERPCore.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Document-type prefixes for <see cref="Entities.NumberSequence"/> and the
|
||||
/// generated document numbers (docs/10 §B.8.2). One prefix per numbered document.
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Per-document-type, per-year running counter behind human document numbers
|
||||
/// (FR-X-03): <c>PR-2026-00001</c>, <c>PO-2026-00042</c>, … Numbers are issued
|
||||
/// inside the document's transaction so they are unique and gap-controlled.
|
||||
/// Model: docs/10 Part C.7.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Purchase-order line (FR-PROC-03). <see cref="Tax"/> is the line tax rate
|
||||
/// (e.g. 0.18); <see cref="QtyReceived"/> accrues as GRNs confirm (FR-PROC-07).
|
||||
/// Model: docs/10 Part C.2.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Purchase order header (FR-PROC-03..06). Mutable aggregate with a
|
||||
/// <see cref="RowVersion"/> ETag token; editable while open (FR-PROC-05).
|
||||
/// Phase 1 auto-approves on creation; <see cref="ApprovalRequired"/> is retained
|
||||
/// for the future approval workflow. Totals are computed server-side from lines
|
||||
/// (not stored). Model: docs/10 Part C.2.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
public ICollection<PoLine> Lines { get; set; } = new List<PoLine>();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Purchase requisition header (FR-PROC-01). <see cref="RequestedBy"/> is the audit
|
||||
/// actor from the token (never the body). Model: docs/10 Part C.2.
|
||||
/// </summary>
|
||||
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<RequisitionLine> Lines { get; set; } = new List<RequisitionLine>();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>Requisition line (FR-PROC-01). Model: docs/10 Part C.2.</summary>
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Request for Quotation header (FR-PROC-02) raised from a requisition. Vendor
|
||||
/// quotations attach for comparison. Model: docs/10 Part C.2.
|
||||
/// </summary>
|
||||
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<RfqLine> Lines { get; set; } = new List<RfqLine>();
|
||||
public ICollection<VendorQuotation> Quotations { get; set; } = new List<VendorQuotation>();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>RFQ line — an item + quantity being quoted (FR-PROC-02). Model: docs/10 Part C.2.</summary>
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>system</c> user (id 1) is the
|
||||
/// fallback actor until `/auth/login` lands (§6). Model: docs/10 Part C.7.
|
||||
/// </summary>
|
||||
public class User
|
||||
{
|
||||
/// <summary>Seeded fallback actor used while auth is deferred.</summary>
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A vendor's quotation against an RFQ (FR-PROC-02). Per-item pricing lives in
|
||||
/// <see cref="Lines"/>.
|
||||
/// <para>
|
||||
/// Deviation note: docs/10 Part C.2 models <c>VENDOR_QUOTATION</c> with scalar
|
||||
/// <c>unit_price</c>/<c>lead_days</c> and no item reference, which cannot represent
|
||||
/// the per-line pricing the API contract requires (docs/11 §3.2). This header +
|
||||
/// <see cref="VendorQuotationLine"/> split follows the authoritative API shape.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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<VendorQuotationLine> Lines { get; set; } = new List<VendorQuotationLine>();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>Per-item quoted price and lead time within a <see cref="VendorQuotation"/> (docs/11 §3.2).</summary>
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Phase 1 auto-approves on
|
||||
/// creation, so <see cref="PendingApproval"/> is reserved (not entered) until the
|
||||
/// approval workflow is enabled (FR-PROC-04). Stored as a string.
|
||||
/// </summary>
|
||||
public enum PurchaseOrderStatus
|
||||
{
|
||||
Draft,
|
||||
PendingApproval,
|
||||
Approved,
|
||||
PartiallyReceived,
|
||||
FullyReceived,
|
||||
Closed,
|
||||
Cancelled
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>Purchase-requisition lifecycle (docs/11 §3.1; docs/10 §B.8.1). Stored as a string.</summary>
|
||||
public enum RequisitionStatus
|
||||
{
|
||||
Draft,
|
||||
Submitted
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>RFQ lifecycle (docs/11 §3.2). Stored as a string.</summary>
|
||||
public enum RfqStatus
|
||||
{
|
||||
Open,
|
||||
Closed
|
||||
}
|
||||
@@ -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<PoLineDto> 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<CreatePoLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class UpdatePurchaseOrderRequest
|
||||
{
|
||||
[Required] public long VendorId { get; set; }
|
||||
public long? RequisitionId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class CancelPurchaseOrderRequest
|
||||
{
|
||||
[StringLength(500)] public string? Reason { get; set; }
|
||||
}
|
||||
@@ -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<RequisitionLineDto> 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<CreateRequisitionLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
@@ -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<RfqLineDto> Lines);
|
||||
|
||||
public sealed record QuotationLineDto(long ItemId, decimal UnitPrice, int LeadDays);
|
||||
|
||||
public sealed record VendorQuotationDto(
|
||||
long QuotationId, long RfqId, long VendorId, IReadOnlyList<QuotationLineDto> Lines);
|
||||
|
||||
/// <summary>Per-item, per-vendor price matrix for <c>GET /rfqs/{id}/comparison</c>.</summary>
|
||||
public sealed record RfqComparisonCellDto(long VendorId, long QuotationId, decimal UnitPrice, int LeadDays);
|
||||
public sealed record RfqComparisonRowDto(long ItemId, decimal Qty, IReadOnlyList<RfqComparisonCellDto> Quotes);
|
||||
public sealed record RfqComparisonDto(long RfqId, IReadOnlyList<long> VendorIds, IReadOnlyList<RfqComparisonRowDto> 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; }
|
||||
/// <summary>Vendors the RFQ is issued to (validated for existence; quotations reference them).</summary>
|
||||
public List<long> VendorIds { get; set; } = new();
|
||||
[Required, MinLength(1)] public List<CreateRfqLineInput> 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<CreateQuotationLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ public interface ICurrentUser
|
||||
/// <summary>The audit actor identity (token `sub`), or "system" when unauthenticated.</summary>
|
||||
string UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Numeric audit actor for stamping document <c>createdBy</c>/<c>requestedBy</c> FKs.
|
||||
/// Resolves the token <c>sub</c> to a user id; falls back to the seeded system
|
||||
/// user (<see cref="Entities.User.SystemUserId"/>) while auth is deferred (§6).
|
||||
/// </summary>
|
||||
long AuditUserId { get; }
|
||||
|
||||
/// <summary>True when the request carries an authenticated principal.</summary>
|
||||
bool IsAuthenticated { get; }
|
||||
}
|
||||
|
||||
@@ -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<NumberSequence>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<NumberSequence> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<PurchaseOrder>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PurchaseOrder> 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<string>().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<PoLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PoLine> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Requisition>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Requisition> 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<string>().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<RequisitionLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RequisitionLine> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Rfq>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Rfq> 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<string>().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<RfqLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RfqLine> 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<VendorQuotation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<VendorQuotation> 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<VendorQuotationLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<VendorQuotationLine> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> 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<string>().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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,20 @@ public class ErpDbContext : DbContext
|
||||
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
|
||||
public DbSet<Bin> Bins => Set<Bin>();
|
||||
|
||||
// --- Cross-cutting (docs/10 Part C.7) ---
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<NumberSequence> NumberSequences => Set<NumberSequence>();
|
||||
|
||||
// --- Procurement (docs/10 Part C.2) ---
|
||||
public DbSet<Requisition> Requisitions => Set<Requisition>();
|
||||
public DbSet<RequisitionLine> RequisitionLines => Set<RequisitionLine>();
|
||||
public DbSet<Rfq> Rfqs => Set<Rfq>();
|
||||
public DbSet<RfqLine> RfqLines => Set<RfqLine>();
|
||||
public DbSet<VendorQuotation> VendorQuotations => Set<VendorQuotation>();
|
||||
public DbSet<VendorQuotationLine> VendorQuotationLines => Set<VendorQuotationLine>();
|
||||
public DbSet<PurchaseOrder> PurchaseOrders => Set<PurchaseOrder>();
|
||||
public DbSet<PoLine> PoLines => Set<PoLine>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
+987
@@ -0,0 +1,987 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<long>("BinId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("BinId"));
|
||||
|
||||
b.Property<string>("BinType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<long>("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<long>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("CategoryId"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<long?>("ParentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||
{
|
||||
b.Property<long>("ItemId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ItemId"));
|
||||
|
||||
b.Property<long>("BaseUomId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("CategoryId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long?>("DefaultVendorId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("ItemType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.Property<string>("Sku")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Active");
|
||||
|
||||
b.Property<string>("TaxClass")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("TrackingMode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime?>("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<long>("ReorderId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ReorderId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("ReorderPoint")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<decimal>("ReorderQty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<long>("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<long>("SequenceId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceId"));
|
||||
|
||||
b.Property<string>("DocType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("doc_type");
|
||||
|
||||
b.Property<long>("LastNumber")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("last_number");
|
||||
|
||||
b.Property<int>("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<long>("PoLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("PoLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("PoId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<decimal>("QtyReceived")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<decimal>("Tax")
|
||||
.HasPrecision(9, 4)
|
||||
.HasColumnType("numeric(9,4)");
|
||||
|
||||
b.Property<decimal>("UnitPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<long>("UomId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("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<long>("PoId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("PoId"));
|
||||
|
||||
b.Property<bool>("ApprovalRequired")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long?>("RequisitionId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("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<long>("RequisitionId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RequisitionId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long>("RequestedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("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<long>("ReqLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ReqLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<DateOnly?>("RequiredBy")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<long>("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<long>("RfqId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RfqId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long>("RequisitionId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("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<long>("RfqLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RfqLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<long>("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<long>("UomId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("UomId"));
|
||||
|
||||
b.Property<string>("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<long>("ConversionId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ConversionId"));
|
||||
|
||||
b.Property<decimal>("Factor")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("numeric(18,6)");
|
||||
|
||||
b.Property<long>("FromUomId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("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<long>("UserId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("UserId"));
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("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<long>("VendorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("VendorId"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(3)
|
||||
.HasColumnType("character varying(3)")
|
||||
.HasDefaultValue("LKR");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Active");
|
||||
|
||||
b.Property<string>("TaxReg")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Terms")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("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<long>("QuotationId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("QuotationId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("RfqId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("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<long>("QuotationLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("QuotationLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("LeadDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("QuotationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("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<long>("WarehouseId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("WarehouseId"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProcurement : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "number_sequences",
|
||||
columns: table => new
|
||||
{
|
||||
SequenceId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
doc_type = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
year = table.Column<int>(type: "integer", nullable: false),
|
||||
last_number = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_number_sequences", x => x.SequenceId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "users",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Username = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Status = table.Column<string>(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<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
RequestedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(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<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
VendorId = table.Column<long>(type: "bigint", nullable: false),
|
||||
RequisitionId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
ApprovalRequired = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
xmin = table.Column<uint>(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<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RequisitionId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Qty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
RequiredBy = table.Column<DateOnly>(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<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
RequisitionId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(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<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
PoId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UomId = table.Column<long>(type: "bigint", nullable: false),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Qty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
UnitPrice = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
Tax = table.Column<decimal>(type: "numeric(9,4)", precision: 9, scale: 4, nullable: false),
|
||||
QtyReceived = table.Column<decimal>(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<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RfqId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Qty = table.Column<decimal>(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<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RfqId = table.Column<long>(type: "bigint", nullable: false),
|
||||
VendorId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_vendor_quotations", x => x.QuotationId);
|
||||
table.ForeignKey(
|
||||
name: "FK_vendor_quotations_rfqs_RfqId",
|
||||
column: x => x.RfqId,
|
||||
principalTable: "rfqs",
|
||||
principalColumn: "RfqId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_vendor_quotations_vendors_VendorId",
|
||||
column: x => x.VendorId,
|
||||
principalTable: "vendors",
|
||||
principalColumn: "VendorId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "vendor_quotation_lines",
|
||||
columns: table => new
|
||||
{
|
||||
QuotationLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
QuotationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UnitPrice = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
LeadDays = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_vendor_quotation_lines", x => x.QuotationLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_vendor_quotation_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_vendor_quotation_lines_vendor_quotations_QuotationId",
|
||||
column: x => x.QuotationId,
|
||||
principalTable: "vendor_quotations",
|
||||
principalColumn: "QuotationId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "users",
|
||||
columns: new[] { "UserId", "DisplayName", "Status", "Username" },
|
||||
values: new object[] { 1L, "System", "Active", "system" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_number_sequences_doc_type_year",
|
||||
table: "number_sequences",
|
||||
columns: new[] { "doc_type", "year" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_po_lines_ItemId",
|
||||
table: "po_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_po_lines_PoId",
|
||||
table: "po_lines",
|
||||
column: "PoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_po_lines_UomId",
|
||||
table: "po_lines",
|
||||
column: "UomId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_po_lines_WarehouseId",
|
||||
table: "po_lines",
|
||||
column: "WarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_CreatedBy",
|
||||
table: "purchase_orders",
|
||||
column: "CreatedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_DocNo",
|
||||
table: "purchase_orders",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_RequisitionId",
|
||||
table: "purchase_orders",
|
||||
column: "RequisitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_Status",
|
||||
table: "purchase_orders",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_VendorId",
|
||||
table: "purchase_orders",
|
||||
column: "VendorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisition_lines_ItemId",
|
||||
table: "requisition_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisition_lines_RequisitionId",
|
||||
table: "requisition_lines",
|
||||
column: "RequisitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisitions_DocNo",
|
||||
table: "requisitions",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisitions_RequestedBy",
|
||||
table: "requisitions",
|
||||
column: "RequestedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisitions_Status",
|
||||
table: "requisitions",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_rfq_lines_ItemId",
|
||||
table: "rfq_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_rfq_lines_RfqId",
|
||||
table: "rfq_lines",
|
||||
column: "RfqId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_rfqs_DocNo",
|
||||
table: "rfqs",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_rfqs_RequisitionId",
|
||||
table: "rfqs",
|
||||
column: "RequisitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_users_Username",
|
||||
table: "users",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_vendor_quotation_lines_ItemId",
|
||||
table: "vendor_quotation_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_vendor_quotation_lines_QuotationId",
|
||||
table: "vendor_quotation_lines",
|
||||
column: "QuotationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_vendor_quotations_RfqId_VendorId",
|
||||
table: "vendor_quotations",
|
||||
columns: new[] { "RfqId", "VendorId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_vendor_quotations_VendorId",
|
||||
table: "vendor_quotations",
|
||||
column: "VendorId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "number_sequences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "po_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "requisition_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "rfq_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "vendor_quotation_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "purchase_orders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "vendor_quotations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "rfqs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "requisitions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,270 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.ToTable("item_reorders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.NumberSequence", b =>
|
||||
{
|
||||
b.Property<long>("SequenceId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceId"));
|
||||
|
||||
b.Property<string>("DocType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("doc_type");
|
||||
|
||||
b.Property<long>("LastNumber")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("last_number");
|
||||
|
||||
b.Property<int>("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<long>("PoLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("PoLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("PoId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<decimal>("QtyReceived")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<decimal>("Tax")
|
||||
.HasPrecision(9, 4)
|
||||
.HasColumnType("numeric(9,4)");
|
||||
|
||||
b.Property<decimal>("UnitPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<long>("UomId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("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<long>("PoId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("PoId"));
|
||||
|
||||
b.Property<bool>("ApprovalRequired")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long?>("RequisitionId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("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<long>("RequisitionId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RequisitionId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long>("RequestedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("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<long>("ReqLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ReqLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<DateOnly?>("RequiredBy")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<long>("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<long>("RfqId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RfqId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long>("RequisitionId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("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<long>("RfqLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RfqLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<long>("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<long>("UomId")
|
||||
@@ -239,6 +503,46 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.ToTable("uom_conversions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<long>("UserId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("UserId"));
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("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<long>("VendorId")
|
||||
@@ -301,6 +605,63 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.ToTable("vendors", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b =>
|
||||
{
|
||||
b.Property<long>("QuotationId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("QuotationId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("RfqId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("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<long>("QuotationLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("QuotationLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("LeadDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("QuotationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("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<long>("WarehouseId")
|
||||
@@ -393,6 +754,127 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
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")
|
||||
@@ -420,6 +902,44 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
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");
|
||||
@@ -432,6 +952,28 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
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");
|
||||
|
||||
@@ -48,6 +48,12 @@ builder.Services.AddScoped<ICategoryService, CategoryService>();
|
||||
builder.Services.AddScoped<IVendorService, VendorService>();
|
||||
builder.Services.AddScoped<IWarehouseService, WarehouseService>();
|
||||
|
||||
// Cross-cutting + procurement services (docs/11 §3)
|
||||
builder.Services.AddScoped<INumberSequenceService, NumberSequenceService>();
|
||||
builder.Services.AddScoped<IRequisitionService, RequisitionService>();
|
||||
builder.Services.AddScoped<IRfqService, RfqService>();
|
||||
builder.Services.AddScoped<IPurchaseOrderService, PurchaseOrderService>();
|
||||
|
||||
// Health checks (EF Core DB)
|
||||
builder.Services.AddHealthChecks().AddDbContextCheck<ErpDbContext>();
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Issues gap-controlled, per-year document numbers (FR-X-03). Call inside the
|
||||
/// document's UoW transaction so the reserved number rolls back with the document
|
||||
/// on failure.
|
||||
/// </summary>
|
||||
public interface INumberSequenceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserve and return the next number for <paramref name="docType"/> in the
|
||||
/// current year, formatted e.g. <c>PO-2026-00042</c>.
|
||||
/// </summary>
|
||||
Task<string> NextAsync(string docType, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>Purchase-order business logic (docs/11 §3.3; FR-PROC-03..07).</summary>
|
||||
public interface IPurchaseOrderService
|
||||
{
|
||||
Task<PagedResponse<PurchaseOrderSummaryDto>> ListAsync(
|
||||
PageQuery query, PurchaseOrderStatus? status, long? vendorId, CancellationToken ct = default);
|
||||
|
||||
Task<ETagged<PurchaseOrderDto>?> GetAsync(long poId, CancellationToken ct = default);
|
||||
Task<ETagged<PurchaseOrderDto>> CreateAsync(CreatePurchaseOrderRequest request, CancellationToken ct = default);
|
||||
Task<ETagged<PurchaseOrderDto>> UpdateAsync(long poId, UpdatePurchaseOrderRequest request, uint expectedRowVersion, CancellationToken ct = default);
|
||||
Task<PurchaseOrderDto> ApproveAsync(long poId, CancellationToken ct = default);
|
||||
Task<PurchaseOrderDto> CancelAsync(long poId, string? reason, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>Purchase-requisition business logic (docs/11 §3.1).</summary>
|
||||
public interface IRequisitionService
|
||||
{
|
||||
Task<PagedResponse<RequisitionSummaryDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||
Task<RequisitionDto?> GetAsync(long requisitionId, CancellationToken ct = default);
|
||||
Task<RequisitionDto> CreateAsync(CreateRequisitionRequest request, CancellationToken ct = default);
|
||||
Task<RequisitionDto> SubmitAsync(long requisitionId, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using ERPCore.Dtos.Procurement;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>RFQ & vendor-quotation business logic (docs/11 §3.2).</summary>
|
||||
public interface IRfqService
|
||||
{
|
||||
Task<RfqDto?> GetAsync(long rfqId, CancellationToken ct = default);
|
||||
Task<RfqDto> CreateAsync(CreateRfqRequest request, CancellationToken ct = default);
|
||||
Task<VendorQuotationDto> AddQuotationAsync(long rfqId, CreateQuotationRequest request, CancellationToken ct = default);
|
||||
Task<RfqComparisonDto> GetComparisonAsync(long rfqId, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Data;
|
||||
using ERPCore.Infra.Persistence;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Atomic document-number issuer. Uses a single <c>INSERT … ON CONFLICT … DO UPDATE
|
||||
/// … RETURNING</c> so concurrent issues for the same (docType, year) cannot get the
|
||||
/// same number (the row is locked for the duration of the upsert). Runs as a direct
|
||||
/// ADO.NET command enlisted in the DbContext's current transaction so it commits or
|
||||
/// rolls back with the document (FR-X-03). EF's <c>SqlQuery</c> is avoided here
|
||||
/// because it wraps the statement in a subquery, which PostgreSQL disallows for a
|
||||
/// data-modifying statement.
|
||||
/// </summary>
|
||||
public sealed class NumberSequenceService : INumberSequenceService
|
||||
{
|
||||
private readonly ErpDbContext _db;
|
||||
|
||||
public NumberSequenceService(ErpDbContext db) => _db = db;
|
||||
|
||||
public async Task<string> NextAsync(string docType, CancellationToken ct = default)
|
||||
{
|
||||
var year = DateTime.UtcNow.Year;
|
||||
|
||||
var conn = _db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.Transaction = _db.Database.CurrentTransaction?.GetDbTransaction();
|
||||
cmd.CommandText = """
|
||||
INSERT INTO number_sequences (doc_type, year, last_number)
|
||||
VALUES (@docType, @year, 1)
|
||||
ON CONFLICT (doc_type, year)
|
||||
DO UPDATE SET last_number = number_sequences.last_number + 1
|
||||
RETURNING last_number;
|
||||
""";
|
||||
AddParam(cmd, "docType", docType);
|
||||
AddParam(cmd, "year", year);
|
||||
|
||||
var result = await cmd.ExecuteScalarAsync(ct)
|
||||
?? throw new InvalidOperationException($"Failed to issue a document number for '{docType}'.");
|
||||
var next = Convert.ToInt64(result);
|
||||
|
||||
return $"{docType}-{year}-{next:D5}";
|
||||
}
|
||||
|
||||
private static void AddParam(IDbCommand cmd, string name, object value)
|
||||
{
|
||||
var p = cmd.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value;
|
||||
cmd.Parameters.Add(p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Purchase-order service. Phase 1 auto-approves on creation (FR-PROC-04),
|
||||
/// PO is freely editable while open (FR-PROC-05), totals are computed server-side
|
||||
/// (02-SECURITY C.2), and cancel is blocked once any receipt exists.
|
||||
/// </summary>
|
||||
public sealed class PurchaseOrderService : IPurchaseOrderService
|
||||
{
|
||||
private const string BaseCurrency = "LKR";
|
||||
|
||||
private readonly IRepository<PurchaseOrder> _pos;
|
||||
private readonly IRepository<Vendor> _vendors;
|
||||
private readonly IRepository<Requisition> _requisitions;
|
||||
private readonly IRepository<Item> _items;
|
||||
private readonly IRepository<Uom> _uoms;
|
||||
private readonly IRepository<Warehouse> _warehouses;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public PurchaseOrderService(
|
||||
IRepository<PurchaseOrder> pos, IRepository<Vendor> vendors, IRepository<Requisition> requisitions,
|
||||
IRepository<Item> items, IRepository<Uom> uoms, IRepository<Warehouse> warehouses,
|
||||
INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow)
|
||||
{
|
||||
_pos = pos;
|
||||
_vendors = vendors;
|
||||
_requisitions = requisitions;
|
||||
_items = items;
|
||||
_uoms = uoms;
|
||||
_warehouses = warehouses;
|
||||
_numbers = numbers;
|
||||
_currentUser = currentUser;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<PurchaseOrderSummaryDto>> ListAsync(
|
||||
PageQuery query, PurchaseOrderStatus? status, long? vendorId, CancellationToken ct = default)
|
||||
{
|
||||
var q = _pos.Query().AsNoTracking().Include(p => p.Lines).AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(p => EF.Functions.ILike(p.DocNo, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(p => p.Status == status);
|
||||
if (vendorId is not null) q = q.Where(p => p.VendorId == vendorId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(p => p.PoId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var summaries = rows.Select(p => new PurchaseOrderSummaryDto(
|
||||
p.PoId, p.DocNo, p.VendorId, p.Status, p.ApprovalRequired, p.CreatedAt, ComputeTotals(p.Lines))).ToList();
|
||||
|
||||
return PagedResponse<PurchaseOrderSummaryDto>.Create(summaries, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<ETagged<PurchaseOrderDto>?> GetAsync(long poId, CancellationToken ct = default)
|
||||
{
|
||||
var po = await _pos.Query().AsNoTracking()
|
||||
.Include(p => p.Lines)
|
||||
.FirstOrDefaultAsync(p => p.PoId == poId, ct);
|
||||
return po is null ? null : new ETagged<PurchaseOrderDto>(Map(po), po.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<ETagged<PurchaseOrderDto>> CreateAsync(CreatePurchaseOrderRequest request, CancellationToken ct = default)
|
||||
{
|
||||
await ValidateReferencesAsync(request.VendorId, request.RequisitionId, request.Lines, ct);
|
||||
var actor = _currentUser.AuditUserId;
|
||||
|
||||
var po = await _uow.ExecuteInTransactionAsync(async token =>
|
||||
{
|
||||
var docNo = await _numbers.NextAsync(DocumentTypes.PurchaseOrder, token);
|
||||
var entity = new PurchaseOrder
|
||||
{
|
||||
DocNo = docNo,
|
||||
VendorId = request.VendorId,
|
||||
RequisitionId = request.RequisitionId,
|
||||
// Phase 1: approvalRequired defaults off → auto-approved on creation (FR-PROC-04).
|
||||
ApprovalRequired = false,
|
||||
Status = PurchaseOrderStatus.Approved,
|
||||
CreatedBy = actor,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Lines = request.Lines.Select(ToLine).ToList()
|
||||
};
|
||||
await _pos.AddAsync(entity, token);
|
||||
return entity;
|
||||
}, ct);
|
||||
|
||||
return new ETagged<PurchaseOrderDto>(Map(po), po.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<ETagged<PurchaseOrderDto>> UpdateAsync(
|
||||
long poId, UpdatePurchaseOrderRequest request, uint expectedRowVersion, CancellationToken ct = default)
|
||||
{
|
||||
var po = await _pos.Query()
|
||||
.Include(p => p.Lines)
|
||||
.FirstOrDefaultAsync(p => p.PoId == poId, ct)
|
||||
?? throw new NotFoundException($"Purchase order {poId} was not found.");
|
||||
|
||||
if (po.RowVersion != expectedRowVersion)
|
||||
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The purchase order was modified by another request.", 412);
|
||||
|
||||
if (!IsEditable(po.Status))
|
||||
throw new DomainException(ErrorCodes.PoNotEditable, $"Purchase order {poId} is {po.Status} and cannot be edited.", 409);
|
||||
|
||||
await ValidateReferencesAsync(request.VendorId, request.RequisitionId, request.Lines, ct);
|
||||
|
||||
po.VendorId = request.VendorId;
|
||||
po.RequisitionId = request.RequisitionId;
|
||||
po.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Full line replacement (Phase 1: no receipts yet, so qtyReceived is 0 on every line).
|
||||
po.Lines.Clear();
|
||||
foreach (var input in request.Lines)
|
||||
po.Lines.Add(ToLine(input));
|
||||
|
||||
try
|
||||
{
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The purchase order was modified by another request.", 412);
|
||||
}
|
||||
|
||||
return new ETagged<PurchaseOrderDto>(Map(po), po.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<PurchaseOrderDto> ApproveAsync(long poId, CancellationToken ct = default)
|
||||
{
|
||||
var po = await _pos.Query()
|
||||
.Include(p => p.Lines)
|
||||
.FirstOrDefaultAsync(p => p.PoId == poId, ct)
|
||||
?? throw new NotFoundException($"Purchase order {poId} was not found.");
|
||||
|
||||
// Phase 1 no-op: POs are already Approved on creation. Kept for the future
|
||||
// approval workflow (PendingApproval → Approved) — FR-PROC-04.
|
||||
if (po.Status == PurchaseOrderStatus.PendingApproval)
|
||||
{
|
||||
po.Status = PurchaseOrderStatus.Approved;
|
||||
po.UpdatedAt = DateTime.UtcNow;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return Map(po);
|
||||
}
|
||||
|
||||
public async Task<PurchaseOrderDto> CancelAsync(long poId, string? reason, CancellationToken ct = default)
|
||||
{
|
||||
var po = await _pos.Query()
|
||||
.Include(p => p.Lines)
|
||||
.FirstOrDefaultAsync(p => p.PoId == poId, ct)
|
||||
?? throw new NotFoundException($"Purchase order {poId} was not found.");
|
||||
|
||||
if (po.Lines.Any(l => l.QtyReceived > 0))
|
||||
throw new ConflictException($"Purchase order {poId} cannot be cancelled because goods have been received against it.");
|
||||
|
||||
if (po.Status != PurchaseOrderStatus.Cancelled)
|
||||
{
|
||||
po.Status = PurchaseOrderStatus.Cancelled;
|
||||
po.UpdatedAt = DateTime.UtcNow;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return Map(po);
|
||||
}
|
||||
|
||||
private static bool IsEditable(PurchaseOrderStatus status) => status is not (
|
||||
PurchaseOrderStatus.FullyReceived or PurchaseOrderStatus.Closed or PurchaseOrderStatus.Cancelled);
|
||||
|
||||
private static PoLine ToLine(CreatePoLineInput l) => new()
|
||||
{
|
||||
ItemId = l.ItemId,
|
||||
UomId = l.UomId,
|
||||
WarehouseId = l.WarehouseId,
|
||||
Qty = l.Qty,
|
||||
UnitPrice = l.UnitPrice,
|
||||
Tax = l.Tax,
|
||||
QtyReceived = 0
|
||||
};
|
||||
|
||||
private static PoTotalsDto ComputeTotals(IEnumerable<PoLine> lines)
|
||||
{
|
||||
decimal sub = 0, tax = 0;
|
||||
foreach (var l in lines)
|
||||
{
|
||||
var net = l.Qty * l.UnitPrice;
|
||||
sub += net;
|
||||
tax += net * l.Tax;
|
||||
}
|
||||
sub = Math.Round(sub, 2, MidpointRounding.AwayFromZero);
|
||||
tax = Math.Round(tax, 2, MidpointRounding.AwayFromZero);
|
||||
return new PoTotalsDto(sub, tax, sub + tax, BaseCurrency);
|
||||
}
|
||||
|
||||
private async Task ValidateReferencesAsync(
|
||||
long vendorId, long? requisitionId, IReadOnlyCollection<CreatePoLineInput> lines, CancellationToken ct)
|
||||
{
|
||||
var vendor = await _vendors.Query().AsNoTracking().FirstOrDefaultAsync(v => v.VendorId == vendorId, ct);
|
||||
if (vendor is null)
|
||||
throw new DomainException(ErrorCodes.Validation, $"Vendor {vendorId} does not exist.", 422);
|
||||
if (vendor.Status != EntityStatus.Active)
|
||||
throw new DomainException(ErrorCodes.Validation, $"Vendor {vendorId} is inactive.", 422);
|
||||
|
||||
if (requisitionId is not null
|
||||
&& !await _requisitions.Query().AnyAsync(r => r.RequisitionId == requisitionId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"Requisition {requisitionId} does not exist.", 422);
|
||||
|
||||
await EnsureAllExistAsync(_items.Query().Select(i => i.ItemId), lines.Select(l => l.ItemId), "Item", ct);
|
||||
await EnsureAllExistAsync(_uoms.Query().Select(u => u.UomId), lines.Select(l => l.UomId), "UOM", ct);
|
||||
await EnsureAllExistAsync(_warehouses.Query().Select(w => w.WarehouseId), lines.Select(l => l.WarehouseId), "Warehouse", ct);
|
||||
}
|
||||
|
||||
private static async Task EnsureAllExistAsync(
|
||||
IQueryable<long> keySource, IEnumerable<long> requested, string label, CancellationToken ct)
|
||||
{
|
||||
var ids = requested.Distinct().ToList();
|
||||
var found = await keySource.Where(k => ids.Contains(k)).ToListAsync(ct);
|
||||
var missing = ids.Except(found).ToList();
|
||||
if (missing.Count > 0)
|
||||
throw new DomainException(ErrorCodes.Validation, $"{label}(s) not found: {string.Join(", ", missing)}.", 422);
|
||||
}
|
||||
|
||||
private static PurchaseOrderDto Map(PurchaseOrder p) => new(
|
||||
p.PoId, p.DocNo, p.VendorId, p.RequisitionId, p.Status, p.ApprovalRequired,
|
||||
p.CreatedBy, p.CreatedAt, p.UpdatedAt, ComputeTotals(p.Lines),
|
||||
p.Lines.OrderBy(l => l.PoLineId).Select(l => new PoLineDto(
|
||||
l.PoLineId, l.ItemId, l.UomId, l.WarehouseId, l.Qty, l.UnitPrice, l.Tax, l.QtyReceived)).ToList());
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
public sealed class RequisitionService : IRequisitionService
|
||||
{
|
||||
private readonly IRepository<Requisition> _requisitions;
|
||||
private readonly IRepository<Item> _items;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public RequisitionService(
|
||||
IRepository<Requisition> requisitions, IRepository<Item> items,
|
||||
INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow)
|
||||
{
|
||||
_requisitions = requisitions;
|
||||
_items = items;
|
||||
_numbers = numbers;
|
||||
_currentUser = currentUser;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<RequisitionSummaryDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||
{
|
||||
var q = _requisitions.Query().AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(r => EF.Functions.ILike(r.DocNo, $"%{term}%"));
|
||||
}
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(r => r.RequisitionId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(r => new RequisitionSummaryDto(r.RequisitionId, r.DocNo, r.Status, r.RequestedBy, r.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<RequisitionSummaryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<RequisitionDto?> GetAsync(long requisitionId, CancellationToken ct = default)
|
||||
{
|
||||
var req = await _requisitions.Query().AsNoTracking()
|
||||
.Include(r => r.Lines)
|
||||
.FirstOrDefaultAsync(r => r.RequisitionId == requisitionId, ct);
|
||||
return req is null ? null : Map(req);
|
||||
}
|
||||
|
||||
public async Task<RequisitionDto> CreateAsync(CreateRequisitionRequest request, CancellationToken ct = default)
|
||||
{
|
||||
await EnsureItemsExistAsync(request.Lines.Select(l => l.ItemId), ct);
|
||||
var actor = _currentUser.AuditUserId;
|
||||
|
||||
var req = await _uow.ExecuteInTransactionAsync(async token =>
|
||||
{
|
||||
var docNo = await _numbers.NextAsync(DocumentTypes.Requisition, token);
|
||||
var entity = new Requisition
|
||||
{
|
||||
DocNo = docNo,
|
||||
RequestedBy = actor,
|
||||
Status = RequisitionStatus.Draft,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Lines = request.Lines.Select(l => new RequisitionLine
|
||||
{
|
||||
ItemId = l.ItemId,
|
||||
Qty = l.Qty,
|
||||
RequiredBy = l.RequiredBy
|
||||
}).ToList()
|
||||
};
|
||||
await _requisitions.AddAsync(entity, token);
|
||||
return entity;
|
||||
}, ct);
|
||||
|
||||
return Map(req);
|
||||
}
|
||||
|
||||
public async Task<RequisitionDto> SubmitAsync(long requisitionId, CancellationToken ct = default)
|
||||
{
|
||||
var req = await _requisitions.Query()
|
||||
.Include(r => r.Lines)
|
||||
.FirstOrDefaultAsync(r => r.RequisitionId == requisitionId, ct)
|
||||
?? throw new NotFoundException($"Requisition {requisitionId} was not found.");
|
||||
|
||||
if (req.Status != RequisitionStatus.Submitted)
|
||||
{
|
||||
req.Status = RequisitionStatus.Submitted;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return Map(req);
|
||||
}
|
||||
|
||||
private async Task EnsureItemsExistAsync(IEnumerable<long> itemIds, CancellationToken ct)
|
||||
{
|
||||
var ids = itemIds.Distinct().ToList();
|
||||
var found = await _items.Query().AsNoTracking()
|
||||
.Where(i => ids.Contains(i.ItemId)).Select(i => i.ItemId).ToListAsync(ct);
|
||||
var missing = ids.Except(found).ToList();
|
||||
if (missing.Count > 0)
|
||||
throw new DomainException(ErrorCodes.Validation, $"Item(s) not found: {string.Join(", ", missing)}.", 422);
|
||||
}
|
||||
|
||||
private static RequisitionDto Map(Requisition r) => new(
|
||||
r.RequisitionId, r.DocNo, r.Status, r.RequestedBy, r.CreatedAt,
|
||||
r.Lines.OrderBy(l => l.ReqLineId)
|
||||
.Select(l => new RequisitionLineDto(l.ReqLineId, l.ItemId, l.Qty, l.RequiredBy))
|
||||
.ToList());
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
public sealed class RfqService : IRfqService
|
||||
{
|
||||
private readonly IRepository<Rfq> _rfqs;
|
||||
private readonly IRepository<Requisition> _requisitions;
|
||||
private readonly IRepository<Item> _items;
|
||||
private readonly IRepository<Vendor> _vendors;
|
||||
private readonly IRepository<VendorQuotation> _quotations;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public RfqService(
|
||||
IRepository<Rfq> rfqs, IRepository<Requisition> requisitions, IRepository<Item> items,
|
||||
IRepository<Vendor> vendors, IRepository<VendorQuotation> quotations,
|
||||
INumberSequenceService numbers, IUnitOfWork uow)
|
||||
{
|
||||
_rfqs = rfqs;
|
||||
_requisitions = requisitions;
|
||||
_items = items;
|
||||
_vendors = vendors;
|
||||
_quotations = quotations;
|
||||
_numbers = numbers;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<RfqDto?> GetAsync(long rfqId, CancellationToken ct = default)
|
||||
{
|
||||
var rfq = await _rfqs.Query().AsNoTracking()
|
||||
.Include(r => r.Lines)
|
||||
.FirstOrDefaultAsync(r => r.RfqId == rfqId, ct);
|
||||
return rfq is null ? null : MapRfq(rfq);
|
||||
}
|
||||
|
||||
public async Task<RfqDto> CreateAsync(CreateRfqRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (!await _requisitions.Query().AnyAsync(r => r.RequisitionId == request.RequisitionId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"Requisition {request.RequisitionId} does not exist.", 422);
|
||||
|
||||
await EnsureItemsExistAsync(request.Lines.Select(l => l.ItemId), ct);
|
||||
await EnsureVendorsExistAsync(request.VendorIds, ct);
|
||||
|
||||
var rfq = await _uow.ExecuteInTransactionAsync(async token =>
|
||||
{
|
||||
var docNo = await _numbers.NextAsync(DocumentTypes.Rfq, token);
|
||||
var entity = new Rfq
|
||||
{
|
||||
DocNo = docNo,
|
||||
RequisitionId = request.RequisitionId,
|
||||
Status = RfqStatus.Open,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Lines = request.Lines.Select(l => new RfqLine { ItemId = l.ItemId, Qty = l.Qty }).ToList()
|
||||
};
|
||||
await _rfqs.AddAsync(entity, token);
|
||||
return entity;
|
||||
}, ct);
|
||||
|
||||
return MapRfq(rfq);
|
||||
}
|
||||
|
||||
public async Task<VendorQuotationDto> AddQuotationAsync(long rfqId, CreateQuotationRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var rfq = await _rfqs.Query().AsNoTracking()
|
||||
.Include(r => r.Lines)
|
||||
.FirstOrDefaultAsync(r => r.RfqId == rfqId, ct)
|
||||
?? throw new NotFoundException($"RFQ {rfqId} was not found.");
|
||||
|
||||
if (!await _vendors.Query().AnyAsync(v => v.VendorId == request.VendorId, ct))
|
||||
throw new DomainException(ErrorCodes.Validation, $"Vendor {request.VendorId} does not exist.", 422);
|
||||
|
||||
var rfqItemIds = rfq.Lines.Select(l => l.ItemId).ToHashSet();
|
||||
var offLine = request.Lines.Select(l => l.ItemId).FirstOrDefault(id => !rfqItemIds.Contains(id));
|
||||
if (offLine != 0)
|
||||
throw new DomainException(ErrorCodes.Validation, $"Item {offLine} is not part of RFQ {rfqId}.", 422);
|
||||
|
||||
if (await _quotations.Query().AnyAsync(q => q.RfqId == rfqId && q.VendorId == request.VendorId, ct))
|
||||
throw new ConflictException($"Vendor {request.VendorId} has already quoted RFQ {rfqId}.");
|
||||
|
||||
var quotation = new VendorQuotation
|
||||
{
|
||||
RfqId = rfqId,
|
||||
VendorId = request.VendorId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Lines = request.Lines.Select(l => new VendorQuotationLine
|
||||
{
|
||||
ItemId = l.ItemId,
|
||||
UnitPrice = l.UnitPrice,
|
||||
LeadDays = l.LeadDays
|
||||
}).ToList()
|
||||
};
|
||||
await _quotations.AddAsync(quotation, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return new VendorQuotationDto(
|
||||
quotation.QuotationId, quotation.RfqId, quotation.VendorId,
|
||||
quotation.Lines.Select(l => new QuotationLineDto(l.ItemId, l.UnitPrice, l.LeadDays)).ToList());
|
||||
}
|
||||
|
||||
public async Task<RfqComparisonDto> GetComparisonAsync(long rfqId, CancellationToken ct = default)
|
||||
{
|
||||
var rfq = await _rfqs.Query().AsNoTracking()
|
||||
.Include(r => r.Lines)
|
||||
.Include(r => r.Quotations).ThenInclude(q => q.Lines)
|
||||
.FirstOrDefaultAsync(r => r.RfqId == rfqId, ct)
|
||||
?? throw new NotFoundException($"RFQ {rfqId} was not found.");
|
||||
|
||||
var vendorIds = rfq.Quotations.Select(q => q.VendorId).Distinct().OrderBy(v => v).ToList();
|
||||
|
||||
var rows = rfq.Lines.OrderBy(l => l.RfqLineId).Select(line =>
|
||||
{
|
||||
var cells = rfq.Quotations
|
||||
.Select(q => new { q.VendorId, q.QuotationId, Line = q.Lines.FirstOrDefault(ql => ql.ItemId == line.ItemId) })
|
||||
.Where(x => x.Line is not null)
|
||||
.OrderBy(x => x.VendorId)
|
||||
.Select(x => new RfqComparisonCellDto(x.VendorId, x.QuotationId, x.Line!.UnitPrice, x.Line!.LeadDays))
|
||||
.ToList();
|
||||
return new RfqComparisonRowDto(line.ItemId, line.Qty, cells);
|
||||
}).ToList();
|
||||
|
||||
return new RfqComparisonDto(rfqId, vendorIds, rows);
|
||||
}
|
||||
|
||||
private async Task EnsureItemsExistAsync(IEnumerable<long> itemIds, CancellationToken ct)
|
||||
{
|
||||
var ids = itemIds.Distinct().ToList();
|
||||
var found = await _items.Query().AsNoTracking()
|
||||
.Where(i => ids.Contains(i.ItemId)).Select(i => i.ItemId).ToListAsync(ct);
|
||||
var missing = ids.Except(found).ToList();
|
||||
if (missing.Count > 0)
|
||||
throw new DomainException(ErrorCodes.Validation, $"Item(s) not found: {string.Join(", ", missing)}.", 422);
|
||||
}
|
||||
|
||||
private async Task EnsureVendorsExistAsync(IEnumerable<long> vendorIds, CancellationToken ct)
|
||||
{
|
||||
var ids = vendorIds.Distinct().ToList();
|
||||
if (ids.Count == 0) return;
|
||||
var found = await _vendors.Query().AsNoTracking()
|
||||
.Where(v => ids.Contains(v.VendorId)).Select(v => v.VendorId).ToListAsync(ct);
|
||||
var missing = ids.Except(found).ToList();
|
||||
if (missing.Count > 0)
|
||||
throw new DomainException(ErrorCodes.Validation, $"Vendor(s) not found: {string.Join(", ", missing)}.", 422);
|
||||
}
|
||||
|
||||
private static RfqDto MapRfq(Rfq r) => new(
|
||||
r.RfqId, r.DocNo, r.RequisitionId, r.Status,
|
||||
r.Lines.OrderBy(l => l.RfqLineId).Select(l => new RfqLineDto(l.RfqLineId, l.ItemId, l.Qty)).ToList());
|
||||
}
|
||||
Reference in New Issue
Block a user