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) <noreply@anthropic.com>
This commit is contained in:
@@ -170,7 +170,7 @@ async def download_products(
|
|||||||
"sku": p["sku"],
|
"sku": p["sku"],
|
||||||
"price": p.get("price", "0"),
|
"price": p.get("price", "0"),
|
||||||
"vat": p.get("vat", "19"),
|
"vat": p.get("vat", "19"),
|
||||||
"vat_included": p.get("vat_included", "1"),
|
"vat_included": str(p.get("vat_included", "1")),
|
||||||
"bundleItems": p.get("bundleItems", []),
|
"bundleItems": p.get("bundleItems", []),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,35 @@ async def get_price_sync_status() -> dict:
|
|||||||
await db.close()
|
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):
|
async def run_catalog_price_sync(run_id: str):
|
||||||
global _current_price_sync
|
global _current_price_sync
|
||||||
async with _price_sync_lock:
|
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)
|
await _finish_run(run_id, "completed", log_lines, products_total=0)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Index products by SKU for kit component lookup
|
||||||
|
products_by_sku = {p["sku"]: p for p in products}
|
||||||
|
|
||||||
# Connect to Oracle
|
# Connect to Oracle
|
||||||
conn = await asyncio.to_thread(database.get_oracle_connection)
|
conn = await asyncio.to_thread(database.get_oracle_connection)
|
||||||
try:
|
try:
|
||||||
@@ -136,16 +168,54 @@ async def run_catalog_price_sync(run_id: str):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
vat = float(product.get("vat", "19"))
|
vat = float(product.get("vat", "19"))
|
||||||
vat_included = product.get("vat_included", "1")
|
|
||||||
|
|
||||||
# Calculate price with TVA
|
# Calculate price with TVA (vat_included can be int 1 or str "1")
|
||||||
if vat_included == "1":
|
if str(product.get("vat_included", "1")) == "1":
|
||||||
price_cu_tva = price
|
price_cu_tva = price
|
||||||
else:
|
else:
|
||||||
price_cu_tva = price * (1 + vat / 100)
|
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:
|
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
|
continue
|
||||||
|
|
||||||
# Determine id_articol and policy
|
# Determine id_articol and policy
|
||||||
|
|||||||
@@ -468,7 +468,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
mapped_codmat_data = {}
|
mapped_codmat_data = {}
|
||||||
if mapped_skus_in_orders:
|
if mapped_skus_in_orders:
|
||||||
mapped_codmat_data = await asyncio.to_thread(
|
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
|
# Build id_map for mapped codmats and validate/ensure their prices
|
||||||
mapped_id_map = {}
|
mapped_id_map = {}
|
||||||
|
|||||||
@@ -364,14 +364,26 @@ def validate_and_ensure_prices_dual(codmats: set[str], id_pol_vanzare: int,
|
|||||||
return codmat_policy_map
|
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.
|
"""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}]}
|
Returns: {sku: [{"codmat": str, "id_articol": int, "cont": str|None, "cantitate_roa": float|None}]}
|
||||||
"""
|
"""
|
||||||
if not mapped_skus:
|
if not mapped_skus:
|
||||||
return {}
|
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 = {}
|
result = {}
|
||||||
sku_list = list(mapped_skus)
|
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]
|
batch = sku_list[i:i+500]
|
||||||
placeholders = ",".join([f":s{j}" for j in range(len(batch))])
|
placeholders = ",".join([f":s{j}" for j in range(len(batch))])
|
||||||
params = {f"s{j}": sku for j, sku in enumerate(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"""
|
cur.execute(f"""
|
||||||
SELECT at.sku, at.codmat, na.id_articol, na.cont, at.cantitate_roa
|
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
|
FROM ARTICOLE_TERTI at
|
||||||
JOIN NOM_ARTICOLE na ON na.codmat = at.codmat AND na.sters = 0 AND na.inactiv = 0
|
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 at.sku IN ({placeholders}) AND at.activ = 1 AND at.sters = 0
|
||||||
|
) WHERE rn = 1
|
||||||
""", params)
|
""", params)
|
||||||
for row in cur:
|
for row in cur:
|
||||||
sku = row[0]
|
sku = row[0]
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
-- END;
|
-- END;
|
||||||
-- 20.03.2026 - dual policy vanzare/productie, kit pricing distributed/separate_line, SKU→CODMAT via ARTICOLE_TERTI
|
-- 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 - 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
|
CREATE OR REPLACE PACKAGE PACK_IMPORT_COMENZI AS
|
||||||
|
|
||||||
@@ -175,6 +176,56 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
|||||||
RETURN v_result;
|
RETURN v_result;
|
||||||
END resolve_id_articol;
|
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
|
-- 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);
|
(v_discount_share / v_kit_comps(i_comp).cantitate_roa);
|
||||||
|
|
||||||
BEGIN
|
BEGIN
|
||||||
PACK_COMENZI.adauga_articol_comanda(
|
merge_or_insert_articol(
|
||||||
V_ID_COMANDA => v_id_comanda,
|
p_id_comanda => v_id_comanda,
|
||||||
V_ID_ARTICOL => v_kit_comps(i_comp).id_articol,
|
p_id_articol => v_kit_comps(i_comp).id_articol,
|
||||||
V_ID_POL => v_kit_comps(i_comp).id_pol_comp,
|
p_id_pol => v_kit_comps(i_comp).id_pol_comp,
|
||||||
V_CANTITATE => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web,
|
p_cantitate => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web,
|
||||||
V_PRET => v_pret_ajustat,
|
p_pret => v_pret_ajustat,
|
||||||
V_ID_UTIL => c_id_util,
|
p_id_util => c_id_util,
|
||||||
V_ID_SECTIE => p_id_sectie,
|
p_id_sectie => p_id_sectie,
|
||||||
V_PTVA => v_kit_comps(i_comp).ptva);
|
p_ptva => v_kit_comps(i_comp).ptva);
|
||||||
v_articole_procesate := v_articole_procesate + 1;
|
v_articole_procesate := v_articole_procesate + 1;
|
||||||
EXCEPTION
|
EXCEPTION
|
||||||
WHEN OTHERS THEN
|
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
|
FOR i_comp IN 1 .. v_kit_comps.COUNT LOOP
|
||||||
IF v_kit_comps(i_comp).id_articol IS NOT NULL THEN
|
IF v_kit_comps(i_comp).id_articol IS NOT NULL THEN
|
||||||
BEGIN
|
BEGIN
|
||||||
PACK_COMENZI.adauga_articol_comanda(
|
merge_or_insert_articol(
|
||||||
V_ID_COMANDA => v_id_comanda,
|
p_id_comanda => v_id_comanda,
|
||||||
V_ID_ARTICOL => v_kit_comps(i_comp).id_articol,
|
p_id_articol => v_kit_comps(i_comp).id_articol,
|
||||||
V_ID_POL => v_kit_comps(i_comp).id_pol_comp,
|
p_id_pol => v_kit_comps(i_comp).id_pol_comp,
|
||||||
V_CANTITATE => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web,
|
p_cantitate => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web,
|
||||||
V_PRET => v_kit_comps(i_comp).pret_cu_tva,
|
p_pret => v_kit_comps(i_comp).pret_cu_tva,
|
||||||
V_ID_UTIL => c_id_util,
|
p_id_util => c_id_util,
|
||||||
V_ID_SECTIE => p_id_sectie,
|
p_id_sectie => p_id_sectie,
|
||||||
V_PTVA => v_kit_comps(i_comp).ptva);
|
p_ptva => v_kit_comps(i_comp).ptva);
|
||||||
v_articole_procesate := v_articole_procesate + 1;
|
v_articole_procesate := v_articole_procesate + 1;
|
||||||
EXCEPTION
|
EXCEPTION
|
||||||
WHEN OTHERS THEN
|
WHEN OTHERS THEN
|
||||||
@@ -576,14 +627,14 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
|||||||
END;
|
END;
|
||||||
|
|
||||||
BEGIN
|
BEGIN
|
||||||
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
|
merge_or_insert_articol(p_id_comanda => v_id_comanda,
|
||||||
V_ID_ARTICOL => v_id_articol,
|
p_id_articol => v_id_articol,
|
||||||
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
|
p_id_pol => NVL(v_id_pol_articol, p_id_pol),
|
||||||
V_CANTITATE => v_cantitate_roa,
|
p_cantitate => v_cantitate_roa,
|
||||||
V_PRET => v_pret_unitar,
|
p_pret => v_pret_unitar,
|
||||||
V_ID_UTIL => c_id_util,
|
p_id_util => c_id_util,
|
||||||
V_ID_SECTIE => p_id_sectie,
|
p_id_sectie => p_id_sectie,
|
||||||
V_PTVA => v_vat);
|
p_ptva => v_vat);
|
||||||
v_articole_procesate := v_articole_procesate + 1;
|
v_articole_procesate := v_articole_procesate + 1;
|
||||||
EXCEPTION
|
EXCEPTION
|
||||||
WHEN OTHERS THEN
|
WHEN OTHERS THEN
|
||||||
|
|||||||
Reference in New Issue
Block a user