fix(price): remove baseprice detection, use directional price match
baseprice > price was wrongly treated as "quantity discount" — it's just GoMag's promotional price. Now: price_gomag <= pret_roa is always OK, only flag when GoMag charges MORE than ROA. Reset cached price_match at startup for re-evaluation. Fix dashboard dot color for mismatches. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
7
TODOS.md
7
TODOS.md
@@ -27,10 +27,3 @@
|
|||||||
**Effort:** S (human: ~2h / CC: ~10min)
|
**Effort:** S (human: ~2h / CC: ~10min)
|
||||||
**Context:** TIER 2 matched county+city without street, reusing VFP-era addresses with wrong streets. After removal (2026-04-06), new imports create correct addresses. Old wrong addresses stay. Could identify them by: address has id_loc but no linked order rows, and was last modified before 2026-04-06.
|
**Context:** TIER 2 matched county+city without street, reusing VFP-era addresses with wrong streets. After removal (2026-04-06), new imports create correct addresses. Old wrong addresses stay. Could identify them by: address has id_loc but no linked order rows, and was last modified before 2026-04-06.
|
||||||
**Depends on:** TIER 2 removal deployed and verified.
|
**Depends on:** TIER 2 removal deployed and verified.
|
||||||
|
|
||||||
## P3: Extract match-column badge styles to CSS classes
|
|
||||||
**What:** Replace inline styles on Kit and Disc. badges (in shared.js) with CSS classes (e.g., `.match-badge-kit`, `.match-badge-disc`).
|
|
||||||
**Why:** Currently both badges use identical inline `style="background:var(--X-light);color:var(--X-text);font-size:10px;padding:2px 6px"`. If a 3rd badge type appears, inline styles become a maintenance burden.
|
|
||||||
**Effort:** XS (human: ~30min / CC: ~5min)
|
|
||||||
**Context:** Low priority. Two inline-styled badges is fine. Trigger: when a 3rd badge type is needed in the price match column.
|
|
||||||
**Depends on:** Quantity discount feature shipped.
|
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ async def backfill_price_match():
|
|||||||
from ..database import get_sqlite
|
from ..database import get_sqlite
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
|
# Reset all cached price_match to re-evaluate with current logic
|
||||||
|
await db.execute("UPDATE orders SET price_match = NULL WHERE price_match IS NOT NULL")
|
||||||
|
await db.commit()
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute("""
|
||||||
SELECT order_number FROM orders
|
SELECT order_number FROM orders
|
||||||
WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED')
|
WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED')
|
||||||
@@ -465,9 +468,6 @@ async def order_detail(order_number: str):
|
|||||||
item["price_match"] = pi.get("match")
|
item["price_match"] = pi.get("match")
|
||||||
if pi.get("kit"):
|
if pi.get("kit"):
|
||||||
item["kit"] = True
|
item["kit"] = True
|
||||||
if pi.get("quantity_discount"):
|
|
||||||
item["quantity_discount"] = True
|
|
||||||
item["baseprice"] = pi.get("baseprice")
|
|
||||||
order_price_check = price_data.get("summary", {})
|
order_price_check = price_data.get("summary", {})
|
||||||
# Cache price_match in SQLite if changed
|
# Cache price_match in SQLite if changed
|
||||||
if order_price_check.get("oracle_available") is not False:
|
if order_price_check.get("oracle_available") is not False:
|
||||||
|
|||||||
@@ -714,13 +714,6 @@ def get_prices_for_order(items: list[dict], app_settings: dict, conn=None) -> di
|
|||||||
result_items[idx]["kit"] = True
|
result_items[idx]["kit"] = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Quantity discount: baseprice > price means GoMag applied a volume discount
|
|
||||||
baseprice = float(item.get("baseprice") or 0)
|
|
||||||
if baseprice > 0 and baseprice > pret_gomag + 0.01:
|
|
||||||
result_items[idx]["quantity_discount"] = True
|
|
||||||
result_items[idx]["baseprice"] = baseprice
|
|
||||||
continue
|
|
||||||
|
|
||||||
pret_roa_total = 0.0
|
pret_roa_total = 0.0
|
||||||
all_resolved = True
|
all_resolved = True
|
||||||
|
|
||||||
@@ -766,7 +759,7 @@ def get_prices_for_order(items: list[dict], app_settings: dict, conn=None) -> di
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
pret_roa = round(pret_roa_total, 4)
|
pret_roa = round(pret_roa_total, 4)
|
||||||
match = abs(pret_gomag - pret_roa) < 0.01
|
match = pret_gomag <= pret_roa + 0.01
|
||||||
result_items[idx]["pret_roa"] = pret_roa
|
result_items[idx]["pret_roa"] = pret_roa
|
||||||
result_items[idx]["match"] = match
|
result_items[idx]["match"] = match
|
||||||
checked += 1
|
checked += 1
|
||||||
|
|||||||
@@ -510,7 +510,7 @@ function diffDots(o, mobile) {
|
|||||||
if (o.address_mismatch===1)
|
if (o.address_mismatch===1)
|
||||||
d += `<span style="${s};background:var(--info)" title="Adresa diferita"></span>`;
|
d += `<span style="${s};background:var(--info)" title="Adresa diferita"></span>`;
|
||||||
if (o.price_match===false)
|
if (o.price_match===false)
|
||||||
d += `<span style="${s};background:var(--success)" title="Pret diferit"></span>`;
|
d += `<span style="${s};background:var(--error)" title="Pret GoMag > ROA"></span>`;
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -614,11 +614,9 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
|||||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0));
|
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 clickAttr = opts.onQuickMap ? `onclick="_sharedModalQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}',${idx})"` : '';
|
||||||
const priceInfo = { pret_roa: item.pret_roa, match: item.price_match };
|
const priceInfo = { pret_roa: item.pret_roa, match: item.price_match };
|
||||||
const priceMismatchHtml = item.quantity_discount
|
const priceMismatchHtml = priceInfo.match === false
|
||||||
? `<div style="font-size:0.7rem"><span class="badge" style="background:var(--warning-light);color:var(--warning-text);font-size:9px;padding:1px 5px" aria-label="Discount aplicat, pret catalog ${fmtNum(item.baseprice)} lei">Disc.</span> <span style="text-decoration:line-through;opacity:0.6">${fmtNum(item.baseprice)}</span> ${fmtNum(item.price)} lei</div>`
|
|
||||||
: (priceInfo.match === false
|
|
||||||
? `<div class="text-danger" style="font-size:0.7rem">ROA: ${fmtNum(priceInfo.pret_roa)} lei</div>`
|
? `<div class="text-danger" style="font-size:0.7rem">ROA: ${fmtNum(priceInfo.pret_roa)} lei</div>`
|
||||||
: '');
|
: '';
|
||||||
return `<div class="dif-item">
|
return `<div class="dif-item">
|
||||||
<div class="dif-row">
|
<div class="dif-row">
|
||||||
<span class="dif-sku${opts.onQuickMap ? ' dif-codmat-link' : ''}" ${clickAttr}>${esc(item.sku)}</span>
|
<span class="dif-sku${opts.onQuickMap ? ' dif-codmat-link' : ''}" ${clickAttr}>${esc(item.sku)}</span>
|
||||||
@@ -690,10 +688,6 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
|||||||
if (item.kit) {
|
if (item.kit) {
|
||||||
matchDot = '<span class="badge" style="background:var(--info-light);color:var(--info-text);font-size:10px;padding:2px 6px">Kit</span>';
|
matchDot = '<span class="badge" style="background:var(--info-light);color:var(--info-text);font-size:10px;padding:2px 6px">Kit</span>';
|
||||||
rowStyle = '';
|
rowStyle = '';
|
||||||
} else if (item.quantity_discount) {
|
|
||||||
const bpTitle = item.baseprice ? `Catalog: ${fmtNum(item.baseprice)} lei` : 'Discount GoMag';
|
|
||||||
matchDot = `<span class="badge" style="background:var(--warning-light);color:var(--warning-text);font-size:10px;padding:2px 6px" title="${bpTitle}" aria-label="Discount aplicat, pret catalog ${fmtNum(item.baseprice)} lei">Disc.</span>`;
|
|
||||||
rowStyle = '';
|
|
||||||
} else if (priceInfo.pret_roa == null && priceInfo.match == null) {
|
} else if (priceInfo.pret_roa == null && priceInfo.match == null) {
|
||||||
matchDot = '<span class="dot dot-gray"></span>';
|
matchDot = '<span class="dot dot-gray"></span>';
|
||||||
rowStyle = '';
|
rowStyle = '';
|
||||||
@@ -709,7 +703,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
|||||||
<td>${esc(item.product_name || '-')}</td>
|
<td>${esc(item.product_name || '-')}</td>
|
||||||
<td>${renderCodmatCell(item)}</td>
|
<td>${renderCodmatCell(item)}</td>
|
||||||
<td class="text-end">${item.quantity || 0}</td>
|
<td class="text-end">${item.quantity || 0}</td>
|
||||||
<td class="text-end font-data">${item.quantity_discount && item.baseprice ? `<span style="text-decoration:line-through;opacity:0.6;font-size:0.8em">${fmtNum(item.baseprice)}</span> ${fmtNum(item.price)}` : (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 font-data">${pretRoaHtml}</td>
|
||||||
<td class="text-end">${item.vat != null ? Number(item.vat) : '-'}</td>
|
<td class="text-end">${item.vat != null ? Number(item.vat) : '-'}</td>
|
||||||
<td class="text-end font-data">${fmtNum(valoare)}</td>
|
<td class="text-end font-data">${fmtNum(valoare)}</td>
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
|
|
||||||
<script>window.ROOT_PATH = "{{ rp }}";</script>
|
<script>window.ROOT_PATH = "{{ rp }}";</script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="{{ rp }}/static/js/shared.js?v=34"></script>
|
<script src="{{ rp }}/static/js/shared.js?v=35"></script>
|
||||||
<script>
|
<script>
|
||||||
// Dark mode toggle
|
// Dark mode toggle
|
||||||
function toggleDarkMode() {
|
function toggleDarkMode() {
|
||||||
|
|||||||
@@ -114,5 +114,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=43"></script>
|
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=44"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -636,49 +636,39 @@ class TestGetPricesForOrderCantitateRoa:
|
|||||||
assert result["summary"]["mismatches"] == 0
|
assert result["summary"]["mismatches"] == 0
|
||||||
|
|
||||||
|
|
||||||
class TestGetPricesForOrderQuantityDiscount:
|
class TestGetPricesDirectionalMatch:
|
||||||
"""baseprice > price means GoMag applied a discount — skip price comparison."""
|
"""Price match is directional: gomag <= roa is OK, gomag > roa is mismatch."""
|
||||||
|
|
||||||
def test_discount_detected(self):
|
def test_gomag_below_roa_is_match(self):
|
||||||
"""baseprice > price: quantity_discount=True, match=None, mismatches=0."""
|
"""GoMag price lower than ROA (promo/volume discount) → match=True."""
|
||||||
items = [{"sku": "SKU-DISC", "price": 28.59, "baseprice": 33.0, "quantity": 48,
|
items = [{"sku": "SKU-DISC", "price": 28.59, "baseprice": 33.0, "quantity": 48,
|
||||||
"codmat_details": [{"codmat": "COD1", "cantitate_roa": 1,
|
"codmat_details": [{"codmat": "COD1", "cantitate_roa": 1,
|
||||||
"id_articol": 100, "cont": "345"}]}]
|
"id_articol": 100, "cont": "345"}]}]
|
||||||
conn = _mock_oracle_conn(pol_cu_tva=True, price_map={100: (28.99, 1.19)})
|
conn = _mock_oracle_conn(pol_cu_tva=True, price_map={100: (28.99, 1.19)})
|
||||||
result = get_prices_for_order(items, {"id_pol": "1"}, conn=conn)
|
result = get_prices_for_order(items, {"id_pol": "1"}, conn=conn)
|
||||||
assert result["items"][0].get("quantity_discount") is True
|
assert result["items"][0]["match"] is True
|
||||||
assert result["items"][0]["match"] is None
|
assert result["items"][0]["pret_roa"] == 28.99
|
||||||
assert result["summary"]["mismatches"] == 0
|
assert result["summary"]["mismatches"] == 0
|
||||||
|
|
||||||
def test_no_discount_when_baseprice_equals_price(self):
|
def test_gomag_above_roa_is_mismatch(self):
|
||||||
"""baseprice == price: normal comparison."""
|
"""GoMag price higher than ROA → match=False, mismatch counted."""
|
||||||
items = [{"sku": "SKU-FULL", "price": 28.99, "baseprice": 28.99, "quantity": 1,
|
items = [{"sku": "SKU-HIGH", "price": 30.00, "quantity": 1,
|
||||||
"codmat_details": [{"codmat": "COD2", "cantitate_roa": 1,
|
"codmat_details": [{"codmat": "COD2", "cantitate_roa": 1,
|
||||||
"id_articol": 200, "cont": "345"}]}]
|
"id_articol": 200, "cont": "345"}]}]
|
||||||
conn = _mock_oracle_conn(pol_cu_tva=True, price_map={200: (28.99, 1.19)})
|
conn = _mock_oracle_conn(pol_cu_tva=True, price_map={200: (28.99, 1.19)})
|
||||||
result = get_prices_for_order(items, {"id_pol": "1"}, conn=conn)
|
result = get_prices_for_order(items, {"id_pol": "1"}, conn=conn)
|
||||||
assert result["items"][0].get("quantity_discount") is not True
|
assert result["items"][0]["match"] is False
|
||||||
assert result["items"][0]["match"] is True
|
assert result["summary"]["mismatches"] == 1
|
||||||
|
|
||||||
def test_no_discount_when_baseprice_missing(self):
|
def test_gomag_equals_roa_is_match(self):
|
||||||
"""baseprice=0 (missing): normal comparison."""
|
"""GoMag price equals ROA → match=True."""
|
||||||
items = [{"sku": "SKU-OLD", "price": 28.99, "quantity": 1,
|
items = [{"sku": "SKU-FULL", "price": 28.99, "quantity": 1,
|
||||||
"codmat_details": [{"codmat": "COD3", "cantitate_roa": 1,
|
"codmat_details": [{"codmat": "COD3", "cantitate_roa": 1,
|
||||||
"id_articol": 300, "cont": "345"}]}]
|
"id_articol": 300, "cont": "345"}]}]
|
||||||
conn = _mock_oracle_conn(pol_cu_tva=True, price_map={300: (28.99, 1.19)})
|
conn = _mock_oracle_conn(pol_cu_tva=True, price_map={300: (28.99, 1.19)})
|
||||||
result = get_prices_for_order(items, {"id_pol": "1"}, conn=conn)
|
result = get_prices_for_order(items, {"id_pol": "1"}, conn=conn)
|
||||||
assert result["items"][0].get("quantity_discount") is not True
|
|
||||||
assert result["items"][0]["match"] is True
|
assert result["items"][0]["match"] is True
|
||||||
|
assert result["summary"]["mismatches"] == 0
|
||||||
def test_kit_takes_precedence_over_discount(self):
|
|
||||||
"""Kit check runs before discount check — kit wins."""
|
|
||||||
items = [{"sku": "SKU-KITDISC", "price": 20.0, "baseprice": 25.0, "quantity": 10,
|
|
||||||
"codmat_details": [{"codmat": "COD4", "cantitate_roa": 2,
|
|
||||||
"id_articol": 400, "cont": "345"}]}]
|
|
||||||
conn = _mock_oracle_conn(pol_cu_tva=True, price_map={400: (10.0, 1.19)})
|
|
||||||
result = get_prices_for_order(items, {"id_pol": "1"}, conn=conn)
|
|
||||||
assert result["items"][0].get("kit") is True
|
|
||||||
assert result["items"][0].get("quantity_discount") is not True
|
|
||||||
|
|
||||||
|
|
||||||
# ── normalize_company_name (II, PFA, INTREPRINDERE INDIVIDUALA) ──
|
# ── normalize_company_name (II, PFA, INTREPRINDERE INDIVIDUALA) ──
|
||||||
|
|||||||
Reference in New Issue
Block a user