bmc_hub/app/modules/orders/backend/service.py

287 lines
9.8 KiB
Python
Raw Permalink Normal View History

import logging
from typing import Any, Dict, List, Optional
from app.core.database import execute_query
logger = logging.getLogger(__name__)
def _to_float(value: Any, default: float = 0.0) -> float:
if value is None:
return default
try:
return float(value)
except (TypeError, ValueError):
return default
def _apply_common_filters(
base_query: str,
params: List[Any],
customer_id: Optional[int],
sag_id: Optional[int],
q: Optional[str],
customer_alias: str,
sag_alias: str,
description_alias: str,
) -> tuple[str, List[Any]]:
query = base_query
if customer_id:
query += f" AND {customer_alias}.id = %s"
params.append(customer_id)
if sag_id:
query += f" AND {sag_alias}.id = %s"
params.append(sag_id)
if q:
like = f"%{q.lower()}%"
query += (
f" AND (LOWER({description_alias}) LIKE %s"
f" OR LOWER(COALESCE({sag_alias}.titel, '')) LIKE %s"
f" OR LOWER(COALESCE({customer_alias}.name, '')) LIKE %s)"
)
params.extend([like, like, like])
return query, params
def _fetch_sales_lines(customer_id: Optional[int], sag_id: Optional[int], q: Optional[str]) -> List[Dict[str, Any]]:
query = """
SELECT
si.id,
si.sag_id,
s.titel AS sag_title,
s.customer_id,
c.name AS customer_name,
si.description,
si.quantity,
si.unit,
si.unit_price,
si.amount,
si.currency,
si.status,
si.line_date,
si.product_id
FROM sag_salgsvarer si
JOIN sag_sager s ON s.id = si.sag_id
LEFT JOIN customers c ON c.id = s.customer_id
WHERE s.deleted_at IS NULL
AND LOWER(si.type) = 'sale'
AND LOWER(si.status) != 'cancelled'
"""
params: List[Any] = []
query, params = _apply_common_filters(query, params, customer_id, sag_id, q, "c", "s", "si.description")
query += " ORDER BY si.line_date DESC NULLS LAST, si.id DESC"
rows = execute_query(query, tuple(params)) or []
lines: List[Dict[str, Any]] = []
for row in rows:
qty = _to_float(row.get("quantity"), 0.0)
unit_price = _to_float(row.get("unit_price"), 0.0)
amount = _to_float(row.get("amount"), qty * unit_price)
lines.append(
{
"line_key": f"sale:{row['id']}",
"source_type": "sale",
"source_id": row["id"],
"reference_id": row["id"],
"sag_id": row.get("sag_id"),
"sag_title": row.get("sag_title"),
"customer_id": row.get("customer_id"),
"customer_name": row.get("customer_name"),
"description": row.get("description") or "Salgslinje",
"quantity": qty if qty > 0 else 1.0,
"unit": row.get("unit") or "stk",
"unit_price": unit_price,
"discount_percentage": 0.0,
"amount": amount,
"currency": row.get("currency") or "DKK",
"status": row.get("status") or "draft",
"line_date": str(row.get("line_date")) if row.get("line_date") else None,
"product_id": row.get("product_id"),
"selected": True,
}
)
return lines
def _fetch_subscription_lines(customer_id: Optional[int], sag_id: Optional[int], q: Optional[str]) -> List[Dict[str, Any]]:
query = """
SELECT
i.id,
i.subscription_id,
i.line_no,
i.product_id,
i.description,
i.quantity,
i.unit_price,
i.line_total,
s.id AS sub_id,
s.subscription_number,
s.status AS subscription_status,
s.billing_interval,
s.sag_id,
sg.titel AS sag_title,
s.customer_id,
c.name AS customer_name
FROM sag_subscription_items i
JOIN sag_subscriptions s ON s.id = i.subscription_id
JOIN sag_sager sg ON sg.id = s.sag_id
LEFT JOIN customers c ON c.id = s.customer_id
WHERE sg.deleted_at IS NULL
AND LOWER(s.status) IN ('draft', 'active', 'paused')
"""
params: List[Any] = []
query, params = _apply_common_filters(query, params, customer_id, sag_id, q, "c", "sg", "i.description")
query += " ORDER BY s.id DESC, i.line_no ASC, i.id ASC"
rows = execute_query(query, tuple(params)) or []
lines: List[Dict[str, Any]] = []
for row in rows:
qty = _to_float(row.get("quantity"), 1.0)
unit_price = _to_float(row.get("unit_price"), 0.0)
amount = _to_float(row.get("line_total"), qty * unit_price)
lines.append(
{
"line_key": f"subscription:{row['id']}",
"source_type": "subscription",
"source_id": row["id"],
"reference_id": row.get("subscription_id"),
"subscription_number": row.get("subscription_number"),
"sag_id": row.get("sag_id"),
"sag_title": row.get("sag_title"),
"customer_id": row.get("customer_id"),
"customer_name": row.get("customer_name"),
"description": row.get("description") or "Abonnementslinje",
"quantity": qty if qty > 0 else 1.0,
"unit": "stk",
"unit_price": unit_price,
"discount_percentage": 0.0,
"amount": amount,
"currency": "DKK",
"status": row.get("subscription_status") or "draft",
"line_date": None,
"product_id": row.get("product_id"),
"selected": True,
"meta": {
"billing_interval": row.get("billing_interval"),
"line_no": row.get("line_no"),
},
}
)
return lines
def _fetch_hardware_lines(customer_id: Optional[int], sag_id: Optional[int], q: Optional[str]) -> List[Dict[str, Any]]:
query = """
SELECT
sh.id AS relation_id,
sh.sag_id,
sh.note,
s.titel AS sag_title,
s.customer_id,
c.name AS customer_name,
h.id AS hardware_id,
h.asset_type,
h.brand,
h.model,
h.serial_number,
h.status AS hardware_status
FROM sag_hardware sh
JOIN sag_sager s ON s.id = sh.sag_id
JOIN hardware_assets h ON h.id = sh.hardware_id
LEFT JOIN customers c ON c.id = s.customer_id
WHERE sh.deleted_at IS NULL
AND s.deleted_at IS NULL
AND h.deleted_at IS NULL
"""
params: List[Any] = []
query, params = _apply_common_filters(
query,
params,
customer_id,
sag_id,
q,
"c",
"s",
"CONCAT(COALESCE(h.brand, ''), ' ', COALESCE(h.model, ''), ' ', COALESCE(h.serial_number, ''))",
)
query += " ORDER BY sh.id DESC"
rows = execute_query(query, tuple(params)) or []
lines: List[Dict[str, Any]] = []
for row in rows:
serial = row.get("serial_number")
serial_part = f" (S/N: {serial})" if serial else ""
brand_model = " ".join([part for part in [row.get("brand"), row.get("model")] if part]).strip()
label = brand_model or row.get("asset_type") or "Hardware"
desc = f"Hardware: {label}{serial_part}"
if row.get("note"):
desc = f"{desc} - {row['note']}"
lines.append(
{
"line_key": f"hardware:{row['relation_id']}",
"source_type": "hardware",
"source_id": row["relation_id"],
"reference_id": row.get("hardware_id"),
"sag_id": row.get("sag_id"),
"sag_title": row.get("sag_title"),
"customer_id": row.get("customer_id"),
"customer_name": row.get("customer_name"),
"description": desc,
"quantity": 1.0,
"unit": "stk",
"unit_price": 0.0,
"discount_percentage": 0.0,
"amount": 0.0,
"currency": "DKK",
"status": row.get("hardware_status") or "active",
"line_date": None,
"product_id": None,
"selected": True,
}
)
return lines
def aggregate_order_lines(
customer_id: Optional[int] = None,
sag_id: Optional[int] = None,
q: Optional[str] = None,
) -> Dict[str, Any]:
"""Aggregate order-ready lines across sale, subscription and hardware sources."""
sales_lines = _fetch_sales_lines(customer_id=customer_id, sag_id=sag_id, q=q)
subscription_lines = _fetch_subscription_lines(customer_id=customer_id, sag_id=sag_id, q=q)
hardware_lines = _fetch_hardware_lines(customer_id=customer_id, sag_id=sag_id, q=q)
all_lines = sales_lines + subscription_lines + hardware_lines
total_amount = sum(_to_float(line.get("amount")) for line in all_lines)
selected_amount = sum(_to_float(line.get("amount")) for line in all_lines if line.get("selected"))
customer_ids = sorted(
{
int(line["customer_id"])
for line in all_lines
if line.get("customer_id") is not None
}
)
return {
"lines": all_lines,
"summary": {
"line_count": len(all_lines),
"line_count_sales": len(sales_lines),
"line_count_subscriptions": len(subscription_lines),
"line_count_hardware": len(hardware_lines),
"customer_count": len(customer_ids),
"total_amount": round(total_amount, 2),
"selected_amount": round(selected_amount, 2),
"currency": "DKK",
},
}