price adjustment and stock adjustments

This commit is contained in:
2026-10-12 14:08:23 +05:30
committed by ImanThiyanga
parent a788349138
commit d0354ed169
166 changed files with 98818 additions and 252 deletions
+299
View File
@@ -0,0 +1,299 @@
# Expense Module Plan
> **Status:** implemented 2026-10-07 (phases 1–5) using the §10 defaults. Deviations from this plan are listed in
> §11 and in `Backend/PROGRESS.md` / `Frontend/PROGRESS.md`. Not yet verified against a live GL service.
> **Lives under:** the existing **Accounts** nav item (`NavItemId = 12`, `/dashboard/accounts`).
> **GL transport:** `docs/12-GENERAL-LEDGER-INTEGRATION.md` (typed calls via `IGeneralLedgerService`).
> **Reference implementation to mirror:** `PurchaseInvoicePaymentService` (call GL before commit, so a
> failed post rolls back the whole operation).
---
## 1. Goal
Record day-to-day business expenses that don't come through procurement (electricity bill, rent,
salaries paid outside payroll, stationery, …). Users can:
- pick a **category** (Utility, Rental, Salaries, Other),
- pick a **title** under it (Utility → Electricity, Water, …),
- enter the **amount**, date, payee and bill reference,
- **pay** it now or later (in one or more installments) from a GL cash/bank account,
- see the **payment status** (Unpaid / Partially Paid / Paid).
Every posted expense and every payment creates a real GL journal entry, so the P&L shows it.
---
## 2. Concepts
```
ExpenseCategory (Utility, Rental, Salaries, Other) ← seeded, admins can add more
└── ExpenseTitle (Electricity, Water, …) ← seeded, admins can add more
└── default GL expense account code
Expense (one bill: category + title + amount + status)
└── ExpensePayment (0..n installments, each posts a GL entry)
```
### 2.1 Seeded categories and titles
| Category | Seeded titles |
|---|---|
| **Utility** | Electricity, Water, Telephone, Internet, Gas |
| **Rental** | Building Rent, Vehicle Rent, Equipment Rent |
| **Salaries** | Staff Salaries, Wages, Overtime, Bonus, EPF / ETF Contribution |
| **Other** | Stationery, Transport, Repairs & Maintenance, Cleaning, Miscellaneous |
- Seeded rows have `IsSystem = true`. They **cannot be deleted or renamed**, only deactivated.
- Users with the `accounts.expense-setup` permission can add **custom titles** under any category,
and **custom categories**.
- Every title has a **GL expense account code**. Seeded titles default to the codes in
`appsettings.json` (§5). Users can override the code per title.
---
## 3. Data model (backend)
All new entities go in `Domain/Entities/`, with configurations in `Infra/Persistence/Configurations/`
and one EF migration.
### 3.1 `ExpenseCategory`
| Field | Type | Notes |
|---|---|---|
| ExpenseCategoryId | int PK | |
| Code | string(30), unique | `UTILITY`, `RENTAL`, `SALARIES`, `OTHER`, or user-defined |
| Name | string(100), unique | |
| IsSystem | bool | seeded = true |
| IsActive | bool | |
| SortOrder | int | |
| CreatedBy / CreatedAt / UpdatedBy / UpdatedAt | | standard audit columns |
### 3.2 `ExpenseTitle`
| Field | Type | Notes |
|---|---|---|
| ExpenseTitleId | int PK | |
| ExpenseCategoryId | int FK | |
| Name | string(100) | unique **within** the category |
| GlExpenseAccountCode | string(20) | the account debited when an expense with this title is posted |
| IsSystem | bool | |
| IsActive | bool | |
| SortOrder | int | |
| audit columns | | |
### 3.3 `Expense` (header)
| Field | Type | Notes |
|---|---|---|
| ExpenseId | int PK | |
| DocNo | string | `EXP-{branch}-{yyyy}-{seq}` via `NumberSequenceService` (DocType `EXPENSE`) |
| BranchId | int FK | branch-scoped like other documents (`BRANCH_ISOLATION_PLAN.md`) |
| ExpenseDate | DateOnly | the bill date, which is also the GL entry date on posting |
| DueDate | DateOnly? | optional, used for "overdue" in the list |
| ExpenseCategoryId | int FK | |
| ExpenseTitleId | int FK | must belong to the chosen category |
| Payee | string(150)? | free text (e.g. "CEB", "Landlord – Mr. Silva") |
| VendorId | int? FK | optional link to an existing vendor |
| BillReference | string(50)? | the supplier's bill/account number |
| Description | string(500)? | |
| Amount | decimal(18,2) | > 0 |
| PaidAmount | decimal(18,2) | maintained by payments |
| BalanceAmount | decimal(18,2) | `Amount - PaidAmount` |
| Status | enum `ExpenseStatus` | `Draft`, `Posted`, `Cancelled` |
| PaymentStatus | enum `ExpensePaymentStatus` | `Unpaid`, `PartiallyPaid`, `Paid`, derived from Paid/Balance |
| GlExpenseAccountCode | string(20) | **snapshot** of the title's code at posting time |
| GlJournalNo | string? | the accrual journal entry |
| PeriodMonth | string(7)? | optional "covers" period, e.g. `2026-09` for September's electricity |
| RowVersion | uint | ETag concurrency, same as `PurchaseInvoice` |
| audit columns | | |
### 3.4 `ExpensePayment`
Mirrors `PurchaseInvoicePayment` field for field:
| Field | Type | Notes |
|---|---|---|
| ExpensePaymentId | int PK | |
| ExpenseId | int FK | |
| Amount | decimal(18,2) | ≤ Expense.BalanceAmount |
| PaymentDate | DateTime | same `ResolvePaymentDate` rule (no future dates, one day of slack) |
| PaymentMethod | enum | `Cash`, `BankTransfer`, `Cheque`, `Card` |
| GlBankAccountId | long | account from `IGeneralLedgerService.ListBankAccountsAsync` |
| BankAccountName | string | snapshot |
| ChequeNo | string? | required when method = Cheque |
| Reference | string? | |
| GlJournalNo | string? | |
| CreatedBy / CreatedAt | | |
### 3.5 Enums (`Domain/Enums/`)
`ExpenseStatus { Draft, Posted, Cancelled }`
`ExpensePaymentStatus { Unpaid, PartiallyPaid, Paid }`
`ExpensePaymentMethod { Cash, BankTransfer, Cheque, Card }`
---
## 4. Workflow and GL postings
```
Draft ──Post──► Posted (Unpaid) ──Pay──► Partially Paid ──Pay──► Paid
│ │
└──Delete └──Cancel (only if PaidAmount = 0) ──► Cancelled
```
| Action | Rule | GL entry |
|---|---|---|
| **Create (Draft)** | Editable, no GL impact | — |
| **Post** | Draft only. Snapshots the title's GL code. | **Dr** title's expense account / **Cr** Accrued Expenses (`Expense:GlAccruedExpensesCode`) |
| **Pay** | Posted and Balance > 0, installments allowed | **Dr** Accrued Expenses / **Cr** selected cash/bank account |
| **Post & Pay** (one click, the common "paid in cash right now" case) | Draft. Runs Post and a full Pay in **one transaction**. | Both entries above |
| **Cancel** | Posted with no payments | Reversing entry: **Dr** Accrued / **Cr** expense account |
| **Delete** | Draft only | — |
All GL calls happen inside `_uow.ExecuteInTransactionAsync` **before** the local rows are saved,
the same pattern as `PurchaseInvoicePaymentService`. If GL is down or rejects the entry, nothing is saved.
`SourceModule = "EXPENSE"` / `"EXPENSE_PAYMENT"`, `Reference = DocNo`.
**Why accrue then pay** (rather than Dr Expense / Cr Bank on payment): unpaid bills still show
in the P&L for the month they belong to, and the Accrued Expenses balance equals the total unpaid bills.
"Post & Pay" keeps the one-step case fast.
---
## 5. Configuration (`appsettings.json`)
```json
"Expense": {
"GlAccruedExpensesCode": "2300",
"DefaultGlAccountCodes": {
"UTILITY": "6100",
"RENTAL": "6200",
"SALARIES": "6300",
"OTHER": "6900"
}
}
```
Seeded titles take their category's default code. The actual codes must match the GL service's chart
of accounts. **Confirm them with the GL owner before seeding** (open question Q3).
---
## 6. API (`Controllers/ExpensesController.cs`, `ExpenseSetupController.cs`)
Policy: `NavPolicies.Prefix + "accounts"`, same door as the cheque/bank screens.
### Setup (categories and titles)
| Method | Route | Purpose |
|---|---|---|
| GET | `/api/v1/expense-categories?includeInactive=` | list categories, each with its titles |
| POST | `/api/v1/expense-categories` | add a custom category |
| PUT | `/api/v1/expense-categories/{id}` | rename (non-system only) / toggle active |
| POST | `/api/v1/expense-categories/{id}/titles` | add a custom title |
| PUT | `/api/v1/expense-titles/{id}` | rename (non-system only) / change GL code / toggle active |
### Expenses
| Method | Route | Purpose |
|---|---|---|
| GET | `/api/v1/expenses` | paged list. Filters: branch, category, title, status, paymentStatus, date range, search (DocNo/payee/reference) |
| GET | `/api/v1/expenses/{id}` | detail + payments (ETag header) |
| POST | `/api/v1/expenses` | create draft |
| PUT | `/api/v1/expenses/{id}` | update draft (`If-Match`) |
| DELETE | `/api/v1/expenses/{id}` | delete draft |
| POST | `/api/v1/expenses/{id}/post` | post (accrue) |
| POST | `/api/v1/expenses/{id}/post-and-pay` | post + full payment, body = payment request |
| POST | `/api/v1/expenses/{id}/cancel` | cancel (reason required) |
| GET | `/api/v1/expenses/{id}/payments` | payment history |
| POST | `/api/v1/expenses/{id}/payments` | add a payment / installment |
| GET | `/api/v1/expenses/summary?from=&to=&branchId=` | totals by category → title, plus paid/unpaid totals (for the dashboard cards) |
### New error codes (`System/Errors/ErrorCodes`)
`EXPENSE_NOT_DRAFT`, `EXPENSE_NOT_PAYABLE`, `EXPENSE_PAYMENT_EXCEEDS_BALANCE`,
`EXPENSE_HAS_PAYMENTS`, `EXPENSE_TITLE_CATEGORY_MISMATCH`, `EXPENSE_TITLE_INACTIVE`,
`EXPENSE_SYSTEM_ROW_READONLY`, `EXPENSE_BANK_ACCOUNT_NOT_FOUND`, `EXPENSE_CHEQUE_NO_REQUIRED`.
---
## 7. Backend file checklist
| Layer | Files |
|---|---|
| Entities | `ExpenseCategory.cs`, `ExpenseTitle.cs`, `Expense.cs`, `ExpensePayment.cs` |
| Enums | `ExpenseStatus.cs`, `ExpensePaymentStatus.cs`, `ExpensePaymentMethod.cs` |
| EF config | `ExpenseConfiguration.cs` (all four tables, seed data, indexes, check constraint `Amount > 0`, **MariaDB-safe quoting**, see commit c97a7e9) |
| DbContext | 4 `DbSet`s in `ErpDbContext.cs` |
| DTOs | `Dtos/Expense/ExpenseDtos.cs`, `ExpenseSetupDtos.cs` + FluentValidation validators |
| Services | `ExpenseSetupService`, `ExpenseService`, `ExpensePaymentService` (+ interfaces) |
| Controllers | `ExpenseSetupController`, `ExpensesController` |
| Nav / perms | `SubNavItemConfiguration`: `accounts.expenses` (id 23, sort 4) and `accounts.expense-setup` (id 24, sort 5); `PermissionConfiguration` entries |
| Number seq | DocType `EXPENSE` |
| Audit | `IAuditService` entries on create/post/pay/cancel |
| Program.cs | DI registrations |
| Migration | `AddExpenseModule` |
---
## 8. Frontend (`Frontend/erp-system/app/dashboard/accounts/`)
| Route | Screen |
|---|---|
| `expenses/page.tsx` | List: summary cards (This month total · Unpaid · Overdue), filter bar (category, title, status, payment status, dates), table with **payment status badge** (Unpaid red / Partial amber / Paid green) |
| `expenses/new/page.tsx` | Form: Branch · Date · **Category** dropdown → **Title** dropdown (filtered by category, with an inline "+ Add title" if permitted) · Payee/Vendor · Bill ref · Period · Amount · Description. Buttons: **Save Draft**, **Post**, **Post & Pay** (opens the payment dialog) |
| `expenses/[id]/page.tsx` | Detail, GL journal no, payment history, **Add Payment** dialog (amount defaults to balance · date · method · cash/bank account from `bankAccountsApi` · cheque no · reference), Cancel |
| `expense-setup/page.tsx` | Categories as tabs or accordion → titles table (name, GL code, active, system lock icon); add/edit dialogs |
API client: `lib/api/expenses.ts`. Types: `types/expense.ts`. Reuse the payment dialog pattern from the
purchase-invoice payment screen.
---
## 9. Implementation phases
1. **Setup data**: entities, config, seed, migration, setup API + setup screen.
2. **Expense CRUD**: draft create/edit/delete, list/detail screens, DocNo sequence.
3. **GL posting**: Post, Cancel, Post & Pay, config codes.
4. **Payments**: installments, payment status, cheque method.
5. **Reporting**: summary endpoint, list cards, export to CSV.
6. Docs: add `docs/16-BACKEND-EXPENSES.md` (or extend `00-CORE.md` nav), update `Backend/PROGRESS.md` / `Frontend/PROGRESS.md`.
---
## 10. Open questions
| # | Question | Default if not answered |
|---|---|---|
| Q1 | One title per expense, or **multiple lines** per expense (e.g. one bill covering electricity + water)? | One title per expense. Lines can be added later without breaking the API. |
| Q2 | Can users add **new categories**, or only new titles under the four fixed ones? | **Decided 2026-10-08: titles only.** The four categories are fixed; the create-category endpoint and UI were removed. |
| Q3 | Real GL account codes for Accrued Expenses and each category? | Placeholders in §5 |
| Q4 | **Salaries vs. HRM Payroll:** payroll runs don't post to GL today. If payroll GL posting is added later, the "Salaries" category could double-count. Should Salaries expenses be blocked or warned once payroll posts? | Allow for now, add a warning note on the Salaries category |
| Q5 | Tax (VAT) on expenses: capture input tax separately? | Out of scope for phase 1, amount is gross |
| Q6 | Attachments (scan of the bill)? | Out of scope for phase 1 |
| Q7 | Approval step before posting (maker/checker)? | No. Post permission is enough. |
| Q8 | Recurring expenses (monthly rent auto-draft)? | Phase 2 |
---
## 11. As built: deviations from this plan
- **Nav/permission ids:** sub-items 44 (`accounts.expenses`) and 45 (`accounts.expense-setup`), permissions 54/55.
Ids 23/24 in §7 were already taken.
- **Config (§5):** only `Expense:GlAccruedExpensesCode` is in `appsettings.json`. The per-category default codes are
seeded directly (`ExpenseConfiguration.cs`), because `HasData` cannot read configuration. Change them per title on
the Expense Setup screen.
- **GL accounts (2026-10-08):** GL generates account codes itself (its `POST /accounts` takes no code), so the
planned 6100–6900 codes could never exist. The dev GL now has `2300` Accrued Expenses (liability) and a
non-postable `5200` Operating Expenses control account with children `5200-01` Utilities, `5200-02` Rent,
`5200-03` Salaries & Wages and `5200-04` Other Expenses. The seed and migration `UpdateExpenseGlCodes` point the
seeded titles at those. Another GL instance will generate its own codes, so check them on Expense Setup.
- **Status columns:** categories and titles use the codebase's `EntityStatus` (Active/Inactive) instead of an `IsActive` bool.
- **Payments:** one submission can have several lines (cash + bank). It posts **one** GL journal, and each line becomes
an `ExpensePayment` row that shares that journal number. `PaymentMethod`, `ChequeNo` and `Reference` apply to the
whole submission.
- **Cancel:** also allowed on a Draft (no GL entry). A Posted expense with no payments gets a reversing entry dated today
(`GlReversalJournalNo`). The reason is stored in `CancelReason`.
- **No FluentValidation:** request validation uses DataAnnotations plus service checks, matching the rest of the API.
- **Audit:** no explicit `IAuditService` calls. The DbContext's ChangeTracker audit captures every insert and update.