fix(import): resolve correct id_articol for duplicate CODMATs + gestiune setting
Unified id_articol selection logic in Python (resolve_codmat_ids) and PL/SQL (resolve_id_articol): filters sters=0 AND inactiv=0, prefers article with stock in configured gestiune, falls back to MAX(id_articol). Eliminates mismatch where Python and PL/SQL could pick different id_articol for the same CODMAT, causing ORA-20000 price-not-found errors. - Add resolve_codmat_ids helper in validation_service.py (single batch query) - Refactor validate_skus/validate_prices/ensure_prices to use it - Add resolve_id_articol function in PL/SQL package body - Add p_id_gestiune parameter to importa_comanda (spec + body) - Add /api/settings/gestiuni endpoint and id_gestiune setting - Add gestiune dropdown in settings UI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -108,7 +108,7 @@ def build_articles_json(items, order=None, settings=None) -> str:
|
||||
return json.dumps(articles)
|
||||
|
||||
|
||||
def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None) -> dict:
|
||||
def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None, id_gestiune: int = None) -> dict:
|
||||
"""Import a single order into Oracle ROA.
|
||||
|
||||
Returns dict with:
|
||||
@@ -255,6 +255,7 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
addr_fact_id, # p_id_adresa_facturare
|
||||
id_pol, # p_id_pol
|
||||
id_sectie, # p_id_sectie
|
||||
id_gestiune, # p_id_gestiune
|
||||
id_comanda # v_id_comanda (OUT)
|
||||
])
|
||||
|
||||
|
||||
@@ -334,9 +334,17 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
# (can happen if previous import committed partially without rollback)
|
||||
await _fix_stale_error_orders(existing_map, run_id)
|
||||
|
||||
# Load app settings early (needed for id_gestiune in SKU validation)
|
||||
app_settings = await sqlite_service.get_app_settings()
|
||||
id_pol = id_pol or int(app_settings.get("id_pol") or 0) or settings.ID_POL
|
||||
id_sectie = id_sectie or int(app_settings.get("id_sectie") or 0) or settings.ID_SECTIE
|
||||
id_gestiune = int(app_settings.get("id_gestiune") or 0) or None # None = orice gestiune
|
||||
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNE={id_gestiune}")
|
||||
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNE={id_gestiune}")
|
||||
|
||||
# Step 2b: Validate SKUs (reuse same connection)
|
||||
all_skus = order_reader.get_all_skus(orders)
|
||||
validation = await asyncio.to_thread(validation_service.validate_skus, all_skus, conn)
|
||||
validation = await asyncio.to_thread(validation_service.validate_skus, all_skus, conn, id_gestiune)
|
||||
importable, skipped = validation_service.classify_orders(orders, validation)
|
||||
|
||||
# ── Split importable into truly_importable vs already_in_roa ──
|
||||
@@ -390,12 +398,6 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
)
|
||||
|
||||
# Step 2d: Pre-validate prices for importable articles
|
||||
# Load app settings (for transport/discount CODMAT config AND id_pol/id_sectie override)
|
||||
app_settings = await sqlite_service.get_app_settings()
|
||||
id_pol = id_pol or int(app_settings.get("id_pol") or 0) or settings.ID_POL
|
||||
id_sectie = id_sectie or int(app_settings.get("id_sectie") or 0) or settings.ID_SECTIE
|
||||
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
|
||||
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
|
||||
if id_pol and (truly_importable or already_in_roa):
|
||||
_update_progress("validation", "Validating prices...", 0, len(truly_importable))
|
||||
_log_line(run_id, "Validare preturi...")
|
||||
@@ -505,7 +507,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
result = await asyncio.to_thread(
|
||||
import_service.import_single_order,
|
||||
order, id_pol=id_pol, id_sectie=id_sectie,
|
||||
app_settings=app_settings
|
||||
app_settings=app_settings, id_gestiune=id_gestiune
|
||||
)
|
||||
|
||||
# Build order items data for storage (R9)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from .. import database
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -29,7 +28,67 @@ def check_orders_in_roa(min_date, conn) -> dict:
|
||||
return existing
|
||||
|
||||
|
||||
def validate_skus(skus: set[str], conn=None) -> dict:
|
||||
def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> dict[str, int]:
|
||||
"""Resolve CODMATs to best id_articol: prefers article with stock, then MAX(id_articol).
|
||||
Filters: sters=0 AND inactiv=0.
|
||||
Returns: {codmat: id_articol}
|
||||
"""
|
||||
if not codmats:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
codmat_list = list(codmats)
|
||||
|
||||
# Build stoc subquery dynamically for index optimization
|
||||
if id_gestiune is not None:
|
||||
stoc_filter = "AND s.id_gestiune = :id_gestiune"
|
||||
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_gestiune is not None:
|
||||
params["id_gestiune"] = id_gestiune
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT codmat, id_articol FROM (
|
||||
SELECT na.codmat, na.id_articol,
|
||||
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]] = row[1]
|
||||
finally:
|
||||
if own_conn:
|
||||
database.pool.release(conn)
|
||||
|
||||
logger.info(f"resolve_codmat_ids: {len(result)}/{len(codmats)} resolved (gestiune={id_gestiune})")
|
||||
return result
|
||||
|
||||
|
||||
def validate_skus(skus: set[str], conn=None, id_gestiune: int = None) -> dict:
|
||||
"""Validate a set of SKUs against Oracle.
|
||||
Returns: {mapped: set, direct: set, missing: set, direct_id_map: {codmat: id_articol}}
|
||||
- mapped: found in ARTICOLE_TERTI (active)
|
||||
@@ -41,8 +100,6 @@ def validate_skus(skus: set[str], conn=None) -> dict:
|
||||
return {"mapped": set(), "direct": set(), "missing": set(), "direct_id_map": {}}
|
||||
|
||||
mapped = set()
|
||||
direct = set()
|
||||
direct_id_map = {}
|
||||
sku_list = list(skus)
|
||||
|
||||
own_conn = conn is None
|
||||
@@ -64,18 +121,14 @@ def validate_skus(skus: set[str], conn=None) -> dict:
|
||||
for row in cur:
|
||||
mapped.add(row[0])
|
||||
|
||||
# Check NOM_ARTICOLE for remaining — also fetch id_articol
|
||||
remaining = [s for s in batch if s not in mapped]
|
||||
if remaining:
|
||||
placeholders2 = ",".join([f":n{j}" for j in range(len(remaining))])
|
||||
params2 = {f"n{j}": sku for j, sku in enumerate(remaining)}
|
||||
cur.execute(f"""
|
||||
SELECT codmat, id_articol FROM NOM_ARTICOLE
|
||||
WHERE codmat IN ({placeholders2}) AND sters = 0 AND inactiv = 0
|
||||
""", params2)
|
||||
for row in cur:
|
||||
direct.add(row[0])
|
||||
direct_id_map[row[0]] = row[1]
|
||||
# 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_gestiune, conn)
|
||||
direct = set(direct_id_map.keys())
|
||||
else:
|
||||
direct_id_map = {}
|
||||
direct = set()
|
||||
finally:
|
||||
if own_conn:
|
||||
database.pool.release(conn)
|
||||
@@ -113,37 +166,15 @@ def validate_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: di
|
||||
if not codmats:
|
||||
return {"has_price": set(), "missing_price": set()}
|
||||
|
||||
codmat_to_id = {}
|
||||
codmat_to_id = dict(direct_id_map) if direct_id_map else {}
|
||||
ids_with_price = set()
|
||||
codmat_list = list(codmats)
|
||||
|
||||
# Pre-populate from direct_id_map if available
|
||||
if direct_id_map:
|
||||
for cm in codmat_list:
|
||||
if cm in direct_id_map:
|
||||
codmat_to_id[cm] = direct_id_map[cm]
|
||||
|
||||
own_conn = conn is None
|
||||
if own_conn:
|
||||
conn = database.get_oracle_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# Step 1: Get ID_ARTICOL for CODMATs not already in direct_id_map
|
||||
remaining = [cm for cm in codmat_list if cm not in codmat_to_id]
|
||||
if remaining:
|
||||
for i in range(0, len(remaining), 500):
|
||||
batch = remaining[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)}
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT id_articol, codmat FROM NOM_ARTICOLE
|
||||
WHERE codmat IN ({placeholders})
|
||||
""", params)
|
||||
for row in cur:
|
||||
codmat_to_id[row[1]] = row[0]
|
||||
|
||||
# Step 2: Check which ID_ARTICOLs have a price in the policy
|
||||
# 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]
|
||||
@@ -191,28 +222,10 @@ def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict
|
||||
return
|
||||
id_valuta = row[0]
|
||||
|
||||
# Build batch params using direct_id_map where available
|
||||
# Build batch params using direct_id_map (already resolved via resolve_codmat_ids)
|
||||
batch_params = []
|
||||
need_lookup = []
|
||||
codmat_id_map = dict(direct_id_map) if direct_id_map else {}
|
||||
|
||||
for codmat in codmats:
|
||||
if codmat not in codmat_id_map:
|
||||
need_lookup.append(codmat)
|
||||
|
||||
# Batch lookup remaining CODMATs
|
||||
if need_lookup:
|
||||
for i in range(0, len(need_lookup), 500):
|
||||
batch = need_lookup[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)}
|
||||
cur.execute(f"""
|
||||
SELECT codmat, id_articol FROM NOM_ARTICOLE
|
||||
WHERE codmat IN ({placeholders}) AND sters = 0 AND inactiv = 0
|
||||
""", params)
|
||||
for r in cur:
|
||||
codmat_id_map[r[0]] = r[1]
|
||||
|
||||
for codmat in codmats:
|
||||
id_articol = codmat_id_map.get(codmat)
|
||||
if not id_articol:
|
||||
|
||||
Reference in New Issue
Block a user