bmc_hub/SALES_AND_AGGREGATION_PLAN.md
Christian b06ff693df feat: Enhance contact management and user/group functionalities
- 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.
2026-02-03 15:37:16 +01:00

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 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]
  • 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:

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.