feat(safety): price comparison on order detail
Add ROA price comparison to order detail modal — operators can now see if GoMag prices match Oracle before invoicing. Eliminates the #1 risk of invoicing with wrong prices. Backend: - New get_prices_for_order() in validation_service.py — batch Oracle query with dual-policy routing (sales/production by cont 341/345), PRETURI_CU_TVA handling, kit total calculation - Extend GET /api/sync/order/{orderNumber} with per-item pret_roa and order-level price_check summary - GET /api/dashboard/orders returns price_match=null (lightweight) Frontend: - Modal: price check badge (green/red/grey), "Pret GoMag" + "Pret ROA" columns, match dot per row, mismatch rows highlighted - Dashboard: price dot column (₽) in orders table - Mobile: inline mismatch indicator Cache-bust: shared.js?v=16, dashboard.js?v=28 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -469,7 +469,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
document.getElementById('detailIdPartener').textContent = '-';
|
||||
document.getElementById('detailIdAdresaFact').textContent = '-';
|
||||
document.getElementById('detailIdAdresaLivr').textContent = '-';
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="7" class="text-center">Se incarca...</td></tr>';
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="9" class="text-center">Se incarca...</td></tr>';
|
||||
document.getElementById('detailError').style.display = 'none';
|
||||
const receiptEl = document.getElementById('detailReceipt');
|
||||
if (receiptEl) receiptEl.innerHTML = '';
|
||||
@@ -479,6 +479,8 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
if (invInfo) invInfo.style.display = 'none';
|
||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||
if (mobileContainer) mobileContainer.innerHTML = '';
|
||||
const priceCheckEl = document.getElementById('detailPriceCheck');
|
||||
if (priceCheckEl) priceCheckEl.innerHTML = '';
|
||||
|
||||
const modalEl = document.getElementById('orderDetailModal');
|
||||
const existing = bootstrap.Modal.getInstance(modalEl);
|
||||
@@ -498,6 +500,20 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
document.getElementById('detailCustomer').textContent = order.customer_name || '-';
|
||||
document.getElementById('detailDate').textContent = fmtDate(order.order_date);
|
||||
document.getElementById('detailStatus').innerHTML = orderStatusBadge(order.status);
|
||||
|
||||
// Price check badge
|
||||
const priceCheckEl = document.getElementById('detailPriceCheck');
|
||||
if (priceCheckEl) {
|
||||
const pc = order.price_check;
|
||||
if (!pc || pc.oracle_available === false) {
|
||||
priceCheckEl.innerHTML = '<span class="badge" style="background:var(--cancelled-light);color:var(--text-muted)">Preturi ROA indisponibile</span>';
|
||||
} else if (pc.mismatches === 0) {
|
||||
priceCheckEl.innerHTML = '<span class="badge" style="background:var(--success-light);color:var(--success-text)">✓ Preturi OK</span>';
|
||||
} else {
|
||||
priceCheckEl.innerHTML = `<span class="badge" style="background:var(--error-light);color:var(--error-text)">${pc.mismatches} diferente de pret</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('detailIdComanda').textContent = order.id_comanda || '-';
|
||||
document.getElementById('detailIdPartener').textContent = order.id_partener || '-';
|
||||
document.getElementById('detailIdAdresaFact').textContent = order.id_adresa_facturare || '-';
|
||||
@@ -520,7 +536,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
|
||||
const items = data.items || [];
|
||||
if (items.length === 0) {
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="7" class="text-center text-muted">Niciun articol</td></tr>';
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="9" class="text-center text-muted">Niciun articol</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -537,6 +553,10 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
: `<code>${esc(item.codmat || '–')}</code>`;
|
||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0));
|
||||
const clickAttr = opts.onQuickMap ? `onclick="_sharedModalQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}',${idx})"` : '';
|
||||
const priceInfo = (order.price_check?.items || {})[idx];
|
||||
const priceMismatchHtml = priceInfo?.match === false
|
||||
? `<div class="text-danger" style="font-size:0.7rem">ROA: ${fmtNum(priceInfo.pret_roa)} lei</div>`
|
||||
: '';
|
||||
return `<div class="dif-item">
|
||||
<div class="dif-row">
|
||||
<span class="dif-sku${opts.onQuickMap ? ' dif-codmat-link' : ''}" ${clickAttr}>${esc(item.sku)}</span>
|
||||
@@ -548,6 +568,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
<span class="dif-val">${fmtNum(valoare)} lei</span>
|
||||
<span class="dif-vat text-muted" style="font-size:0.75rem">TVA ${item.vat != null ? Number(item.vat) : '?'}</span>
|
||||
</div>
|
||||
${priceMismatchHtml}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
@@ -601,14 +622,29 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
|
||||
let tableHtml = items.map((item, idx) => {
|
||||
const valoare = Number(item.price || 0) * Number(item.quantity || 0);
|
||||
return `<tr>
|
||||
const priceInfo = (order.price_check?.items || {})[idx];
|
||||
const pretRoaHtml = priceInfo?.pret_roa != null ? fmtNum(priceInfo.pret_roa) : '–';
|
||||
let matchDot, rowStyle;
|
||||
if (priceInfo == null) {
|
||||
matchDot = '<span class="dot dot-gray"></span>';
|
||||
rowStyle = '';
|
||||
} else if (priceInfo.match === false) {
|
||||
matchDot = '<span class="dot dot-red"></span>';
|
||||
rowStyle = ' style="background:var(--error-light)"';
|
||||
} else {
|
||||
matchDot = '<span class="dot dot-green"></span>';
|
||||
rowStyle = '';
|
||||
}
|
||||
return `<tr${rowStyle}>
|
||||
<td><code class="${opts.onQuickMap ? 'codmat-link' : ''}" ${clickAttrFn(item, idx)}>${esc(item.sku)}</code></td>
|
||||
<td>${esc(item.product_name || '-')}</td>
|
||||
<td>${renderCodmatCell(item)}</td>
|
||||
<td class="text-end">${item.quantity || 0}</td>
|
||||
<td class="text-end">${item.price != null ? fmtNum(item.price) : '-'}</td>
|
||||
<td class="text-end font-data">${item.price != null ? fmtNum(item.price) : '-'}</td>
|
||||
<td class="text-end font-data">${pretRoaHtml}</td>
|
||||
<td class="text-end">${item.vat != null ? Number(item.vat) : '-'}</td>
|
||||
<td class="text-end">${fmtNum(valoare)}</td>
|
||||
<td class="text-end font-data">${fmtNum(valoare)}</td>
|
||||
<td class="text-center">${matchDot}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
@@ -619,8 +655,10 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
tableHtml += `<tr class="table-light">
|
||||
<td></td><td class="text-muted">Transport</td>
|
||||
<td>${tCodmat ? '<code>' + esc(tCodmat) + '</code>' : ''}</td>
|
||||
<td class="text-end">1</td><td class="text-end">${fmtNum(order.delivery_cost)}</td>
|
||||
<td class="text-end">${tVat}</td><td class="text-end">${fmtNum(order.delivery_cost)}</td>
|
||||
<td class="text-end">1</td><td class="text-end font-data">${fmtNum(order.delivery_cost)}</td>
|
||||
<td></td>
|
||||
<td class="text-end">${tVat}</td><td class="text-end font-data">${fmtNum(order.delivery_cost)}</td>
|
||||
<td></td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
@@ -635,16 +673,20 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
if (amt > 0) tableHtml += `<tr class="table-light">
|
||||
<td></td><td class="text-muted">Discount</td>
|
||||
<td>${dCodmat ? '<code>' + esc(dCodmat) + '</code>' : ''}</td>
|
||||
<td class="text-end">\u20131</td><td class="text-end">${fmtNum(amt)}</td>
|
||||
<td class="text-end">${Number(rate)}</td><td class="text-end">\u2013${fmtNum(amt)}</td>
|
||||
<td class="text-end">\u20131</td><td class="text-end font-data">${fmtNum(amt)}</td>
|
||||
<td></td>
|
||||
<td class="text-end">${Number(rate)}</td><td class="text-end font-data">\u2013${fmtNum(amt)}</td>
|
||||
<td></td>
|
||||
</tr>`;
|
||||
});
|
||||
} else {
|
||||
tableHtml += `<tr class="table-light">
|
||||
<td></td><td class="text-muted">Discount</td>
|
||||
<td>${dCodmat ? '<code>' + esc(dCodmat) + '</code>' : ''}</td>
|
||||
<td class="text-end">\u20131</td><td class="text-end">${fmtNum(order.discount_total)}</td>
|
||||
<td class="text-end">-</td><td class="text-end">\u2013${fmtNum(order.discount_total)}</td>
|
||||
<td class="text-end">\u20131</td><td class="text-end font-data">${fmtNum(order.discount_total)}</td>
|
||||
<td></td>
|
||||
<td class="text-end">-</td><td class="text-end font-data">\u2013${fmtNum(order.discount_total)}</td>
|
||||
<td></td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user