- Added ContactCompanyLink model for linking contacts to companies with primary role handling. - Implemented endpoint to link contacts to companies, including conflict resolution for existing links. - Updated auth service to support additional password hashing schemes. - Improved sag creation and update processes with new fields and validation for status. - Enhanced UI for user and group management, including modals for group assignment and permissions. - Introduced new product catalog and improved sales item structure for better billing and aggregation. - Added recursive aggregation logic for financial calculations in cases. - Implemented strict status lifecycle for billing items to prevent double-billing.
139 lines
5.6 KiB
Markdown
139 lines
5.6 KiB
Markdown
# 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.
|