- 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.
5.6 KiB
5.6 KiB
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.
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 oftmodule_timeswherebillable=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:
- Inputs:
case_id. - Fetch Direct Items: Query
sag_salgsvarerfor this case. - Fetch Direct Time: Query
tmodule_timesfor this case. Calculate value usinghourly_rate. - Fetch Children: Query
sag_relationer(orsag_sagerparent_id) to find children. - Recursion: For each child, recursively call
get_case_totals(child_id). - Summation: Return object with
own_totalandsub_total.
Python Service Method:
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)
draft: Default. Editable. Included in Preliminary Total.approved: Locked by Project Manager. Ready for Finance.- Action: Lock for Billing.
- Effect: Rows become read-only.
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
approvedlines -> Create Invoice Draft. - Full Billing: Bill All Approved -> Generates invoice for all
approveditems and time. - Aggregation Billing:
- The invoicing engine must accept a
case_structureto decide if it prints one line per sub-case or expands all lines. Default to One line per sub-case for cleanliness.
- The invoicing engine must accept a
4.3 Validation
- Ensure all Approved items have a valid
cost_price(warn if 0). - Ensure Time Registrations are
approvedbefore they can be billed.