fix: Lenient filtering for subscription matrix - include all recurring products
This commit is contained in:
parent
89d378cf8a
commit
2ba9f5e103
@ -152,8 +152,10 @@ class SubscriptionMatrixService:
|
|||||||
# Initialize products only if they have data within the selected month range
|
# Initialize products only if they have data within the selected month range
|
||||||
try:
|
try:
|
||||||
# Fill in data from invoices, but only for months within range
|
# Fill in data from invoices, but only for months within range
|
||||||
|
logger.info(f"🔍 [MATRIX] Processing {len(invoices)} invoices")
|
||||||
for invoice in invoices:
|
for invoice in invoices:
|
||||||
invoice_number = invoice.get('bookedInvoiceNumber') or invoice.get('draftInvoiceNumber')
|
invoice_number = invoice.get('bookedInvoiceNumber') or invoice.get('draftInvoiceNumber')
|
||||||
|
logger.debug(f"🔍 [MATRIX] Processing invoice {invoice_number}")
|
||||||
# Determine status based on invoice type/endpoint it came from
|
# Determine status based on invoice type/endpoint it came from
|
||||||
# Priority: use 'status' field, fallback to inferring from presence of certain fields
|
# Priority: use 'status' field, fallback to inferring from presence of certain fields
|
||||||
invoice_status = invoice.get('status', 'unknown')
|
invoice_status = invoice.get('status', 'unknown')
|
||||||
@ -187,24 +189,35 @@ class SubscriptionMatrixService:
|
|||||||
|
|
||||||
# Process invoice lines
|
# Process invoice lines
|
||||||
lines = invoice.get('lines', [])
|
lines = invoice.get('lines', [])
|
||||||
|
logger.debug(f"🔍 [MATRIX] Invoice {invoice_number} has {len(lines)} lines")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
product_number = line.get('product', {}).get('productNumber')
|
product_number = line.get('product', {}).get('productNumber')
|
||||||
product_name = line.get('product', {}).get('name') or line.get('description')
|
product_name = line.get('product', {}).get('name') or line.get('description')
|
||||||
line_description = (line.get('description') or product_name or "").lower()
|
line_description = (line.get('description') or product_name or "").lower()
|
||||||
|
|
||||||
|
# Check for period data
|
||||||
|
line_period_data = line.get('period', {})
|
||||||
|
line_has_period_field = bool(line_period_data.get('from'))
|
||||||
line_period = self._extract_period_from_text(line_description) or invoice_period
|
line_period = self._extract_period_from_text(line_description) or invoice_period
|
||||||
has_period = bool(line.get('period', {}).get('from') if line.get('period') else None) or bool(line_period)
|
has_period = line_has_period_field or bool(line_period)
|
||||||
|
|
||||||
# Only include lines that indicate a period or subscription or explicit period fields
|
# Check keyword matching
|
||||||
if (
|
has_periode_keyword = "periode" in line_description or "periode" in invoice_text
|
||||||
"periode" not in line_description
|
has_abonnement_keyword = "abonnement" in line_description or "abonnement" in invoice_text
|
||||||
and "abonnement" not in line_description
|
|
||||||
and "periode" not in invoice_text
|
# More lenient filtering: Include if it has keywords OR period data OR appears to be a recurring product
|
||||||
and "abonnement" not in invoice_text
|
# Skip only obvious one-time items (installation, setup fees, hardware, etc.)
|
||||||
and not has_period
|
skip_keywords = ['installation', 'opsætning', 'setup', 'hardware', 'engangs']
|
||||||
):
|
should_skip = any(keyword in line_description for keyword in skip_keywords)
|
||||||
|
|
||||||
|
if should_skip:
|
||||||
|
logger.debug(f"🔍 [MATRIX] Skipping one-time item: {product_name[:50] if product_name else 'NO NAME'}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Include if it has subscription indicators OR has period data OR just include everything
|
||||||
|
# We'll filter later based on products appearing in multiple months
|
||||||
|
logger.debug(f"🔍 [MATRIX] Including: {product_name[:50] if product_name else 'NO NAME'} (periode={has_periode_keyword}, abon={has_abonnement_keyword}, period={has_period})")
|
||||||
|
|
||||||
if not product_number and not product_name:
|
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
|
||||||
@ -291,9 +304,30 @@ class SubscriptionMatrixService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error aggregating data: {e}")
|
logger.error(f"❌ Error aggregating data: {e}")
|
||||||
|
|
||||||
|
# Filter out products that only appear in one month (one-time purchases, not subscriptions)
|
||||||
|
# Keep products that appear in 2+ months OR have subscription keywords
|
||||||
|
filtered_matrix = {}
|
||||||
|
for product_key, months_data in product_matrix.items():
|
||||||
|
# Count how many months have actual data (not missing status)
|
||||||
|
months_with_data = sum(1 for cell in months_data.values() if cell["status"] != "missing")
|
||||||
|
|
||||||
|
# Check if product name suggests subscription
|
||||||
|
product_name = product_names.get(product_key, "")
|
||||||
|
has_subscription_indicators = any(
|
||||||
|
keyword in product_name.lower()
|
||||||
|
for keyword in ['periode', 'abonnement', 'hosting', 'office', 'microsoft', 'subscription']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include if it appears in multiple months OR has subscription keywords
|
||||||
|
if months_with_data >= 2 or has_subscription_indicators:
|
||||||
|
filtered_matrix[product_key] = months_data
|
||||||
|
logger.debug(f"✅ Including product: {product_name} (appears in {months_with_data} months, subscription_indicators={has_subscription_indicators})")
|
||||||
|
else:
|
||||||
|
logger.debug(f"❌ Filtering out: {product_name} (only in {months_with_data} month, no subscription indicators)")
|
||||||
|
|
||||||
# Convert to output format
|
# Convert to output format
|
||||||
products = []
|
products = []
|
||||||
for product_number, months_data in sorted(product_matrix.items()):
|
for product_number, months_data in sorted(filtered_matrix.items()):
|
||||||
rows = []
|
rows = []
|
||||||
total_amount: float = 0.0
|
total_amount: float = 0.0
|
||||||
total_paid: float = 0.0
|
total_paid: float = 0.0
|
||||||
|
|||||||
62
find_ar2_customer.py
Normal file
62
find_ar2_customer.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""Find which e-conomic customer has Hosting - AR2 product"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add app to path
|
||||||
|
sys.path.insert(0, '/Users/christianthomas/DEV/bmc_hub_dev')
|
||||||
|
|
||||||
|
from app.services.economic_service import get_economic_service
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
service = get_economic_service()
|
||||||
|
|
||||||
|
# Search products from screenshot: Office 365, Microsoft 365, Hosting - AR2
|
||||||
|
search_terms = ['AR2', 'HOSTING', 'OFFICE 365', 'MICROSOFT 365', 'FAKTURERINGSGEBYR']
|
||||||
|
|
||||||
|
# Try a few known customer numbers
|
||||||
|
test_customers = [3, 9, 17, 43, 44473156, 30480748, 702007708]
|
||||||
|
|
||||||
|
for customer_num in test_customers:
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Checking customer {customer_num}")
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
invoices = await service.get_customer_invoices(customer_num, include_lines=True)
|
||||||
|
|
||||||
|
if not invoices:
|
||||||
|
print(f" No invoices found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Found {len(invoices)} invoices")
|
||||||
|
found_products = set()
|
||||||
|
|
||||||
|
for inv in invoices:
|
||||||
|
inv_num = inv.get('bookedInvoiceNumber') or inv.get('draftInvoiceNumber')
|
||||||
|
lines = inv.get('lines', [])
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
product_name = line.get('product', {}).get('name') or line.get('description', '')
|
||||||
|
|
||||||
|
# Check if any search term matches
|
||||||
|
for term in search_terms:
|
||||||
|
if term in product_name.upper():
|
||||||
|
found_products.add(product_name)
|
||||||
|
print(f" ✅ Invoice {inv_num} ({inv.get('date')}): {product_name}")
|
||||||
|
print(f" Amount: {line.get('netAmount', 0)}")
|
||||||
|
print(f" Period: {line.get('period')}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_products:
|
||||||
|
print(f"\n 📦 Unique products found:")
|
||||||
|
for prod in sorted(found_products):
|
||||||
|
print(f" - {prod}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
76
search_all_ar2.py
Normal file
76
search_all_ar2.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"""Search ALL e-conomic invoices for Hosting - AR2"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
# Add app to path
|
||||||
|
sys.path.insert(0, '/Users/christianthomas/DEV/bmc_hub_dev')
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
headers = {
|
||||||
|
"X-AppSecretToken": settings.ECONOMIC_APP_SECRET_TOKEN,
|
||||||
|
"X-AgreementGrantToken": settings.ECONOMIC_AGREEMENT_GRANT_TOKEN,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints = [
|
||||||
|
"https://restapi.e-conomic.com/invoices/drafts",
|
||||||
|
"https://restapi.e-conomic.com/invoices/sent",
|
||||||
|
"https://restapi.e-conomic.com/invoices/booked",
|
||||||
|
"https://restapi.e-conomic.com/invoices/paid",
|
||||||
|
"https://restapi.e-conomic.com/invoices/unpaid"
|
||||||
|
]
|
||||||
|
|
||||||
|
customers_with_ar2 = set()
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
for endpoint in endpoints:
|
||||||
|
print(f"\nChecking {endpoint.split('/')[-1]}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with session.get(endpoint, headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
invoices = data.get('collection', [])
|
||||||
|
print(f" Found {len(invoices)} invoices")
|
||||||
|
|
||||||
|
for inv in invoices:
|
||||||
|
# Fetch full invoice with lines
|
||||||
|
self_link = inv.get('self')
|
||||||
|
if not self_link:
|
||||||
|
continue
|
||||||
|
|
||||||
|
async with session.get(self_link, headers=headers) as inv_resp:
|
||||||
|
if inv_resp.status == 200:
|
||||||
|
full_inv = await inv_resp.json()
|
||||||
|
lines = full_inv.get('lines', [])
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
product_name = line.get('product', {}).get('name') or line.get('description', '')
|
||||||
|
|
||||||
|
if 'AR2' in product_name.upper() or 'HOSTING' in product_name.upper() or 'ABONNEMENT' in product_name.upper() or 'PERIODE' in product_name.upper():
|
||||||
|
customer_num = full_inv.get('customer', {}).get('customerNumber')
|
||||||
|
inv_num = full_inv.get('bookedInvoiceNumber') or full_inv.get('draftInvoiceNumber')
|
||||||
|
inv_date = full_inv.get('date')
|
||||||
|
period = line.get('period', {})
|
||||||
|
|
||||||
|
customers_with_ar2.add(customer_num)
|
||||||
|
print(f" ✅ Customer {customer_num} | Invoice {inv_num} | {inv_date} | {product_name[:60]}")
|
||||||
|
if period:
|
||||||
|
print(f" Period: {period}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error: {e}")
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"📊 SUMMARY: Found {len(customers_with_ar2)} customers with Hosting - AR2")
|
||||||
|
print(f" Customer numbers: {sorted(customers_with_ar2)}")
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Loading…
Reference in New Issue
Block a user