From 7dd39f9712dea06468148078f6218b6e23471dcc Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Tue, 17 Mar 2026 13:10:20 +0000 Subject: [PATCH] feat(order-detail): show CODMAT for direct SKUs + mapping validations - Enrich order detail items with NOM_ARTICOLE data for direct SKUs (SKU=CODMAT) that have no ARTICOLE_TERTI entry - Validate CODMAT exists in nomenclator before saving mapping (400) - Block redundant self-mapping when SKU is already direct CODMAT (409) - Show "direct" badge in CODMAT column for direct SKUs - Show info alert in quick map modal for direct SKUs - Display backend validation errors inline in modal Co-Authored-By: Claude Opus 4.6 (1M context) --- api/app/routers/sync.py | 40 +++++++++++++++++++++++++++++ api/app/services/mapping_service.py | 18 +++++++++++++ api/app/static/js/dashboard.js | 40 +++++++++++++++++++++++------ api/app/templates/dashboard.html | 5 ++-- 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index d323687..dd5aeec 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -317,6 +317,29 @@ def _get_articole_terti_for_skus(skus: set) -> dict: return result +def _get_nom_articole_for_direct_skus(skus: set) -> dict: + """Query NOM_ARTICOLE for SKUs that exist directly as CODMAT (direct mapping).""" + from .. import database + result = {} + sku_list = list(skus) + conn = database.get_oracle_connection() + try: + 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)} + cur.execute(f""" + SELECT codmat, denumire FROM NOM_ARTICOLE + WHERE codmat IN ({placeholders}) AND sters = 0 AND inactiv = 0 + """, params) + for row in cur: + result[row[0]] = row[1] or "" + finally: + database.pool.release(conn) + return result + + @router.get("/api/sync/order/{order_number}") async def order_detail(order_number: str): """Get order detail with line items (R9), enriched with ARTICOLE_TERTI data.""" @@ -334,6 +357,23 @@ async def order_detail(order_number: str): if sku and sku in codmat_map: item["codmat_details"] = codmat_map[sku] + # Enrich direct SKUs (SKU=CODMAT in NOM_ARTICOLE, no ARTICOLE_TERTI entry) + direct_skus = {item["sku"] for item in items + if item.get("sku") and item.get("mapping_status") == "direct" + and not item.get("codmat_details")} + if direct_skus: + nom_map = await asyncio.to_thread(_get_nom_articole_for_direct_skus, direct_skus) + for item in items: + sku = item.get("sku") + if sku and sku in nom_map and not item.get("codmat_details"): + item["codmat_details"] = [{ + "codmat": sku, + "cantitate_roa": 1, + "procent_pret": 100, + "denumire": nom_map[sku], + "direct": True + }] + # Enrich with invoice data order = detail.get("order", {}) if order.get("factura_numar") and order.get("factura_data"): diff --git a/api/app/services/mapping_service.py b/api/app/services/mapping_service.py index 0f9b153..25bfc6a 100644 --- a/api/app/services/mapping_service.py +++ b/api/app/services/mapping_service.py @@ -159,6 +159,24 @@ def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret with database.pool.acquire() as conn: with conn.cursor() as cur: + # Validate CODMAT exists in NOM_ARTICOLE + cur.execute(""" + SELECT COUNT(*) FROM NOM_ARTICOLE + WHERE codmat = :codmat AND sters = 0 AND inactiv = 0 + """, {"codmat": codmat}) + if cur.fetchone()[0] == 0: + raise HTTPException(status_code=400, detail="CODMAT-ul nu exista in nomenclator") + + # Warn if SKU is already a direct CODMAT in NOM_ARTICOLE + if sku == codmat: + cur.execute(""" + SELECT COUNT(*) FROM NOM_ARTICOLE + WHERE codmat = :sku AND sters = 0 AND inactiv = 0 + """, {"sku": sku}) + if cur.fetchone()[0] > 0: + raise HTTPException(status_code=409, + detail="SKU-ul exista direct in nomenclator ca CODMAT, nu necesita mapare") + # Check for active duplicate cur.execute(""" SELECT COUNT(*) FROM ARTICOLE_TERTI diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js index 1936b1f..8748a29 100644 --- a/api/app/static/js/dashboard.js +++ b/api/app/static/js/dashboard.js @@ -478,6 +478,9 @@ function renderCodmatCell(item) { } if (item.codmat_details.length === 1) { const d = item.codmat_details[0]; + if (d.direct) { + return `${esc(d.codmat)} direct`; + } return `${esc(d.codmat)}`; } return item.codmat_details.map(d => @@ -596,7 +599,7 @@ async function openDashOrderDetail(orderNumber) { if (mobileContainer) { mobileContainer.innerHTML = '
' + items.map((item, idx) => { const codmatText = item.codmat_details?.length - ? item.codmat_details.map(d => `${esc(d.codmat)}`).join(' ') + ? item.codmat_details.map(d => `${esc(d.codmat)}${d.direct ? ' direct' : ''}`).join(' ') : `${esc(item.codmat || '–')}`; const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); return `
@@ -642,15 +645,34 @@ function openQuickMap(sku, productName, orderNumber, itemIdx) { const container = document.getElementById('qmCodmatLines'); container.innerHTML = ''; - // Pre-populate with existing codmat_details if available + // Check if this is a direct SKU (SKU=CODMAT in NOM_ARTICOLE) const item = (window._detailItems || [])[itemIdx]; const details = item?.codmat_details; - if (details && details.length > 0) { - details.forEach(d => { - addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, procent: d.procent_pret, denumire: d.denumire }); - }); - } else { + const isDirect = details?.length === 1 && details[0].direct === true; + const directInfo = document.getElementById('qmDirectInfo'); + const saveBtn = document.getElementById('qmSaveBtn'); + + if (isDirect) { + if (directInfo) { + directInfo.innerHTML = ` SKU = CODMAT direct in nomenclator (${escHtml(details[0].codmat)} — ${escHtml(details[0].denumire || '')}).
Poti suprascrie cu un alt CODMAT daca e necesar (ex: reambalare).`; + directInfo.style.display = ''; + } + if (saveBtn) { + saveBtn.textContent = 'Suprascrie mapare'; + } addQmCodmatLine(); + } else { + if (directInfo) directInfo.style.display = 'none'; + if (saveBtn) saveBtn.textContent = 'Salveaza'; + + // Pre-populate with existing codmat_details if available + if (details && details.length > 0) { + details.forEach(d => { + addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, procent: d.procent_pret, denumire: d.denumire }); + }); + } else { + addQmCodmatLine(); + } } new bootstrap.Modal(document.getElementById('quickMapModal')).show(); @@ -762,7 +784,9 @@ async function saveQuickMapping() { if (currentQmOrderNumber) openDashOrderDetail(currentQmOrderNumber); loadDashOrders(); } else { - alert('Eroare: ' + (data.error || 'Unknown')); + const msg = data.detail || data.error || 'Unknown'; + document.getElementById('qmPctWarning').textContent = msg; + document.getElementById('qmPctWarning').style.display = ''; } } catch (err) { alert('Eroare: ' + err.message); diff --git a/api/app/templates/dashboard.html b/api/app/templates/dashboard.html index 06f3efc..b5932e1 100644 --- a/api/app/templates/dashboard.html +++ b/api/app/templates/dashboard.html @@ -192,11 +192,12 @@ +
@@ -204,5 +205,5 @@ {% endblock %} {% block scripts %} - + {% endblock %}