feat(dashboard): add logs page, pagination, quick mapping modal, price pre-validation
- Add /logs page with per-order sync run details, filters (Toate/Importate/Fara Mapare/Erori) - Add price pre-validation (validate_prices + ensure_prices) to prevent ORA-20000 on direct articles - Add find_new_orders() to detect orders not yet in Oracle COMENZI - Extend missing_skus table with order context (order_count, order_numbers, customers) - Add server-side pagination on /api/validate/missing-skus and /missing-skus page - Replace confusing "Skip"/"Err" with "Fara Mapare"/"Erori" terminology - Add inline mapping modal on dashboard (replaces navigation to /mappings) - Add 2-row stat cards: orders (Comenzi Noi/Ready/Importate/Fara Mapare/Erori) + articles - Add ID_POL/ID_GESTIUNE/ID_SECTIE to config.py and .env - Update .gitignore (venv, *.db, api/api/, logs/) - 33/33 unit tests pass, E2E verified with Playwright Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,3 +69,123 @@ def classify_orders(orders, validation_result):
|
||||
importable.append(order)
|
||||
|
||||
return importable, skipped
|
||||
|
||||
def find_new_orders(order_numbers: list[str]) -> set[str]:
|
||||
"""Check which order numbers do NOT already exist in Oracle COMENZI.
|
||||
Returns: set of order numbers that are truly new (not yet imported).
|
||||
"""
|
||||
if not order_numbers:
|
||||
return set()
|
||||
|
||||
existing = set()
|
||||
num_list = list(order_numbers)
|
||||
|
||||
with database.pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
for i in range(0, len(num_list), 500):
|
||||
batch = num_list[i:i+500]
|
||||
placeholders = ",".join([f":o{j}" for j in range(len(batch))])
|
||||
params = {f"o{j}": num for j, num in enumerate(batch)}
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT DISTINCT comanda_externa FROM COMENZI
|
||||
WHERE comanda_externa IN ({placeholders}) AND sters = 0
|
||||
""", params)
|
||||
for row in cur:
|
||||
existing.add(row[0])
|
||||
|
||||
new_orders = set(order_numbers) - existing
|
||||
logger.info(f"Order check: {len(new_orders)} new, {len(existing)} already exist out of {len(order_numbers)} total")
|
||||
return new_orders
|
||||
|
||||
def validate_prices(codmats: set[str], id_pol: int) -> dict:
|
||||
"""Check which CODMATs have a price entry in CRM_POLITICI_PRET_ART for the given policy.
|
||||
Returns: {"has_price": set_of_codmats, "missing_price": set_of_codmats}
|
||||
"""
|
||||
if not codmats:
|
||||
return {"has_price": set(), "missing_price": set()}
|
||||
|
||||
codmat_to_id = {}
|
||||
ids_with_price = set()
|
||||
codmat_list = list(codmats)
|
||||
|
||||
with database.pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# Step 1: Get ID_ARTICOL for each CODMAT
|
||||
for i in range(0, len(codmat_list), 500):
|
||||
batch = codmat_list[i:i+500]
|
||||
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
|
||||
params = {f"c{j}": cm for j, cm in enumerate(batch)}
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT id_articol, codmat FROM NOM_ARTICOLE
|
||||
WHERE codmat IN ({placeholders})
|
||||
""", params)
|
||||
for row in cur:
|
||||
codmat_to_id[row[1]] = row[0]
|
||||
|
||||
# Step 2: Check which ID_ARTICOLs have a price in the policy
|
||||
id_list = list(codmat_to_id.values())
|
||||
for i in range(0, len(id_list), 500):
|
||||
batch = id_list[i:i+500]
|
||||
placeholders = ",".join([f":a{j}" for j in range(len(batch))])
|
||||
params = {f"a{j}": aid for j, aid in enumerate(batch)}
|
||||
params["id_pol"] = id_pol
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT DISTINCT pa.ID_ARTICOL FROM CRM_POLITICI_PRET_ART pa
|
||||
WHERE pa.ID_POL = :id_pol AND pa.ID_ARTICOL IN ({placeholders})
|
||||
""", params)
|
||||
for row in cur:
|
||||
ids_with_price.add(row[0])
|
||||
|
||||
# Map back to CODMATs
|
||||
has_price = {cm for cm, aid in codmat_to_id.items() if aid in ids_with_price}
|
||||
missing_price = codmats - has_price
|
||||
|
||||
logger.info(f"Price validation (policy {id_pol}): {len(has_price)} have price, {len(missing_price)} missing price")
|
||||
return {"has_price": has_price, "missing_price": missing_price}
|
||||
|
||||
def ensure_prices(codmats: set[str], id_pol: int):
|
||||
"""Insert price 0 entries for CODMATs missing from the given price policy."""
|
||||
if not codmats:
|
||||
return
|
||||
|
||||
with database.pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# Get ID_VALUTA for this policy
|
||||
cur.execute("""
|
||||
SELECT ID_VALUTA FROM CRM_POLITICI_PRETURI WHERE ID_POL = :id_pol
|
||||
""", {"id_pol": id_pol})
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
logger.error(f"Price policy {id_pol} not found in CRM_POLITICI_PRETURI")
|
||||
return
|
||||
id_valuta = row[0]
|
||||
|
||||
for codmat in codmats:
|
||||
# Get ID_ARTICOL
|
||||
cur.execute("""
|
||||
SELECT id_articol FROM NOM_ARTICOLE WHERE codmat = :codmat
|
||||
""", {"codmat": codmat})
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
logger.warning(f"CODMAT {codmat} not found in NOM_ARTICOLE, skipping price insert")
|
||||
continue
|
||||
id_articol = row[0]
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO CRM_POLITICI_PRET_ART
|
||||
(ID_POL_ART, ID_POL, ID_ARTICOL, PRET, ID_COMANDA, ID_VALUTA,
|
||||
ID_UTIL, DATAORA, PROC_TVAV, ID_PARTR, ID_PARTZ,
|
||||
PRETFTVA, PRETCTVA, CANTITATE, ID_UM, PRET_MIN, PRET_MIN_TVA)
|
||||
VALUES
|
||||
(SEQ_CRM_POLITICI_PRET_ART.NEXTVAL, :id_pol, :id_articol, 0, NULL, :id_valuta,
|
||||
-3, SYSDATE, 1.19, NULL, NULL,
|
||||
0, 0, 0, NULL, 0, 0)
|
||||
""", {"id_pol": id_pol, "id_articol": id_articol, "id_valuta": id_valuta})
|
||||
logger.info(f"Pret 0 adaugat pentru CODMAT {codmat} in politica {id_pol}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"Ensure prices done: {len(codmats)} CODMATs processed for policy {id_pol}")
|
||||
|
||||
Reference in New Issue
Block a user