# Sales & Purchases Module — Comprehensive Analysis & Implementation Plan

**Date:** 2026-02-24
**Analyst:** AI System Architect
**Version:** 1.0
**Status:** Draft for Review

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Requirements Summary](#2-requirements-summary)
3. [Existing System Reference](#3-existing-system-reference)
4. [Sales Module — Backend Specification](#4-sales-module--backend-specification)
5. [Sales Module — Frontend Specification](#5-sales-module--frontend-specification)
6. [Purchases Module — Backend Specification](#6-purchases-module--backend-specification)
7. [Purchases Module — Frontend Specification](#7-purchases-module--frontend-specification)
8. [Shared Components](#8-shared-components)
9. [Database Schema Design](#9-database-schema-design)
10. [API Endpoint Reference](#10-api-endpoint-reference)
11. [Integration Points](#11-integration-points)
12. [Epic & Task Breakdown](#12-epic--task-breakdown)

---

## 1. Executive Summary

This document provides a comprehensive analysis and implementation plan for the **Sales** and **Purchases** modules of Moon ERP. These two modules are the core transactional modules that connect the existing Core (Products, Partners, Units), Accounting (Journal Entries, Tax, AR/AP, Cost Centers), and Inventory (Warehouses, Stock Receipts/Issues, Costing) modules into a complete business workflow.

### Key Design Principles
- **Mirror existing patterns**: Follow the same API conventions used by Expenses, Payment Vouchers, Stock Receipts (draft → approved/posted → cancelled lifecycle)
- **Configurable complexity**: Users can use the full flow (Quotation → Order → Invoice → Delivery) or skip to direct invoicing/billing
- **Bilingual**: All titles, descriptions, notes support Arabic (`_ar` suffix) and English
- **Multi-tenant**: All tables include `company_id`, all queries scoped by tenant
- **Auto-numbering**: All documents use the existing Sequences module for number generation
- **Decimal precision**: All monetary amounts use `decimal(15,3)` (matching existing pattern)
- **Soft deletes**: Only draft documents can be deleted; others are cancelled with reversal

---

## 2. Requirements Summary

### Sales Module
| Feature | Detail |
|---------|--------|
| Document Flow | Quotation/Proforma → Sales Order → Sales Invoice → Sales Return/Credit Note |
| Delivery Notes | Optional, with shipping tracking (carrier, tracking number, address) |
| Discounts | Three levels: line-item, document-level, quantity-based tiered pricing |
| Tax | Flexible: one tax for entire document OR different tax per line item |
| Payments | Partial payments allowed + configurable payment terms (Net 30/60, installments) |
| Currency | Multi-currency with exchange rate conversion to base currency |
| Stock Deduction | Configurable: deduct on invoice posting OR on delivery note confirmation |
| Accounting | Auto journal entries on posting: Revenue + COGS (Cost of Goods Sold) |
| Cost Centers | One cost center per document (document-level) |
| Approval | Configurable multi-level approval (can be set to auto-approve, simple, or amount-based levels) |
| Commissions | Salesperson commission tracking (percentage or fixed, configurable rules) |
| Attachments | File attachments on any sales document (uses existing Attachments module) |
| PDF/Print | Printable PDF templates for all document types (AR/EN, RTL/LTR) |
| Sequences | Use existing Sequences module — user configures which sequence maps to which document type |
| Settings | Separate Sales settings page |
| Reports | Full analytics with custom filters (date, branch, warehouse, cost center, category, salesperson, partner) |

### Purchases Module
| Feature | Detail |
|---------|--------|
| Document Flow | Purchase Request → Purchase Order → Purchase Invoice/Bill → Purchase Return/Debit Note |
| Direct Bill | Users can skip PR/PO and create a Bill directly |
| Goods Receipt (GRN) | Configurable: direct stock-in on Bill, OR separate GRN, OR GRN + quality check |
| Discounts | Mirror Sales: line + document + tiered |
| Tax | Mirror Sales: document-level or per-line |
| Payments | Mirror Sales: partial + payment terms |
| Currency | Multi-currency (same as Sales) |
| Supplier Price Lists | Store default prices per supplier per product, auto-fill on PO creation (configurable, can enable/disable) |
| Supplier Comparison | Compare prices across suppliers when creating PO (configurable) |
| Accounting | Auto journal entries: Debit Expense/Inventory, Credit Payable + Tax |
| Cost Centers | Document-level (same as Sales) |
| Approval | Same configurable multi-level workflow as Sales |
| Attachments | Same as Sales |
| PDF/Print | Same as Sales |
| Settings | Separate Purchases settings page |
| Reports | Full analytics with custom filters (same structure as Sales) |

---

## 3. Existing System Reference

### Patterns to Follow

#### 3.1 Document Lifecycle (from Expenses/Vouchers)
```
draft → approved → posted → (cancelled with reversal)
```
- **Draft**: Editable, deletable
- **Approved**: Locked, waiting for posting (or auto-posted)
- **Posted**: Creates journal entry, updates balances, deducts/adds stock
- **Cancelled**: Reverses journal entry, reverses stock, marks as cancelled

#### 3.2 Stock Operations (from Inventory module)
- **Stock Receipt (GRN)**: `POST /api/inventory/receipts` → items with `product_id`, `variant_id`, `unit_id`, `quantity`, `unit_cost`, `batch_number`, `expiry_date` → approve → `StockService.increaseStock()` + recalculate weighted average cost
- **Stock Issue (GDN)**: `POST /api/inventory/issues` → items without cost (auto-calculated from average on approval) → approve → `StockService.decreaseStock()` → validates sufficient stock (unless `allow_negative_stock`)
- **Reference fields**: `reference_type` (sale, purchase, return, etc.) + `reference_number` for cross-linking

#### 3.3 Journal Entry Creation (from Expenses)
```
On approval/posting:
1. Build journal entry lines (debit/credit accounts, amounts)
2. POST /api/accounting/journal-entries with auto_post=true
3. Store returned journal_entry_id on the document
On cancellation:
1. POST /api/accounting/journal-entries/{id}/reverse
```

#### 3.4 Payment Voucher Pattern
- Header fields: `date`, `partner_id`, `total_amount`, `payment_method` (cash/check/bank_transfer/card/mixed), `paying_account_id`, `bank_account_id`, `check_number`, `check_date`, `check_bank`, `currency_id`, `exchange_rate`, `cost_center_id`, `branch_id`
- Multi-line: each line has `account_id`, `amount`, `payment_method`, `description`
- Auto-number prefix: `PV-2026-0001`

#### 3.5 API Conventions
- **Pagination**: `?page=1` (max 25 per page, `meta.last_page` for total pages)
- **Filters**: query params: `status`, `partner_id`, `branch_id`, `date_from`, `date_to`, `search`
- **Response**: `{ "data": {...}, "message": "..." }` or `{ "data": [...], "links": {...}, "meta": {...} }`
- **Bilingual**: `name` + `name_ar`, `description` + `description_ar`, `notes` + `notes_ar`
- **Soft deletes**: `deleted_at` column, only draft status allows DELETE
- **Monetary**: `decimal(15,3)` for all amounts
- **Date format**: `YYYY-MM-DD`

---

## 4. Sales Module — Backend Specification

### 4.1 Sales Settings

**Purpose**: Store module-level configuration for the entire Sales module.

**Implementation**: Use the existing Settings module pattern (key-value store with `sales.` prefix).

**Settings keys:**

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `sales.stock_deduction_point` | enum | `invoice` | When to deduct stock: `invoice` (on invoice posting) or `delivery` (on delivery note confirmation) |
| `sales.approval_workflow` | enum | `none` | Approval mode: `none` (auto-approve), `simple` (single approval), `multi_level` (amount-based levels) |
| `sales.approval_thresholds` | JSON | `[]` | Array of `{min_amount, max_amount, role_id}` for multi-level approval |
| `sales.default_payment_terms_days` | integer | `0` | Default payment terms in days (0 = immediate, 30 = Net 30, etc.) |
| `sales.auto_generate_delivery` | boolean | `false` | Automatically create Delivery Note when Sales Order is confirmed |
| `sales.commission_enabled` | boolean | `false` | Enable salesperson commission tracking |
| `sales.default_commission_rate` | decimal | `0.000` | Default commission percentage for new salespeople |
| `sales.default_tax_id` | FK/null | `null` | Default tax rate applied to new sales documents |
| `sales.default_cost_center_id` | FK/null | `null` | Default cost center for new sales documents |
| `sales.default_warehouse_id` | FK/null | `null` | Default warehouse for stock operations |
| `sales.price_includes_tax` | boolean | `false` | Whether sale prices are tax-inclusive by default |
| `sales.allow_below_min_price` | boolean | `false` | Allow selling below product's `min_sale_price` |
| `sales.enable_delivery_notes` | boolean | `true` | Enable/disable delivery notes feature |
| `sales.quotation_validity_days` | integer | `30` | Default validity period for quotations |
| `sales.revenue_account_id` | FK | required | Default revenue GL account for journal entries |
| `sales.cogs_account_id` | FK | required | Cost of Goods Sold GL account |
| `sales.receivable_account_id` | FK | required | Accounts Receivable GL account |
| `sales.tax_payable_account_id` | FK | required | Tax Payable GL account |
| `sales.discount_account_id` | FK/null | `null` | Sales Discount GL account (if tracking discounts separately) |

**Endpoints:**
- `GET /api/sales/settings` — retrieve all sales settings
- `PUT /api/sales/settings` — bulk update settings `{ "key1": "value1", "key2": "value2" }`

---

### 4.2 Sales Quotation / Proforma Invoice

**Purpose**: Formal price proposal sent to a customer. Can be converted to Sales Order or directly to Invoice.

**Database table: `sales_quotations`**

| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| `id` | bigint unsigned | PK | Auto-increment |
| `company_id` | bigint unsigned | NO | Tenant FK |
| `branch_id` | bigint unsigned | YES | Branch FK |
| `quotation_number` | varchar(50) | NO | Auto-generated from Sequences (e.g. `QUO-2026-0001`) |
| `date` | date | NO | Quotation date |
| `valid_until` | date | YES | Expiry date (default: date + `quotation_validity_days` setting) |
| `partner_id` | bigint unsigned | NO | Customer FK → `business_partners` |
| `partner_name` | varchar(255) | YES | Snapshot of customer name at creation time |
| `partner_address` | text | YES | Snapshot of customer address |
| `partner_tax_number` | varchar(50) | YES | Snapshot of customer tax ID |
| `currency_id` | bigint unsigned | NO | Currency FK → `currencies` |
| `exchange_rate` | decimal(15,6) | NO | Exchange rate to base currency at quotation date |
| `cost_center_id` | bigint unsigned | YES | Cost Center FK |
| `salesperson_id` | bigint unsigned | YES | User FK — the salesperson responsible |
| `payment_terms_days` | integer | YES | Payment terms (Net X days) |
| `payment_terms_note` | varchar(255) | YES | Free-text payment terms description |
| `shipping_address` | text | YES | Delivery address (if different from partner address) |
| `notes` | text | YES | English notes |
| `notes_ar` | text | YES | Arabic notes |
| `internal_notes` | text | YES | Internal notes (not shown on PDF) |
| `discount_type` | enum('percentage','fixed') | YES | Document-level discount type |
| `discount_value` | decimal(15,3) | YES | Discount percentage or fixed amount |
| `discount_amount` | decimal(15,3) | NO | Calculated document discount amount |
| `subtotal` | decimal(15,3) | NO | Sum of line totals before document discount |
| `tax_amount` | decimal(15,3) | NO | Total tax |
| `total` | decimal(15,3) | NO | Grand total (subtotal - discount + tax) |
| `status` | enum | NO | `draft`, `sent`, `pending_approval`, `approved`, `rejected`, `confirmed`, `expired`, `cancelled`, `converted` |
| `approved_by` | bigint unsigned | YES | User who approved |
| `approved_at` | timestamp | YES | Approval timestamp |
| `sent_at` | timestamp | YES | When sent to customer |
| `converted_to_type` | enum('order','invoice') | YES | What it was converted to |
| `converted_to_id` | bigint unsigned | YES | ID of the order/invoice it was converted to |
| `created_by` | bigint unsigned | NO | Creator FK |
| `updated_by` | bigint unsigned | YES | Last updater FK |
| `created_at` | timestamp | NO | |
| `updated_at` | timestamp | NO | |
| `deleted_at` | timestamp | YES | Soft delete |

**Line items table: `sales_quotation_items`**

| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| `id` | bigint unsigned | PK | |
| `quotation_id` | bigint unsigned | NO | FK → `sales_quotations` |
| `sort_order` | integer | NO | Display order of items |
| `product_id` | bigint unsigned | NO | FK → `products` |
| `product_variant_id` | bigint unsigned | YES | FK → `product_variants` |
| `unit_id` | bigint unsigned | NO | FK → `units` |
| `description` | varchar(500) | YES | Custom line description (overrides product name) |
| `description_ar` | varchar(500) | YES | Arabic custom description |
| `quantity` | decimal(15,3) | NO | Quantity |
| `unit_price` | decimal(15,3) | NO | Price per unit |
| `discount_type` | enum('percentage','fixed') | YES | Line-level discount type |
| `discount_value` | decimal(15,3) | YES | Line discount value |
| `discount_amount` | decimal(15,3) | NO | Calculated line discount |
| `tax_id` | bigint unsigned | YES | FK → `tax_rates` (per-line tax) |
| `tax_rate` | decimal(8,4) | YES | Snapshot of tax rate at creation |
| `tax_amount` | decimal(15,3) | NO | Calculated line tax |
| `line_total` | decimal(15,3) | NO | (qty × price - discount) + tax |
| `notes` | varchar(500) | YES | Line-level notes |

**Tiered Pricing Logic:**
- When adding a product to a line, check if there are quantity-based pricing rules defined for the product
- If `quantity >= tier_threshold`, apply the tier price automatically
- Tiers stored in a new table `product_pricing_tiers`:
  - `product_id`, `min_quantity`, `max_quantity`, `price_type` (percentage_discount/fixed_price), `value`
- Backend calculates and suggests; frontend can override

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/sales/quotations` | List quotations with pagination. **Filters**: `status`, `partner_id`, `salesperson_id`, `branch_id`, `date_from`, `date_to`, `search` (searches number, partner name), `currency_id` |
| `POST` | `/api/sales/quotations` | Create new quotation in `draft` status. Request body: all header fields + `items[]` array. Auto-generates `quotation_number` from Sequences. Auto-calculates: `discount_amount`, `subtotal`, `tax_amount`, `total`, each line's `discount_amount`, `tax_amount`, `line_total`. Validates: partner exists, products exist, units valid for products, tax_id exists if provided, currency_id exists, exchange_rate > 0 |
| `GET` | `/api/sales/quotations/{id}` | Get quotation with all relationships loaded: `items.product`, `items.unit`, `items.tax`, `partner`, `salesperson`, `currency`, `costCenter`, `attachments`, `approvalLogs` |
| `PUT` | `/api/sales/quotations/{id}` | Update quotation. Only allowed when status = `draft` or `rejected`. Recalculates all amounts. Can update items (send full items array — backend syncs: creates new, updates existing, deletes removed) |
| `DELETE` | `/api/sales/quotations/{id}` | Soft-delete. Only allowed when status = `draft` |
| `POST` | `/api/sales/quotations/{id}/send` | Mark as `sent`, record `sent_at` timestamp. Optionally triggers email notification (future) |
| `POST` | `/api/sales/quotations/{id}/submit-approval` | Change status to `pending_approval`. Creates approval log entry. Only from `draft` or `sent` |
| `POST` | `/api/sales/quotations/{id}/approve` | Approve quotation. Changes status to `approved`. Records `approved_by`, `approved_at`. Checks if user has approval permission for this amount level. If approval_workflow = `none`, this is automatic on submit |
| `POST` | `/api/sales/quotations/{id}/reject` | Reject quotation. Changes status to `rejected`. Request body: `{ "reason": "..." }`. Records rejection in approval log |
| `POST` | `/api/sales/quotations/{id}/convert-to-order` | Creates a new Sales Order from this quotation. Copies all header fields and items. Sets `converted_to_type = 'order'`, `converted_to_id = new_order.id`. Changes quotation status to `converted`. Returns the new Sales Order |
| `POST` | `/api/sales/quotations/{id}/convert-to-invoice` | Creates a new Sales Invoice directly from quotation (skipping order). Same logic as above but creates invoice. Changes status to `converted` |
| `POST` | `/api/sales/quotations/{id}/cancel` | Cancel quotation. Changes status to `cancelled`. Only from `draft`, `sent`, `approved` |
| `GET` | `/api/sales/quotations/{id}/pdf` | Generate PDF. Query param: `?lang=ar|en`. Returns `application/pdf` blob |
| `POST` | `/api/sales/quotations/{id}/duplicate` | Create a copy of this quotation as a new draft. Copies all fields and items, generates new number |

---

### 4.3 Sales Order

**Purpose**: Confirmed customer order that triggers delivery and invoicing.

**Database table: `sales_orders`**

Same columns as `sales_quotations` with these differences/additions:

| Column | Type | Description |
|--------|------|-------------|
| `order_number` | varchar(50) | Auto-generated (e.g. `SO-2026-0001`) |
| `quotation_id` | bigint unsigned, nullable | FK if created from a quotation |
| `expected_delivery_date` | date, nullable | When customer expects delivery |
| `status` | enum | `draft`, `pending_approval`, `approved`, `confirmed`, `partially_delivered`, `fully_delivered`, `partially_invoiced`, `fully_invoiced`, `closed`, `cancelled` |
| `delivery_status` | enum | `pending`, `partial`, `complete` (computed from delivery notes) |
| `invoice_status` | enum | `pending`, `partial`, `complete` (computed from invoices) |

**Line items table: `sales_order_items`**

Same as quotation items with additions:

| Column | Type | Description |
|--------|------|-------------|
| `order_id` | bigint unsigned | FK → `sales_orders` |
| `delivered_quantity` | decimal(15,3) | How much has been delivered (updated by Delivery Notes) |
| `invoiced_quantity` | decimal(15,3) | How much has been invoiced (updated by Invoices) |
| `remaining_delivery_qty` | decimal(15,3) | Virtual: `quantity - delivered_quantity` |
| `remaining_invoice_qty` | decimal(15,3) | Virtual: `quantity - invoiced_quantity` |

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/sales/orders` | List with filters: `status`, `delivery_status`, `invoice_status`, `partner_id`, `salesperson_id`, `branch_id`, `date_from`, `date_to`, `quotation_id`, `search` |
| `POST` | `/api/sales/orders` | Create order. Optional `quotation_id` to auto-populate from quotation. Same validation as quotations |
| `GET` | `/api/sales/orders/{id}` | Get with items, deliveries, invoices, partner, attachments, approvalLogs |
| `PUT` | `/api/sales/orders/{id}` | Update (draft/rejected only) |
| `DELETE` | `/api/sales/orders/{id}` | Soft-delete (draft only) |
| `POST` | `/api/sales/orders/{id}/submit-approval` | Submit for approval |
| `POST` | `/api/sales/orders/{id}/approve` | Approve |
| `POST` | `/api/sales/orders/{id}/reject` | Reject with reason |
| `POST` | `/api/sales/orders/{id}/confirm` | Confirm order (marks as `confirmed`) |
| `POST` | `/api/sales/orders/{id}/create-delivery` | Create Delivery Note from order. Request: `{ "items": [{ "order_item_id": 1, "quantity": 5, "warehouse_id": 1 }] }` — supports partial delivery (deliver some items, not all). Validates: qty ≤ remaining_delivery_qty |
| `POST` | `/api/sales/orders/{id}/create-invoice` | Create Invoice from order. Same partial logic. Request: `{ "items": [{ "order_item_id": 1, "quantity": 5 }] }` |
| `POST` | `/api/sales/orders/{id}/cancel` | Cancel (only if no deliveries/invoices posted against it) |
| `POST` | `/api/sales/orders/{id}/close` | Close order early (mark remaining quantities as cancelled) |
| `GET` | `/api/sales/orders/{id}/pdf` | Generate PDF |
| `POST` | `/api/sales/orders/{id}/duplicate` | Duplicate as new draft |

---

### 4.4 Sales Invoice

**Purpose**: The financial document that records the sale, creates journal entries, and optionally deducts stock.

**Database table: `sales_invoices`**

Same base structure as orders with these differences:

| Column | Type | Description |
|--------|------|-------------|
| `invoice_number` | varchar(50) | Auto-generated (e.g. `INV-2026-0001`) |
| `order_id` | bigint unsigned, nullable | FK if created from a sales order |
| `quotation_id` | bigint unsigned, nullable | FK if created directly from quotation |
| `due_date` | date | Payment due date (calculated from `date + payment_terms_days`) |
| `status` | enum | `draft`, `pending_approval`, `approved`, `posted`, `partially_paid`, `paid`, `overdue`, `cancelled` |
| `amount_paid` | decimal(15,3) | Total amount paid so far |
| `balance_due` | decimal(15,3) | `total - amount_paid` |
| `journal_entry_id` | bigint unsigned, nullable | FK to the journal entry created on posting |
| `cogs_journal_entry_id` | bigint unsigned, nullable | FK to the COGS journal entry |
| `posted_at` | timestamp, nullable | When the invoice was posted |

**Line items table: `sales_invoice_items`**

| Column | Type | Description |
|--------|------|-------------|
| `invoice_id` | bigint unsigned | FK |
| `order_item_id` | bigint unsigned, nullable | FK if from order |
| `warehouse_id` | bigint unsigned, nullable | Source warehouse for stock deduction |
| `unit_cost` | decimal(15,3) | Cost at time of posting (from weighted average) — for COGS calculation |
| (all other columns same as quotation items) | | |

**Posting Logic (when status changes to `posted`):**

1. **Revenue Journal Entry:**
   - Debit: `sales.receivable_account_id` → total amount
   - Credit: `sales.revenue_account_id` → subtotal - discount
   - Credit: `sales.tax_payable_account_id` → tax amount
   - Credit: `sales.discount_account_id` → discount amount (if configured)
   - Partner: `partner_id` on the receivable line
   - Cost Center: `cost_center_id`
   - Currency: `currency_id`, `exchange_rate`

2. **COGS Journal Entry (for products with `track_inventory = true`):**
   - For each line where product has inventory tracking:
     - Get `average_cost` from `stock_balances` for this product+warehouse
     - Set `unit_cost = average_cost`
     - Debit: `sales.cogs_account_id` → quantity × unit_cost
     - Credit: warehouse's `account_id` (Inventory account) → quantity × unit_cost

3. **Stock Deduction (if `sales.stock_deduction_point = 'invoice'`):**
   - For each line where product has inventory tracking:
     - Create Stock Issue internally: `reference_type = 'sale'`, `reference_number = invoice_number`
     - Deduct from specified `warehouse_id`
     - Validate sufficient stock (unless `allow_negative_stock`)
     - Update `stock_balances` (quantity, average_cost, total_value)

4. **AR Update:**
   - The journal entry automatically creates AR balance through the receivable account line

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/sales/invoices` | List. Filters: `status`, `partner_id`, `salesperson_id`, `branch_id`, `date_from`, `date_to`, `due_date_from`, `due_date_to`, `payment_status` (unpaid/partial/paid/overdue), `order_id`, `search` |
| `POST` | `/api/sales/invoices` | Create. Optional: `order_id` to auto-populate from order |
| `GET` | `/api/sales/invoices/{id}` | Get with items, payments, journal entries, order, partner, attachments |
| `PUT` | `/api/sales/invoices/{id}` | Update (draft only) |
| `DELETE` | `/api/sales/invoices/{id}` | Soft-delete (draft only) |
| `POST` | `/api/sales/invoices/{id}/submit-approval` | Submit for approval |
| `POST` | `/api/sales/invoices/{id}/approve` | Approve |
| `POST` | `/api/sales/invoices/{id}/reject` | Reject |
| `POST` | `/api/sales/invoices/{id}/post` | Post invoice — executes all the posting logic above |
| `POST` | `/api/sales/invoices/{id}/cancel` | Cancel posted invoice — reverses journal entries, reverses stock issue, resets AR |
| `POST` | `/api/sales/invoices/{id}/record-payment` | Quick payment recording. Body: `{ "date", "amount", "payment_method", "bank_account_id", "check_number", "reference", "notes" }`. Creates Sales Payment record + journal entry |
| `GET` | `/api/sales/invoices/{id}/payments` | List all payments for this invoice |
| `GET` | `/api/sales/invoices/{id}/payment-schedule` | View installment schedule (if payment terms include installments) |
| `GET` | `/api/sales/invoices/{id}/pdf` | Generate PDF |
| `POST` | `/api/sales/invoices/{id}/duplicate` | Duplicate as new draft |

---

### 4.5 Sales Return / Credit Note

**Purpose**: Reverse a full or partial sale. Returns goods to stock and reverses accounting entries.

**Database table: `sales_returns`**

| Column | Type | Description |
|--------|------|-------------|
| `return_number` | varchar(50) | Auto-generated (e.g. `SCN-2026-0001` — Sales Credit Note) |
| `invoice_id` | bigint unsigned | FK to the original invoice being returned against |
| `reason` | text | Reason for return |
| `reason_ar` | text | Arabic reason |
| `status` | enum | `draft`, `pending_approval`, `approved`, `posted`, `cancelled` |
| `journal_entry_id` | bigint unsigned, nullable | Reversal JE |
| `cogs_journal_entry_id` | bigint unsigned, nullable | COGS reversal JE |
| (other common fields same as invoice) | | |

**Line items table: `sales_return_items`**

| Column | Type | Description |
|--------|------|-------------|
| `return_id` | bigint unsigned | FK |
| `invoice_item_id` | bigint unsigned | FK to original invoice item |
| `warehouse_id` | bigint unsigned | Warehouse to return goods to |
| `quantity` | decimal(15,3) | Quantity being returned (must ≤ original - already_returned) |
| `unit_price` | decimal(15,3) | Same as original invoice price |
| `unit_cost` | decimal(15,3) | Same cost as original invoice line (for COGS reversal) |
| (tax, discount fields) | | Copied from original |

**Posting Logic:**
1. Reverse Revenue JE: DR Revenue + Tax Payable, CR Accounts Receivable
2. Reverse COGS JE (if tracked): DR Inventory, CR COGS
3. Add stock back: Create Stock Receipt with `reference_type = 'return'`, `reference_number = return_number`
4. Update partner AR balance
5. Update original invoice: reduce amounts or create credit to be applied against future invoices

**Validation:**
- `return_quantity` per item ≤ `original_invoice_quantity - already_returned_quantity`
- Can only return against a `posted` invoice
- Cannot return more than what was paid (or create a credit balance)

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/sales/returns` | List. Filters: `status`, `partner_id`, `invoice_id`, `branch_id`, `date_from`, `date_to`, `search` |
| `POST` | `/api/sales/returns` | Create from invoice_id. Body: `{ "invoice_id", "date", "reason", "items": [{ "invoice_item_id", "quantity", "warehouse_id" }] }`. Auto-populates prices from original invoice |
| `GET` | `/api/sales/returns/{id}` | Get with items, invoice, partner, journal entries |
| `PUT` | `/api/sales/returns/{id}` | Update (draft only) |
| `DELETE` | `/api/sales/returns/{id}` | Soft-delete (draft only) |
| `POST` | `/api/sales/returns/{id}/submit-approval` | Submit for approval |
| `POST` | `/api/sales/returns/{id}/approve` | Approve |
| `POST` | `/api/sales/returns/{id}/reject` | Reject |
| `POST` | `/api/sales/returns/{id}/post` | Post — executes reversal logic |
| `POST` | `/api/sales/returns/{id}/cancel` | Cancel — reverses the reversal |
| `GET` | `/api/sales/returns/{id}/pdf` | Generate PDF |

---

### 4.6 Delivery Note

**Purpose**: Track physical shipment of goods. Optionally deducts stock (depending on `stock_deduction_point` setting).

**Database table: `sales_delivery_notes`**

| Column | Type | Description |
|--------|------|-------------|
| `delivery_number` | varchar(50) | Auto-generated (e.g. `DN-2026-0001`) |
| `order_id` | bigint unsigned | FK to Sales Order |
| `invoice_id` | bigint unsigned, nullable | FK if linked to invoice |
| `warehouse_id` | bigint unsigned | Source warehouse |
| `status` | enum | `draft`, `confirmed`, `shipped`, `delivered`, `cancelled` |
| `shipping_address` | text | Delivery address |
| `carrier_name` | varchar(255), nullable | Shipping carrier company |
| `tracking_number` | varchar(255), nullable | Shipment tracking number |
| `shipping_method` | varchar(100), nullable | Shipping method (ground, air, express, etc.) |
| `shipping_cost` | decimal(15,3), nullable | Shipping cost amount |
| `estimated_delivery` | date, nullable | Estimated delivery date |
| `shipped_at` | timestamp, nullable | Actual ship date |
| `delivered_at` | timestamp, nullable | Actual delivery date |
| `received_by` | varchar(255), nullable | Person who received the delivery |
| `notes` | text, nullable | Notes |
| `notes_ar` | text, nullable | Arabic notes |
| (common company/branch/partner/created_by fields) | | |

**Line items: `sales_delivery_note_items`**

| Column | Type | Description |
|--------|------|-------------|
| `delivery_note_id` | bigint unsigned | FK |
| `order_item_id` | bigint unsigned | FK to Sales Order item |
| `product_id` | bigint unsigned | FK |
| `product_variant_id` | bigint unsigned, nullable | FK |
| `unit_id` | bigint unsigned | FK |
| `quantity` | decimal(15,3) | Quantity to deliver |
| `batch_number` | varchar(100), nullable | Batch/lot number |
| `serial_numbers` | JSON, nullable | Array of serial numbers |
| `notes` | varchar(500), nullable | Line notes |

**Confirmation Logic (status → confirmed):**
- If `stock_deduction_point = 'delivery'`:
  - Create Stock Issue: `reference_type = 'sale'`, `reference_number = delivery_number`
  - Deduct stock from `warehouse_id`
- Update Sales Order: `delivered_quantity` per matching `order_item_id`
- Recalculate order `delivery_status` (pending/partial/complete)

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/sales/delivery-notes` | List. Filters: `status`, `partner_id`, `order_id`, `warehouse_id`, `branch_id`, `date_from`, `date_to`, `search` |
| `POST` | `/api/sales/delivery-notes` | Create. Usually from `POST /api/sales/orders/{id}/create-delivery`. Body includes `order_id`, `warehouse_id`, `items[]` |
| `GET` | `/api/sales/delivery-notes/{id}` | Get with items, order, partner |
| `PUT` | `/api/sales/delivery-notes/{id}` | Update (draft only) |
| `DELETE` | `/api/sales/delivery-notes/{id}` | Soft-delete (draft only) |
| `POST` | `/api/sales/delivery-notes/{id}/confirm` | Confirm — deducts stock if applicable |
| `POST` | `/api/sales/delivery-notes/{id}/ship` | Mark as shipped. Body: `{ "carrier_name", "tracking_number", "shipping_method", "shipped_at" }` |
| `POST` | `/api/sales/delivery-notes/{id}/deliver` | Mark as delivered. Body: `{ "delivered_at", "received_by" }` |
| `POST` | `/api/sales/delivery-notes/{id}/cancel` | Cancel — reverse stock if deducted |
| `GET` | `/api/sales/delivery-notes/{id}/pdf` | Generate PDF |

---

### 4.7 Sales Payment

**Purpose**: Record customer payments against invoices. Supports partial payments and multiple payment methods.

**Database table: `sales_payments`**

| Column | Type | Description |
|--------|------|-------------|
| `payment_number` | varchar(50) | Auto-generated (e.g. `SPAY-2026-0001`) |
| `invoice_id` | bigint unsigned | FK to Sales Invoice |
| `partner_id` | bigint unsigned | FK (copied from invoice for quick querying) |
| `date` | date | Payment date |
| `amount` | decimal(15,3) | Payment amount |
| `currency_id` | bigint unsigned | FK |
| `exchange_rate` | decimal(15,6) | Exchange rate at payment date |
| `payment_method` | enum | `cash`, `bank_transfer`, `check`, `credit_card`, `mixed` |
| `bank_account_id` | bigint unsigned, nullable | FK (if bank/check/card) |
| `check_number` | varchar(50), nullable | Check number |
| `check_date` | date, nullable | Check date |
| `check_bank` | varchar(255), nullable | Check issuing bank |
| `reference` | varchar(255), nullable | External reference number |
| `notes` | text, nullable | Notes |
| `notes_ar` | text, nullable | Arabic notes |
| `receiving_account_id` | bigint unsigned | GL account receiving the payment (Cash/Bank account) |
| `journal_entry_id` | bigint unsigned, nullable | FK to JE |
| `status` | enum | `draft`, `posted`, `cancelled` |
| `posted_at` | timestamp, nullable | |
| (common fields) | | |

**Payment Terms / Installment Schedule table: `sales_payment_schedules`**

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned | PK |
| `invoice_id` | bigint unsigned | FK |
| `installment_number` | integer | 1, 2, 3... |
| `due_date` | date | When this installment is due |
| `amount` | decimal(15,3) | Amount due for this installment |
| `amount_paid` | decimal(15,3) | How much has been paid |
| `status` | enum | `pending`, `partially_paid`, `paid`, `overdue` |

**Posting Logic:**
1. Create JE: DR `receiving_account_id` (Cash/Bank), CR `receivable_account_id` (AR)
2. Update invoice: `amount_paid += payment.amount`, `balance_due = total - amount_paid`
3. Update invoice status: if `balance_due = 0` → `paid`, elif `amount_paid > 0` → `partially_paid`
4. If installment schedule exists, update the relevant installment

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/sales/payments` | List. Filters: `status`, `partner_id`, `invoice_id`, `payment_method`, `branch_id`, `date_from`, `date_to`, `search` |
| `POST` | `/api/sales/payments` | Create payment. Body: `{ "invoice_id", "date", "amount", "payment_method", "bank_account_id", "check_number", "check_date", "receiving_account_id", "reference", "notes" }`. Validates: amount ≤ invoice.balance_due |
| `GET` | `/api/sales/payments/{id}` | Get with invoice, journal entry |
| `PUT` | `/api/sales/payments/{id}` | Update (draft only) |
| `DELETE` | `/api/sales/payments/{id}` | Delete (draft only) |
| `POST` | `/api/sales/payments/{id}/post` | Post — creates JE, updates invoice |
| `POST` | `/api/sales/payments/{id}/cancel` | Cancel — reverses JE, reverses invoice payment |

---

### 4.8 Approval Workflow (Shared between Sales & Purchases)

**Purpose**: Configurable document approval system supporting none, simple, or multi-level amount-based approval.

**Database table: `approval_workflows`**

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned | PK |
| `company_id` | bigint unsigned | FK |
| `module` | enum | `sales`, `purchases` |
| `document_type` | enum | `quotation`, `order`, `invoice`, `return`, `delivery_note`, `purchase_request`, `purchase_order`, `bill`, `purchase_return`, `grn` |
| `is_active` | boolean | Whether this workflow is active |
| `created_by` | bigint unsigned | FK |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |

**Approval levels table: `approval_workflow_levels`**

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned | PK |
| `workflow_id` | bigint unsigned | FK → `approval_workflows` |
| `level_order` | integer | 1, 2, 3... (sequence of approval) |
| `min_amount` | decimal(15,3) | Minimum document amount for this level to apply |
| `max_amount` | decimal(15,3), nullable | Maximum (null = unlimited) |
| `approver_type` | enum | `role`, `user` |
| `approver_role_id` | bigint unsigned, nullable | FK to roles (any user with this role can approve) |
| `approver_user_id` | bigint unsigned, nullable | FK to specific user |
| `auto_approve` | boolean | If true, this level is automatically approved |

**Approval log table: `approval_logs`**

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned | PK |
| `company_id` | bigint unsigned | FK |
| `document_type` | varchar(50) | Polymorphic type (e.g. `sales_quotation`, `purchase_order`) |
| `document_id` | bigint unsigned | Polymorphic ID |
| `level` | integer | Which approval level |
| `status` | enum | `pending`, `approved`, `rejected` |
| `approver_id` | bigint unsigned, nullable | User who acted |
| `comments` | text, nullable | Approval/rejection comments |
| `acted_at` | timestamp, nullable | When the action was taken |
| `created_at` | timestamp | When the pending record was created |

**Workflow Logic:**
1. Document submitted for approval → check if active workflow exists for this module + document_type
2. If no workflow → auto-approve immediately
3. If workflow exists → determine which levels apply based on document total
4. Create `approval_logs` entries with `status = pending` for each required level
5. Notify users with matching role/user
6. When user approves → mark their level as approved → check if more levels pending
7. If all levels approved → change document status to `approved`
8. If any level rejected → change document status to `rejected`, notify creator

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/approval-workflows` | List all workflows. Filter: `module`, `document_type`, `is_active` |
| `POST` | `/api/approval-workflows` | Create workflow with levels. Body: `{ "module", "document_type", "is_active", "levels": [{ "level_order", "min_amount", "max_amount", "approver_type", "approver_role_id", "approver_user_id", "auto_approve" }] }` |
| `GET` | `/api/approval-workflows/{id}` | Get with levels |
| `PUT` | `/api/approval-workflows/{id}` | Update with levels (replace all levels) |
| `DELETE` | `/api/approval-workflows/{id}` | Delete |
| `GET` | `/api/approval-logs` | List pending approvals for current user. Filters: `module`, `document_type`, `status` |
| `POST` | `/api/approval-logs/{id}/approve` | Approve. Body: `{ "comments": "..." }` |
| `POST` | `/api/approval-logs/{id}/reject` | Reject. Body: `{ "comments": "..." }` |

---

### 4.9 Sales Commission

**Purpose**: Track salesperson commissions calculated from posted invoices.

**Database table: `sales_commissions`**

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned | PK |
| `company_id` | bigint unsigned | FK |
| `salesperson_id` | bigint unsigned | FK to users |
| `invoice_id` | bigint unsigned | FK to sales_invoices |
| `invoice_number` | varchar(50) | Snapshot for quick display |
| `invoice_date` | date | Snapshot |
| `partner_id` | bigint unsigned | Customer FK |
| `invoice_total` | decimal(15,3) | Total invoice amount |
| `commission_base` | decimal(15,3) | Amount commission is calculated on (may exclude tax, discount) |
| `commission_rate` | decimal(8,4) | Applied rate (percentage) |
| `commission_amount` | decimal(15,3) | Calculated commission |
| `status` | enum | `pending`, `approved`, `paid`, `cancelled` |
| `paid_date` | date, nullable | When paid to salesperson |
| `paid_via` | varchar(100), nullable | How it was paid |
| `notes` | text, nullable | |

**Commission rules table: `sales_commission_rules`**

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned | PK |
| `company_id` | bigint unsigned | FK |
| `salesperson_id` | bigint unsigned, nullable | Null = applies to all salespeople |
| `product_category_id` | bigint unsigned, nullable | Null = applies to all products |
| `product_id` | bigint unsigned, nullable | Specific product (null = all) |
| `commission_type` | enum | `percentage`, `fixed_per_unit`, `fixed_per_invoice` |
| `rate` | decimal(15,3) | Rate value |
| `min_invoice_amount` | decimal(15,3), nullable | Minimum invoice amount to qualify |
| `is_active` | boolean | Whether rule is active |
| `priority` | integer | Higher priority rules override lower ones |

**Calculation Logic:**
1. On invoice posting → if `commission_enabled` and `salesperson_id` is set
2. For each invoice line, find matching commission rule:
   - Priority order: specific product > specific category > specific salesperson > default
3. Calculate commission per line, sum to total
4. Create `sales_commissions` record with `status = pending`

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/sales/commissions` | List. Filters: `salesperson_id`, `partner_id`, `status`, `date_from`, `date_to` |
| `GET` | `/api/sales/commissions/summary` | Summary by salesperson: total pending, approved, paid |
| `POST` | `/api/sales/commissions/{id}/approve` | Approve commission |
| `POST` | `/api/sales/commissions/{id}/mark-paid` | Mark as paid. Body: `{ "paid_date", "paid_via", "notes" }` |
| `POST` | `/api/sales/commissions/bulk-approve` | Bulk approve. Body: `{ "ids": [1,2,3] }` |
| `POST` | `/api/sales/commissions/bulk-mark-paid` | Bulk mark paid |
| `GET` | `/api/sales/commission-rules` | List rules |
| `POST` | `/api/sales/commission-rules` | Create rule |
| `PUT` | `/api/sales/commission-rules/{id}` | Update rule |
| `DELETE` | `/api/sales/commission-rules/{id}` | Delete rule |

---

### 4.10 Tiered Pricing

**Purpose**: Quantity-based pricing tiers for products, applied automatically on sales documents.

**Database table: `product_pricing_tiers`**

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned | PK |
| `company_id` | bigint unsigned | FK |
| `product_id` | bigint unsigned | FK → products |
| `min_quantity` | decimal(15,3) | Minimum quantity for this tier |
| `max_quantity` | decimal(15,3), nullable | Maximum quantity (null = unlimited) |
| `price_type` | enum | `fixed_price` (override unit price), `percentage_discount` (% off sale price) |
| `value` | decimal(15,3) | The fixed price or discount percentage |
| `is_active` | boolean | Active flag |

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/core/products/{id}/pricing-tiers` | List tiers for a product |
| `POST` | `/api/core/products/{id}/pricing-tiers` | Create tier |
| `PUT` | `/api/core/products/{id}/pricing-tiers/{tierId}` | Update tier |
| `DELETE` | `/api/core/products/{id}/pricing-tiers/{tierId}` | Delete tier |

---

### 4.11 Sales Reports

**Endpoints & Details:**

| Endpoint | Description | Filters |
|----------|-------------|---------|
| `GET /api/sales/reports/summary` | Overview: total quotations, orders, invoices, returns, net sales, collected amount, outstanding | `date_from`, `date_to`, `branch_id`, `currency_id` |
| `GET /api/sales/reports/by-product` | Revenue, quantity, returns per product. Sortable by revenue, qty, profit | `date_from`, `date_to`, `branch_id`, `warehouse_id`, `category_id`, `limit` |
| `GET /api/sales/reports/by-customer` | Revenue per customer. Top customers ranking | `date_from`, `date_to`, `branch_id`, `salesperson_id`, `limit` |
| `GET /api/sales/reports/by-salesperson` | Revenue, commissions, invoice count per salesperson | `date_from`, `date_to`, `branch_id` |
| `GET /api/sales/reports/by-category` | Revenue breakdown by product category | `date_from`, `date_to`, `branch_id` |
| `GET /api/sales/reports/trends` | Daily/weekly/monthly sales over time (for charts) | `date_from`, `date_to`, `branch_id`, `group_by` (day/week/month) |
| `GET /api/sales/reports/overdue-invoices` | Unpaid invoices past due date with aging buckets | `as_of_date`, `branch_id`, `partner_id` |
| `GET /api/sales/reports/payment-collection` | Payments received breakdown by period and method | `date_from`, `date_to`, `branch_id`, `payment_method` |
| `GET /api/sales/reports/conversion-funnel` | Quotation → Order → Invoice conversion rates | `date_from`, `date_to`, `branch_id`, `salesperson_id` |
| `GET /api/sales/reports/profit-by-product` | Gross profit per product (revenue - COGS) | `date_from`, `date_to`, `branch_id`, `warehouse_id`, `category_id` |

---

### 4.12 Sales PDF Generation

**Templates needed:**
1. **Quotation/Proforma** — company header, customer info, items table, validity date, payment terms, signature line
2. **Sales Order** — same as quotation + expected delivery date
3. **Sales Invoice** — same + due date, payment info, tax breakdown, legal disclaimers
4. **Delivery Note** — company header, customer info, shipping address, items (qty only, no prices), carrier info, receiving signature line
5. **Credit Note** — same as invoice + original invoice reference, reason for return

**All templates must support:**
- Arabic (RTL) and English (LTR) layouts
- Company logo
- Customizable footer text
- Tax number display (per legal requirements)
- QR code for electronic invoicing (future)

---

## 5. Sales Module — Frontend Specification

### 5.1 Angular Feature Structure

```
src/app/features/sales/
├── sales.routes.ts                    # Lazy-loaded routes
├── sales-dashboard/
│   ├── sales-dashboard.component.ts
│   ├── sales-dashboard.component.html
│   └── sales-dashboard.component.scss
├── quotations/
│   ├── quotation-list/
│   │   ├── quotation-list.component.ts
│   │   ├── quotation-list.component.html
│   │   └── quotation-list.component.scss
│   └── quotation-form/
│       ├── quotation-form.component.ts    # Used for both create & edit
│       ├── quotation-form.component.html
│       └── quotation-form.component.scss
├── orders/
│   ├── order-list/
│   ├── order-form/
│   └── order-detail/                     # View with delivery & invoice status
├── invoices/
│   ├── invoice-list/
│   ├── invoice-form/
│   └── invoice-detail/                   # View with payments & JE links
├── returns/
│   ├── return-list/
│   └── return-form/
├── delivery-notes/
│   ├── delivery-list/
│   └── delivery-form/
├── payments/
│   ├── payment-list/
│   └── payment-form/
├── commissions/
│   ├── commission-list/
│   └── commission-rules/
└── settings/
    └── sales-settings.component.ts       # Sales configuration page
```

### 5.2 NgRx Store Structure

```
src/app/core/store/sales/
├── quotations/
│   ├── quotation.actions.ts
│   ├── quotation.effects.ts
│   ├── quotation.reducer.ts
│   └── quotation.selectors.ts
├── orders/
│   ├── order.actions.ts
│   ├── order.effects.ts
│   ├── order.reducer.ts
│   └── order.selectors.ts
├── invoices/
│   ├── invoice.actions.ts
│   ├── invoice.effects.ts
│   ├── invoice.reducer.ts
│   └── invoice.selectors.ts
├── returns/
├── delivery-notes/
├── payments/
└── commissions/
```

### 5.3 Services

```
src/app/core/services/
├── sales-quotation.service.ts
├── sales-order.service.ts
├── sales-invoice.service.ts
├── sales-return.service.ts
├── sales-delivery-note.service.ts
├── sales-payment.service.ts
├── sales-commission.service.ts
├── sales-settings.service.ts
├── sales-report.service.ts
└── approval-workflow.service.ts         # Shared with Purchases
```

### 5.4 Key UI Components

#### 5.4.1 Document Form Component (shared pattern for all document types)
- **Header section**: Partner selector (p-select with search), date picker, currency selector with exchange rate display, salesperson selector, cost center selector, warehouse selector
- **Line items section**: Dynamic table with add/remove rows
  - Product selector with search (shows name, SKU, barcode)
  - Auto-fill: unit price from product sale_price, unit from product base_unit
  - Variant selector (if product has variants)
  - Unit selector (from product's allowed units with conversion)
  - Quantity input (triggers tiered pricing check)
  - Unit price (editable, warns if below min_sale_price)
  - Line discount (toggle: percentage/fixed)
  - Tax selector (per line, or inherits from document header)
  - Line total (auto-calculated)
  - Drag-and-drop reordering
- **Totals section**: Subtotal, document discount, tax breakdown, grand total (all auto-calculated, RTL-aware)
- **Notes section**: Tabs for English/Arabic notes, internal notes
- **Actions**: Save Draft, Submit for Approval, Approve, Post, Cancel, Print PDF, Duplicate

#### 5.4.2 Document List Component (shared pattern)
- PrimeNG DataTable with:
  - Column sorting (number, date, partner, total, status)
  - Status filter (multi-select chips)
  - Date range filter
  - Partner search filter
  - Quick actions per row: View, Edit, Duplicate, PDF, status change actions
  - Status badge with color coding (draft=gray, pending=yellow, approved=blue, posted=green, cancelled=red)
  - Export to Excel/PDF
- RTL-aware layout

#### 5.4.3 Sales Dashboard
- **KPI Cards**: Total sales this month, outstanding invoices, overdue amount, quotation conversion rate, top salesperson
- **Charts**: Sales trend (line chart, last 12 months), Sales by category (doughnut), Top 10 customers (bar), Payment collection trend (bar)
- **Quick Actions**: New Quotation, New Invoice, Record Payment
- **Recent Activity**: Latest quotations, orders, invoices

#### 5.4.4 Sales Settings Page
- Form-based settings with sections:
  - **General**: Default warehouse, default tax, default cost center, price includes tax toggle
  - **Stock**: Stock deduction point toggle (invoice/delivery), enable delivery notes toggle
  - **Quotations**: Validity days, allow below min price toggle
  - **Approval**: Workflow mode selector, threshold configuration table
  - **Commissions**: Enable toggle, default rate, commission rules management (CRUD table)
  - **Accounts**: Revenue account, COGS account, Receivable account, Tax payable account, Discount account

### 5.5 Translation Keys

All new keys under `SALES` namespace in both `en.json` and `ar.json`:

```
SALES.MODULE_NAME / وحدة المبيعات
SALES.DASHBOARD / لوحة المبيعات
SALES.QUOTATIONS / عروض الأسعار
SALES.QUOTATION / عرض سعر
SALES.NEW_QUOTATION / عرض سعر جديد
SALES.EDIT_QUOTATION / تعديل عرض السعر
SALES.ORDERS / أوامر البيع
SALES.ORDER / أمر بيع
SALES.NEW_ORDER / أمر بيع جديد
SALES.INVOICES / فواتير المبيعات
SALES.INVOICE / فاتورة مبيعات
SALES.NEW_INVOICE / فاتورة جديدة
SALES.RETURNS / مرتجعات المبيعات
SALES.RETURN / مرتجع مبيعات
SALES.CREDIT_NOTE / إشعار دائن
SALES.DELIVERY_NOTES / إشعارات التسليم
SALES.DELIVERY_NOTE / إشعار تسليم
SALES.PAYMENTS / المدفوعات
SALES.PAYMENT / دفعة
SALES.RECORD_PAYMENT / تسجيل دفعة
SALES.COMMISSIONS / العمولات
SALES.SETTINGS / إعدادات المبيعات
SALES.REPORTS / تقارير المبيعات
SALES.CONVERT_TO_ORDER / تحويل لأمر بيع
SALES.CONVERT_TO_INVOICE / تحويل لفاتورة
SALES.SUBMIT_APPROVAL / إرسال للموافقة
SALES.POST_INVOICE / ترحيل الفاتورة
SALES.BALANCE_DUE / الرصيد المستحق
SALES.AMOUNT_PAID / المبلغ المدفوع
SALES.OVERDUE / متأخر السداد
SALES.PAYMENT_TERMS / شروط الدفع
SALES.DUE_DATE / تاريخ الاستحقاق
SALES.VALID_UNTIL / صالح حتى
SALES.SALESPERSON / مندوب المبيعات
SALES.SHIPPING / الشحن
SALES.CARRIER / شركة الشحن
SALES.TRACKING_NUMBER / رقم التتبع
SALES.LINE_DISCOUNT / خصم السطر
SALES.DOCUMENT_DISCOUNT / خصم المستند
SALES.SUBTOTAL / المجموع الفرعي
SALES.TAX / الضريبة
SALES.GRAND_TOTAL / الإجمالي
SALES.CONVERSION_FUNNEL / قمع التحويل
SALES.TOP_CUSTOMERS / أفضل العملاء
SALES.NET_SALES / صافي المبيعات
(+ ~50 more for reports, status labels, etc.)
```

---

## 6. Purchases Module — Backend Specification

### 6.1 Purchases Settings

**Same pattern as Sales Settings, with `purchases.` prefix.**

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `purchases.grn_mode` | enum | `direct` | GRN mode: `direct` (stock on bill), `grn` (separate GRN), `grn_quality` (GRN + quality check) |
| `purchases.approval_workflow` | enum | `none` | Same as Sales |
| `purchases.approval_thresholds` | JSON | `[]` | Same as Sales |
| `purchases.default_payment_terms_days` | integer | `0` | |
| `purchases.default_tax_id` | FK/null | `null` | |
| `purchases.default_cost_center_id` | FK/null | `null` | |
| `purchases.default_warehouse_id` | FK/null | `null` | |
| `purchases.price_includes_tax` | boolean | `false` | |
| `purchases.enable_purchase_requests` | boolean | `true` | Enable/disable PR feature |
| `purchases.enable_supplier_price_lists` | boolean | `false` | Enable supplier price list management |
| `purchases.enable_supplier_comparison` | boolean | `false` | Enable price comparison across suppliers |
| `purchases.inventory_account_id` | FK | required | Inventory GL account |
| `purchases.expense_account_id` | FK | required | Default expense GL account (for non-inventory items) |
| `purchases.payable_account_id` | FK | required | Accounts Payable GL account |
| `purchases.tax_receivable_account_id` | FK | required | Input Tax (Tax Receivable) GL account |
| `purchases.discount_account_id` | FK/null | `null` | Purchase Discount GL account |

**Endpoints:**
- `GET /api/purchases/settings`
- `PUT /api/purchases/settings`

---

### 6.2 Purchase Request (PR)

**Purpose**: Internal request from a department to purchase items. Requires approval before becoming a PO.

**Database table: `purchase_requests`**

| Column | Type | Description |
|--------|------|-------------|
| `request_number` | varchar(50) | Auto-generated (e.g. `PR-2026-0001`) |
| `date` | date | Request date |
| `requested_by` | bigint unsigned | User who created the request |
| `department` | varchar(255), nullable | Requesting department |
| `department_ar` | varchar(255), nullable | Arabic department name |
| `needed_by` | date, nullable | Date items are needed |
| `priority` | enum | `low`, `normal`, `high`, `urgent` |
| `reason` | text | Justification for purchase |
| `reason_ar` | text, nullable | Arabic justification |
| `status` | enum | `draft`, `pending_approval`, `approved`, `rejected`, `converted`, `cancelled` |
| `notes` | text, nullable | |
| `notes_ar` | text, nullable | |
| (common company/branch/cost_center fields) | | |

**Line items: `purchase_request_items`**

| Column | Type | Description |
|--------|------|-------------|
| `request_id` | bigint unsigned | FK |
| `sort_order` | integer | |
| `product_id` | bigint unsigned | FK |
| `product_variant_id` | bigint unsigned, nullable | |
| `unit_id` | bigint unsigned | FK |
| `description` | varchar(500), nullable | |
| `description_ar` | varchar(500), nullable | |
| `quantity` | decimal(15,3) | Requested quantity |
| `estimated_price` | decimal(15,3), nullable | Estimated unit cost |
| `estimated_total` | decimal(15,3), nullable | Calculated |
| `preferred_supplier_id` | bigint unsigned, nullable | Suggested supplier |
| `notes` | varchar(500), nullable | |

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/purchases/requests` | List. Filters: `status`, `requested_by`, `priority`, `branch_id`, `date_from`, `date_to`, `search` |
| `POST` | `/api/purchases/requests` | Create draft PR |
| `GET` | `/api/purchases/requests/{id}` | Get with items |
| `PUT` | `/api/purchases/requests/{id}` | Update (draft/rejected only) |
| `DELETE` | `/api/purchases/requests/{id}` | Delete (draft only) |
| `POST` | `/api/purchases/requests/{id}/submit-approval` | Submit |
| `POST` | `/api/purchases/requests/{id}/approve` | Approve |
| `POST` | `/api/purchases/requests/{id}/reject` | Reject |
| `POST` | `/api/purchases/requests/{id}/convert-to-order` | Convert to Purchase Order. Body: `{ "supplier_id": 1 }` — creates PO with items from PR |
| `POST` | `/api/purchases/requests/{id}/cancel` | Cancel |

---

### 6.3 Purchase Order (PO)

**Purpose**: Official order sent to supplier. Mirrors Sales Order structure.

**Database table: `purchase_orders`**

Same structure as `sales_orders` but for purchasing:

| Column | Type | Description |
|--------|------|-------------|
| `order_number` | varchar(50) | Auto-generated (e.g. `PO-2026-0001`) |
| `partner_id` | bigint unsigned | Supplier FK |
| `request_id` | bigint unsigned, nullable | FK if from Purchase Request |
| `expected_delivery_date` | date, nullable | When supplier should deliver |
| `status` | enum | `draft`, `pending_approval`, `approved`, `confirmed`, `sent`, `partially_received`, `fully_received`, `partially_billed`, `fully_billed`, `closed`, `cancelled` |
| `receive_status` | enum | `pending`, `partial`, `complete` |
| `bill_status` | enum | `pending`, `partial`, `complete` |
| (all other columns mirror sales_orders: currency, exchange_rate, cost_center, discount, tax, totals, etc.) | | |

**Line items: `purchase_order_items`**

Same as `sales_order_items` but with:

| Column | Type | Description |
|--------|------|-------------|
| `order_id` | bigint unsigned | FK |
| `received_quantity` | decimal(15,3) | From GRNs |
| `billed_quantity` | decimal(15,3) | From Bills |
| `remaining_receive_qty` | virtual | `quantity - received_quantity` |
| `remaining_bill_qty` | virtual | `quantity - billed_quantity` |

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/purchases/orders` | List. Filters: `status`, `receive_status`, `bill_status`, `partner_id`, `branch_id`, `date_from`, `date_to`, `request_id`, `search` |
| `POST` | `/api/purchases/orders` | Create. Optional `request_id` to populate from PR. If `enable_supplier_price_lists`: auto-fill prices from supplier's price list |
| `GET` | `/api/purchases/orders/{id}` | Get with items, GRNs, bills |
| `PUT` | `/api/purchases/orders/{id}` | Update (draft/rejected only) |
| `DELETE` | `/api/purchases/orders/{id}` | Delete (draft only) |
| `POST` | `/api/purchases/orders/{id}/submit-approval` | Submit for approval |
| `POST` | `/api/purchases/orders/{id}/approve` | Approve |
| `POST` | `/api/purchases/orders/{id}/reject` | Reject |
| `POST` | `/api/purchases/orders/{id}/confirm` | Confirm and optionally mark as sent to supplier |
| `POST` | `/api/purchases/orders/{id}/send` | Mark as sent to supplier |
| `POST` | `/api/purchases/orders/{id}/create-grn` | Create Goods Receipt. Body: `{ "items": [{ "order_item_id", "quantity", "warehouse_id", "unit_cost", "batch_number", "expiry_date" }] }` |
| `POST` | `/api/purchases/orders/{id}/create-bill` | Create Purchase Invoice/Bill. Body: `{ "items": [{ "order_item_id", "quantity" }] }` |
| `POST` | `/api/purchases/orders/{id}/cancel` | Cancel |
| `POST` | `/api/purchases/orders/{id}/close` | Close early |
| `GET` | `/api/purchases/orders/{id}/pdf` | Generate PDF |
| `POST` | `/api/purchases/orders/{id}/duplicate` | Duplicate |

---

### 6.4 Goods Receipt Note (GRN)

**Purpose**: Records physical receipt of goods from suppliers. Links to PO. Adds stock to warehouse.

**Database table: `purchase_grns`**

| Column | Type | Description |
|--------|------|-------------|
| `grn_number` | varchar(50) | Auto-generated (e.g. `GRN-2026-0001`) — uses same sequence as inventory receipts or a separate one |
| `order_id` | bigint unsigned, nullable | FK to Purchase Order |
| `bill_id` | bigint unsigned, nullable | FK to Purchase Bill (if direct bill) |
| `warehouse_id` | bigint unsigned | Destination warehouse |
| `partner_id` | bigint unsigned | Supplier FK |
| `date` | date | Receipt date |
| `status` | enum | `draft`, `pending_quality`, `quality_approved`, `quality_rejected`, `approved`, `cancelled` |
| `quality_notes` | text, nullable | Quality inspector notes |
| `quality_checked_by` | bigint unsigned, nullable | User who did quality check |
| `quality_checked_at` | timestamp, nullable | |
| `notes` | text, nullable | |
| `notes_ar` | text, nullable | |
| (common fields) | | |

**Line items: `purchase_grn_items`**

| Column | Type | Description |
|--------|------|-------------|
| `grn_id` | bigint unsigned | FK |
| `order_item_id` | bigint unsigned, nullable | FK to PO item |
| `product_id` | bigint unsigned | FK |
| `product_variant_id` | bigint unsigned, nullable | FK |
| `unit_id` | bigint unsigned | FK |
| `quantity` | decimal(15,3) | Received quantity |
| `accepted_quantity` | decimal(15,3) | After quality check (may be less than received) |
| `rejected_quantity` | decimal(15,3) | Failed quality check |
| `unit_cost` | decimal(15,3) | Cost per unit from PO |
| `total_cost` | decimal(15,3) | Calculated |
| `batch_number` | varchar(100), nullable | |
| `expiry_date` | date, nullable | |
| `serial_numbers` | JSON, nullable | |
| `rejection_reason` | varchar(500), nullable | Why items were rejected |

**Approval Logic:**
- If `grn_mode = 'direct'`: GRN not used — stock added on bill approval
- If `grn_mode = 'grn'`: On GRN approval → create Inventory Receipt (`reference_type = 'purchase'`) → increase stock
- If `grn_mode = 'grn_quality'`: GRN goes draft → pending_quality → quality_approved/rejected → approved → stock added
  - Quality check: inspector reviews, marks accepted/rejected per line
  - Only `accepted_quantity` is added to stock

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/purchases/grns` | List. Filters: `status`, `partner_id`, `order_id`, `warehouse_id`, `branch_id`, `date_from`, `date_to`, `search` |
| `POST` | `/api/purchases/grns` | Create. Usually from `POST /api/purchases/orders/{id}/create-grn` |
| `GET` | `/api/purchases/grns/{id}` | Get with items, order, partner |
| `PUT` | `/api/purchases/grns/{id}` | Update (draft only) |
| `DELETE` | `/api/purchases/grns/{id}` | Delete (draft only) |
| `POST` | `/api/purchases/grns/{id}/submit-quality` | Submit for quality check (status → pending_quality) |
| `POST` | `/api/purchases/grns/{id}/quality-check` | Record quality results. Body: `{ "items": [{ "grn_item_id", "accepted_quantity", "rejected_quantity", "rejection_reason" }], "quality_notes" }` |
| `POST` | `/api/purchases/grns/{id}/approve` | Approve — adds stock |
| `POST` | `/api/purchases/grns/{id}/cancel` | Cancel — reverse stock |

---

### 6.5 Purchase Invoice / Bill

**Purpose**: Records the supplier's invoice for accounting and payment tracking.

**Database table: `purchase_bills`**

Mirrors `sales_invoices` structure:

| Column | Type | Description |
|--------|------|-------------|
| `bill_number` | varchar(50) | Auto-generated (e.g. `BILL-2026-0001`) |
| `supplier_invoice_number` | varchar(100), nullable | The supplier's own invoice number |
| `supplier_invoice_date` | date, nullable | Date on supplier's invoice |
| `order_id` | bigint unsigned, nullable | FK to PO |
| `due_date` | date | Payment due date |
| `status` | enum | `draft`, `pending_approval`, `approved`, `posted`, `partially_paid`, `paid`, `overdue`, `cancelled` |
| `amount_paid` | decimal(15,3) | |
| `balance_due` | decimal(15,3) | |
| `journal_entry_id` | bigint unsigned, nullable | |
| (all other fields mirror sales_invoices: partner, currency, discount, tax, etc.) | | |

**Line items: `purchase_bill_items`**

| Column | Type | Description |
|--------|------|-------------|
| `bill_id` | bigint unsigned | FK |
| `order_item_id` | bigint unsigned, nullable | FK to PO item |
| `warehouse_id` | bigint unsigned, nullable | Destination warehouse (if grn_mode = direct) |
| `product_id` | bigint unsigned | FK |
| `product_variant_id` | bigint unsigned, nullable | |
| `unit_id` | bigint unsigned | |
| `quantity` | decimal(15,3) | |
| `unit_cost` | decimal(15,3) | Cost per unit |
| (discount, tax fields same as sales) | | |

**Posting Logic:**
1. **Journal Entry:**
   - For inventory products: DR `purchases.inventory_account_id` (or product's inventory account)
   - For non-inventory/services: DR `purchases.expense_account_id` (or line-specific expense account)
   - DR `purchases.tax_receivable_account_id` (Input Tax)
   - CR `purchases.payable_account_id` (Accounts Payable)
   - Partner on payable line

2. **Stock (if `grn_mode = 'direct'`):**
   - Create Inventory Receipt for each line with inventory tracking
   - `reference_type = 'purchase'`, `reference_number = bill_number`
   - Add stock to `warehouse_id`

3. **AP Update:** Through journal entry payable lines

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/purchases/bills` | List. Filters mirror sales invoices + `supplier_invoice_number` |
| `POST` | `/api/purchases/bills` | Create. Optional `order_id`. If no PO, this is a direct bill |
| `GET` | `/api/purchases/bills/{id}` | Get with items, payments, JE, PO |
| `PUT` | `/api/purchases/bills/{id}` | Update (draft only) |
| `DELETE` | `/api/purchases/bills/{id}` | Delete (draft only) |
| `POST` | `/api/purchases/bills/{id}/submit-approval` | Submit |
| `POST` | `/api/purchases/bills/{id}/approve` | Approve |
| `POST` | `/api/purchases/bills/{id}/reject` | Reject |
| `POST` | `/api/purchases/bills/{id}/post` | Post — creates JE, adds stock if direct mode |
| `POST` | `/api/purchases/bills/{id}/cancel` | Cancel — reverses JE and stock |
| `POST` | `/api/purchases/bills/{id}/record-payment` | Quick payment |
| `GET` | `/api/purchases/bills/{id}/payments` | List payments |
| `GET` | `/api/purchases/bills/{id}/pdf` | Generate PDF |

---

### 6.6 Purchase Return / Debit Note

**Purpose**: Return goods to supplier. Mirrors Sales Return.

**Database table: `purchase_returns`**

Same structure as `sales_returns` but:

| Column | Type | Description |
|--------|------|-------------|
| `return_number` | varchar(50) | Auto-generated (e.g. `PDN-2026-0001` — Purchase Debit Note) |
| `bill_id` | bigint unsigned | FK to original Purchase Bill |
| (all other fields mirror sales_returns) | | |

**Posting Logic:**
1. Reverse JE: DR Payable, CR Inventory/Expense + Tax Receivable
2. Deduct stock: Create Stock Issue with `reference_type = 'return'`
3. Update partner AP balance

**API Endpoints:** Mirror `sales/returns` but under `/api/purchases/returns`

---

### 6.7 Purchase Payment

**Purpose**: Record payments to suppliers. Mirrors Sales Payment.

**Database table: `purchase_payments`**

Same structure as `sales_payments` but with `paying_account_id` instead of `receiving_account_id`.

**Posting Logic:**
1. JE: DR Payable (AP), CR Cash/Bank (paying_account)
2. Update bill: amount_paid, balance_due, status

**API Endpoints:** Mirror `sales/payments` but under `/api/purchases/payments`

---

### 6.8 Supplier Price Lists

**Purpose**: Store and manage supplier-specific pricing for products.

**Database table: `supplier_price_lists`**

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned | PK |
| `company_id` | bigint unsigned | FK |
| `partner_id` | bigint unsigned | Supplier FK |
| `product_id` | bigint unsigned | FK |
| `product_variant_id` | bigint unsigned, nullable | FK |
| `unit_id` | bigint unsigned | FK |
| `price` | decimal(15,3) | Supplier's price |
| `currency_id` | bigint unsigned | Currency of this price |
| `min_quantity` | decimal(15,3), nullable | Minimum order quantity for this price |
| `lead_time_days` | integer, nullable | Delivery lead time from this supplier |
| `valid_from` | date, nullable | Price validity start |
| `valid_until` | date, nullable | Price validity end |
| `notes` | text, nullable | |
| `is_active` | boolean | |
| `last_updated_at` | timestamp | When this price was last confirmed |

**API Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/purchases/supplier-prices` | List all. Filters: `partner_id`, `product_id`, `is_active` |
| `POST` | `/api/purchases/supplier-prices` | Create |
| `PUT` | `/api/purchases/supplier-prices/{id}` | Update |
| `DELETE` | `/api/purchases/supplier-prices/{id}` | Delete |
| `GET` | `/api/purchases/supplier-prices/compare` | Compare suppliers for a product. Query: `product_id` (required). Returns all suppliers' prices for this product, sorted by price |
| `POST` | `/api/purchases/supplier-prices/bulk-import` | Import from CSV/Excel |

---

### 6.9 Purchase Reports

Mirror Sales Reports under `/api/purchases/reports/`:
- `summary` — Total POs, Bills, Returns, Outstanding payables
- `by-product` — Quantity purchased, cost per product
- `by-supplier` — Spend per supplier, top suppliers
- `by-category` — Spend by product category
- `trends` — Monthly/weekly purchase trends
- `overdue-bills` — Unpaid bills past due date
- `payment-outflow` — Payments sent by period and method
- `price-variance` — Actual vs expected prices (PO price vs Bill price)
- `supplier-performance` — On-time delivery rate, quality rejection rate per supplier

---

## 7. Purchases Module — Frontend Specification

### 7.1 Angular Feature Structure

```
src/app/features/purchases/
├── purchases.routes.ts
├── purchases-dashboard/
├── requests/                          # Purchase Requests
│   ├── request-list/
│   └── request-form/
├── orders/                            # Purchase Orders
│   ├── order-list/
│   ├── order-form/
│   └── order-detail/
├── grns/                              # Goods Receipt Notes
│   ├── grn-list/
│   └── grn-form/
├── bills/                             # Purchase Invoices/Bills
│   ├── bill-list/
│   ├── bill-form/
│   └── bill-detail/
├── returns/                           # Purchase Returns
│   ├── return-list/
│   └── return-form/
├── payments/                          # Purchase Payments
│   ├── payment-list/
│   └── payment-form/
├── supplier-prices/                   # Supplier Price Lists
│   ├── price-list/
│   └── price-comparison/
└── settings/
    └── purchase-settings.component.ts
```

### 7.2 NgRx Store, Services, UI Components

Mirror Sales module structure. See Sales Frontend specification above — same patterns apply:
- NgRx store for each entity (requests, orders, grns, bills, returns, payments, supplier-prices)
- Services for each API endpoint group
- Document form component reuses same line-item editing pattern
- Document list component reuses same DataTable pattern
- Purchase Dashboard mirrors Sales Dashboard with purchasing KPIs

### 7.3 Unique Purchase UI Features

- **Purchase Request Form**: Simpler than PO (no pricing required, just products and quantities). Priority selector. Reason field. Preferred supplier per line
- **GRN Form**: Shows PO items with quantities to receive. Quality check section (if enabled): accepted/rejected/reason per line
- **Supplier Price Comparison**: Side-by-side table showing all suppliers' prices for a product, with delivery time and minimum quantity. Highlight best price. One-click "Select Supplier" to create PO
- **Direct Bill**: Bill form without PO reference. Includes warehouse selector per line for stock receipt

---

## 8. Shared Components

### 8.1 Approval Workflow Manager
- **Settings UI**: Workflows CRUD table with levels editor (inline editing)
- **Pending Approvals Widget**: Show on dashboard — list of documents waiting for current user's approval with Approve/Reject actions
- **Approval Timeline**: On document detail page — visual timeline showing approval progress

### 8.2 Document Line Items Editor
- Reusable Angular component for the line-items table used across all document types
- Inputs: product list, unit list, tax list, line items array
- Outputs: line changes, totals recalculated
- Features: add/remove/reorder rows, auto-calculate per line and totals

### 8.3 PDF Viewer/Downloader
- Shared component for previewing and downloading PDFs
- Opens in a dialog or new tab

---

## 9. Database Schema Design

### New Tables Summary

**Sales (10 tables):**
1. `sales_quotations` + `sales_quotation_items`
2. `sales_orders` + `sales_order_items`
3. `sales_invoices` + `sales_invoice_items`
4. `sales_returns` + `sales_return_items`
5. `sales_delivery_notes` + `sales_delivery_note_items`
6. `sales_payments`
7. `sales_payment_schedules`
8. `sales_commissions`
9. `sales_commission_rules`
10. `product_pricing_tiers`

**Purchases (9 tables):**
1. `purchase_requests` + `purchase_request_items`
2. `purchase_orders` + `purchase_order_items`
3. `purchase_bills` + `purchase_bill_items`
4. `purchase_returns` + `purchase_return_items`
5. `purchase_grns` + `purchase_grn_items`
6. `purchase_payments`
7. `purchase_payment_schedules`
8. `supplier_price_lists`

**Shared (3 tables):**
1. `approval_workflows` + `approval_workflow_levels`
2. `approval_logs`

**Modified existing tables:**
- `products`: Add `product_pricing_tiers` relationship
- `settings`: New keys with `sales.` and `purchases.` prefixes

---

## 10. API Endpoint Reference

### Total New Endpoints

| Module | Endpoints |
|--------|-----------|
| Sales Settings | 2 |
| Sales Quotations | 14 |
| Sales Orders | 14 |
| Sales Invoices | 15 |
| Sales Returns | 11 |
| Sales Delivery Notes | 10 |
| Sales Payments | 7 |
| Sales Commissions | 10 |
| Product Pricing Tiers | 4 |
| Sales Reports | 10 |
| Sales PDF | 5 |
| Purchases Settings | 2 |
| Purchase Requests | 10 |
| Purchase Orders | 15 |
| Purchase GRNs | 9 |
| Purchase Bills | 14 |
| Purchase Returns | 11 |
| Purchase Payments | 7 |
| Supplier Price Lists | 6 |
| Purchase Reports | 9 |
| Purchase PDF | 5 |
| Approval Workflows | 8 |
| **Total** | **~198 endpoints** |

---

## 11. Integration Points

### With Existing Modules

| Existing Module | Integration |
|-----------------|-------------|
| **Products** | Product lookup, variant selection, unit conversion, pricing tiers, serial/batch tracking |
| **Partners** | Customer/supplier selection, address snapshots, tax number, AR/AP balance updates |
| **Accounting - Journal Entries** | Auto-create JE on invoice/bill posting, reversal on cancellation |
| **Accounting - AR/AP** | Invoice posting updates AR, bill posting updates AP, payments reduce balances |
| **Accounting - Tax Rates** | Tax calculation on documents, tax reporting |
| **Accounting - Cost Centers** | Document-level cost center tracking |
| **Accounting - Currencies** | Multi-currency support, exchange rate lookup |
| **Inventory - Stock Balances** | Stock availability check, average cost lookup |
| **Inventory - Receipts** | GRN creates inventory receipt, returns create inventory receipt |
| **Inventory - Issues** | Invoice/DN creates inventory issue, purchase returns create issue |
| **Inventory - Costing** | Weighted average cost updates on purchase receipt |
| **Core - Sequences** | Auto-numbering for all document types |
| **Core - Attachments** | File attachments on all documents |
| **Core - Notifications** | Approval notifications, overdue reminders |
| **Core - Settings** | Module-level configuration storage |

---

## 12. Epic & Task Breakdown

### Epic 1: Sales Module — Backend
11 tasks (Settings, Quotations, Orders, Invoices, Returns, Delivery Notes, Payments, Approval Workflow, Commissions, Tiered Pricing, Reports + PDF)

### Epic 2: Sales Module — Frontend
12 tasks (Services, NgRx Store, Quotation pages, Order pages, Invoice pages, Return pages, Delivery Note pages, Payment pages, Commission pages, Sales Dashboard, Sales Settings page, Translations)

### Epic 3: Purchases Module — Backend
11 tasks (Settings, Purchase Requests, Purchase Orders, GRNs, Bills, Returns, Payments, Supplier Price Lists, Reports + PDF, shared approval already in Epic 1)

### Epic 4: Purchases Module — Frontend
12 tasks (Services, NgRx Store, PR pages, PO pages, GRN pages, Bill pages, Return pages, Payment pages, Supplier Prices pages, Purchase Dashboard, Purchase Settings page, Translations)

**Total: ~46 detailed tasks across 4 epics**

---

*End of Plan Document*
