feat: add vendor optional fields and configuration hooks
- Implemented VendorOptionalFields component to handle optional vendor details (mobile, email, address) with validation. - Created useCustomerConfig hook to load optional customer field settings from Sales configuration. - Developed useProcurementConfig hook to manage Procurement configuration flags. - Introduced accessConfigApi for managing access configuration settings. - Added openingBalancesApi for handling opening balances operations. - Created procurementConfigApi for managing procurement configuration settings. - Implemented nav-routes for permission-based routing. - Developed PDF generation for purchase orders with dynamic vendor and item details. - Added purchase-order tax helpers for tax calculations and formatting. - Defined types for opening balances and related operations.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Access configuration — a singleton row (same shape as <see cref="GrnConfig"/>).
|
||||
/// <see cref="DefaultRoleId"/> is the role a person gets on their first sign-in, and when they have none:
|
||||
/// without it they would have no permissions at all. It must be a least-privilege role and can never be the
|
||||
/// administrator role (enforced when it is saved).
|
||||
/// </summary>
|
||||
public class AccessConfig
|
||||
{
|
||||
/// <summary>Always 1 - the singleton row's id.</summary>
|
||||
public const int SingletonId = 1;
|
||||
|
||||
public int ConfigId { get; set; }
|
||||
|
||||
public int? DefaultRoleId { get; set; }
|
||||
public Role? DefaultRole { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public int? UpdatedBy { get; set; }
|
||||
public User? UpdatedByUser { get; set; }
|
||||
|
||||
/// <summary>Optimistic concurrency token (ETag source).</summary>
|
||||
public uint RowVersion { get; set; }
|
||||
}
|
||||
@@ -39,8 +39,9 @@ public class ApprovalConfig
|
||||
/// <summary>With 2 levels: a change smaller than this percent (largest across its lines) needs only one approval. 0 = always two.</summary>
|
||||
public decimal PriceAdjustmentLevel2ThresholdPercent { get; set; }
|
||||
|
||||
/// <summary>Comma-separated role codes allowed to give the second approval; empty = anyone with the Approve action.</summary>
|
||||
public string? Level2RoleCodes { get; set; }
|
||||
/// <summary>Comma-separated role IDS allowed to give the second approval; empty = anyone with the Approve action.
|
||||
/// (Ids, not codes: a role can be renamed without breaking this rule.)</summary>
|
||||
public string? Level2RoleIds { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int? UpdatedBy { get; set; }
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One posted opening balance (cash and bank, or stock) — the record that the opening entry was made, when,
|
||||
/// by whom, and which General Ledger journal carries it. The General Ledger has no "opening posted" notion, so
|
||||
/// this table is what refuses a second posting: cash/bank sets <see cref="OnceKey"/> (unique index), so it can
|
||||
/// be posted once. Stock leaves it null and can be loaded in several runs; each item in a warehouse can be
|
||||
/// opened only once (an item that already has stock movements there is refused).
|
||||
/// </summary>
|
||||
public class OpeningBalanceRun
|
||||
{
|
||||
public int OpeningBalanceRunId { get; set; }
|
||||
|
||||
public OpeningBalanceKind Kind { get; set; }
|
||||
|
||||
/// <summary>Set (to the kind's name) only for kinds that may be posted once; a unique index on it is the
|
||||
/// once-only guard. Null for stock, which may be loaded in batches (MySQL allows many NULLs under a unique index).</summary>
|
||||
public string? OnceKey { get; set; }
|
||||
|
||||
/// <summary>The opening date — the GL journal's entry date. Reports starting after it show these amounts as opening.</summary>
|
||||
public DateOnly CutoverDate { get; set; }
|
||||
|
||||
/// <summary>The GL journal number (JV-…). Null only for the instant between claiming the run and the GL
|
||||
/// confirming the posting, inside one transaction; never null on a committed row.</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
|
||||
/// <summary>Sum of the debit amounts (what the Opening Balance Equity account is credited with).</summary>
|
||||
public decimal TotalAmount { get; set; }
|
||||
|
||||
public string Currency { get; set; } = "LKR";
|
||||
|
||||
public int PostedBy { get; set; }
|
||||
public DateTime PostedAt { get; set; }
|
||||
|
||||
public ICollection<OpeningBalanceLine> Lines { get; set; } = new List<OpeningBalanceLine>();
|
||||
public ICollection<OpeningStockLine> StockLines { get; set; } = new List<OpeningStockLine>();
|
||||
}
|
||||
|
||||
/// <summary>One cash or bank account's opening amount within an <see cref="OpeningBalanceRun"/>.
|
||||
/// Positive = a debit balance (money held); negative = a credit balance (an overdraft).</summary>
|
||||
public class OpeningBalanceLine
|
||||
{
|
||||
public int OpeningBalanceLineId { get; set; }
|
||||
|
||||
public int OpeningBalanceRunId { get; set; }
|
||||
public OpeningBalanceRun? Run { get; set; }
|
||||
|
||||
public string AccountCode { get; set; } = string.Empty;
|
||||
public string AccountName { get; set; } = string.Empty;
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>One item's opening stock in one warehouse within a stock <see cref="OpeningBalanceRun"/>.</summary>
|
||||
public class OpeningStockLine
|
||||
{
|
||||
public int OpeningStockLineId { get; set; }
|
||||
|
||||
public int OpeningBalanceRunId { get; set; }
|
||||
public OpeningBalanceRun? Run { get; set; }
|
||||
|
||||
public int ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public int WarehouseId { get; set; }
|
||||
public Warehouse? Warehouse { get; set; }
|
||||
|
||||
/// <summary>Quantity in the item's base UOM.</summary>
|
||||
public decimal Qty { get; set; }
|
||||
public decimal UnitCost { get; set; }
|
||||
/// <summary>Qty x UnitCost, rounded to 4 dp — what the inventory account is debited with.</summary>
|
||||
public decimal Value { get; set; }
|
||||
}
|
||||
@@ -21,5 +21,10 @@ public class PoLine
|
||||
public decimal Qty { get; set; }
|
||||
public decimal UnitPrice { get; set; }//
|
||||
public decimal Tax { get; set; }
|
||||
|
||||
/// <summary>Tax amount for this line = round(Qty x UnitPrice x <see cref="Tax"/>, 2). Stored so the
|
||||
/// figure on the PO, its PDF and the totals never drifts from what was approved. Always 0 while
|
||||
/// Procurement configuration's PO tax switch is off.</summary>
|
||||
public decimal TaxAmount { get; set; }
|
||||
public decimal QtyReceived { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Procurement configuration — a singleton row, same shape as <see cref="GrnConfig"/>.
|
||||
/// Controls the optional vendor fields and whether purchase orders carry tax.
|
||||
/// </summary>
|
||||
public class ProcurementConfig
|
||||
{
|
||||
/// <summary>Always 1 - the singleton row's id.</summary>
|
||||
public const int SingletonId = 1;
|
||||
|
||||
public int ConfigId { get; set; }
|
||||
|
||||
/// <summary>Which contact detail vendors carry: none (default), a mobile number or an email.
|
||||
/// A value sent for a field that is not selected is rejected server-side (CONFIG_DISABLED).</summary>
|
||||
public VendorContactMode VendorContactMode { get; set; } = VendorContactMode.None;
|
||||
|
||||
/// <summary>When on, vendors carry an optional company address.</summary>
|
||||
public bool VendorAddressEnabled { get; set; }
|
||||
|
||||
/// <summary>When on (default — the field existed before it became optional), vendors carry optional payment terms.
|
||||
/// When off the field is hidden and a value for it is rejected (CONFIG_DISABLED); stored values are kept.</summary>
|
||||
public bool VendorPaymentTermsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>When on (default), vendors carry an optional tax registration number. Same rules as payment terms.
|
||||
/// Purchase invoices still snapshot a vendor's stored number either way.</summary>
|
||||
public bool VendorTaxRegEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>When on, purchase-order lines carry a tax rate (prefilled from the item) and tax amount;
|
||||
/// when off, tax is hidden everywhere on POs and a non-zero tax is rejected server-side.</summary>
|
||||
public bool PoTaxEnabled { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public int? UpdatedBy { get; set; }
|
||||
public User? UpdatedByUser { get; set; }
|
||||
|
||||
/// <summary>Optimistic concurrency token (ETag source).</summary>
|
||||
public uint RowVersion { get; set; }
|
||||
}
|
||||
@@ -3,25 +3,30 @@ using ERPCore.Domain.Enums;
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Local shadow/projection of an AuthHex <c>Role</c> row, mirroring the same
|
||||
/// pattern <see cref="User"/> uses for AuthHex identities: <see cref="AuthRoleId"/>
|
||||
/// maps to AuthHex's Guid <c>RoleId</c>, while the local <see cref="RoleId"/> (int)
|
||||
/// is what <see cref="Permission"/>/<see cref="RolePermission"/>/<see cref="User.RoleId"/>
|
||||
/// FKs reference. AuthHex remains the source of truth; writes are forwarded there
|
||||
/// first (<c>IAuthHexClient</c>) and mirrored here on success.
|
||||
/// An ERP role. The ERP owns roles and what they may do: <see cref="User.RoleId"/> points here and
|
||||
/// <see cref="RolePermission"/> holds the permissions. AuthHex only authenticates people and knows nothing about roles.
|
||||
/// </summary>
|
||||
public class Role
|
||||
{
|
||||
public int RoleId { get; set; }
|
||||
public Guid AuthRoleId { get; set; }
|
||||
|
||||
/// <summary>Legacy link to the AuthHex role this one used to mirror. Unused now; kept (nullable) until the AuthHex
|
||||
/// role table is retired, then dropped.</summary>
|
||||
public Guid? AuthRoleId { get; set; }
|
||||
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsSystemRole { get; set; }
|
||||
|
||||
/// <summary>True for the administrator role: its holders pass every permission check, bypass branch scoping and can
|
||||
/// manage users and roles. This flag — never the role's code or name — is what "admin" means, so the role can be
|
||||
/// renamed freely. Not settable through the API; only the seeded Admin role carries it.</summary>
|
||||
public bool IsAdmin { get; set; }
|
||||
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -51,6 +51,31 @@ public class SalesConfig
|
||||
public int? DefaultWarehouseId { get; set; }
|
||||
public Warehouse? DefaultWarehouse { get; set; }
|
||||
|
||||
// ---- Optional customer fields. Each is on by default (the field existed before it became optional). When off the
|
||||
// field is hidden on the customer forms and a new value for it is rejected server-side (CONFIG_DISABLED); values
|
||||
// already stored are kept.
|
||||
|
||||
/// <summary>Customers carry an optional phone number.</summary>
|
||||
public bool CustomerPhoneEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>Customers carry an optional email address.</summary>
|
||||
public bool CustomerEmailEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>Customers carry an optional address (lines 1 and 2).</summary>
|
||||
public bool CustomerAddressEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>Customers carry an optional city.</summary>
|
||||
public bool CustomerCityEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>Customers carry an optional country.</summary>
|
||||
public bool CustomerCountryEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>Customers have a credit limit.</summary>
|
||||
public bool CustomerCreditLimitEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>Customers have credit days (how long an invoice may stay unpaid — the customer's "age").</summary>
|
||||
public bool CustomerCreditDaysEnabled { get; set; } = true;
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public int? UpdatedBy { get; set; }
|
||||
|
||||
@@ -15,6 +15,13 @@ public class Vendor
|
||||
public string? Terms { get; set; }
|
||||
public string? TaxReg { get; set; }
|
||||
public string Currency { get; set; } = "LKR";
|
||||
|
||||
/// <summary>Mobile number — only recorded while Procurement configuration's vendor contact mode is Mobile.</summary>
|
||||
public string? Mobile { get; set; }
|
||||
/// <summary>Email — only recorded while the vendor contact mode is Email.</summary>
|
||||
public string? Email { get; set; }
|
||||
/// <summary>Company address — only recorded while vendor address is enabled.</summary>
|
||||
public string? Address { get; set; }
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
public int BranchId { get; set; }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Which opening balance a run carries. Each kind is posted ONCE for the life of the system, so a
|
||||
/// unique index on the kind is the "already posted" guard (the General Ledger itself does not stop a
|
||||
/// second opening entry). Stored as a string.
|
||||
/// </summary>
|
||||
public enum OpeningBalanceKind
|
||||
{
|
||||
CashBank,
|
||||
Stock
|
||||
}
|
||||
@@ -3,8 +3,10 @@ namespace ERPCore.Domain.Enums;
|
||||
/// <summary>
|
||||
/// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Issuing a PO (create
|
||||
/// non-draft, or submit a Draft) enters <see cref="PendingApproval"/>; an Admin who
|
||||
/// isn't the creator must Approve it to reach <see cref="Approved"/>, or Reject it back
|
||||
/// to <see cref="Draft"/> (FR-PROC-04). Stored as a string.
|
||||
/// isn't the creator must Approve it to reach <see cref="Approved"/>, or Reject it to
|
||||
/// <see cref="Rejected"/> (FR-PROC-04) - a status of its own, so a rejected order is never mistaken for a fresh
|
||||
/// Draft or one still awaiting approval. A Rejected order is editable like a Draft and goes back through
|
||||
/// approval when resubmitted. Stored as a string.
|
||||
/// </summary>
|
||||
public enum PurchaseOrderStatus
|
||||
{
|
||||
@@ -14,5 +16,6 @@ public enum PurchaseOrderStatus
|
||||
PartiallyReceived,
|
||||
FullyReceived,
|
||||
Closed,
|
||||
Cancelled
|
||||
Cancelled,
|
||||
Rejected
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Which single contact detail a deployment records for vendors (Procurement configuration).
|
||||
/// <see cref="None"/> hides the field everywhere. Stored as a string.
|
||||
/// </summary>
|
||||
public enum VendorContactMode
|
||||
{
|
||||
None,
|
||||
Mobile,
|
||||
Email
|
||||
}
|
||||
Reference in New Issue
Block a user