From b221b257a3516891ef06593b3dfda1c2b801718c Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Fri, 20 Mar 2026 15:07:53 +0000 Subject: [PATCH] fix: price sync kit components + vat_included type bug - Fix vat_included comparison: GoMag API returns int 1, not str "1", causing all prices to be multiplied by TVA again (double TVA) - Normalize vat_included to string in gomag_client at parse time - Price sync now processes kit components individually by looking up each component's CODMAT as standalone GoMag product - Add _insert_component_price for components without existing Oracle price - resolve_mapped_codmats: ROW_NUMBER dedup for CODMATs with multiple NOM_ARTICOLE entries, prefer article with current stock - pack_import_comenzi: merge_or_insert_articol to merge quantities when same article appears from kit + individual on same order Co-Authored-By: Claude Opus 4.6 (1M context) --- api/app/services/gomag_client.py | 2 +- api/app/services/price_sync_service.py | 78 ++++++++++++- api/app/services/sync_service.py | 3 +- api/app/services/validation_service.py | 40 ++++++- .../06_pack_import_comenzi.pck | 103 +++++++++++++----- 5 files changed, 189 insertions(+), 37 deletions(-) diff --git a/api/app/services/gomag_client.py b/api/app/services/gomag_client.py index 3ca523a..81ef907 100644 --- a/api/app/services/gomag_client.py +++ b/api/app/services/gomag_client.py @@ -170,7 +170,7 @@ async def download_products( "sku": p["sku"], "price": p.get("price", "0"), "vat": p.get("vat", "19"), - "vat_included": p.get("vat_included", "1"), + "vat_included": str(p.get("vat_included", "1")), "bundleItems": p.get("bundleItems", []), }) diff --git a/api/app/services/price_sync_service.py b/api/app/services/price_sync_service.py index b8be8e1..6b7670f 100644 --- a/api/app/services/price_sync_service.py +++ b/api/app/services/price_sync_service.py @@ -62,6 +62,35 @@ async def get_price_sync_status() -> dict: await db.close() +def _insert_component_price(id_articol: int, id_pol: int, price_cu_tva: float, + proc_tvav: float, conn): + """Insert a new price entry in crm_politici_pret_art for a kit component.""" + with conn.cursor() as cur: + cur.execute(""" + SELECT PRETURI_CU_TVA, ID_VALUTA FROM CRM_POLITICI_PRETURI WHERE ID_POL = :pol + """, {"pol": id_pol}) + row = cur.fetchone() + if not row: + return + preturi_cu_tva, id_valuta = row + + if preturi_cu_tva == 1: + pret = price_cu_tva + else: + pret = round(price_cu_tva / proc_tvav, 4) + + cur.execute(""" + INSERT INTO CRM_POLITICI_PRET_ART + (ID_POL, ID_ARTICOL, PRET, ID_VALUTA, + ID_UTIL, DATAORA, PROC_TVAV, PRETFTVA, PRETCTVA) + VALUES + (:id_pol, :id_articol, :pret, :id_valuta, + -3, SYSDATE, :proc_tvav, 0, 0) + """, {"id_pol": id_pol, "id_articol": id_articol, "pret": pret, + "id_valuta": id_valuta, "proc_tvav": proc_tvav}) + conn.commit() + + async def run_catalog_price_sync(run_id: str): global _current_price_sync async with _price_sync_lock: @@ -96,6 +125,9 @@ async def run_catalog_price_sync(run_id: str): await _finish_run(run_id, "completed", log_lines, products_total=0) return + # Index products by SKU for kit component lookup + products_by_sku = {p["sku"]: p for p in products} + # Connect to Oracle conn = await asyncio.to_thread(database.get_oracle_connection) try: @@ -136,16 +168,54 @@ async def run_catalog_price_sync(run_id: str): continue vat = float(product.get("vat", "19")) - vat_included = product.get("vat_included", "1") - # Calculate price with TVA - if vat_included == "1": + # Calculate price with TVA (vat_included can be int 1 or str "1") + if str(product.get("vat_included", "1")) == "1": price_cu_tva = price else: price_cu_tva = price * (1 + vat / 100) - # Skip kits (>1 CODMAT) + # For kits, sync each component individually from standalone GoMag prices if sku in mapped_data and len(mapped_data[sku]) > 1: + for comp in mapped_data[sku]: + comp_codmat = comp["codmat"] + comp_product = products_by_sku.get(comp_codmat) + if not comp_product: + continue # Component not in GoMag as standalone product + + comp_price_str = comp_product.get("price", "0") + comp_price = float(comp_price_str) if comp_price_str else 0 + if comp_price <= 0: + continue + + comp_vat = float(comp_product.get("vat", "19")) + + # vat_included can be int 1 or str "1" + if str(comp_product.get("vat_included", "1")) == "1": + comp_price_cu_tva = comp_price + else: + comp_price_cu_tva = comp_price * (1 + comp_vat / 100) + + comp_cont_str = str(comp.get("cont") or "").strip() + comp_pol = id_pol_productie if (comp_cont_str in ("341", "345") and id_pol_productie) else id_pol + + matched += 1 + result = await asyncio.to_thread( + validation_service.compare_and_update_price, + comp["id_articol"], comp_pol, comp_price_cu_tva, conn + ) + if result and result["updated"]: + updated += 1 + _log(f" {comp_codmat}: {result['old_price']:.2f} → {result['new_price']:.2f} (kit {sku})") + elif result is None: + # No price entry — insert one with correct price + proc_tvav = 1 + (comp_vat / 100) + await asyncio.to_thread( + _insert_component_price, + comp["id_articol"], comp_pol, comp_price_cu_tva, proc_tvav, conn + ) + updated += 1 + _log(f" {comp_codmat}: NOU → {comp_price_cu_tva:.2f} (kit {sku})") continue # Determine id_articol and policy diff --git a/api/app/services/sync_service.py b/api/app/services/sync_service.py index c5b65da..9abd778 100644 --- a/api/app/services/sync_service.py +++ b/api/app/services/sync_service.py @@ -468,7 +468,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None mapped_codmat_data = {} if mapped_skus_in_orders: mapped_codmat_data = await asyncio.to_thread( - validation_service.resolve_mapped_codmats, mapped_skus_in_orders, conn + validation_service.resolve_mapped_codmats, mapped_skus_in_orders, conn, + id_gestiuni=id_gestiuni ) # Build id_map for mapped codmats and validate/ensure their prices mapped_id_map = {} diff --git a/api/app/services/validation_service.py b/api/app/services/validation_service.py index f07a667..da91b4a 100644 --- a/api/app/services/validation_service.py +++ b/api/app/services/validation_service.py @@ -364,14 +364,26 @@ def validate_and_ensure_prices_dual(codmats: set[str], id_pol_vanzare: int, return codmat_policy_map -def resolve_mapped_codmats(mapped_skus: set[str], conn) -> dict[str, list[dict]]: +def resolve_mapped_codmats(mapped_skus: set[str], conn, + id_gestiuni: list[int] = None) -> dict[str, list[dict]]: """For mapped SKUs, get their underlying CODMATs from ARTICOLE_TERTI + nom_articole. + Uses ROW_NUMBER to pick the best id_articol per (SKU, CODMAT) pair: + prefers article with stock in current month, then MAX(id_articol) as fallback. + This avoids inflating results when a CODMAT has multiple NOM_ARTICOLE entries. + Returns: {sku: [{"codmat": str, "id_articol": int, "cont": str|None, "cantitate_roa": float|None}]} """ if not mapped_skus: return {} + # Build stoc subquery gestiune filter (same pattern as resolve_codmat_ids) + if id_gestiuni: + gest_placeholders = ",".join([f":g{k}" for k in range(len(id_gestiuni))]) + stoc_filter = f"AND s.id_gestiune IN ({gest_placeholders})" + else: + stoc_filter = "" + result = {} sku_list = list(mapped_skus) @@ -380,12 +392,30 @@ def resolve_mapped_codmats(mapped_skus: set[str], conn) -> dict[str, list[dict]] batch = sku_list[i:i+500] placeholders = ",".join([f":s{j}" for j in range(len(batch))]) params = {f"s{j}": sku for j, sku in enumerate(batch)} + if id_gestiuni: + for k, gid in enumerate(id_gestiuni): + params[f"g{k}"] = gid cur.execute(f""" - SELECT at.sku, at.codmat, na.id_articol, na.cont, at.cantitate_roa - FROM ARTICOLE_TERTI at - JOIN NOM_ARTICOLE na ON na.codmat = at.codmat AND na.sters = 0 AND na.inactiv = 0 - WHERE at.sku IN ({placeholders}) AND at.activ = 1 AND at.sters = 0 + SELECT sku, codmat, id_articol, cont, cantitate_roa FROM ( + SELECT at.sku, at.codmat, na.id_articol, na.cont, at.cantitate_roa, + ROW_NUMBER() OVER ( + PARTITION BY at.sku, at.codmat + ORDER BY + CASE WHEN EXISTS ( + SELECT 1 FROM stoc s + WHERE s.id_articol = na.id_articol + {stoc_filter} + AND s.an = EXTRACT(YEAR FROM SYSDATE) + AND s.luna = EXTRACT(MONTH FROM SYSDATE) + AND s.cants + s.cant - s.cante > 0 + ) THEN 0 ELSE 1 END, + na.id_articol DESC + ) AS rn + FROM ARTICOLE_TERTI at + JOIN NOM_ARTICOLE na ON na.codmat = at.codmat AND na.sters = 0 AND na.inactiv = 0 + WHERE at.sku IN ({placeholders}) AND at.activ = 1 AND at.sters = 0 + ) WHERE rn = 1 """, params) for row in cur: sku = row[0] diff --git a/api/database-scripts/06_pack_import_comenzi.pck b/api/database-scripts/06_pack_import_comenzi.pck index f9dd7e1..432d1a6 100644 --- a/api/database-scripts/06_pack_import_comenzi.pck +++ b/api/database-scripts/06_pack_import_comenzi.pck @@ -62,6 +62,7 @@ -- END; -- 20.03.2026 - dual policy vanzare/productie, kit pricing distributed/separate_line, SKU→CODMAT via ARTICOLE_TERTI -- 20.03.2026 - kit discount deferred cross-kit (separate_line, merge-on-collision) +-- 20.03.2026 - merge_or_insert_articol: merge cantitati cand kit+individual au acelasi articol/pret -- ==================================================================== CREATE OR REPLACE PACKAGE PACK_IMPORT_COMENZI AS @@ -175,6 +176,56 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS RETURN v_result; END resolve_id_articol; + -- ================================================================ + -- Helper: merge-or-insert articol pe comanda + -- Daca aceeasi combinatie (ID_COMANDA, ID_ARTICOL, PTVA, PRET, SIGN(CANTITATE)) + -- exista deja, aduna cantitatea; altfel insereaza linie noua. + -- Previne crash la duplicate cand acelasi articol apare din kit + individual. + -- ================================================================ + PROCEDURE merge_or_insert_articol( + p_id_comanda IN NUMBER, + p_id_articol IN NUMBER, + p_id_pol IN NUMBER, + p_cantitate IN NUMBER, + p_pret IN NUMBER, + p_id_util IN NUMBER, + p_id_sectie IN NUMBER, + p_ptva IN NUMBER + ) IS + v_cnt NUMBER; + BEGIN + SELECT COUNT(*) INTO v_cnt + FROM COMENZI_ELEMENTE + WHERE ID_COMANDA = p_id_comanda + AND ID_ARTICOL = p_id_articol + AND NVL(PTVA, 0) = NVL(p_ptva, 0) + AND PRET = p_pret + AND SIGN(CANTITATE) = SIGN(p_cantitate) + AND STERS = 0; + + IF v_cnt > 0 THEN + UPDATE COMENZI_ELEMENTE + SET CANTITATE = CANTITATE + p_cantitate + WHERE ID_COMANDA = p_id_comanda + AND ID_ARTICOL = p_id_articol + AND NVL(PTVA, 0) = NVL(p_ptva, 0) + AND PRET = p_pret + AND SIGN(CANTITATE) = SIGN(p_cantitate) + AND STERS = 0 + AND ROWNUM = 1; + ELSE + PACK_COMENZI.adauga_articol_comanda( + V_ID_COMANDA => p_id_comanda, + V_ID_ARTICOL => p_id_articol, + V_ID_POL => p_id_pol, + V_CANTITATE => p_cantitate, + V_PRET => p_pret, + V_ID_UTIL => p_id_util, + V_ID_SECTIE => p_id_sectie, + V_PTVA => p_ptva); + END IF; + END merge_or_insert_articol; + -- ================================================================ -- Procedura principala pentru importul unei comenzi -- ================================================================ @@ -439,15 +490,15 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS (v_discount_share / v_kit_comps(i_comp).cantitate_roa); BEGIN - PACK_COMENZI.adauga_articol_comanda( - V_ID_COMANDA => v_id_comanda, - V_ID_ARTICOL => v_kit_comps(i_comp).id_articol, - V_ID_POL => v_kit_comps(i_comp).id_pol_comp, - V_CANTITATE => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web, - V_PRET => v_pret_ajustat, - V_ID_UTIL => c_id_util, - V_ID_SECTIE => p_id_sectie, - V_PTVA => v_kit_comps(i_comp).ptva); + merge_or_insert_articol( + p_id_comanda => v_id_comanda, + p_id_articol => v_kit_comps(i_comp).id_articol, + p_id_pol => v_kit_comps(i_comp).id_pol_comp, + p_cantitate => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web, + p_pret => v_pret_ajustat, + p_id_util => c_id_util, + p_id_sectie => p_id_sectie, + p_ptva => v_kit_comps(i_comp).ptva); v_articole_procesate := v_articole_procesate + 1; EXCEPTION WHEN OTHERS THEN @@ -473,15 +524,15 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS FOR i_comp IN 1 .. v_kit_comps.COUNT LOOP IF v_kit_comps(i_comp).id_articol IS NOT NULL THEN BEGIN - PACK_COMENZI.adauga_articol_comanda( - V_ID_COMANDA => v_id_comanda, - V_ID_ARTICOL => v_kit_comps(i_comp).id_articol, - V_ID_POL => v_kit_comps(i_comp).id_pol_comp, - V_CANTITATE => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web, - V_PRET => v_kit_comps(i_comp).pret_cu_tva, - V_ID_UTIL => c_id_util, - V_ID_SECTIE => p_id_sectie, - V_PTVA => v_kit_comps(i_comp).ptva); + merge_or_insert_articol( + p_id_comanda => v_id_comanda, + p_id_articol => v_kit_comps(i_comp).id_articol, + p_id_pol => v_kit_comps(i_comp).id_pol_comp, + p_cantitate => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web, + p_pret => v_kit_comps(i_comp).pret_cu_tva, + p_id_util => c_id_util, + p_id_sectie => p_id_sectie, + p_ptva => v_kit_comps(i_comp).ptva); v_articole_procesate := v_articole_procesate + 1; EXCEPTION WHEN OTHERS THEN @@ -576,14 +627,14 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS END; BEGIN - PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda, - V_ID_ARTICOL => v_id_articol, - V_ID_POL => NVL(v_id_pol_articol, p_id_pol), - V_CANTITATE => v_cantitate_roa, - V_PRET => v_pret_unitar, - V_ID_UTIL => c_id_util, - V_ID_SECTIE => p_id_sectie, - V_PTVA => v_vat); + merge_or_insert_articol(p_id_comanda => v_id_comanda, + p_id_articol => v_id_articol, + p_id_pol => NVL(v_id_pol_articol, p_id_pol), + p_cantitate => v_cantitate_roa, + p_pret => v_pret_unitar, + p_id_util => c_id_util, + p_id_sectie => p_id_sectie, + p_ptva => v_vat); v_articole_procesate := v_articole_procesate + 1; EXCEPTION WHEN OTHERS THEN