feat: Show 12-month matrix with empty cells for missing invoices

- Generate all 12 months automatically (last 12 months from today)
- Display empty cells with 'missing' status for months without invoices
- Makes it easy to spot billing gaps
- Empty cells show 0 kr and null invoice_number
This commit is contained in:
Christian 2026-01-26 17:17:14 +01:00
parent 6de869c86a
commit 1b48e659a8

View File

@ -140,13 +140,39 @@ class SubscriptionMatrixService:
months: Number of months to include
Returns:
List of product records with aggregated monthly data
List of product records with aggregated monthly data (including empty months)
"""
# Generate list of all months to display (last N months from today)
all_months = self._generate_month_range(months)
# Structure: {product_number: {year_month: {amount, status, invoice_number, period_from, period_to}}}
product_matrix = defaultdict(dict)
product_names = {} # Cache product names
# Initialize all products with empty cells for all months
try:
for invoice in invoices:
lines = invoice.get('lines', [])
for line in lines:
product_number = line.get('product', {}).get('productNumber')
product_name = line.get('product', {}).get('name') or line.get('description')
if product_number and product_number not in product_names:
product_names[product_number] = product_name or f"Product {product_number}"
# Initialize all products with all months (empty)
for product_number in product_names.keys():
for year_month in all_months:
product_matrix[product_number][year_month] = {
"amount": 0.0,
"status": "missing",
"invoice_number": None,
"period_from": None,
"period_to": None,
"period_label": None
}
# Now fill in data from invoices
for invoice in invoices:
invoice_number = invoice.get('bookedInvoiceNumber') or invoice.get('draftInvoiceNumber')
# Determine status based on invoice type/endpoint it came from
@ -214,24 +240,15 @@ class SubscriptionMatrixService:
# Calculate period label
period_label = self._format_period_label(period_from, period_to)
# Initialize cell if doesn't exist
if year_month not in product_matrix[product_number]:
product_matrix[product_number][year_month] = {
"amount": 0.0,
"status": "missing",
"invoice_number": None,
"period_from": None,
"period_to": None
}
# Update cell
cell = product_matrix[product_number][year_month]
cell["amount"] = amount # Take last amount (or sum if multiple?)
cell["status"] = self._determine_status(invoice_status)
cell["invoice_number"] = invoice_number
cell["period_from"] = period_from.isoformat().split('T')[0]
cell["period_to"] = period_to.isoformat().split('T')[0]
cell["period_label"] = period_label
# Update cell (it should already exist from initialization)
if year_month in product_matrix.get(product_number, {}):
cell = product_matrix[product_number][year_month]
cell["amount"] = amount # Take last amount (or sum if multiple?)
cell["status"] = self._determine_status(invoice_status)
cell["invoice_number"] = invoice_number
cell["period_from"] = period_from.isoformat().split('T')[0]
cell["period_to"] = period_to.isoformat().split('T')[0]
cell["period_label"] = period_label
except Exception as e:
logger.error(f"❌ Error aggregating data: {e}")
@ -268,6 +285,36 @@ class SubscriptionMatrixService:
return products
@staticmethod
def _generate_month_range(num_months: int) -> List[str]:
"""
Generate list of month keys for the last N months
Args:
num_months: Number of months to generate (e.g., 12)
Returns:
Sorted list of 'YYYY-MM' strings going back from today
"""
months = []
today = datetime.now()
for i in range(num_months):
# Go back i months from today using calendar arithmetic
year = today.year
month = today.month - i
# Adjust year and month if month goes negative
while month <= 0:
month += 12
year -= 1
month_key = f"{year:04d}-{month:02d}"
months.append(month_key)
# Return sorted chronologically (oldest first)
return sorted(months)
@staticmethod
def _format_period_label(period_from: datetime, period_to: datetime) -> str:
"""Format period as readable label (e.g., 'Jan', 'Jan-Mar', etc)"""