Add ROA price comparison to order detail modal — operators can now see if GoMag prices match Oracle before invoicing. Eliminates the #1 risk of invoicing with wrong prices. Backend: - New get_prices_for_order() in validation_service.py — batch Oracle query with dual-policy routing (sales/production by cont 341/345), PRETURI_CU_TVA handling, kit total calculation - Extend GET /api/sync/order/{orderNumber} with per-item pret_roa and order-level price_check summary - GET /api/dashboard/orders returns price_match=null (lightweight) Frontend: - Modal: price check badge (green/red/grey), "Pret GoMag" + "Pret ROA" columns, match dot per row, mismatch rows highlighted - Dashboard: price dot column (₽) in orders table - Mobile: inline mismatch indicator Cache-bust: shared.js?v=16, dashboard.js?v=28 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
775 lines
31 KiB
Python
775 lines
31 KiB
Python
import logging
|
|
from .. import database
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def check_orders_in_roa(min_date, conn) -> dict:
|
|
"""Check which orders already exist in Oracle COMENZI by date range.
|
|
Returns: {comanda_externa: id_comanda} for all existing orders.
|
|
Much faster than IN-clause batching — single query using date index.
|
|
"""
|
|
if conn is None:
|
|
return {}
|
|
|
|
existing = {}
|
|
try:
|
|
with conn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT comanda_externa, id_comanda FROM COMENZI
|
|
WHERE data_comanda >= :min_date
|
|
AND comanda_externa IS NOT NULL AND sters = 0
|
|
""", {"min_date": min_date})
|
|
for row in cur:
|
|
existing[str(row[0])] = row[1]
|
|
except Exception as e:
|
|
logger.error(f"check_orders_in_roa failed: {e}")
|
|
|
|
logger.info(f"ROA order check (since {min_date}): {len(existing)} existing orders found")
|
|
return existing
|
|
|
|
|
|
def resolve_codmat_ids(codmats: set[str], id_gestiuni: list[int] = None, conn=None) -> dict[str, dict]:
|
|
"""Resolve CODMATs to best id_articol + cont: prefers article with stock, then MAX(id_articol).
|
|
Filters: sters=0 AND inactiv=0.
|
|
id_gestiuni: list of warehouse IDs to check stock in, or None for all.
|
|
Returns: {codmat: {"id_articol": int, "cont": str|None}}
|
|
"""
|
|
if not codmats:
|
|
return {}
|
|
|
|
result = {}
|
|
codmat_list = list(codmats)
|
|
|
|
# Build stoc subquery dynamically for index optimization
|
|
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 = ""
|
|
|
|
own_conn = conn is None
|
|
if own_conn:
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
for i in range(0, len(codmat_list), 500):
|
|
batch = codmat_list[i:i+500]
|
|
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
|
|
params = {f"c{j}": cm for j, cm in enumerate(batch)}
|
|
if id_gestiuni:
|
|
for k, gid in enumerate(id_gestiuni):
|
|
params[f"g{k}"] = gid
|
|
|
|
cur.execute(f"""
|
|
SELECT codmat, id_articol, cont FROM (
|
|
SELECT na.codmat, na.id_articol, na.cont,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY na.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 nom_articole na
|
|
WHERE na.codmat IN ({placeholders})
|
|
AND na.sters = 0 AND na.inactiv = 0
|
|
) WHERE rn = 1
|
|
""", params)
|
|
for row in cur:
|
|
result[row[0]] = {"id_articol": row[1], "cont": row[2]}
|
|
finally:
|
|
if own_conn:
|
|
database.pool.release(conn)
|
|
|
|
logger.info(f"resolve_codmat_ids: {len(result)}/{len(codmats)} resolved (gestiuni={id_gestiuni})")
|
|
return result
|
|
|
|
|
|
def validate_skus(skus: set[str], conn=None, id_gestiuni: list[int] = None) -> dict:
|
|
"""Validate a set of SKUs against Oracle.
|
|
Returns: {mapped: set, direct: set, missing: set, direct_id_map: {codmat: {"id_articol": int, "cont": str|None}}}
|
|
- mapped: found in ARTICOLE_TERTI (active)
|
|
- direct: found in NOM_ARTICOLE by codmat (not in ARTICOLE_TERTI)
|
|
- missing: not found anywhere
|
|
- direct_id_map: {codmat: {"id_articol": int, "cont": str|None}} for direct SKUs
|
|
"""
|
|
if not skus:
|
|
return {"mapped": set(), "direct": set(), "missing": set(), "direct_id_map": {}}
|
|
|
|
mapped = set()
|
|
sku_list = list(skus)
|
|
|
|
own_conn = conn is None
|
|
if own_conn:
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
# Check in batches of 500
|
|
for i in range(0, len(sku_list), 500):
|
|
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)}
|
|
|
|
# Check ARTICOLE_TERTI
|
|
cur.execute(f"""
|
|
SELECT DISTINCT sku FROM ARTICOLE_TERTI
|
|
WHERE sku IN ({placeholders}) AND activ = 1 AND sters = 0
|
|
""", params)
|
|
for row in cur:
|
|
mapped.add(row[0])
|
|
|
|
# Resolve remaining SKUs via resolve_codmat_ids (consistent id_articol selection)
|
|
all_remaining = [s for s in sku_list if s not in mapped]
|
|
if all_remaining:
|
|
direct_id_map = resolve_codmat_ids(set(all_remaining), id_gestiuni, conn)
|
|
direct = set(direct_id_map.keys())
|
|
else:
|
|
direct_id_map = {}
|
|
direct = set()
|
|
|
|
finally:
|
|
if own_conn:
|
|
database.pool.release(conn)
|
|
|
|
missing = skus - mapped - direct
|
|
|
|
logger.info(f"SKU validation: {len(mapped)} mapped, {len(direct)} direct, {len(missing)} missing")
|
|
return {"mapped": mapped, "direct": direct, "missing": missing,
|
|
"direct_id_map": direct_id_map}
|
|
|
|
def classify_orders(orders, validation_result):
|
|
"""Classify orders as importable or skipped based on SKU validation.
|
|
Returns: (importable_orders, skipped_orders)
|
|
Each skipped entry is a tuple of (order, list_of_missing_skus).
|
|
"""
|
|
ok_skus = validation_result["mapped"] | validation_result["direct"]
|
|
importable = []
|
|
skipped = []
|
|
|
|
for order in orders:
|
|
order_skus = {item.sku for item in order.items if item.sku}
|
|
order_missing = order_skus - ok_skus
|
|
|
|
if order_missing:
|
|
skipped.append((order, list(order_missing)))
|
|
else:
|
|
importable.append(order)
|
|
|
|
return importable, skipped
|
|
|
|
def _extract_id_map(direct_id_map: dict) -> dict:
|
|
"""Extract {codmat: id_articol} from either enriched or simple format."""
|
|
if not direct_id_map:
|
|
return {}
|
|
result = {}
|
|
for cm, val in direct_id_map.items():
|
|
if isinstance(val, dict):
|
|
result[cm] = val["id_articol"]
|
|
else:
|
|
result[cm] = val
|
|
return result
|
|
|
|
|
|
def validate_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict=None) -> dict:
|
|
"""Check which CODMATs have a price entry in CRM_POLITICI_PRET_ART for the given policy.
|
|
If direct_id_map is provided, skips the NOM_ARTICOLE lookup for those CODMATs.
|
|
Returns: {"has_price": set_of_codmats, "missing_price": set_of_codmats}
|
|
"""
|
|
if not codmats:
|
|
return {"has_price": set(), "missing_price": set()}
|
|
|
|
codmat_to_id = _extract_id_map(direct_id_map)
|
|
ids_with_price = set()
|
|
|
|
own_conn = conn is None
|
|
if own_conn:
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
# Check which ID_ARTICOLs have a price in the policy
|
|
id_list = list(codmat_to_id.values())
|
|
for i in range(0, len(id_list), 500):
|
|
batch = id_list[i:i+500]
|
|
placeholders = ",".join([f":a{j}" for j in range(len(batch))])
|
|
params = {f"a{j}": aid for j, aid in enumerate(batch)}
|
|
params["id_pol"] = id_pol
|
|
|
|
cur.execute(f"""
|
|
SELECT DISTINCT pa.ID_ARTICOL FROM CRM_POLITICI_PRET_ART pa
|
|
WHERE pa.ID_POL = :id_pol AND pa.ID_ARTICOL IN ({placeholders})
|
|
""", params)
|
|
for row in cur:
|
|
ids_with_price.add(row[0])
|
|
finally:
|
|
if own_conn:
|
|
database.pool.release(conn)
|
|
|
|
# Map back to CODMATs
|
|
has_price = {cm for cm, aid in codmat_to_id.items() if aid in ids_with_price}
|
|
missing_price = codmats - has_price
|
|
|
|
logger.info(f"Price validation (policy {id_pol}): {len(has_price)} have price, {len(missing_price)} missing price")
|
|
return {"has_price": has_price, "missing_price": missing_price}
|
|
|
|
def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict=None,
|
|
cota_tva: float = None):
|
|
"""Insert price 0 entries for CODMATs missing from the given price policy.
|
|
Uses batch executemany instead of individual INSERTs.
|
|
Relies on TRG_CRM_POLITICI_PRET_ART trigger for ID_POL_ART sequence.
|
|
cota_tva: VAT rate from settings (e.g. 21) — used for PROC_TVAV metadata.
|
|
"""
|
|
if not codmats:
|
|
return
|
|
|
|
proc_tvav = 1 + (cota_tva / 100) if cota_tva else 1.21
|
|
|
|
own_conn = conn is None
|
|
if own_conn:
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
# Get ID_VALUTA for this policy
|
|
cur.execute("""
|
|
SELECT ID_VALUTA FROM CRM_POLITICI_PRETURI WHERE ID_POL = :id_pol
|
|
""", {"id_pol": id_pol})
|
|
row = cur.fetchone()
|
|
if not row:
|
|
logger.error(f"Price policy {id_pol} not found in CRM_POLITICI_PRETURI")
|
|
return
|
|
id_valuta = row[0]
|
|
|
|
# Build batch params using direct_id_map (already resolved via resolve_codmat_ids)
|
|
batch_params = []
|
|
codmat_id_map = _extract_id_map(direct_id_map)
|
|
|
|
for codmat in codmats:
|
|
id_articol = codmat_id_map.get(codmat)
|
|
if not id_articol:
|
|
logger.warning(f"CODMAT {codmat} not found in NOM_ARTICOLE, skipping price insert")
|
|
continue
|
|
batch_params.append({
|
|
"id_pol": id_pol,
|
|
"id_articol": id_articol,
|
|
"id_valuta": id_valuta,
|
|
"proc_tvav": proc_tvav
|
|
})
|
|
|
|
if batch_params:
|
|
cur.executemany("""
|
|
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, 0, :id_valuta,
|
|
-3, SYSDATE, :proc_tvav, 0, 0)
|
|
""", batch_params)
|
|
logger.info(f"Batch inserted {len(batch_params)} price entries for policy {id_pol} (PROC_TVAV={proc_tvav})")
|
|
|
|
conn.commit()
|
|
finally:
|
|
if own_conn:
|
|
database.pool.release(conn)
|
|
|
|
logger.info(f"Ensure prices done: {len(codmats)} CODMATs processed for policy {id_pol}")
|
|
|
|
|
|
def validate_and_ensure_prices_dual(codmats: set[str], id_pol_vanzare: int,
|
|
id_pol_productie: int, conn, direct_id_map: dict,
|
|
cota_tva: float = 21) -> dict[str, int]:
|
|
"""Dual-policy price validation: assign each CODMAT to sales or production policy.
|
|
|
|
Logic:
|
|
1. Check both policies in one SQL
|
|
2. If article in one policy → use that
|
|
3. If article in BOTH → prefer id_pol_vanzare
|
|
4. If article in NEITHER → check cont: 341/345 → production, else → sales; insert price 0
|
|
|
|
Returns: codmat_policy_map = {codmat: assigned_id_pol}
|
|
"""
|
|
if not codmats:
|
|
return {}
|
|
|
|
codmat_policy_map = {}
|
|
id_map = _extract_id_map(direct_id_map)
|
|
|
|
# Collect all id_articol values we need to check
|
|
id_to_codmats = {} # {id_articol: [codmat, ...]}
|
|
for cm in codmats:
|
|
aid = id_map.get(cm)
|
|
if aid:
|
|
id_to_codmats.setdefault(aid, []).append(cm)
|
|
|
|
if not id_to_codmats:
|
|
return {}
|
|
|
|
# Query both policies in one SQL
|
|
existing = {} # {id_articol: set of id_pol}
|
|
id_list = list(id_to_codmats.keys())
|
|
with conn.cursor() as cur:
|
|
for i in range(0, len(id_list), 500):
|
|
batch = id_list[i:i+500]
|
|
placeholders = ",".join([f":a{j}" for j in range(len(batch))])
|
|
params = {f"a{j}": aid for j, aid in enumerate(batch)}
|
|
params["id_pol_v"] = id_pol_vanzare
|
|
params["id_pol_p"] = id_pol_productie
|
|
|
|
cur.execute(f"""
|
|
SELECT pa.ID_ARTICOL, pa.ID_POL FROM CRM_POLITICI_PRET_ART pa
|
|
WHERE pa.ID_POL IN (:id_pol_v, :id_pol_p) AND pa.ID_ARTICOL IN ({placeholders})
|
|
""", params)
|
|
for row in cur:
|
|
existing.setdefault(row[0], set()).add(row[1])
|
|
|
|
# Classify each codmat
|
|
missing_vanzare = set() # CODMATs needing price 0 in sales policy
|
|
missing_productie = set() # CODMATs needing price 0 in production policy
|
|
|
|
for aid, cms in id_to_codmats.items():
|
|
pols = existing.get(aid, set())
|
|
for cm in cms:
|
|
if pols:
|
|
if id_pol_vanzare in pols:
|
|
codmat_policy_map[cm] = id_pol_vanzare
|
|
elif id_pol_productie in pols:
|
|
codmat_policy_map[cm] = id_pol_productie
|
|
else:
|
|
# Not in any policy — classify by cont
|
|
info = direct_id_map.get(cm, {})
|
|
cont = info.get("cont", "") if isinstance(info, dict) else ""
|
|
cont_str = str(cont or "").strip()
|
|
if cont_str in ("341", "345"):
|
|
codmat_policy_map[cm] = id_pol_productie
|
|
missing_productie.add(cm)
|
|
else:
|
|
codmat_policy_map[cm] = id_pol_vanzare
|
|
missing_vanzare.add(cm)
|
|
|
|
# Ensure prices for missing articles in each policy
|
|
if missing_vanzare:
|
|
ensure_prices(missing_vanzare, id_pol_vanzare, conn, direct_id_map, cota_tva=cota_tva)
|
|
if missing_productie:
|
|
ensure_prices(missing_productie, id_pol_productie, conn, direct_id_map, cota_tva=cota_tva)
|
|
|
|
logger.info(
|
|
f"Dual-policy: {len(codmat_policy_map)} CODMATs assigned "
|
|
f"(vanzare={sum(1 for v in codmat_policy_map.values() if v == id_pol_vanzare)}, "
|
|
f"productie={sum(1 for v in codmat_policy_map.values() if v == id_pol_productie)})"
|
|
)
|
|
return codmat_policy_map
|
|
|
|
|
|
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)
|
|
|
|
with conn.cursor() as cur:
|
|
for i in range(0, len(sku_list), 500):
|
|
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 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]
|
|
if sku not in result:
|
|
result[sku] = []
|
|
result[sku].append({
|
|
"codmat": row[1],
|
|
"id_articol": row[2],
|
|
"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) == 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()
|
|
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:
|
|
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/bax SKUs (>1 component, or single component with cantitate_roa > 1)
|
|
kit_skus = {sku for sku, comps in mapped_codmat_data.items()
|
|
if len(comps) > 1 or (len(comps) == 1 and (comps[0].get("cantitate_roa") or 1) > 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
|
|
|
|
|
|
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)
|