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:
|
if other_ref:
|
||||||
invoice_text_parts.append(str(other_ref))
|
invoice_text_parts.append(str(other_ref))
|
||||||
invoice_text = " ".join(invoice_text_parts).lower()
|
invoice_text = " ".join(invoice_text_parts).lower()
|
||||||
|
invoice_period = self._extract_period_from_text(invoice_text)
|
||||||
|
|
||||||
# Process invoice lines
|
# Process invoice lines
|
||||||
lines = invoice.get('lines', [])
|
lines = invoice.get('lines', [])
|
||||||
@ -194,13 +195,14 @@ class SubscriptionMatrixService:
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not product_number:
|
if not product_number and not product_name:
|
||||||
logger.debug(f"Skipping line without product number: {line}")
|
logger.debug(f"Skipping line without product number: {line}")
|
||||||
continue
|
continue
|
||||||
|
product_key = product_number or (product_name or "").strip().lower()
|
||||||
|
|
||||||
# Cache product name
|
# Cache product name
|
||||||
if product_number not in product_names:
|
if product_key not in product_names:
|
||||||
product_names[product_number] = product_name or f"Product {product_number}"
|
product_names[product_key] = product_name or f"Product {product_number}"
|
||||||
|
|
||||||
# Extract period from line
|
# Extract period from line
|
||||||
period_from_str = line.get('period', {}).get('from')
|
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 no period on line, use invoice date as month
|
||||||
if not period_from_str:
|
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
|
period_from_str = invoice_date
|
||||||
# Assume monthly billing if no end date
|
# Assume monthly billing if no end date
|
||||||
period_from = datetime.fromisoformat(invoice_date.replace('Z', '+00:00'))
|
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)
|
period_label = self._format_period_label(period_from, period_to)
|
||||||
|
|
||||||
# Initialize product if first time within range
|
# 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:
|
for month_key in all_months:
|
||||||
product_matrix[product_number][month_key] = {
|
product_matrix[product_key][month_key] = {
|
||||||
"amount": 0.0,
|
"amount": 0.0,
|
||||||
"status": "missing",
|
"status": "missing",
|
||||||
"invoice_number": None,
|
"invoice_number": None,
|
||||||
@ -258,9 +265,9 @@ class SubscriptionMatrixService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Update cell
|
# Update cell
|
||||||
cell = product_matrix[product_number][year_month]
|
cell = product_matrix[product_key][year_month]
|
||||||
cell["amount"] = amount # Take last amount (or sum if multiple?)
|
cell["amount"] += amount
|
||||||
cell["status"] = self._determine_status(invoice_status)
|
cell["status"] = self._merge_status(cell["status"], self._determine_status(invoice_status))
|
||||||
cell["invoice_number"] = invoice_number
|
cell["invoice_number"] = invoice_number
|
||||||
cell["period_from"] = period_from.isoformat().split('T')[0]
|
cell["period_from"] = period_from.isoformat().split('T')[0]
|
||||||
cell["period_to"] = period_to.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 chronologically (oldest first)
|
||||||
return sorted(months)
|
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
|
@staticmethod
|
||||||
def _format_period_label(period_from: datetime, period_to: datetime) -> str:
|
def _format_period_label(period_from: datetime, period_to: datetime) -> str:
|
||||||
"""Format period as readable label (e.g., 'Jan', 'Jan-Mar', etc)"""
|
"""Format period as readable label (e.g., 'Jan', 'Jan-Mar', etc)"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user