bmc_hub/SALES_AND_AGGREGATION_PLAN.md

139 lines
5.6 KiB
Markdown
Raw Permalink Normal View History

# 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.