diff --git a/TODOS.md b/TODOS.md index 685aff7..8022878 100644 --- a/TODOS.md +++ b/TODOS.md @@ -27,3 +27,10 @@ **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. **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. diff --git a/api/app/database.py b/api/app/database.py index 77dd0c9..0dc2d1f 100644 --- a/api/app/database.py +++ b/api/app/database.py @@ -169,6 +169,7 @@ CREATE TABLE IF NOT EXISTS order_items ( product_name TEXT, quantity REAL, price REAL, + baseprice REAL, vat REAL, mapping_status TEXT, codmat TEXT, @@ -357,6 +358,14 @@ def init_sqlite(): conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}") logger.info(f"Migrated orders: added column {col}") + # Migrate order_items: add baseprice column + cursor = conn.execute("PRAGMA table_info(order_items)") + oi_cols = {row[1] for row in cursor.fetchall()} + if "baseprice" not in oi_cols: + conn.execute("ALTER TABLE order_items ADD COLUMN baseprice REAL") + conn.execute("UPDATE orders SET price_match = NULL WHERE price_match = 0") + logger.info("Migrated order_items: added baseprice; reset price_match for re-check") + conn.commit() # Backfill address_mismatch from stored address JSON diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index bbb2420..335ff72 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -465,6 +465,9 @@ async def order_detail(order_number: str): item["price_match"] = pi.get("match") if pi.get("kit"): item["kit"] = True + if pi.get("quantity_discount"): + item["quantity_discount"] = True + item["baseprice"] = pi.get("baseprice") order_price_check = price_data.get("summary", {}) # Cache price_match in SQLite if changed if order_price_check.get("oracle_available") is not False: diff --git a/api/app/services/order_reader.py b/api/app/services/order_reader.py index 4826c43..576c2ad 100644 --- a/api/app/services/order_reader.py +++ b/api/app/services/order_reader.py @@ -17,6 +17,7 @@ class OrderItem: price: float quantity: float vat: float + baseprice: float = 0.0 @dataclass class OrderBilling: @@ -116,7 +117,8 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData: name=str(item.get("name", "")), price=float(item.get("price", 0) or 0), quantity=float(item.get("quantity", 0) or 0), - vat=float(item.get("vat", 0) or 0) + vat=float(item.get("vat", 0) or 0), + baseprice=float(item.get("baseprice", 0) or 0) )) # Parse billing diff --git a/api/app/services/sqlite_service.py b/api/app/services/sqlite_service.py index 061549d..af68c49 100644 --- a/api/app/services/sqlite_service.py +++ b/api/app/services/sqlite_service.py @@ -200,16 +200,17 @@ async def save_orders_batch(orders_data: list[dict]): all_items.append(( d["order_number"], item.get("sku"), item.get("product_name"), - item.get("quantity"), item.get("price"), item.get("vat"), + item.get("quantity"), item.get("price"), item.get("baseprice"), + item.get("vat"), item.get("mapping_status"), item.get("codmat"), item.get("id_articol"), item.get("cantitate_roa") )) if all_items: await db.executemany(""" INSERT OR IGNORE INTO order_items - (order_number, sku, product_name, quantity, price, vat, - mapping_status, codmat, id_articol, cantitate_roa) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (order_number, sku, product_name, quantity, price, baseprice, + vat, mapping_status, codmat, id_articol, cantitate_roa) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, all_items) await db.commit() @@ -535,13 +536,14 @@ async def add_order_items(order_number: str, items: list): try: await db.executemany(""" INSERT OR IGNORE INTO order_items - (order_number, sku, product_name, quantity, price, vat, - mapping_status, codmat, id_articol, cantitate_roa) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (order_number, sku, product_name, quantity, price, baseprice, + vat, mapping_status, codmat, id_articol, cantitate_roa) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ (order_number, item.get("sku"), item.get("product_name"), - item.get("quantity"), item.get("price"), item.get("vat"), + item.get("quantity"), item.get("price"), item.get("baseprice"), + item.get("vat"), item.get("mapping_status"), item.get("codmat"), item.get("id_articol"), item.get("cantitate_roa")) for item in items diff --git a/api/app/services/sync_service.py b/api/app/services/sync_service.py index b6c7e31..736946c 100644 --- a/api/app/services/sync_service.py +++ b/api/app/services/sync_service.py @@ -265,7 +265,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order) order_items_data = [ {"sku": item.sku, "product_name": item.name, - "quantity": item.quantity, "price": item.price, "vat": item.vat, + "quantity": item.quantity, "price": item.price, + "baseprice": item.baseprice, "vat": item.vat, "mapping_status": "unknown", "codmat": None, "id_articol": None, "cantitate_roa": None} for item in order.items @@ -590,7 +591,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None id_comanda_roa = existing_map.get(order.number) order_items_data = [ {"sku": item.sku, "product_name": item.name, - "quantity": item.quantity, "price": item.price, "vat": item.vat, + "quantity": item.quantity, "price": item.price, + "baseprice": item.baseprice, "vat": item.vat, "mapping_status": "mapped" if item.sku in validation["mapped"] else "direct", "codmat": None, "id_articol": None, "cantitate_roa": None} for item in order.items @@ -630,7 +632,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order) order_items_data = [ {"sku": item.sku, "product_name": item.name, - "quantity": item.quantity, "price": item.price, "vat": item.vat, + "quantity": item.quantity, "price": item.price, + "baseprice": item.baseprice, "vat": item.vat, "mapping_status": "missing" if item.sku in validation["missing"] else "mapped" if item.sku in validation["mapped"] else "direct", "codmat": None, "id_articol": None, "cantitate_roa": None} @@ -778,7 +781,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None ms = "mapped" if item.sku in validation["mapped"] else "direct" order_items_data.append({ "sku": item.sku, "product_name": item.name, - "quantity": item.quantity, "price": item.price, "vat": item.vat, + "quantity": item.quantity, "price": item.price, + "baseprice": item.baseprice, "vat": item.vat, "mapping_status": ms, "codmat": None, "id_articol": None, "cantitate_roa": None }) diff --git a/api/app/services/validation_service.py b/api/app/services/validation_service.py index 361b8ca..bca7f02 100644 --- a/api/app/services/validation_service.py +++ b/api/app/services/validation_service.py @@ -714,6 +714,13 @@ def get_prices_for_order(items: list[dict], app_settings: dict, conn=None) -> di result_items[idx]["kit"] = True 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 all_resolved = True diff --git a/api/app/static/js/shared.js b/api/app/static/js/shared.js index 0a4829d..99bc467 100644 --- a/api/app/static/js/shared.js +++ b/api/app/static/js/shared.js @@ -614,9 +614,11 @@ async function renderOrderDetailModal(orderNumber, opts) { 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 = { pret_roa: item.pret_roa, match: item.price_match }; - const priceMismatchHtml = priceInfo.match === false - ? `
ROA: ${fmtNum(priceInfo.pret_roa)} lei
` - : ''; + const priceMismatchHtml = item.quantity_discount + ? `
Disc. ${fmtNum(item.baseprice)} ${fmtNum(item.price)} lei
` + : (priceInfo.match === false + ? `
ROA: ${fmtNum(priceInfo.pret_roa)} lei
` + : ''); return `
${esc(item.sku)} @@ -688,6 +690,10 @@ async function renderOrderDetailModal(orderNumber, opts) { if (item.kit) { matchDot = 'Kit'; rowStyle = ''; + } else if (item.quantity_discount) { + const bpTitle = item.baseprice ? `Catalog: ${fmtNum(item.baseprice)} lei` : 'Discount GoMag'; + matchDot = `Disc.`; + rowStyle = ''; } else if (priceInfo.pret_roa == null && priceInfo.match == null) { matchDot = ''; rowStyle = ''; @@ -703,7 +709,7 @@ async function renderOrderDetailModal(orderNumber, opts) { ${esc(item.product_name || '-')} ${renderCodmatCell(item)} ${item.quantity || 0} - ${item.price != null ? fmtNum(item.price) : '-'} + ${item.quantity_discount && item.baseprice ? `${fmtNum(item.baseprice)} ${fmtNum(item.price)}` : (item.price != null ? fmtNum(item.price) : '-')} ${pretRoaHtml} ${item.vat != null ? Number(item.vat) : '-'} ${fmtNum(valoare)} diff --git a/api/app/templates/base.html b/api/app/templates/base.html index f70c6dd..0ac16f9 100644 --- a/api/app/templates/base.html +++ b/api/app/templates/base.html @@ -168,7 +168,7 @@ - +