feat(safety): price comparison on order detail
Add ROA price comparison to order detail modal — operators can now see if GoMag prices match Oracle before invoicing. Eliminates the #1 risk of invoicing with wrong prices. Backend: - New get_prices_for_order() in validation_service.py — batch Oracle query with dual-policy routing (sales/production by cont 341/345), PRETURI_CU_TVA handling, kit total calculation - Extend GET /api/sync/order/{orderNumber} with per-item pret_roa and order-level price_check summary - GET /api/dashboard/orders returns price_match=null (lightweight) Frontend: - Modal: price check badge (green/red/grey), "Pret GoMag" + "Pret ROA" columns, match dot per row, mismatch rows highlighted - Dashboard: price dot column (₽) in orders table - Mobile: inline mismatch indicator Cache-bust: shared.js?v=16, dashboard.js?v=28 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -586,3 +586,189 @@ def sync_prices_from_order(orders, mapped_codmat_data: dict, direct_id_map: dict
|
||||
database.pool.release(conn)
|
||||
|
||||
return updated
|
||||
|
||||
|
||||
def get_prices_for_order(items: list[dict], app_settings: dict, conn=None) -> dict:
|
||||
"""Compare GoMag prices with ROA prices for order items.
|
||||
|
||||
Args:
|
||||
items: list of order items, each with 'sku', 'price', 'quantity', 'codmat_details'
|
||||
(codmat_details = [{"codmat", "cantitate_roa", "id_articol"?, "cont"?, "direct"?}])
|
||||
app_settings: dict with 'id_pol', 'id_pol_productie'
|
||||
conn: Oracle connection (optional, will acquire if None)
|
||||
|
||||
Returns: {
|
||||
"items": {idx: {"pret_roa": float|None, "match": bool|None, "pret_gomag": float}},
|
||||
"summary": {"mismatches": int, "checked": int, "oracle_available": bool}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
id_pol = int(app_settings.get("id_pol", 0) or 0)
|
||||
id_pol_productie = int(app_settings.get("id_pol_productie", 0) or 0)
|
||||
except (ValueError, TypeError):
|
||||
id_pol = 0
|
||||
id_pol_productie = 0
|
||||
|
||||
def _empty_result(oracle_available: bool) -> dict:
|
||||
return {
|
||||
"items": {
|
||||
idx: {"pret_roa": None, "match": None, "pret_gomag": float(item.get("price") or 0)}
|
||||
for idx, item in enumerate(items)
|
||||
},
|
||||
"summary": {"mismatches": 0, "checked": 0, "oracle_available": oracle_available}
|
||||
}
|
||||
|
||||
if not items or not id_pol:
|
||||
return _empty_result(oracle_available=False)
|
||||
|
||||
own_conn = conn is None
|
||||
try:
|
||||
if own_conn:
|
||||
conn = database.get_oracle_connection()
|
||||
|
||||
# Step 1: Collect codmats; use id_articol/cont from codmat_details when already known
|
||||
pre_resolved = {} # {codmat: {"id_articol": int, "cont": str}}
|
||||
all_codmats = set()
|
||||
for item in items:
|
||||
for cd in (item.get("codmat_details") or []):
|
||||
codmat = cd.get("codmat")
|
||||
if not codmat:
|
||||
continue
|
||||
all_codmats.add(codmat)
|
||||
if cd.get("id_articol") and codmat not in pre_resolved:
|
||||
pre_resolved[codmat] = {
|
||||
"id_articol": cd["id_articol"],
|
||||
"cont": cd.get("cont") or "",
|
||||
}
|
||||
|
||||
# Step 2: Resolve missing id_articols via nom_articole
|
||||
need_resolve = all_codmats - set(pre_resolved.keys())
|
||||
if need_resolve:
|
||||
db_resolved = resolve_codmat_ids(need_resolve, conn=conn)
|
||||
pre_resolved.update(db_resolved)
|
||||
|
||||
codmat_info = pre_resolved # {codmat: {"id_articol": int, "cont": str}}
|
||||
|
||||
# Step 3: Get PRETURI_CU_TVA flag once per policy
|
||||
policies = {id_pol}
|
||||
if id_pol_productie and id_pol_productie != id_pol:
|
||||
policies.add(id_pol_productie)
|
||||
|
||||
pol_cu_tva = {} # {id_pol: bool}
|
||||
with conn.cursor() as cur:
|
||||
for pol in policies:
|
||||
cur.execute(
|
||||
"SELECT PRETURI_CU_TVA FROM CRM_POLITICI_PRETURI WHERE ID_POL = :pol",
|
||||
{"pol": pol},
|
||||
)
|
||||
row = cur.fetchone()
|
||||
pol_cu_tva[pol] = (int(row[0] or 0) == 1) if row else False
|
||||
|
||||
# Step 4: Batch query PRET + PROC_TVAV for all id_articols across both policies
|
||||
all_id_articols = list({
|
||||
info["id_articol"]
|
||||
for info in codmat_info.values()
|
||||
if info.get("id_articol")
|
||||
})
|
||||
price_map = {} # {(id_pol, id_articol): (pret, proc_tvav)}
|
||||
|
||||
if all_id_articols:
|
||||
pol_list = list(policies)
|
||||
pol_placeholders = ",".join([f":p{k}" for k in range(len(pol_list))])
|
||||
with conn.cursor() as cur:
|
||||
for i in range(0, len(all_id_articols), 500):
|
||||
batch = all_id_articols[i:i + 500]
|
||||
art_placeholders = ",".join([f":a{j}" for j in range(len(batch))])
|
||||
params = {f"a{j}": aid for j, aid in enumerate(batch)}
|
||||
for k, pol in enumerate(pol_list):
|
||||
params[f"p{k}"] = pol
|
||||
cur.execute(f"""
|
||||
SELECT ID_POL, ID_ARTICOL, PRET, PROC_TVAV
|
||||
FROM CRM_POLITICI_PRET_ART
|
||||
WHERE ID_POL IN ({pol_placeholders}) AND ID_ARTICOL IN ({art_placeholders})
|
||||
""", params)
|
||||
for row in cur:
|
||||
price_map[(row[0], row[1])] = (row[2], row[3])
|
||||
|
||||
# Step 5: Compute pret_roa per item and compare with GoMag price
|
||||
result_items = {}
|
||||
mismatches = 0
|
||||
checked = 0
|
||||
|
||||
for idx, item in enumerate(items):
|
||||
pret_gomag = float(item.get("price") or 0)
|
||||
result_items[idx] = {"pret_gomag": pret_gomag, "pret_roa": None, "match": None}
|
||||
|
||||
codmat_details = item.get("codmat_details") or []
|
||||
if not codmat_details:
|
||||
continue
|
||||
|
||||
is_kit = len(codmat_details) > 1 or (
|
||||
len(codmat_details) == 1
|
||||
and float(codmat_details[0].get("cantitate_roa") or 1) > 1
|
||||
)
|
||||
|
||||
pret_roa_total = 0.0
|
||||
all_resolved = True
|
||||
|
||||
for cd in codmat_details:
|
||||
codmat = cd.get("codmat")
|
||||
if not codmat:
|
||||
all_resolved = False
|
||||
break
|
||||
|
||||
info = codmat_info.get(codmat, {})
|
||||
id_articol = info.get("id_articol")
|
||||
if not id_articol:
|
||||
all_resolved = False
|
||||
break
|
||||
|
||||
# Dual-policy routing: cont 341/345 → production, else → sales
|
||||
cont = str(info.get("cont") or cd.get("cont") or "").strip()
|
||||
if cont in ("341", "345") and id_pol_productie:
|
||||
pol = id_pol_productie
|
||||
else:
|
||||
pol = id_pol
|
||||
|
||||
price_entry = price_map.get((pol, id_articol))
|
||||
if price_entry is None:
|
||||
all_resolved = False
|
||||
break
|
||||
|
||||
pret, proc_tvav = price_entry
|
||||
proc_tvav = float(proc_tvav or 1.19)
|
||||
|
||||
if pol_cu_tva.get(pol):
|
||||
pret_cu_tva = float(pret or 0)
|
||||
else:
|
||||
pret_cu_tva = float(pret or 0) * proc_tvav
|
||||
|
||||
cantitate_roa = float(cd.get("cantitate_roa") or 1)
|
||||
if is_kit:
|
||||
pret_roa_total += pret_cu_tva * cantitate_roa
|
||||
else:
|
||||
pret_roa_total = pret_cu_tva # cantitate_roa==1 for simple items
|
||||
|
||||
if not all_resolved:
|
||||
continue
|
||||
|
||||
pret_roa = round(pret_roa_total, 4)
|
||||
match = abs(pret_gomag - pret_roa) < 0.01
|
||||
result_items[idx]["pret_roa"] = pret_roa
|
||||
result_items[idx]["match"] = match
|
||||
checked += 1
|
||||
if not match:
|
||||
mismatches += 1
|
||||
|
||||
logger.info(f"get_prices_for_order: {checked}/{len(items)} checked, {mismatches} mismatches")
|
||||
return {
|
||||
"items": result_items,
|
||||
"summary": {"mismatches": mismatches, "checked": checked, "oracle_available": True},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"get_prices_for_order failed: {e}")
|
||||
return _empty_result(oracle_available=False)
|
||||
finally:
|
||||
if own_conn and conn:
|
||||
database.pool.release(conn)
|
||||
|
||||
Reference in New Issue
Block a user