feat(pricing): kit/pachet pricing with price list lookup, replace procent_pret

- Oracle PL/SQL: kit pricing logic with Mode A (distributed discount) and
  Mode B (separate discount line), dual policy support, PRETURI_CU_TVA flag
- Eliminate procent_pret from entire stack (Oracle, Python, JS, HTML)
- New settings: kit_pricing_mode, kit_discount_codmat, price_sync_enabled
- Settings UI: cards for Kit Pricing and Price Sync configuration
- Mappings UI: kit badges with lazy-loaded component prices from price list
- Price sync from orders: auto-update ROA prices when web prices differ
- Catalog price sync: new service to sync all GoMag product prices to ROA
- Kit component price validation: pre-check prices before import
- New endpoint GET /api/mappings/{sku}/prices for component price display
- New endpoints POST /api/price-sync/start, GET status, GET history
- DDL script 07_drop_procent_pret.sql (run after deploy confirmation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-19 22:29:18 +00:00
parent bedb93affe
commit 9e5901a8fb
17 changed files with 1313 additions and 268 deletions

View File

@@ -367,7 +367,7 @@ def validate_and_ensure_prices_dual(codmats: set[str], id_pol_vanzare: int,
def resolve_mapped_codmats(mapped_skus: set[str], conn) -> dict[str, list[dict]]:
"""For mapped SKUs, get their underlying CODMATs from ARTICOLE_TERTI + nom_articole.
Returns: {sku: [{"codmat": str, "id_articol": int, "cont": str|None}]}
Returns: {sku: [{"codmat": str, "id_articol": int, "cont": str|None, "cantitate_roa": float|None}]}
"""
if not mapped_skus:
return {}
@@ -382,7 +382,7 @@ def resolve_mapped_codmats(mapped_skus: set[str], conn) -> dict[str, list[dict]]
params = {f"s{j}": sku for j, sku in enumerate(batch)}
cur.execute(f"""
SELECT at.sku, at.codmat, na.id_articol, na.cont
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
@@ -394,8 +394,162 @@ def resolve_mapped_codmats(mapped_skus: set[str], conn) -> dict[str, list[dict]]
result[sku].append({
"codmat": row[1],
"id_articol": row[2],
"cont": row[3]
"cont": row[3],
"cantitate_roa": row[4]
})
logger.info(f"resolve_mapped_codmats: {len(result)} SKUs → {sum(len(v) for v in result.values())} CODMATs")
return result
def validate_kit_component_prices(mapped_codmat_data: dict, id_pol: int,
id_pol_productie: int = None, conn=None) -> dict:
"""Pre-validate that kit components have non-zero prices in crm_politici_pret_art.
Args:
mapped_codmat_data: {sku: [{"codmat", "id_articol", "cont"}, ...]} from resolve_mapped_codmats
id_pol: default sales price policy
id_pol_productie: production price policy (for cont 341/345)
Returns: {sku: [missing_codmats]} for SKUs with missing prices, {} if all OK
"""
missing = {}
own_conn = conn is None
if own_conn:
conn = database.get_oracle_connection()
try:
with conn.cursor() as cur:
for sku, components in mapped_codmat_data.items():
if len(components) <= 1:
continue # Not a kit
sku_missing = []
for comp in components:
cont = str(comp.get("cont") or "").strip()
if cont in ("341", "345") and id_pol_productie:
pol = id_pol_productie
else:
pol = id_pol
cur.execute("""
SELECT PRET FROM crm_politici_pret_art
WHERE id_pol = :pol AND id_articol = :id_art
""", {"pol": pol, "id_art": comp["id_articol"]})
row = cur.fetchone()
if not row or (row[0] is not None and row[0] == 0):
sku_missing.append(comp["codmat"])
if sku_missing:
missing[sku] = sku_missing
finally:
if own_conn:
database.pool.release(conn)
return missing
def compare_and_update_price(id_articol: int, id_pol: int, web_price_cu_tva: float,
conn, tolerance: float = 0.01) -> dict | None:
"""Compare web price with ROA price and update if different.
Handles PRETURI_CU_TVA flag per policy.
Returns: {"updated": bool, "old_price": float, "new_price": float, "codmat": str} or None if no price entry.
"""
with conn.cursor() as cur:
cur.execute("SELECT PRETURI_CU_TVA FROM CRM_POLITICI_PRETURI WHERE ID_POL = :pol", {"pol": id_pol})
pol_row = cur.fetchone()
if not pol_row:
return None
preturi_cu_tva = pol_row[0] # 1 or 0
cur.execute("""
SELECT PRET, PROC_TVAV, na.codmat
FROM crm_politici_pret_art pa
JOIN nom_articole na ON na.id_articol = pa.id_articol
WHERE pa.id_pol = :pol AND pa.id_articol = :id_art
""", {"pol": id_pol, "id_art": id_articol})
row = cur.fetchone()
if not row:
return None
pret_roa, proc_tvav, codmat = row[0], row[1], row[2]
proc_tvav = proc_tvav or 1.19
if preturi_cu_tva == 1:
pret_roa_cu_tva = pret_roa
else:
pret_roa_cu_tva = pret_roa * proc_tvav
if abs(pret_roa_cu_tva - web_price_cu_tva) <= tolerance:
return {"updated": False, "old_price": pret_roa_cu_tva, "new_price": web_price_cu_tva, "codmat": codmat}
if preturi_cu_tva == 1:
new_pret = web_price_cu_tva
else:
new_pret = round(web_price_cu_tva / proc_tvav, 4)
cur.execute("""
UPDATE crm_politici_pret_art SET PRET = :pret, DATAORA = SYSDATE
WHERE id_pol = :pol AND id_articol = :id_art
""", {"pret": new_pret, "pol": id_pol, "id_art": id_articol})
conn.commit()
return {"updated": True, "old_price": pret_roa_cu_tva, "new_price": web_price_cu_tva, "codmat": codmat}
def sync_prices_from_order(orders, mapped_codmat_data: dict, direct_id_map: dict,
codmat_policy_map: dict, id_pol: int,
id_pol_productie: int = None, conn=None,
settings: dict = None) -> list:
"""Sync prices from order items to ROA for direct/1:1 mappings.
Skips kit components and transport/discount CODMATs.
Returns: list of {"codmat", "old_price", "new_price"} for updated prices.
"""
if settings and settings.get("price_sync_enabled") != "1":
return []
transport_codmat = (settings or {}).get("transport_codmat", "")
discount_codmat = (settings or {}).get("discount_codmat", "")
kit_discount_codmat = (settings or {}).get("kit_discount_codmat", "")
skip_codmats = {transport_codmat, discount_codmat, kit_discount_codmat} - {""}
# Build set of kit SKUs (>1 component)
kit_skus = {sku for sku, comps in mapped_codmat_data.items() if len(comps) > 1}
updated = []
own_conn = conn is None
if own_conn:
conn = database.get_oracle_connection()
try:
for order in orders:
for item in order.items:
sku = item.sku
if not sku or sku in skip_codmats:
continue
if sku in kit_skus:
continue # Don't sync prices from kit orders
web_price = item.price # already with TVA
if not web_price or web_price <= 0:
continue
# Determine id_articol and price policy for this SKU
if sku in mapped_codmat_data and len(mapped_codmat_data[sku]) == 1:
# 1:1 mapping via ARTICOLE_TERTI
comp = mapped_codmat_data[sku][0]
id_articol = comp["id_articol"]
cantitate_roa = comp.get("cantitate_roa") or 1
web_price_per_unit = web_price / cantitate_roa if cantitate_roa != 1 else web_price
elif sku in (direct_id_map or {}):
info = direct_id_map[sku]
id_articol = info["id_articol"] if isinstance(info, dict) else info
web_price_per_unit = web_price
else:
continue
pol = codmat_policy_map.get(sku, id_pol)
result = compare_and_update_price(id_articol, pol, web_price_per_unit, conn)
if result and result["updated"]:
updated.append(result)
finally:
if own_conn:
database.pool.release(conn)
return updated