From 6b7b63f7d7f9d232f0766cb317f13c9c169d0ddf Mon Sep 17 00:00:00 2001 From: Christian Date: Sun, 25 Jan 2026 14:46:00 +0100 Subject: [PATCH] feat: Add Abonnements Matrix feature with e-conomic invoice aggregation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New SubscriptionMatrixService for billing matrix generation - Products grouped by product number with monthly aggregation - Support for archived, draft, sent, booked, paid, unpaid invoices - Fixed amount calculation with fallback logic (grossAmount, unitNetPrice) - Status mapping based on invoice type (draft, invoiced, paid) - Frontend tab on customer detail page with dynamic table rendering - Fixed Blåhund customer economic number linking --- app/customers/backend/router.py | 48 +++ app/customers/frontend/customer_detail.html | 188 ++++++++++++ app/services/economic_service.py | 139 +++++++++ app/services/subscription_matrix.py | 315 ++++++++++++++++++++ create_test_invoice.py | 47 +++ 5 files changed, 737 insertions(+) create mode 100644 app/services/subscription_matrix.py create mode 100644 create_test_invoice.py diff --git a/app/customers/backend/router.py b/app/customers/backend/router.py index 6dfbd2c..db53da2 100644 --- a/app/customers/backend/router.py +++ b/app/customers/backend/router.py @@ -1355,3 +1355,51 @@ async def get_subscription_comment(customer_id: int): except Exception as e: logger.error(f"❌ Error fetching subscription comment: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/customers/{customer_id}/subscriptions/billing-matrix") +async def get_subscription_billing_matrix( + customer_id: int, + months: int = Query(default=12, ge=1, le=60, description="Number of months to show") +): + """ + Get subscription billing matrix showing monthly invoiced amounts per product + + Rows: Products from e-conomic invoices + Columns: Months + Cells: Amount, status (paid/invoiced/missing), invoice reference + + Data source: e-conomic sales invoices only + """ + try: + # Verify customer exists + customer = execute_query_single( + "SELECT id, economic_customer_number FROM customers WHERE id = %s", + (customer_id,) + ) + + if not customer: + raise HTTPException(status_code=404, detail=f"Customer {customer_id} not found") + + if not customer.get('economic_customer_number'): + logger.warning(f"⚠️ Customer {customer_id} has no e-conomic number") + return { + "customer_id": customer_id, + "error": "Customer not linked to e-conomic", + "products": [] + } + + # Generate matrix + from app.services.subscription_matrix import get_subscription_matrix_service + matrix_service = get_subscription_matrix_service() + matrix = await matrix_service.generate_billing_matrix(customer_id, months) + + logger.info(f"✅ Generated billing matrix for customer {customer_id}") + return matrix + + except HTTPException: + raise + except Exception as e: + logger.error(f"❌ Error generating billing matrix: {e}") + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/app/customers/frontend/customer_detail.html b/app/customers/frontend/customer_detail.html index 23a62ae..c8b5796 100644 --- a/app/customers/frontend/customer_detail.html +++ b/app/customers/frontend/customer_detail.html @@ -301,6 +301,11 @@ Abonnnents tjek +