# Sales and Aggregation Implementation Plan ## 1. Data Model Proposals ### 1.1 `sag_salgsvarer` Improvements We will enhance the existing `sag_salgsvarer` table to support full billing requirements, margin calculation, and product linking. **Current Fields:** - `id`, `sag_id`, `type` (sale), `description`, `quantity`, `unit`, `unit_price`, `amount`, `currency`, `status`, `line_date` **Proposed Additions:** | Field | Type | Description | |-------|------|-------------| | `product_id` | INT (FK) | Link to new `products` catalog (nullable) | | `cost_price` | DECIMAL | For calculating Gross Profit (DB) per line | | `discount_percent` | DECIMAL | Discount given on standard price | | `vat_rate` | DECIMAL | Default 25.00 for DK | | `supplier_id` | INT (FK) | Reference to `vendors` table (if exists) or string | | `billing_method` | VARCHAR | `invoice`, `prepaid`, `internal` (matches `tmodule_times`) | | `is_subscription` | BOOLEAN | If true, pushes to subscription system instead of one-off invoice | ### 1.2 New `products` Table A central catalog for standard items (Hardware, Licenses, Fees) to speed up entry and standardize reporting. ```sql CREATE TABLE products ( id SERIAL PRIMARY KEY, sku VARCHAR(50) UNIQUE, name VARCHAR(255) NOT NULL, description TEXT, category VARCHAR(50), -- 'hardware', 'license', 'consulting' cost_price DECIMAL(10,2), sales_price DECIMAL(10,2), -- Suggested RRP unit VARCHAR(20) DEFAULT 'stk', supplier_id INTEGER, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` ### 1.3 Aggregation Rules The system will distinguish between **Direct** costs/revenue (on the case itself) and **Aggregated** (from sub-cases). - **Direct Revenue** = (Sum of `sag_salgsvarer.amount`) + (Sum of `tmodule_times` where `billable=true` * `hourly_rate`) - **Total Revenue** = Direct Revenue + Sum(Child Cases Total Revenue) ## 2. UI Structure for "Varer" (Items) Tab The "Varer" tab on the Case Detail page will have a split entry/view design. ### 2.1 Top Section: Quick Add A horizontal form to quickly add lines: - **Product Lookup**: Searchable dropdown. - **Manual Override**: Description field auto-filled but editable. - **Numbers**: Qty, Unit, Price. - **Result**: Total Price auto-calculated. - **Action**: "Add Line" button. ### 2.2 Main List: Combined Billing View A unified table showing everything billable on this case: | Type | Date | Description | Qty | Price | Disc | Total | Status | Actions | |------|------|-------------|-----|-------|------|-------|--------|---------| | 🕒 Time | 02-02 | Konsulentbistand | 2.5 | 1200 | 0% | 3000 | `Approved` | [Edit Time] | | 📦 Item | 02-02 | Ubiquiti Switch | 1 | 2500 | 10% | 2250 | `Draft` | [Edit] [Del] | | 🔄 Sub | -- | *Sub-case: Installation i Aarhus* | -- | -- | -- | 5400 | `Calculated` | [Go to Case] | ### 2.3 Summary Footer (Sticky) - **Materials**: Total of Items. - **Labor**: Total of Time. - **Sub-cases**: Total of Children. - **Grand Total**: Ex VAT and Inc VAT. - **Margin**: (Sales - Cost) / Sales %. - **Action**: "Create Invoice Proposal" button. ## 3. Aggregation Logic (Recursive) We will implement a `SalesAggregator` service that traverses the case tree. **Algorithm:** 1. **Inputs**: `case_id`. 2. **Fetch Direct Items**: Query `sag_salgsvarer` for this case. 3. **Fetch Direct Time**: Query `tmodule_times` for this case. Calculate value using `hourly_rate`. 4. **Fetch Children**: Query `sag_relationer` (or `sag_sager` parent_id) to find children. 5. **Recursion**: For each child, recursively call `get_case_totals(child_id)`. 6. **Summation**: Return object with `own_total` and `sub_total`. **Python Service Method:** ```python def get_case_financials(case_id: int) -> CaseFinancials: # 1. Own items items = db.query(SagSalgsvarer).filter(sag_id=case_id).all() item_total = sum(i.amount for i in items) item_cost = sum(i.cost_price * i.quantity for i in items) # 2. Own time times = db.query(TmoduleTimes).filter(case_id=case_id, billable=True).all() time_total = sum(t.original_hours * get_hourly_rate(case_id) for t in times) # 3. Children children = db.query(SagRelationer).filter(kilde_sag_id=case_id).all() child_total = 0 child_cost = 0 for child in children: child_fin = get_case_financials(child.malsag_id) child_total += child_fin.total_revenue child_cost += child_fin.total_cost return CaseFinancials( revenue=item_total + time_total + child_total, cost=item_cost + child_cost, # ... breakdown fields ) ``` ## 4. Preparation for Billing (Status Flow) We define a strict lifecycle for items to prevent double-billing. ### 4.1 Status Lifecycle for Items (`sag_salgsvarer`) 1. **`draft`**: Default. Editable. Included in Preliminary Total. 2. **`approved`**: Locked by Project Manager. Ready for Finance. - *Action*: Lock for Billing. - *Effect*: Rows become read-only. 3. **`billed`**: Processed by Finance (exported to e-conomic). - *Action*: Integration Job runs. - *Effect*: Linked to `invoice_id` (new column). ### 4.2 Billing Triggers - **Partial Billing**: Checkbox select specific `approved` lines -> Create Invoice Draft. - **Full Billing**: Bill All Approved -> Generates invoice for all `approved` items and time. - **Aggregation Billing**: - The invoicing engine must accept a `case_structure` to decide if it prints one line per sub-case or expands all lines. Default to **One line per sub-case** for cleanliness. ### 4.3 Validation - Ensure all Approved items have a valid `cost_price` (warn if 0). - Ensure Time Registrations are `approved` before they can be billed.