price adjustment and stock adjustments
This commit is contained in:
@@ -4,6 +4,114 @@ Legend: `[ ]` not started · `[~]` in progress · `[x]` done
|
||||
Spec: `docs/10-BACKEND-PHASE1.md` (model + rules) · `docs/11-BACKEND-PHASE1.md` (API)
|
||||
Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit** as the code. When ticking `[x]`, append a short note + any deviation.
|
||||
|
||||
## Dual approval — stock & price adjustments (2026-10-12) — `docs/DUAL_APPROVAL_PLAN.md`
|
||||
|
||||
- [x] **Approval engine** — `IApprovalService`/`ApprovalService`: maker–checker rules in one place (requester can't
|
||||
approve `APPROVAL_FORBIDDEN`; the two approvers must differ `APPROVAL_SAME_PERSON`; optional level-2 roles
|
||||
`APPROVAL_ROLE_REQUIRED`; reject needs a reason `APPROVAL_REASON_REQUIRED`; no Admin bypass). One `DocumentApproval`
|
||||
row per decision (`document_approvals`, polymorphic `DocType` ADJ/PADJ + `DocId`). `ApprovalConfig` singleton
|
||||
(`approval_config`, seeded 2/2): levels 0/1/2 per document, a value (LKR) / percent threshold below which two levels
|
||||
drop to one, `Level2RoleCodes`. Approvers are Admins plus roles with the Approve action on the screen's NAV code.
|
||||
`INotificationService.NotifyUsersAsync` (targeted, same InApp gate) + modules `approval.pending/approved/rejected`.
|
||||
- [x] **Stock adjustments** — `AdjustmentStatus` + `PendingSecondApproval`, `Rejected`, `Cancelled` (column widened to 30).
|
||||
`CreateAsync` validates as before (reason, items, serials) then saves a **request**; stock, ledger, serial moves and the
|
||||
GL journal happen in `PostInCurrentTransactionAsync` on the final approval, re-validated then (negative stock / moved
|
||||
serials fail the approval, the request stays pending). Requested serials are kept as JSON on the line
|
||||
(`RequestedSerialIds/Nos`) and new serials are only created at posting. Levels 0 = posts on create (old behaviour).
|
||||
Value for the threshold = |qty| × average FIFO cost. Endpoints `POST /stock-adjustments/{id}/approve|reject|cancel`
|
||||
(approve/reject need `stock.adjustments` Approve), `status` filter on the list. Wastage uses the same path.
|
||||
- [x] **Stock counts** — `CountService.PostAsync` now submits its variances through `AdjustmentService.SubmitAsync`
|
||||
(closing the count in the same transaction), so a count is not a way around approval. Its own copy of the posting/GL
|
||||
code is gone. **Deviation:** serial moves for count variances are recorded against the adjustment, not the count.
|
||||
- [x] **Price adjustments** — `PriceAdjustment` (`PADJ-…`), `PriceAdjustmentLine` (old snapshot + new min/max/flat),
|
||||
`ItemPriceHistory`. Rules: one pending request per item (`PRICE_ADJUSTMENT_ITEM_PENDING`), min ≤ flat ≤ max, at least
|
||||
one change; on final approval the item must still have the snapshot prices (`PRICE_ADJUSTMENT_STALE`), then prices are
|
||||
written and a history row added. `PriceAdjustmentsController` (`/api/v1/price-adjustments` + `GET /items/{id}/price-history`).
|
||||
`ItemService.UpdateAsync` refuses a price change while price approval is on (`PRICE_CHANGE_REQUIRES_APPROVAL`); prices
|
||||
set on item **create** need no approval.
|
||||
- [x] **Inbox & settings** — `ApprovalsController`: `GET /approvals/pending` (what you can approve + your own requests,
|
||||
actionable count), `GET/PUT /approvals/config` (PUT needs `settings.approvals`).
|
||||
- [x] **Migration `AddDualApproval`** — tables, columns, seeds (sub-nav 48 `products.price-adjustments`, 49
|
||||
`settings.approvals`; permissions 58/59), grants 58 to Admin + roles with Products (2/11), 59 to Admin, turns the three
|
||||
approval notifications on. Existing adjustments keep `RequiredApprovals = 0` (posted without approval). **Not yet applied**.
|
||||
- [x] **Tests** — `DualApprovalTests` (20 incl. theories): pending-until-approved, two different approvers, own-request and
|
||||
same-person refusals, reject with reason, levels 0, value threshold, withdraw, level-2 roles, price apply + history, stale
|
||||
price, one pending request per item. Suite 86/86.
|
||||
|
||||
## Bank Deposit module (2026-10-08) — `docs/BANK_DEPOSIT_MODULE_PLAN.md`
|
||||
|
||||
- [x] **Cash in hand** — `ICashInHandService`/`CashInHandService`: till cash per branch and business day. In = slip/bundle
|
||||
`CashAmount` + cash invoice payments (any invoice); out = cash expense payments (expense not Cancelled); Posted deposits
|
||||
reduce it by `BusinessDate`. Closing carries to the next day from `BankDeposit:CashTrackingStartDate` (appsettings,
|
||||
`2026-10-01`); the opening before a window is a few DB `SUM`s. "Available to deposit" = lowest closing from that day
|
||||
to today, so a back-dated deposit can't overdraw a later day. `BusinessDayClock` (singleton) now holds the
|
||||
`DayEndReport:TimeZone` day ↔ UTC helpers for both this and the report.
|
||||
- [x] **Entity + migration `AddBankDepositModule`** — `BankDeposit` (`bank_deposits`, branch query filter, `CHECK Amount > 0`,
|
||||
index `(BranchId, BusinessDate, Status)`), enum `BankDepositStatus` (Posted/Cancelled, no draft), doc type `BD`.
|
||||
Seeds sub-nav `accounts.bank-deposits` (SubNavItemId 47, SortOrder 6) + permission 57, granted (Actions 31) to Admin and
|
||||
every role holding an Accounts permission (26/32/33/34/54). **Not yet applied** — applies on next startup (`MigrateAsync`).
|
||||
- [x] **API** — `BankDepositsController` (`/api/v1/bank-deposits`): list (business-date range, branch, bank account, status;
|
||||
returns `totalAmount` of all matching rows), get, `cash-in-hand?date&branchId`, `cash-in-hand/daily?from&to` (≤ 92 days),
|
||||
create, `{id}/cancel`. **Deviation from plan §6:** one `NAV:accounts.bank-deposits` permission with action suffixes
|
||||
(create = `Create`, cancel = `Delete`) instead of three permission codes — the project's RBAC is one NAV code per screen.
|
||||
- [x] **GL** — create: Dr bank account / Cr cash account (`BANK_DEPOSIT`, entry date = deposit date), posted before commit.
|
||||
Cancel: reversing entry dated today. From must be a GL `Cash` account, to a `Bank` account. The balance check runs
|
||||
inside the transaction after `SELECT … FOR UPDATE` on the branch row, so concurrent deposits for one branch serialize.
|
||||
With branching off, cash is measured across all branches (everything sits on the default branch).
|
||||
- [x] **Day-End Report** — cash figures now come from `ICashInHandService`; day rows add `openingCashInHand`,
|
||||
`cashDeposited`, `closingCashInHand` (range totals: first opening, summed deposits, last closing); the day adds
|
||||
`deposits[]`. `NetCash` unchanged.
|
||||
- [x] **Tests** — `CashInHandServiceTests` (7), `BankDepositServiceTests` (14 incl. theories), plus a deposit case in
|
||||
`DayEndReportServiceTests`. Suite: 59/59 passing.
|
||||
|
||||
## Day-End Sales Report (2026-10-08) — `docs/DAY_END_SALES_REPORT_PLAN.md`
|
||||
|
||||
- [x] **Payment method on sales** — enum `SalesPaymentMethod` (Cash/Card/BankTransfer/Cheque). `SalesInvoicePayment.PaymentMethod`
|
||||
(request field optional on single and bulk payments; omitted ⇒ derived from the account: Cash account ⇒ Cash, Bank ⇒ Card).
|
||||
`SalesSlip` and `BundleSale` gain `CashAmount`/`CardAmount` (request `cardAmount`, optional, rest is cash; error
|
||||
`SALES_CARD_AMOUNT_INVALID`). **Deviation:** the UI only creates invoices, so the cash/card split that matters is the
|
||||
per-payment method; the slip/bundle fields exist for API callers.
|
||||
- [x] **Migration `AddSalesPaymentMethodsAndDayEndReport`** — columns above; backfills slips/bundles as all cash and
|
||||
invoice payments as Cash (column default); seeds sub-nav `sales.day-end-report` (SubNavItemId 46) + permission 56 and
|
||||
grants it to Admin and every role holding `NAV:sales.day-end` (52). **Not yet applied** — applies on next startup
|
||||
(`MigrateAsync`).
|
||||
- [x] **API** — `DayEndReportController` (`GET /api/v1/day-end-report?date&branchId`, `GET /api/v1/day-end-report/daily?from&to&branchId`,
|
||||
≤ 92 days, `DAY_END_REPORT_RANGE_INVALID`). `DayEndReportService`: Posted slips + bundles + invoices; Paid split by
|
||||
method; counts cash/card/mixed/other/unpaid; Posted expenses by `ExpenseDate`; Income = sales − expenses; plus
|
||||
collections on earlier invoices and net cash (till check). Days are `DayEndReport:TimeZone` (appsettings,
|
||||
`Asia/Colombo`) calendar days. **Deviations:** sales returns are not netted off income — `SalesReturnLine` carries
|
||||
no price; rows are bucketed by local day in memory (slim DB projections) since MariaDB can't group by a zone-shifted
|
||||
date portably. Non-admins are forced to their own branch.
|
||||
- [x] **Sales list + profit/loss** (2026-10-08) — the day response adds `sales[]` (doc type/no., time, customer, total,
|
||||
paid, due, paid-by, cost, gross profit) and every day row adds `costOfGoodsSold` (stock ledger Out value per sale
|
||||
document — same source as `SalesDayEndService`'s COGS), `grossProfit` (sales − cost) and `netProfit` (gross − expenses;
|
||||
negative = loss).
|
||||
- [x] **Tests** — `ERPCore.Tests/DayEndReportServiceTests.cs` (mixed documents and methods, cancelled expense excluded,
|
||||
time-zone boundary, earlier-invoice collection + net cash, unpaid invoice, zero-filled days, range validation).
|
||||
|
||||
- [x] **Entities + migration `AddExpenseModule`** — `ExpenseCategory`, `ExpenseTitle`, `Expense`, `ExpensePayment`
|
||||
(`Domain/Entities/ExpenseCategory.cs`, `Expense.cs`), enums in `Domain/Enums/ExpenseStatus.cs`. Seeds 4 system
|
||||
categories (Utility/Rental/Salaries/Other) + 18 system titles with **placeholder** GL codes 6100/6200/6300/6900
|
||||
(`ExpenseConfiguration.cs`). `Expense` is branch-scoped (global query filter) and numbered `EXP-…` per branch.
|
||||
- [x] **Nav/permissions** — sub-items `accounts.expenses` (SubNavItemId 44) and `accounts.expense-setup` (45),
|
||||
permissions 54/55. **Deviation from the plan:** ids 23/24 were already taken.
|
||||
- [x] **API** — `ExpensesController` (`/api/v1/expenses`, list/summary/get/create/update/delete/post/post-and-pay/cancel/payments)
|
||||
and `ExpenseSetupController` (`/api/v1/expense-categories`, `/api/v1/expense-titles`). Reading the setup lists needs
|
||||
only `accounts`; changing them needs `accounts.expense-setup`.
|
||||
- [x] **GL** — Post: Dr title's account / Cr `Expense:GlAccruedExpensesCode` (appsettings, placeholder `2300`).
|
||||
Payment: Dr Accrued / Cr each cash-bank account. Cancel of a Posted expense with no payments posts the reversal.
|
||||
All GL calls happen before commit (same pattern as `PurchaseInvoicePaymentService`). **Deviations:** a payment
|
||||
submission may carry several lines (split cash/bank) and posts **one** journal shared by its payment rows; the
|
||||
per-category default GL codes live in the seed, not in appsettings (config can't feed `HasData`); a Draft can also
|
||||
be cancelled (no GL entry). Post & Pay accepts a full or partial payment (changed 2026-10-08; it first
|
||||
required the full amount). The remainder stays as the balance.
|
||||
- [x] **Migrations applied to dev DB** (2026-10-08): `AddExpenseModule`, `GrantExpensePermissions` (grants 54/55 to
|
||||
Admin and to every role that already reaches Accounts; without it no role had the codes in `/auth/me`, so the
|
||||
sidebar hid the screens), `UpdateExpenseGlCodes`.
|
||||
- [x] **GL accounts created in the dev GL** (2026-10-08): the placeholder 6100 failed with "Account 6100 not found",
|
||||
and GL generates codes itself. Created `2300` Accrued Expenses and `5200` Operating Expenses (control) with
|
||||
`5200-01..04` (Utilities, Rent, Salaries & Wages, Other Expenses). The seeded titles now use those codes.
|
||||
|
||||
## Per-variant content size (2026-08-11) — follow-up to the UOM re-model below
|
||||
|
||||
The UOM re-model put `ContentQty`/`ContentUnit` on `Item` correctly, but the item **create page
|
||||
|
||||
Reference in New Issue
Block a user