diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index ae18285..aab6e69 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -12,7 +12,7 @@ from pydantic import BaseModel from pathlib import Path from typing import Optional -from ..services import sync_service, scheduler_service, sqlite_service, invoice_service +from ..services import sync_service, scheduler_service, sqlite_service, invoice_service, validation_service from .. import database router = APIRouter(tags=["sync"]) @@ -405,8 +405,26 @@ async def order_detail(order_number: str): "direct": True }] + # Price comparison against ROA Oracle + app_settings = await sqlite_service.get_app_settings() + try: + price_data = await asyncio.to_thread( + validation_service.get_prices_for_order, items, app_settings + ) + price_items = price_data.get("items", {}) + for idx, item in enumerate(items): + pi = price_items.get(idx) + if pi: + item["pret_roa"] = pi.get("pret_roa") + item["price_match"] = pi.get("match") + order_price_check = price_data.get("summary", {}) + except Exception as e: + logger.warning(f"Price comparison failed for order {order_number}: {e}") + order_price_check = {"mismatches": 0, "checked": 0, "oracle_available": False} + # Enrich with invoice data order = detail.get("order", {}) + order["price_check"] = order_price_check if order.get("factura_numar") and order.get("factura_data"): order["invoice"] = { "facturat": True, @@ -445,8 +463,7 @@ async def order_detail(order_number: str): except (json.JSONDecodeError, TypeError): pass - # Add settings for receipt display - app_settings = await sqlite_service.get_app_settings() + # Add settings for receipt display (app_settings already fetched above) order["transport_vat"] = app_settings.get("transport_vat") or "21" order["transport_codmat"] = app_settings.get("transport_codmat") or "" order["discount_codmat"] = app_settings.get("discount_codmat") or "" @@ -484,6 +501,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50, # Enrich orders with invoice data — prefer SQLite cache, fallback to Oracle all_orders = result["orders"] for o in all_orders: + o["price_match"] = None # Populated when order detail is opened if o.get("factura_numar") and o.get("factura_data"): # Use cached invoice data from SQLite (only if complete) o["invoice"] = { diff --git a/api/app/services/validation_service.py b/api/app/services/validation_service.py index 317e45d..6bc3bb8 100644 --- a/api/app/services/validation_service.py +++ b/api/app/services/validation_service.py @@ -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) diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js index fee1790..ee7f760 100644 --- a/api/app/static/js/dashboard.js +++ b/api/app/static/js/dashboard.js @@ -305,7 +305,7 @@ async function loadDashOrders() { const orders = data.orders || []; if (orders.length === 0) { - tbody.innerHTML = '
${esc(item.codmat || '–')}`;
const valoare = (Number(item.price || 0) * Number(item.quantity || 0));
const clickAttr = opts.onQuickMap ? `onclick="_sharedModalQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}',${idx})"` : '';
+ const priceInfo = (order.price_check?.items || {})[idx];
+ const priceMismatchHtml = priceInfo?.match === false
+ ? `${esc(item.sku)}' + esc(tCodmat) + '' : ''}' + esc(dCodmat) + '' : ''}' + esc(dCodmat) + '' : ''}