hotfix: robust local order creation with customer mapping fallback
This commit is contained in:
parent
e878336537
commit
03a1b79737
@ -430,6 +430,62 @@ def _create_order_from_selected(customer_id: int, rows: List[Dict[str, Any]], us
|
|||||||
return int(order_id)
|
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")
|
@router.post("/time-queue/send-to-invoices")
|
||||||
async def send_selected_to_invoices(payload: BulkSendRequest, request: Request):
|
async def send_selected_to_invoices(payload: BulkSendRequest, request: Request):
|
||||||
ids = _ensure_ids(payload.ids)
|
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")
|
raise HTTPException(status_code=400, detail="No eligible entries found")
|
||||||
|
|
||||||
# Local order creation must not depend on e-conomic data/mapping.
|
# 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 entries are converted to local orders regardless of billing method.
|
||||||
selected_order_ids = [
|
selected_order_ids = [int(r["id"]) for r in rows]
|
||||||
int(r["id"])
|
|
||||||
for r in rows
|
|
||||||
if bool(r.get("billable", True))
|
|
||||||
]
|
|
||||||
|
|
||||||
if not selected_order_ids:
|
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))
|
placeholders_invoice = ",".join(["%s"] * len(selected_order_ids))
|
||||||
execute_update(
|
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:
|
if int(row["id"]) not in selected_order_ids:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cust_id = row.get("customer_id")
|
resolved_customer_id = _resolve_tmodule_customer_id(row.get("customer_id"), row.get("sag_id"))
|
||||||
if cust_id is None:
|
if not resolved_customer_id:
|
||||||
skipped_missing_customer.append(int(row["id"]))
|
skipped_missing_customer.append(int(row["id"]))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rows_by_customer[int(cust_id)].append(row)
|
rows_by_customer[int(resolved_customer_id)].append(row)
|
||||||
|
|
||||||
created_orders = []
|
created_orders = []
|
||||||
|
failed_customers: List[Dict[str, Any]] = []
|
||||||
for cust_id, cust_rows in rows_by_customer.items():
|
for cust_id, cust_rows in rows_by_customer.items():
|
||||||
|
try:
|
||||||
order_id = _create_order_from_selected(cust_id, cust_rows, user_id)
|
order_id = _create_order_from_selected(cust_id, cust_rows, user_id)
|
||||||
created_orders.append({"customer_id": cust_id, "order_id": order_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 not created_orders:
|
||||||
if skipped_missing_customer:
|
if skipped_missing_customer:
|
||||||
@ -515,6 +577,11 @@ async def send_selected_to_invoices(payload: BulkSendRequest, request: Request):
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
detail="No local orders created: selected entries are missing customer linkage",
|
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")
|
raise HTTPException(status_code=400, detail="No local orders created")
|
||||||
|
|
||||||
# Time queue must never push directly to e-conomic.
|
# Time queue must never push directly to e-conomic.
|
||||||
@ -527,9 +594,10 @@ async def send_selected_to_invoices(payload: BulkSendRequest, request: Request):
|
|||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"selected": len(ids),
|
"selected": len(ids),
|
||||||
"billable_candidates": len(selected_order_ids),
|
"order_candidates": len(selected_order_ids),
|
||||||
"created_orders": created_orders,
|
"created_orders": created_orders,
|
||||||
"skipped_missing_customer": skipped_missing_customer,
|
"skipped_missing_customer": skipped_missing_customer,
|
||||||
|
"failed_customers": failed_customers,
|
||||||
"orders_url": orders_url,
|
"orders_url": orders_url,
|
||||||
"message": "Lokale ordrer oprettet. Overfoer til e-conomic fra Ordre-siden.",
|
"message": "Lokale ordrer oprettet. Overfoer til e-conomic fra Ordre-siden.",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -459,10 +459,14 @@
|
|||||||
return `customer ${x.customer_id}, order ${x.order_id}`;
|
return `customer ${x.customer_id}, order ${x.order_id}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
const skipped = (result.skipped_missing_customer || []);
|
const skipped = (result.skipped_missing_customer || []);
|
||||||
|
const failedCustomers = (result.failed_customers || []);
|
||||||
const orderMessage = orders || 'Ingen ordrer oprettet';
|
const orderMessage = orders || 'Ingen ordrer oprettet';
|
||||||
const nextStep = result.orders_url ? `\n\nAabn ordre: ${result.orders_url}` : '';
|
const nextStep = result.orders_url ? `\n\nAabn ordre: ${result.orders_url}` : '';
|
||||||
const skippedMsg = skipped.length ? `\n\nSprunget over (mangler kunde-link): ${skipped.join(', ')}` : '';
|
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 loadCustomers();
|
||||||
await loadEntries();
|
await loadEntries();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user