fix(sync): detect deleted orders and invoices in ROA
Previously, orders deleted from Oracle (sters=1) remained as IMPORTED in SQLite, and deleted invoices kept stale cache data. Now the refresh button and sync cycle re-verify all imported orders against Oracle: - Deleted orders → marked DELETED_IN_ROA with cleared id_comanda - Deleted invoices → invoice cache fields cleared - New status badge for DELETED_IN_ROA in dashboard and logs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -492,18 +492,26 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
|||||||
|
|
||||||
@router.post("/api/dashboard/refresh-invoices")
|
@router.post("/api/dashboard/refresh-invoices")
|
||||||
async def refresh_invoices():
|
async def refresh_invoices():
|
||||||
"""Force-refresh invoice status from Oracle for all uninvoiced imported orders."""
|
"""Force-refresh invoice/order status from Oracle.
|
||||||
try:
|
|
||||||
uninvoiced = await sqlite_service.get_uninvoiced_imported_orders()
|
|
||||||
if not uninvoiced:
|
|
||||||
return {"updated": 0, "message": "Nicio comanda de verificat"}
|
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
1. Uninvoiced orders → did they get invoiced?
|
||||||
|
2. Invoiced orders → was the invoice deleted?
|
||||||
|
3. All imported orders → was the order deleted from ROA?
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
invoices_added = 0
|
||||||
|
invoices_cleared = 0
|
||||||
|
orders_deleted = 0
|
||||||
|
|
||||||
|
# 1. Check uninvoiced → new invoices
|
||||||
|
uninvoiced = await sqlite_service.get_uninvoiced_imported_orders()
|
||||||
|
if uninvoiced:
|
||||||
id_comanda_list = [o["id_comanda"] for o in uninvoiced]
|
id_comanda_list = [o["id_comanda"] for o in uninvoiced]
|
||||||
invoice_data = await asyncio.to_thread(
|
invoice_data = await asyncio.to_thread(
|
||||||
invoice_service.check_invoices_for_orders, id_comanda_list
|
invoice_service.check_invoices_for_orders, id_comanda_list
|
||||||
)
|
)
|
||||||
id_to_order = {o["id_comanda"]: o["order_number"] for o in uninvoiced}
|
id_to_order = {o["id_comanda"]: o["order_number"] for o in uninvoiced}
|
||||||
updated = 0
|
|
||||||
for idc, inv in invoice_data.items():
|
for idc, inv in invoice_data.items():
|
||||||
order_num = id_to_order.get(idc)
|
order_num = id_to_order.get(idc)
|
||||||
if order_num and inv.get("facturat"):
|
if order_num and inv.get("facturat"):
|
||||||
@@ -516,10 +524,41 @@ async def refresh_invoices():
|
|||||||
total_cu_tva=inv.get("total_cu_tva"),
|
total_cu_tva=inv.get("total_cu_tva"),
|
||||||
data_act=inv.get("data_act"),
|
data_act=inv.get("data_act"),
|
||||||
)
|
)
|
||||||
updated += 1
|
invoices_added += 1
|
||||||
return {"updated": updated, "checked": len(uninvoiced)}
|
|
||||||
|
# 2. Check invoiced → deleted invoices
|
||||||
|
invoiced = await sqlite_service.get_invoiced_imported_orders()
|
||||||
|
if invoiced:
|
||||||
|
id_comanda_list = [o["id_comanda"] for o in invoiced]
|
||||||
|
invoice_data = await asyncio.to_thread(
|
||||||
|
invoice_service.check_invoices_for_orders, id_comanda_list
|
||||||
|
)
|
||||||
|
for o in invoiced:
|
||||||
|
if o["id_comanda"] not in invoice_data:
|
||||||
|
await sqlite_service.clear_order_invoice(o["order_number"])
|
||||||
|
invoices_cleared += 1
|
||||||
|
|
||||||
|
# 3. Check all imported → deleted orders in ROA
|
||||||
|
all_imported = await sqlite_service.get_all_imported_orders()
|
||||||
|
if all_imported:
|
||||||
|
id_comanda_list = [o["id_comanda"] for o in all_imported]
|
||||||
|
existing_ids = await asyncio.to_thread(
|
||||||
|
invoice_service.check_orders_exist, id_comanda_list
|
||||||
|
)
|
||||||
|
for o in all_imported:
|
||||||
|
if o["id_comanda"] not in existing_ids:
|
||||||
|
await sqlite_service.mark_order_deleted_in_roa(o["order_number"])
|
||||||
|
orders_deleted += 1
|
||||||
|
|
||||||
|
checked = len(uninvoiced) + len(invoiced) + len(all_imported)
|
||||||
|
return {
|
||||||
|
"checked": checked,
|
||||||
|
"invoices_added": invoices_added,
|
||||||
|
"invoices_cleared": invoices_cleared,
|
||||||
|
"orders_deleted": orders_deleted,
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e), "updated": 0}
|
return {"error": str(e), "invoices_added": 0}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/api/sync/schedule")
|
@router.put("/api/sync/schedule")
|
||||||
|
|||||||
@@ -43,3 +43,33 @@ def check_invoices_for_orders(id_comanda_list: list) -> dict:
|
|||||||
database.pool.release(conn)
|
database.pool.release(conn)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_orders_exist(id_comanda_list: list) -> set:
|
||||||
|
"""Check which id_comanda values still exist in Oracle COMENZI (sters=0).
|
||||||
|
Returns set of id_comanda that exist.
|
||||||
|
"""
|
||||||
|
if not id_comanda_list or database.pool is None:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
existing = set()
|
||||||
|
conn = database.get_oracle_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
for i in range(0, len(id_comanda_list), 500):
|
||||||
|
batch = id_comanda_list[i:i+500]
|
||||||
|
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
|
||||||
|
params = {f"c{j}": cid for j, cid in enumerate(batch)}
|
||||||
|
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT id_comanda FROM COMENZI
|
||||||
|
WHERE id_comanda IN ({placeholders}) AND sters = 0
|
||||||
|
""", params)
|
||||||
|
for row in cur:
|
||||||
|
existing.add(row[0])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Order existence check failed: {e}")
|
||||||
|
finally:
|
||||||
|
database.pool.release(conn)
|
||||||
|
|
||||||
|
return existing
|
||||||
|
|||||||
@@ -781,6 +781,83 @@ async def update_order_invoice(order_number: str, serie: str = None,
|
|||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_invoiced_imported_orders() -> list:
|
||||||
|
"""Get imported orders that HAVE cached invoice data (for re-verification)."""
|
||||||
|
db = await get_sqlite()
|
||||||
|
try:
|
||||||
|
cursor = await db.execute("""
|
||||||
|
SELECT order_number, id_comanda FROM orders
|
||||||
|
WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED')
|
||||||
|
AND id_comanda IS NOT NULL
|
||||||
|
AND factura_numar IS NOT NULL AND factura_numar != ''
|
||||||
|
""")
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_imported_orders() -> list:
|
||||||
|
"""Get ALL imported orders with id_comanda (for checking if deleted in ROA)."""
|
||||||
|
db = await get_sqlite()
|
||||||
|
try:
|
||||||
|
cursor = await db.execute("""
|
||||||
|
SELECT order_number, id_comanda FROM orders
|
||||||
|
WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED')
|
||||||
|
AND id_comanda IS NOT NULL
|
||||||
|
""")
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_order_invoice(order_number: str):
|
||||||
|
"""Clear cached invoice data when invoice was deleted in ROA."""
|
||||||
|
db = await get_sqlite()
|
||||||
|
try:
|
||||||
|
await db.execute("""
|
||||||
|
UPDATE orders SET
|
||||||
|
factura_serie = NULL,
|
||||||
|
factura_numar = NULL,
|
||||||
|
factura_total_fara_tva = NULL,
|
||||||
|
factura_total_tva = NULL,
|
||||||
|
factura_total_cu_tva = NULL,
|
||||||
|
factura_data = NULL,
|
||||||
|
invoice_checked_at = datetime('now'),
|
||||||
|
updated_at = datetime('now')
|
||||||
|
WHERE order_number = ?
|
||||||
|
""", (order_number,))
|
||||||
|
await db.commit()
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_order_deleted_in_roa(order_number: str):
|
||||||
|
"""Mark an order as deleted in ROA — clears id_comanda and invoice cache."""
|
||||||
|
db = await get_sqlite()
|
||||||
|
try:
|
||||||
|
await db.execute("""
|
||||||
|
UPDATE orders SET
|
||||||
|
status = 'DELETED_IN_ROA',
|
||||||
|
id_comanda = NULL,
|
||||||
|
id_partener = NULL,
|
||||||
|
factura_serie = NULL,
|
||||||
|
factura_numar = NULL,
|
||||||
|
factura_total_fara_tva = NULL,
|
||||||
|
factura_total_tva = NULL,
|
||||||
|
factura_total_cu_tva = NULL,
|
||||||
|
factura_data = NULL,
|
||||||
|
invoice_checked_at = NULL,
|
||||||
|
error_message = 'Comanda stearsa din ROA',
|
||||||
|
updated_at = datetime('now')
|
||||||
|
WHERE order_number = ?
|
||||||
|
""", (order_number,))
|
||||||
|
await db.commit()
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
# ── App Settings ─────────────────────────────────
|
# ── App Settings ─────────────────────────────────
|
||||||
|
|
||||||
async def get_app_settings() -> dict:
|
async def get_app_settings() -> dict:
|
||||||
|
|||||||
@@ -463,17 +463,19 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
logger.warning("Too many errors, stopping sync")
|
logger.warning("Too many errors, stopping sync")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Step 4b: Invoice check — update cached invoice data
|
# Step 4b: Invoice & order status check — sync with Oracle
|
||||||
_update_progress("invoices", "Checking invoices...", 0, 0)
|
_update_progress("invoices", "Checking invoices & order status...", 0, 0)
|
||||||
invoices_updated = 0
|
invoices_updated = 0
|
||||||
|
invoices_cleared = 0
|
||||||
|
orders_deleted = 0
|
||||||
try:
|
try:
|
||||||
|
# 4b-1: Uninvoiced → check for new invoices
|
||||||
uninvoiced = await sqlite_service.get_uninvoiced_imported_orders()
|
uninvoiced = await sqlite_service.get_uninvoiced_imported_orders()
|
||||||
if uninvoiced:
|
if uninvoiced:
|
||||||
id_comanda_list = [o["id_comanda"] for o in uninvoiced]
|
id_comanda_list = [o["id_comanda"] for o in uninvoiced]
|
||||||
invoice_data = await asyncio.to_thread(
|
invoice_data = await asyncio.to_thread(
|
||||||
invoice_service.check_invoices_for_orders, id_comanda_list
|
invoice_service.check_invoices_for_orders, id_comanda_list
|
||||||
)
|
)
|
||||||
# Build reverse map: id_comanda → order_number
|
|
||||||
id_to_order = {o["id_comanda"]: o["order_number"] for o in uninvoiced}
|
id_to_order = {o["id_comanda"]: o["order_number"] for o in uninvoiced}
|
||||||
for idc, inv in invoice_data.items():
|
for idc, inv in invoice_data.items():
|
||||||
order_num = id_to_order.get(idc)
|
order_num = id_to_order.get(idc)
|
||||||
@@ -488,10 +490,39 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
data_act=inv.get("data_act"),
|
data_act=inv.get("data_act"),
|
||||||
)
|
)
|
||||||
invoices_updated += 1
|
invoices_updated += 1
|
||||||
|
|
||||||
|
# 4b-2: Invoiced → check for deleted invoices
|
||||||
|
invoiced = await sqlite_service.get_invoiced_imported_orders()
|
||||||
|
if invoiced:
|
||||||
|
id_comanda_list = [o["id_comanda"] for o in invoiced]
|
||||||
|
invoice_data = await asyncio.to_thread(
|
||||||
|
invoice_service.check_invoices_for_orders, id_comanda_list
|
||||||
|
)
|
||||||
|
for o in invoiced:
|
||||||
|
if o["id_comanda"] not in invoice_data:
|
||||||
|
await sqlite_service.clear_order_invoice(o["order_number"])
|
||||||
|
invoices_cleared += 1
|
||||||
|
|
||||||
|
# 4b-3: All imported → check for deleted orders in ROA
|
||||||
|
all_imported = await sqlite_service.get_all_imported_orders()
|
||||||
|
if all_imported:
|
||||||
|
id_comanda_list = [o["id_comanda"] for o in all_imported]
|
||||||
|
existing_ids = await asyncio.to_thread(
|
||||||
|
invoice_service.check_orders_exist, id_comanda_list
|
||||||
|
)
|
||||||
|
for o in all_imported:
|
||||||
|
if o["id_comanda"] not in existing_ids:
|
||||||
|
await sqlite_service.mark_order_deleted_in_roa(o["order_number"])
|
||||||
|
orders_deleted += 1
|
||||||
|
|
||||||
if invoices_updated:
|
if invoices_updated:
|
||||||
_log_line(run_id, f"Facturi actualizate: {invoices_updated} comenzi facturate")
|
_log_line(run_id, f"Facturi noi: {invoices_updated} comenzi facturate")
|
||||||
|
if invoices_cleared:
|
||||||
|
_log_line(run_id, f"Facturi sterse: {invoices_cleared} facturi eliminate din cache")
|
||||||
|
if orders_deleted:
|
||||||
|
_log_line(run_id, f"Comenzi sterse din ROA: {orders_deleted}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Invoice check failed: {e}")
|
logger.warning(f"Invoice/order status check failed: {e}")
|
||||||
|
|
||||||
# Step 5: Update sync run
|
# Step 5: Update sync run
|
||||||
total_imported = imported_count + already_imported_count # backward-compat
|
total_imported = imported_count + already_imported_count # backward-compat
|
||||||
@@ -514,6 +545,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
"errors": error_count,
|
"errors": error_count,
|
||||||
"missing_skus": len(validation["missing"]),
|
"missing_skus": len(validation["missing"]),
|
||||||
"invoices_updated": invoices_updated,
|
"invoices_updated": invoices_updated,
|
||||||
|
"invoices_cleared": invoices_cleared,
|
||||||
|
"orders_deleted_in_roa": orders_deleted,
|
||||||
}
|
}
|
||||||
|
|
||||||
_update_progress("completed",
|
_update_progress("completed",
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ function orderStatusBadge(status) {
|
|||||||
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
||||||
case 'SKIPPED': return '<span class="badge bg-warning">Omis</span>';
|
case 'SKIPPED': return '<span class="badge bg-warning">Omis</span>';
|
||||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||||
|
case 'DELETED_IN_ROA': return '<span class="badge bg-dark">Sters din ROA</span>';
|
||||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function orderStatusBadge(status) {
|
|||||||
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
||||||
case 'SKIPPED': return '<span class="badge bg-warning">Omis</span>';
|
case 'SKIPPED': return '<span class="badge bg-warning">Omis</span>';
|
||||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||||
|
case 'DELETED_IN_ROA': return '<span class="badge bg-dark">Sters din ROA</span>';
|
||||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user