price adjustment and stock adjustments
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user