diff --git a/api/app/services/import_service.py b/api/app/services/import_service.py index 2000f48..5eaf8e7 100644 --- a/api/app/services/import_service.py +++ b/api/app/services/import_service.py @@ -201,7 +201,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, id_gestiune: int = None) -> dict: +def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None, id_gestiuni: list[int] = None) -> dict: """Import a single order into Oracle ROA. Returns dict with: @@ -339,6 +339,9 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se id_comanda = cur.var(oracledb.DB_TYPE_NUMBER) + # Convert list[int] to CSV string for Oracle VARCHAR2 param + id_gestiune_csv = ",".join(str(g) for g in id_gestiuni) if id_gestiuni else None + cur.callproc("PACK_IMPORT_COMENZI.importa_comanda", [ order_number, # p_nr_comanda_ext order_date, # p_data_comanda @@ -348,7 +351,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_gestiune_csv, # p_id_gestiune (CSV string) id_comanda # v_id_comanda (OUT) ]) diff --git a/api/app/services/sync_service.py b/api/app/services/sync_service.py index 2e13a31..88d8dcb 100644 --- a/api/app/services/sync_service.py +++ b/api/app/services/sync_service.py @@ -346,13 +346,18 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None 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}") + # Parse multi-gestiune CSV: "1,3" → [1, 3], "" → None + id_gestiune_raw = (app_settings.get("id_gestiune") or "").strip() + if id_gestiune_raw and id_gestiune_raw != "0": + id_gestiuni = [int(g) for g in id_gestiune_raw.split(",") if g.strip()] + else: + id_gestiuni = None # None = orice gestiune + logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNI={id_gestiuni}") + _log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNI={id_gestiuni}") # 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, id_gestiune) + validation = await asyncio.to_thread(validation_service.validate_skus, all_skus, conn, id_gestiuni) importable, skipped = validation_service.classify_orders(orders, validation) # ── Split importable into truly_importable vs already_in_roa ── @@ -580,7 +585,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, id_gestiune=id_gestiune + app_settings=app_settings, id_gestiuni=id_gestiuni ) # Build order items data for storage (R9) diff --git a/api/app/services/validation_service.py b/api/app/services/validation_service.py index 8361484..0b484da 100644 --- a/api/app/services/validation_service.py +++ b/api/app/services/validation_service.py @@ -28,9 +28,10 @@ def check_orders_in_roa(min_date, conn) -> dict: return existing -def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> dict[str, dict]: +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: @@ -40,8 +41,9 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> 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" + 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 = "" @@ -54,8 +56,9 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> 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 + if id_gestiuni: + for k, gid in enumerate(id_gestiuni): + params[f"g{k}"] = gid cur.execute(f""" SELECT codmat, id_articol, cont FROM ( @@ -84,11 +87,11 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> if own_conn: database.pool.release(conn) - logger.info(f"resolve_codmat_ids: {len(result)}/{len(codmats)} resolved (gestiune={id_gestiune})") + 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_gestiune: int = None) -> dict: +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) @@ -124,7 +127,7 @@ def validate_skus(skus: set[str], conn=None, id_gestiune: int = None) -> dict: # 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_id_map = resolve_codmat_ids(set(all_remaining), id_gestiuni, conn) direct = set(direct_id_map.keys()) else: direct_id_map = {} diff --git a/api/app/static/js/settings.js b/api/app/static/js/settings.js index 6531fae..7992f83 100644 --- a/api/app/static/js/settings.js +++ b/api/app/static/js/settings.js @@ -18,12 +18,13 @@ async function loadDropdowns() { const politici = await politiciRes.json(); const gestiuni = await gestiuniRes.json(); - const gestiuneEl = document.getElementById('settIdGestiune'); - if (gestiuneEl) { - gestiuneEl.innerHTML = ''; + const gestContainer = document.getElementById('settGestiuniContainer'); + if (gestContainer) { + gestContainer.innerHTML = ''; gestiuni.forEach(g => { - gestiuneEl.innerHTML += ``; + gestContainer.innerHTML += `
`; }); + if (gestiuni.length === 0) gestContainer.innerHTML = 'Nicio gestiune disponibilă'; } const sectieEl = document.getElementById('settIdSectie'); @@ -85,7 +86,15 @@ async function loadSettings() { if (el('settIdPol')) el('settIdPol').value = data.id_pol || ''; if (el('settIdPolProductie')) el('settIdPolProductie').value = data.id_pol_productie || ''; if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || ''; - if (el('settIdGestiune')) el('settIdGestiune').value = data.id_gestiune || ''; + // Multi-gestiune checkboxes + const gestVal = data.id_gestiune || ''; + if (gestVal) { + const selectedIds = gestVal.split(',').map(s => s.trim()); + selectedIds.forEach(id => { + const chk = document.getElementById('gestChk_' + id); + if (chk) chk.checked = true; + }); + } if (el('settGomagApiKey')) el('settGomagApiKey').value = data.gomag_api_key || ''; if (el('settGomagApiShop')) el('settGomagApiShop').value = data.gomag_api_shop || ''; if (el('settGomagDaysBack')) el('settGomagDaysBack').value = data.gomag_order_days_back || '7'; @@ -109,7 +118,7 @@ async function saveSettings() { id_pol: el('settIdPol')?.value?.trim() || '', id_pol_productie: el('settIdPolProductie')?.value?.trim() || '', id_sectie: el('settIdSectie')?.value?.trim() || '', - id_gestiune: el('settIdGestiune')?.value?.trim() || '', + id_gestiune: Array.from(document.querySelectorAll('#settGestiuniContainer input:checked')).map(c => c.value).join(','), gomag_api_key: el('settGomagApiKey')?.value?.trim() || '', gomag_api_shop: el('settGomagApiShop')?.value?.trim() || '', gomag_order_days_back: el('settGomagDaysBack')?.value?.trim() || '7', diff --git a/api/app/templates/settings.html b/api/app/templates/settings.html index dacbeef..b3ff169 100644 --- a/api/app/templates/settings.html +++ b/api/app/templates/settings.html @@ -39,10 +39,11 @@