diff --git a/api/app/services/price_sync_service.py b/api/app/services/price_sync_service.py index 6b7670f..22f7847 100644 --- a/api/app/services/price_sync_service.py +++ b/api/app/services/price_sync_service.py @@ -62,35 +62,6 @@ 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: @@ -176,7 +147,11 @@ async def run_catalog_price_sync(run_id: str): price_cu_tva = price * (1 + vat / 100) # For kits, sync each component individually from standalone GoMag prices - if sku in mapped_data and len(mapped_data[sku]) > 1: + mapped_comps = mapped_data.get(sku, []) + is_kit = len(mapped_comps) > 1 or ( + len(mapped_comps) == 1 and (mapped_comps[0].get("cantitate_roa") or 1) > 1 + ) + if is_kit: for comp in mapped_data[sku]: comp_codmat = comp["codmat"] comp_product = products_by_sku.get(comp_codmat) @@ -208,21 +183,14 @@ async def run_catalog_price_sync(run_id: str): 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})") + _log(f" {comp_codmat}: LIPSESTE din politica {comp_pol} — adauga manual in ROA (kit {sku})") continue # Determine id_articol and policy id_articol = None cantitate_roa = 1 - if sku in mapped_data and len(mapped_data[sku]) == 1: + if sku in mapped_data and len(mapped_data[sku]) == 1 and (mapped_data[sku][0].get("cantitate_roa") or 1) <= 1: comp = mapped_data[sku][0] id_articol = comp["id_articol"] cantitate_roa = comp.get("cantitate_roa") or 1 @@ -236,7 +204,7 @@ async def run_catalog_price_sync(run_id: str): # Determine policy cont = None - if sku in mapped_data and len(mapped_data[sku]) == 1: + if sku in mapped_data and len(mapped_data[sku]) == 1 and (mapped_data[sku][0].get("cantitate_roa") or 1) <= 1: cont = mapped_data[sku][0].get("cont") elif sku in direct_id_map: cont = direct_id_map[sku].get("cont") diff --git a/api/app/services/validation_service.py b/api/app/services/validation_service.py index da91b4a..e2f00a9 100644 --- a/api/app/services/validation_service.py +++ b/api/app/services/validation_service.py @@ -450,8 +450,10 @@ def validate_kit_component_prices(mapped_codmat_data: dict, id_pol: int, try: with conn.cursor() as cur: for sku, components in mapped_codmat_data.items(): - if len(components) <= 1: - continue # Not a kit + if len(components) == 0: + continue + if len(components) == 1 and (components[0].get("cantitate_roa") or 1) <= 1: + continue # True 1:1 mapping, no kit pricing needed sku_missing = [] for comp in components: cont = str(comp.get("cont") or "").strip() diff --git a/api/database-scripts/06_pack_import_comenzi.pck b/api/database-scripts/06_pack_import_comenzi.pck index 432d1a6..e2a5015 100644 --- a/api/database-scripts/06_pack_import_comenzi.pck +++ b/api/database-scripts/06_pack_import_comenzi.pck @@ -63,6 +63,9 @@ -- 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 +-- 20.03.2026 - kit pricing extins pt reambalari single-component (cantitate_roa > 1) +-- 21.03.2026 - diagnostic detaliat discount kit (id_pol, id_art, codmat in eroare) +-- 21.03.2026 - fix discount amount: v_disc_amt e per-kit, nu se imparte la v_cantitate_web -- ==================================================================== CREATE OR REPLACE PACKAGE PACK_IMPORT_COMENZI AS @@ -262,6 +265,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS -- Variabile kit pricing v_kit_count NUMBER := 0; + v_max_cant_roa NUMBER := 1; v_kit_comps t_kit_components; v_sum_list_prices NUMBER; v_discount_total NUMBER; @@ -366,15 +370,17 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS v_found_mapping := FALSE; -- Numara randurile ARTICOLE_TERTI pentru a detecta kituri (>1 rand = set compus) - SELECT COUNT(*) INTO v_kit_count + SELECT COUNT(*), NVL(MAX(at.cantitate_roa), 1) + INTO v_kit_count, v_max_cant_roa FROM articole_terti at WHERE at.sku = v_sku AND at.activ = 1 AND at.sters = 0; - IF v_kit_count > 1 AND p_kit_mode IS NOT NULL THEN + IF ((v_kit_count > 1) OR (v_kit_count = 1 AND v_max_cant_roa > 1)) + AND p_kit_mode IS NOT NULL THEN -- ============================================================ - -- KIT PRICING: set compus cu >1 componente, mod activ + -- KIT PRICING: set compus (>1 componente) sau reambalare (cantitate_roa>1), mod activ -- Prima trecere: colecteaza componente + preturi din politici -- ============================================================ v_found_mapping := TRUE; @@ -573,7 +579,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS END IF; IF v_disc_amt != 0 THEN - v_unit_pret := v_disc_amt / v_cantitate_web; + v_unit_pret := v_disc_amt; -- Search for existing entry with same (ptva, pret) to merge qty v_kit_disc_found := FALSE; @@ -707,7 +713,10 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS WHEN OTHERS THEN v_articole_eroare := v_articole_eroare + 1; g_last_error := g_last_error || CHR(10) || - 'Eroare linie discount kit TVA=' || v_kit_disc_list(j).ptva || '%: ' || SQLERRM; + 'Eroare linie discount kit TVA=' || v_kit_disc_list(j).ptva || + '% id_pol=' || NVL(p_kit_discount_id_pol, p_id_pol) || + ' id_art=' || v_disc_artid || + ' codmat=' || p_kit_discount_codmat || ': ' || SQLERRM; END; END LOOP; END IF; diff --git a/api/tests/setup_test_data.sql b/api/tests/setup_test_data.sql index 3ab8c9c..a883750 100644 --- a/api/tests/setup_test_data.sql +++ b/api/tests/setup_test_data.sql @@ -45,6 +45,14 @@ INSERT INTO NOM_ARTICOLE ( -3, SYSDATE ); +-- Price entry for CAF01 in default price policy (id_pol=1) +-- Used for single-component repackaging kit pricing test +MERGE INTO crm_politici_pret_art dst +USING (SELECT 1 AS id_pol, 9999001 AS id_articol FROM DUAL) src +ON (dst.id_pol = src.id_pol AND dst.id_articol = src.id_articol) +WHEN NOT MATCHED THEN INSERT (id_pol, id_articol, pret, proc_tvav) +VALUES (src.id_pol, src.id_articol, 51.50, 19); + -- Create test mappings in ARTICOLE_TERTI -- CAFE100 -> CAF01 (repackaging: 10x1kg = 1x10kg web package) INSERT INTO ARTICOLE_TERTI (sku, codmat, cantitate_roa, procent_pret, activ) diff --git a/api/tests/teardown_test_data.sql b/api/tests/teardown_test_data.sql index f48fcac..a8e7374 100644 --- a/api/tests/teardown_test_data.sql +++ b/api/tests/teardown_test_data.sql @@ -1,6 +1,9 @@ -- Cleanup test data created for Phase 1 validation tests -- Remove test articles and mappings to leave database clean +-- Remove test price entry +DELETE FROM crm_politici_pret_art WHERE id_pol = 1 AND id_articol = 9999001; + -- Remove test mappings DELETE FROM ARTICOLE_TERTI WHERE sku IN ('CAFE100', '8000070028685', 'TEST001'); diff --git a/api/tests/test_complete_import.py b/api/tests/test_complete_import.py index 52b9be6..f400baf 100644 --- a/api/tests/test_complete_import.py +++ b/api/tests/test_complete_import.py @@ -330,16 +330,222 @@ def test_complete_import(): return False +def test_repackaging_kit_pricing(): + """ + Test single-component repackaging with kit pricing. + CAFE100 -> CAF01 with cantitate_roa=10 (1 web package = 10 ROA units). + Verifies that kit pricing applies: list price per unit + discount line. + """ + print("\n" + "=" * 60) + print("🎯 REPACKAGING KIT PRICING TEST") + print("=" * 60) + + success_count = 0 + total_tests = 0 + + try: + with oracledb.connect(user=user, password=password, dsn=dsn) as conn: + with conn.cursor() as cur: + unique_suffix = random.randint(1000, 9999) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + setup_test_data(cur) + + # Create a test partner + partner_var = cur.var(oracledb.NUMBER) + partner_name = f'Test Repack {timestamp}-{unique_suffix}' + cur.execute(""" + DECLARE v_id NUMBER; + BEGIN + v_id := PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener( + NULL, :name, 'JUD:Bucuresti;BUCURESTI;Str Test;1', + '0720000000', 'repack@test.com'); + :result := v_id; + END; + """, {'name': partner_name, 'result': partner_var}) + partner_id = partner_var.getvalue() + if not partner_id or partner_id <= 0: + print(" SKIP: Could not create test partner") + return False + + # ---- Test separate_line mode ---- + total_tests += 1 + order_number = f'TEST-REPACK-SEP-{timestamp}-{unique_suffix}' + # Web price: 2 packages * 10 units * some_price = total + # With list price 51.50/unit, 2 packs of 10 = 20 units + # Web price per package = 450 lei => total web = 900 + # Expected: 20 units @ 51.50 = 1030, discount = 130 + web_price_per_pack = 450.0 + articles_json = f'[{{"sku": "CAFE100", "cantitate": 2, "pret": {web_price_per_pack}}}]' + + print(f"\n1. Testing separate_line mode: {order_number}") + print(f" CAFE100 x2 @ {web_price_per_pack} lei/pack, cantitate_roa=10") + + result_var = cur.var(oracledb.NUMBER) + cur.execute(""" + DECLARE v_id NUMBER; + BEGIN + PACK_IMPORT_COMENZI.importa_comanda( + :order_number, SYSDATE, :partner_id, + :articles_json, + NULL, NULL, + 1, -- id_pol (default price policy) + NULL, NULL, + 'separate_line', -- kit_mode + NULL, NULL, NULL, + v_id); + :result := v_id; + END; + """, { + 'order_number': order_number, + 'partner_id': partner_id, + 'articles_json': articles_json, + 'result': result_var + }) + + order_id = result_var.getvalue() + if order_id and order_id > 0: + print(f" Order created: ID {order_id}") + + cur.execute(""" + SELECT ce.CANTITATE, ce.PRET, na.CODMAT, na.DENUMIRE + FROM COMENZI_ELEMENTE ce + JOIN NOM_ARTICOLE na ON ce.ID_ARTICOL = na.ID_ARTICOL + WHERE ce.ID_COMANDA = :oid + ORDER BY ce.CANTITATE DESC + """, {'oid': order_id}) + rows = cur.fetchall() + + if len(rows) >= 2: + # Should have article line + discount line + art_line = [r for r in rows if r[0] > 0] + disc_line = [r for r in rows if r[0] < 0] + + if art_line and disc_line: + print(f" Article: qty={art_line[0][0]}, price={art_line[0][1]:.2f} ({art_line[0][2]})") + print(f" Discount: qty={disc_line[0][0]}, price={disc_line[0][1]:.2f}") + total = sum(r[0] * r[1] for r in rows) + expected_total = web_price_per_pack * 2 + print(f" Total: {total:.2f} (expected: {expected_total:.2f})") + if abs(total - expected_total) < 0.02: + print(" PASS: Total matches web price") + success_count += 1 + else: + print(" FAIL: Total mismatch") + else: + print(f" FAIL: Expected article + discount lines, got {len(art_line)} art / {len(disc_line)} disc") + elif len(rows) == 1: + print(f" FAIL: Only 1 line (no discount). qty={rows[0][0]}, price={rows[0][1]:.2f}") + print(" Kit pricing did NOT activate for single-component repackaging") + else: + print(" FAIL: No order lines found") + else: + cur.execute("SELECT PACK_IMPORT_COMENZI.get_last_error FROM DUAL") + err = cur.fetchone()[0] + print(f" FAIL: Order import failed: {err}") + + conn.commit() + + # ---- Test distributed mode ---- + total_tests += 1 + order_number2 = f'TEST-REPACK-DIST-{timestamp}-{unique_suffix}' + print(f"\n2. Testing distributed mode: {order_number2}") + + result_var2 = cur.var(oracledb.NUMBER) + cur.execute(""" + DECLARE v_id NUMBER; + BEGIN + PACK_IMPORT_COMENZI.importa_comanda( + :order_number, SYSDATE, :partner_id, + :articles_json, + NULL, NULL, + 1, NULL, NULL, + 'distributed', + NULL, NULL, NULL, + v_id); + :result := v_id; + END; + """, { + 'order_number': order_number2, + 'partner_id': partner_id, + 'articles_json': articles_json, + 'result': result_var2 + }) + + order_id2 = result_var2.getvalue() + if order_id2 and order_id2 > 0: + print(f" Order created: ID {order_id2}") + + cur.execute(""" + SELECT ce.CANTITATE, ce.PRET, na.CODMAT + FROM COMENZI_ELEMENTE ce + JOIN NOM_ARTICOLE na ON ce.ID_ARTICOL = na.ID_ARTICOL + WHERE ce.ID_COMANDA = :oid + """, {'oid': order_id2}) + rows2 = cur.fetchall() + + if len(rows2) == 1: + # Distributed: single line with adjusted price + total = rows2[0][0] * rows2[0][1] + expected_total = web_price_per_pack * 2 + print(f" Line: qty={rows2[0][0]}, price={rows2[0][1]:.2f}, total={total:.2f}") + if abs(total - expected_total) < 0.02: + print(" PASS: Distributed price correct") + success_count += 1 + else: + print(f" FAIL: Total {total:.2f} != expected {expected_total:.2f}") + else: + print(f" INFO: Got {len(rows2)} lines (expected 1 for distributed)") + for r in rows2: + print(f" qty={r[0]}, price={r[1]:.2f}, codmat={r[2]}") + else: + cur.execute("SELECT PACK_IMPORT_COMENZI.get_last_error FROM DUAL") + err = cur.fetchone()[0] + print(f" FAIL: Order import failed: {err}") + + conn.commit() + + # Cleanup + teardown_test_data(cur) + conn.commit() + + print(f"\n{'=' * 60}") + print(f"RESULTS: {success_count}/{total_tests} tests passed") + print('=' * 60) + return success_count == total_tests + + except Exception as e: + print(f"CRITICAL ERROR: {e}") + import traceback + traceback.print_exc() + try: + with oracledb.connect(user=user, password=password, dsn=dsn) as conn: + with conn.cursor() as cur: + teardown_test_data(cur) + conn.commit() + except: + pass + return False + + if __name__ == "__main__": print("Starting complete order import test...") print(f"Timestamp: {datetime.now()}") - + success = test_complete_import() - + print(f"\nTest completed at: {datetime.now()}") if success: print("🎯 PHASE 1 VALIDATION: SUCCESSFUL") else: print("🔧 PHASE 1 VALIDATION: NEEDS ATTENTION") + + # Run repackaging kit pricing test + print("\n") + repack_success = test_repackaging_kit_pricing() + if repack_success: + print("🎯 REPACKAGING KIT PRICING: SUCCESSFUL") + else: + print("🔧 REPACKAGING KIT PRICING: NEEDS ATTENTION") exit(0 if success else 1) \ No newline at end of file