diff --git a/VERSION b/VERSION index c4510ba..330b0be 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.97 +2.2.98 diff --git a/app/economy/backend/router.py b/app/economy/backend/router.py index 65c5089..f360aa7 100644 --- a/app/economy/backend/router.py +++ b/app/economy/backend/router.py @@ -430,6 +430,62 @@ def _create_order_from_selected(customer_id: int, rows: List[Dict[str, Any]], us return int(order_id) +def _resolve_tmodule_customer_id(raw_customer_id: Optional[int], sag_id: Optional[int]) -> Optional[int]: + """Resolve any incoming customer reference to a valid tmodule_customers.id. + + Accepts: + - direct tmodule customer id + - hub customer id (customers.id) via tmodule_customers.hub_customer_id + - fallback via sag_sager.customer_id -> tmodule_customers.hub_customer_id + """ + def _find_by_tmodule_id(candidate_id: int) -> Optional[int]: + row = execute_query_single("SELECT id FROM tmodule_customers WHERE id = %s", (candidate_id,)) + return int(row["id"]) if row else None + + def _find_by_hub_customer_id(hub_customer_id: int) -> Optional[int]: + row = execute_query_single( + """ + SELECT id + FROM tmodule_customers + WHERE hub_customer_id = %s + ORDER BY id ASC + LIMIT 1 + """, + (hub_customer_id,), + ) + return int(row["id"]) if row else None + + if raw_customer_id is not None: + try: + cid = int(raw_customer_id) + except (TypeError, ValueError): + cid = None + + if cid and cid > 0: + direct = _find_by_tmodule_id(cid) + if direct: + return direct + mapped = _find_by_hub_customer_id(cid) + if mapped: + return mapped + + if sag_id is not None: + try: + sid = int(sag_id) + except (TypeError, ValueError): + sid = None + + if sid and sid > 0: + sag = execute_query_single("SELECT customer_id FROM sag_sager WHERE id = %s", (sid,)) + hub_customer_id = (sag or {}).get("customer_id") if sag else None + if hub_customer_id: + mapped = _find_by_hub_customer_id(int(hub_customer_id)) + if mapped: + return mapped + + return None + + @router.post("/time-queue/send-to-invoices") async def send_selected_to_invoices(payload: BulkSendRequest, request: Request): ids = _ensure_ids(payload.ids) @@ -466,15 +522,11 @@ async def send_selected_to_invoices(payload: BulkSendRequest, request: Request): raise HTTPException(status_code=400, detail="No eligible entries found") # Local order creation must not depend on e-conomic data/mapping. - # We only require billable entries; billing_method can be invoice/prepaid/fixed_price/internal. - selected_order_ids = [ - int(r["id"]) - for r in rows - if bool(r.get("billable", True)) - ] + # Selected entries are converted to local orders regardless of billing method. + selected_order_ids = [int(r["id"]) for r in rows] if not selected_order_ids: - raise HTTPException(status_code=400, detail="No selected entries are billable") + raise HTTPException(status_code=400, detail="No selected entries found") placeholders_invoice = ",".join(["%s"] * len(selected_order_ids)) execute_update( @@ -497,17 +549,27 @@ async def send_selected_to_invoices(payload: BulkSendRequest, request: Request): if int(row["id"]) not in selected_order_ids: continue - cust_id = row.get("customer_id") - if cust_id is None: + resolved_customer_id = _resolve_tmodule_customer_id(row.get("customer_id"), row.get("sag_id")) + if not resolved_customer_id: skipped_missing_customer.append(int(row["id"])) continue - rows_by_customer[int(cust_id)].append(row) + rows_by_customer[int(resolved_customer_id)].append(row) created_orders = [] + failed_customers: List[Dict[str, Any]] = [] for cust_id, cust_rows in rows_by_customer.items(): - order_id = _create_order_from_selected(cust_id, cust_rows, user_id) - created_orders.append({"customer_id": cust_id, "order_id": order_id}) + try: + order_id = _create_order_from_selected(cust_id, cust_rows, user_id) + created_orders.append({"customer_id": cust_id, "order_id": order_id}) + except HTTPException as ex: + failed_customers.append( + { + "customer_id": cust_id, + "entry_ids": [int(r.get("id")) for r in cust_rows if r.get("id") is not None], + "error": str(ex.detail), + } + ) if not created_orders: if skipped_missing_customer: @@ -515,6 +577,11 @@ async def send_selected_to_invoices(payload: BulkSendRequest, request: Request): status_code=400, detail="No local orders created: selected entries are missing customer linkage", ) + if failed_customers: + raise HTTPException( + status_code=400, + detail="No local orders created: customer data is invalid for selected entries", + ) raise HTTPException(status_code=400, detail="No local orders created") # Time queue must never push directly to e-conomic. @@ -527,9 +594,10 @@ async def send_selected_to_invoices(payload: BulkSendRequest, request: Request): return { "success": True, "selected": len(ids), - "billable_candidates": len(selected_order_ids), + "order_candidates": len(selected_order_ids), "created_orders": created_orders, "skipped_missing_customer": skipped_missing_customer, + "failed_customers": failed_customers, "orders_url": orders_url, "message": "Lokale ordrer oprettet. Overfoer til e-conomic fra Ordre-siden.", } diff --git a/app/economy/frontend/time_queue.html b/app/economy/frontend/time_queue.html index 1c73048..4216a8d 100644 --- a/app/economy/frontend/time_queue.html +++ b/app/economy/frontend/time_queue.html @@ -459,10 +459,14 @@ return `customer ${x.customer_id}, order ${x.order_id}`; }).join('\n'); const skipped = (result.skipped_missing_customer || []); + const failedCustomers = (result.failed_customers || []); const orderMessage = orders || 'Ingen ordrer oprettet'; const nextStep = result.orders_url ? `\n\nAabn ordre: ${result.orders_url}` : ''; const skippedMsg = skipped.length ? `\n\nSprunget over (mangler kunde-link): ${skipped.join(', ')}` : ''; - alert(`Lokale ordrer oprettet:\n${orderMessage}${skippedMsg}${nextStep}`); + const failedMsg = failedCustomers.length + ? `\n\nFejl ved kunde-grupper:\n${failedCustomers.map((f) => `customer ${f.customer_id}: ${f.error}`).join('\n')}` + : ''; + alert(`Lokale ordrer oprettet:\n${orderMessage}${skippedMsg}${failedMsg}${nextStep}`); await loadCustomers(); await loadEntries(); } catch (err) {