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:
@@ -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"):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,15 +645,34 @@ 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;
|
||||||
if (details && details.length > 0) {
|
const isDirect = details?.length === 1 && details[0].direct === true;
|
||||||
details.forEach(d => {
|
const directInfo = document.getElementById('qmDirectInfo');
|
||||||
addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, procent: d.procent_pret, denumire: d.denumire });
|
const saveBtn = document.getElementById('qmSaveBtn');
|
||||||
});
|
|
||||||
} else {
|
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();
|
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();
|
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);
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user