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) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-17 13:10:20 +00:00
parent f74322beab
commit 7dd39f9712
4 changed files with 93 additions and 10 deletions

View File

@@ -317,6 +317,29 @@ def _get_articole_terti_for_skus(skus: set) -> dict:
return result 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}") @router.get("/api/sync/order/{order_number}")
async def order_detail(order_number: str): async def order_detail(order_number: str):
"""Get order detail with line items (R9), enriched with ARTICOLE_TERTI data.""" """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: if sku and sku in codmat_map:
item["codmat_details"] = codmat_map[sku] 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 # Enrich with invoice data
order = detail.get("order", {}) order = detail.get("order", {})
if order.get("factura_numar") and order.get("factura_data"): if order.get("factura_numar") and order.get("factura_data"):

View File

@@ -159,6 +159,24 @@ def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret
with database.pool.acquire() as conn: with database.pool.acquire() as conn:
with conn.cursor() as cur: 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 # Check for active duplicate
cur.execute(""" cur.execute("""
SELECT COUNT(*) FROM ARTICOLE_TERTI SELECT COUNT(*) FROM ARTICOLE_TERTI

View File

@@ -478,6 +478,9 @@ function renderCodmatCell(item) {
} }
if (item.codmat_details.length === 1) { if (item.codmat_details.length === 1) {
const d = item.codmat_details[0]; const d = item.codmat_details[0];
if (d.direct) {
return `<code>${esc(d.codmat)}</code> <span class="badge bg-secondary" style="font-size:0.6rem;vertical-align:middle">direct</span>`;
}
return `<code>${esc(d.codmat)}</code>`; return `<code>${esc(d.codmat)}</code>`;
} }
return item.codmat_details.map(d => return item.codmat_details.map(d =>
@@ -596,7 +599,7 @@ async function openDashOrderDetail(orderNumber) {
if (mobileContainer) { if (mobileContainer) {
mobileContainer.innerHTML = '<div class="detail-item-flat">' + items.map((item, idx) => { mobileContainer.innerHTML = '<div class="detail-item-flat">' + items.map((item, idx) => {
const codmatText = item.codmat_details?.length const codmatText = item.codmat_details?.length
? item.codmat_details.map(d => `<code>${esc(d.codmat)}</code>`).join(' ') ? item.codmat_details.map(d => `<code>${esc(d.codmat)}</code>${d.direct ? ' <span class="badge bg-secondary" style="font-size:0.55rem">direct</span>' : ''}`).join(' ')
: `<code>${esc(item.codmat || '')}</code>`; : `<code>${esc(item.codmat || '')}</code>`;
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
return `<div class="dif-item"> return `<div class="dif-item">
@@ -642,9 +645,27 @@ function openQuickMap(sku, productName, orderNumber, itemIdx) {
const container = document.getElementById('qmCodmatLines'); const container = document.getElementById('qmCodmatLines');
container.innerHTML = ''; 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 item = (window._detailItems || [])[itemIdx];
const details = item?.codmat_details; const details = item?.codmat_details;
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 = `<i class="bi bi-info-circle"></i> SKU = CODMAT direct in nomenclator (<code>${escHtml(details[0].codmat)}</code> — ${escHtml(details[0].denumire || '')}).<br><small class="text-muted">Poti suprascrie cu un alt CODMAT daca e necesar (ex: reambalare).</small>`;
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) { if (details && details.length > 0) {
details.forEach(d => { details.forEach(d => {
addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, procent: d.procent_pret, denumire: d.denumire }); addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, procent: d.procent_pret, denumire: d.denumire });
@@ -652,6 +673,7 @@ function openQuickMap(sku, productName, orderNumber, itemIdx) {
} else { } else {
addQmCodmatLine(); addQmCodmatLine();
} }
}
new bootstrap.Modal(document.getElementById('quickMapModal')).show(); new bootstrap.Modal(document.getElementById('quickMapModal')).show();
} }
@@ -762,7 +784,9 @@ async function saveQuickMapping() {
if (currentQmOrderNumber) openDashOrderDetail(currentQmOrderNumber); if (currentQmOrderNumber) openDashOrderDetail(currentQmOrderNumber);
loadDashOrders(); loadDashOrders();
} else { } 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) { } catch (err) {
alert('Eroare: ' + err.message); alert('Eroare: ' + err.message);

View File

@@ -192,11 +192,12 @@
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addQmCodmatLine()" style="font-size:0.8rem; padding:2px 10px"> <button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addQmCodmatLine()" style="font-size:0.8rem; padding:2px 10px">
+ CODMAT + CODMAT
</button> </button>
<div id="qmDirectInfo" class="alert alert-info mt-2" style="display:none; font-size:0.85rem; padding:8px 12px;"></div>
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div> <div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
<button type="button" class="btn btn-primary" onclick="saveQuickMapping()">Salveaza</button> <button type="button" class="btn btn-primary" id="qmSaveBtn" onclick="saveQuickMapping()">Salveaza</button>
</div> </div>
</div> </div>
</div> </div>
@@ -204,5 +205,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=15"></script> <script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=16"></script>
{% endblock %} {% endblock %}