fix: Group products and parse period from invoice title
- Group identical product lines on same row - Parse month from invoice title/notes (e.g., 'Periode May 2025') - Assign lines to correct month from title - Sum amounts per month and merge statuses
This commit is contained in:
parent
8ec12819f7
commit
fbe43b82e1
@ -178,6 +178,7 @@ class SubscriptionMatrixService:
|
||||
if other_ref:
|
||||
invoice_text_parts.append(str(other_ref))
|
||||
invoice_text = " ".join(invoice_text_parts).lower()
|
||||
invoice_period = self._extract_period_from_text(invoice_text)
|
||||
|
||||
# Process invoice lines
|
||||
lines = invoice.get('lines', [])
|
||||
@ -194,13 +195,14 @@ class SubscriptionMatrixService:
|
||||
):
|
||||
continue
|
||||
|
||||
if not product_number:
|
||||
if not product_number and not product_name:
|
||||
logger.debug(f"Skipping line without product number: {line}")
|
||||
continue
|
||||
product_key = product_number or (product_name or "").strip().lower()
|
||||
|
||||
# Cache product name
|
||||
if product_number not in product_names:
|
||||
product_names[product_number] = product_name or f"Product {product_number}"
|
||||
if product_key not in product_names:
|
||||
product_names[product_key] = product_name or f"Product {product_number}"
|
||||
|
||||
# Extract period from line
|
||||
period_from_str = line.get('period', {}).get('from')
|
||||
@ -208,7 +210,12 @@ class SubscriptionMatrixService:
|
||||
|
||||
# If no period on line, use invoice date as month
|
||||
if not period_from_str:
|
||||
if invoice_date:
|
||||
if invoice_period:
|
||||
period_from = invoice_period
|
||||
period_to = self._end_of_month(period_from)
|
||||
period_from_str = period_from.isoformat().split('T')[0]
|
||||
period_to_str = period_to.isoformat().split('T')[0]
|
||||
elif invoice_date:
|
||||
period_from_str = invoice_date
|
||||
# Assume monthly billing if no end date
|
||||
period_from = datetime.fromisoformat(invoice_date.replace('Z', '+00:00'))
|
||||
@ -246,9 +253,9 @@ class SubscriptionMatrixService:
|
||||
period_label = self._format_period_label(period_from, period_to)
|
||||
|
||||
# Initialize product if first time within range
|
||||
if product_number not in product_matrix:
|
||||
if product_key not in product_matrix:
|
||||
for month_key in all_months:
|
||||
product_matrix[product_number][month_key] = {
|
||||
product_matrix[product_key][month_key] = {
|
||||
"amount": 0.0,
|
||||
"status": "missing",
|
||||
"invoice_number": None,
|
||||
@ -258,9 +265,9 @@ class SubscriptionMatrixService:
|
||||
}
|
||||
|
||||
# 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 = product_matrix[product_key][year_month]
|
||||
cell["amount"] += amount
|
||||
cell["status"] = self._merge_status(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]
|
||||
@ -331,6 +338,54 @@ class SubscriptionMatrixService:
|
||||
# Return sorted chronologically (oldest first)
|
||||
return sorted(months)
|
||||
|
||||
@staticmethod
|
||||
def _end_of_month(dt: datetime) -> datetime:
|
||||
"""Return last day of the month for a given date."""
|
||||
next_month = dt.replace(day=28) + timedelta(days=4)
|
||||
return next_month - timedelta(days=next_month.day)
|
||||
|
||||
@staticmethod
|
||||
def _extract_period_from_text(text: str) -> Optional[datetime]:
|
||||
"""Extract month/year from invoice title/notes text (e.g., 'Periode May 2025')."""
|
||||
if not text:
|
||||
return None
|
||||
month_map = {
|
||||
"january": 1, "jan": 1, "januar": 1,
|
||||
"february": 2, "feb": 2, "februar": 2,
|
||||
"march": 3, "mar": 3, "marts": 3,
|
||||
"april": 4, "apr": 4,
|
||||
"may": 5, "maj": 5,
|
||||
"june": 6, "jun": 6, "juni": 6,
|
||||
"july": 7, "jul": 7, "juli": 7,
|
||||
"august": 8, "aug": 8,
|
||||
"september": 9, "sep": 9, "sept": 9,
|
||||
"october": 10, "oct": 10, "okt": 10, "oktober": 10,
|
||||
"november": 11, "nov": 11,
|
||||
"december": 12, "dec": 12
|
||||
}
|
||||
tokens = text.replace(".", " ").replace("/", " ").split()
|
||||
for i, token in enumerate(tokens[:-1]):
|
||||
month = month_map.get(token.lower())
|
||||
if month:
|
||||
year_token = tokens[i + 1]
|
||||
if year_token.isdigit() and len(year_token) == 4:
|
||||
return datetime(int(year_token), month, 1)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _merge_status(existing: str, incoming: str) -> str:
|
||||
"""Keep the most 'complete' status when multiple invoices hit same cell."""
|
||||
priority = {
|
||||
"missing": 0,
|
||||
"draft": 1,
|
||||
"invoiced": 2,
|
||||
"paid": 3,
|
||||
"credited": 2
|
||||
}
|
||||
existing_key = existing or "missing"
|
||||
incoming_key = incoming or "missing"
|
||||
return incoming_key if priority.get(incoming_key, 0) >= priority.get(existing_key, 0) else existing_key
|
||||
|
||||
@staticmethod
|
||||
def _format_period_label(period_from: datetime, period_to: datetime) -> str:
|
||||
"""Format period as readable label (e.g., 'Jan', 'Jan-Mar', etc)"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user