feat(sync): handle cancelled GoMag orders (status Anulata / statusId 7)
- Add web_status column to orders table (generic name for platform status) - Filter cancelled orders during sync, record as CANCELLED in SQLite - Soft-delete previously-imported cancelled orders in Oracle (if not invoiced) - Add CANCELLED filter pill + badge in dashboard UI - New soft_delete_order_in_roa() and mark_order_cancelled() functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -109,7 +109,8 @@ CREATE TABLE IF NOT EXISTS orders (
|
|||||||
invoice_checked_at TEXT,
|
invoice_checked_at TEXT,
|
||||||
order_total REAL,
|
order_total REAL,
|
||||||
delivery_cost REAL,
|
delivery_cost REAL,
|
||||||
discount_total REAL
|
discount_total REAL,
|
||||||
|
web_status TEXT
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_orders_date ON orders(order_date);
|
CREATE INDEX IF NOT EXISTS idx_orders_date ON orders(order_date);
|
||||||
@@ -316,6 +317,7 @@ def init_sqlite():
|
|||||||
("order_total", "REAL"),
|
("order_total", "REAL"),
|
||||||
("delivery_cost", "REAL"),
|
("delivery_cost", "REAL"),
|
||||||
("discount_total", "REAL"),
|
("discount_total", "REAL"),
|
||||||
|
("web_status", "TEXT"),
|
||||||
]:
|
]:
|
||||||
if col not in order_cols:
|
if col not in order_cols:
|
||||||
conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}")
|
conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}")
|
||||||
|
|||||||
@@ -294,3 +294,51 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def soft_delete_order_in_roa(id_comanda: int) -> dict:
|
||||||
|
"""Soft-delete an order in Oracle ROA (set sters=1 on comenzi + comenzi_detalii).
|
||||||
|
Returns {"success": bool, "error": str|None, "details_deleted": int}
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "details_deleted": 0}
|
||||||
|
|
||||||
|
if database.pool is None:
|
||||||
|
result["error"] = "Oracle pool not initialized"
|
||||||
|
return result
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = database.pool.acquire()
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# Soft-delete order details
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE comenzi_detalii SET sters = 1 WHERE id_comanda = :1 AND sters = 0",
|
||||||
|
[id_comanda]
|
||||||
|
)
|
||||||
|
result["details_deleted"] = cur.rowcount
|
||||||
|
|
||||||
|
# Soft-delete the order itself
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE comenzi SET sters = 1 WHERE id_comanda = :1 AND sters = 0",
|
||||||
|
[id_comanda]
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
result["success"] = True
|
||||||
|
logger.info(f"Soft-deleted order ID={id_comanda} in Oracle ROA ({result['details_deleted']} details)")
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
logger.error(f"Error soft-deleting order ID={id_comanda}: {e}")
|
||||||
|
if conn:
|
||||||
|
try:
|
||||||
|
conn.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
try:
|
||||||
|
database.pool.release(conn)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
|||||||
shipping_name: str = None, billing_name: str = None,
|
shipping_name: str = None, billing_name: str = None,
|
||||||
payment_method: str = None, delivery_method: str = None,
|
payment_method: str = None, delivery_method: str = None,
|
||||||
order_total: float = None,
|
order_total: float = None,
|
||||||
delivery_cost: float = None, discount_total: float = None):
|
delivery_cost: float = None, discount_total: float = None,
|
||||||
|
web_status: str = None):
|
||||||
"""Upsert a single order — one row per order_number, status updated in place."""
|
"""Upsert a single order — one row per order_number, status updated in place."""
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
@@ -62,8 +63,8 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
|||||||
id_comanda, id_partener, error_message, missing_skus, items_count,
|
id_comanda, id_partener, error_message, missing_skus, items_count,
|
||||||
last_sync_run_id, shipping_name, billing_name,
|
last_sync_run_id, shipping_name, billing_name,
|
||||||
payment_method, delivery_method, order_total,
|
payment_method, delivery_method, order_total,
|
||||||
delivery_cost, discount_total)
|
delivery_cost, discount_total, web_status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(order_number) DO UPDATE SET
|
ON CONFLICT(order_number) DO UPDATE SET
|
||||||
customer_name = excluded.customer_name,
|
customer_name = excluded.customer_name,
|
||||||
status = CASE
|
status = CASE
|
||||||
@@ -87,13 +88,14 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
|||||||
order_total = COALESCE(excluded.order_total, orders.order_total),
|
order_total = COALESCE(excluded.order_total, orders.order_total),
|
||||||
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
|
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
|
||||||
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
|
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
|
||||||
|
web_status = COALESCE(excluded.web_status, orders.web_status),
|
||||||
updated_at = datetime('now')
|
updated_at = datetime('now')
|
||||||
""", (order_number, order_date, customer_name, status,
|
""", (order_number, order_date, customer_name, status,
|
||||||
id_comanda, id_partener, error_message,
|
id_comanda, id_partener, error_message,
|
||||||
json.dumps(missing_skus) if missing_skus else None,
|
json.dumps(missing_skus) if missing_skus else None,
|
||||||
items_count, sync_run_id, shipping_name, billing_name,
|
items_count, sync_run_id, shipping_name, billing_name,
|
||||||
payment_method, delivery_method, order_total,
|
payment_method, delivery_method, order_total,
|
||||||
delivery_cost, discount_total))
|
delivery_cost, discount_total, web_status))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
finally:
|
finally:
|
||||||
await db.close()
|
await db.close()
|
||||||
@@ -118,7 +120,8 @@ async def save_orders_batch(orders_data: list[dict]):
|
|||||||
Each dict must have: sync_run_id, order_number, order_date, customer_name, status,
|
Each dict must have: sync_run_id, order_number, order_date, customer_name, status,
|
||||||
id_comanda, id_partener, error_message, missing_skus (list|None), items_count,
|
id_comanda, id_partener, error_message, missing_skus (list|None), items_count,
|
||||||
shipping_name, billing_name, payment_method, delivery_method, status_at_run,
|
shipping_name, billing_name, payment_method, delivery_method, status_at_run,
|
||||||
items (list of item dicts), delivery_cost (optional), discount_total (optional).
|
items (list of item dicts), delivery_cost (optional), discount_total (optional),
|
||||||
|
web_status (optional).
|
||||||
"""
|
"""
|
||||||
if not orders_data:
|
if not orders_data:
|
||||||
return
|
return
|
||||||
@@ -131,8 +134,8 @@ async def save_orders_batch(orders_data: list[dict]):
|
|||||||
id_comanda, id_partener, error_message, missing_skus, items_count,
|
id_comanda, id_partener, error_message, missing_skus, items_count,
|
||||||
last_sync_run_id, shipping_name, billing_name,
|
last_sync_run_id, shipping_name, billing_name,
|
||||||
payment_method, delivery_method, order_total,
|
payment_method, delivery_method, order_total,
|
||||||
delivery_cost, discount_total)
|
delivery_cost, discount_total, web_status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(order_number) DO UPDATE SET
|
ON CONFLICT(order_number) DO UPDATE SET
|
||||||
customer_name = excluded.customer_name,
|
customer_name = excluded.customer_name,
|
||||||
status = CASE
|
status = CASE
|
||||||
@@ -156,6 +159,7 @@ async def save_orders_batch(orders_data: list[dict]):
|
|||||||
order_total = COALESCE(excluded.order_total, orders.order_total),
|
order_total = COALESCE(excluded.order_total, orders.order_total),
|
||||||
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
|
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
|
||||||
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
|
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
|
||||||
|
web_status = COALESCE(excluded.web_status, orders.web_status),
|
||||||
updated_at = datetime('now')
|
updated_at = datetime('now')
|
||||||
""", [
|
""", [
|
||||||
(d["order_number"], d["order_date"], d["customer_name"], d["status"],
|
(d["order_number"], d["order_date"], d["customer_name"], d["status"],
|
||||||
@@ -165,7 +169,8 @@ async def save_orders_batch(orders_data: list[dict]):
|
|||||||
d.get("shipping_name"), d.get("billing_name"),
|
d.get("shipping_name"), d.get("billing_name"),
|
||||||
d.get("payment_method"), d.get("delivery_method"),
|
d.get("payment_method"), d.get("delivery_method"),
|
||||||
d.get("order_total"),
|
d.get("order_total"),
|
||||||
d.get("delivery_cost"), d.get("discount_total"))
|
d.get("delivery_cost"), d.get("discount_total"),
|
||||||
|
d.get("web_status"))
|
||||||
for d in orders_data
|
for d in orders_data
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -619,6 +624,7 @@ async def get_run_orders_filtered(run_id: str, status_filter: str = "all",
|
|||||||
"skipped": status_counts.get("SKIPPED", 0),
|
"skipped": status_counts.get("SKIPPED", 0),
|
||||||
"error": status_counts.get("ERROR", 0),
|
"error": status_counts.get("ERROR", 0),
|
||||||
"already_imported": status_counts.get("ALREADY_IMPORTED", 0),
|
"already_imported": status_counts.get("ALREADY_IMPORTED", 0),
|
||||||
|
"cancelled": status_counts.get("CANCELLED", 0),
|
||||||
"total": sum(status_counts.values())
|
"total": sum(status_counts.values())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -715,6 +721,7 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
|||||||
"imported_all": status_counts.get("IMPORTED", 0) + status_counts.get("ALREADY_IMPORTED", 0),
|
"imported_all": status_counts.get("IMPORTED", 0) + status_counts.get("ALREADY_IMPORTED", 0),
|
||||||
"skipped": status_counts.get("SKIPPED", 0),
|
"skipped": status_counts.get("SKIPPED", 0),
|
||||||
"error": status_counts.get("ERROR", 0),
|
"error": status_counts.get("ERROR", 0),
|
||||||
|
"cancelled": status_counts.get("CANCELLED", 0),
|
||||||
"total": sum(status_counts.values()),
|
"total": sum(status_counts.values()),
|
||||||
"uninvoiced_sqlite": uninvoiced_sqlite,
|
"uninvoiced_sqlite": uninvoiced_sqlite,
|
||||||
}
|
}
|
||||||
@@ -860,6 +867,32 @@ async def mark_order_deleted_in_roa(order_number: str):
|
|||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_order_cancelled(order_number: str, web_status: str = "Anulata"):
|
||||||
|
"""Mark an order as cancelled from GoMag. Clears id_comanda and invoice cache."""
|
||||||
|
db = await get_sqlite()
|
||||||
|
try:
|
||||||
|
await db.execute("""
|
||||||
|
UPDATE orders SET
|
||||||
|
status = 'CANCELLED',
|
||||||
|
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,
|
||||||
|
web_status = ?,
|
||||||
|
error_message = 'Comanda anulata in GoMag',
|
||||||
|
updated_at = datetime('now')
|
||||||
|
WHERE order_number = ?
|
||||||
|
""", (web_status, 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:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ async def prepare_sync(id_pol: int = None, id_sectie: int = None) -> dict:
|
|||||||
"phase_text": "Starting...",
|
"phase_text": "Starting...",
|
||||||
"progress_current": 0,
|
"progress_current": 0,
|
||||||
"progress_total": 0,
|
"progress_total": 0,
|
||||||
"counts": {"imported": 0, "skipped": 0, "errors": 0, "already_imported": 0},
|
"counts": {"imported": 0, "skipped": 0, "errors": 0, "already_imported": 0, "cancelled": 0},
|
||||||
}
|
}
|
||||||
return {"run_id": run_id, "status": "starting"}
|
return {"run_id": run_id, "status": "starting"}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
"phase_text": "Reading JSON files...",
|
"phase_text": "Reading JSON files...",
|
||||||
"progress_current": 0,
|
"progress_current": 0,
|
||||||
"progress_total": 0,
|
"progress_total": 0,
|
||||||
"counts": {"imported": 0, "skipped": 0, "errors": 0, "already_imported": 0},
|
"counts": {"imported": 0, "skipped": 0, "errors": 0, "already_imported": 0, "cancelled": 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
_update_progress("reading", "Reading JSON files...")
|
_update_progress("reading", "Reading JSON files...")
|
||||||
@@ -212,6 +212,104 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
summary = {"run_id": run_id, "status": "completed", "message": "No orders found", "json_files": json_count}
|
summary = {"run_id": run_id, "status": "completed", "message": "No orders found", "json_files": json_count}
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
# ── Separate cancelled orders (GoMag status "Anulata" / statusId "7") ──
|
||||||
|
cancelled_orders = [o for o in orders if o.status_id == "7" or (o.status and o.status.lower() == "anulata")]
|
||||||
|
active_orders = [o for o in orders if o not in cancelled_orders]
|
||||||
|
cancelled_count = len(cancelled_orders)
|
||||||
|
|
||||||
|
if cancelled_orders:
|
||||||
|
_log_line(run_id, f"Comenzi anulate in GoMag: {cancelled_count}")
|
||||||
|
|
||||||
|
# Record cancelled orders in SQLite
|
||||||
|
cancelled_batch = []
|
||||||
|
for order in cancelled_orders:
|
||||||
|
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,
|
||||||
|
"mapping_status": "unknown", "codmat": None,
|
||||||
|
"id_articol": None, "cantitate_roa": None}
|
||||||
|
for item in order.items
|
||||||
|
]
|
||||||
|
cancelled_batch.append({
|
||||||
|
"sync_run_id": run_id, "order_number": order.number,
|
||||||
|
"order_date": order.date, "customer_name": customer,
|
||||||
|
"status": "CANCELLED", "status_at_run": "CANCELLED",
|
||||||
|
"id_comanda": None, "id_partener": None,
|
||||||
|
"error_message": "Comanda anulata in GoMag",
|
||||||
|
"missing_skus": None,
|
||||||
|
"items_count": len(order.items),
|
||||||
|
"shipping_name": shipping_name, "billing_name": billing_name,
|
||||||
|
"payment_method": payment_method, "delivery_method": delivery_method,
|
||||||
|
"order_total": order.total or None,
|
||||||
|
"delivery_cost": order.delivery_cost or None,
|
||||||
|
"discount_total": order.discount_total or None,
|
||||||
|
"web_status": order.status or "Anulata",
|
||||||
|
"items": order_items_data,
|
||||||
|
})
|
||||||
|
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → ANULAT in GoMag")
|
||||||
|
|
||||||
|
await sqlite_service.save_orders_batch(cancelled_batch)
|
||||||
|
|
||||||
|
# Check if any cancelled orders were previously imported
|
||||||
|
from ..database import get_sqlite as _get_sqlite
|
||||||
|
db_check = await _get_sqlite()
|
||||||
|
try:
|
||||||
|
cancelled_numbers = [o.number for o in cancelled_orders]
|
||||||
|
placeholders = ",".join("?" for _ in cancelled_numbers)
|
||||||
|
cursor = await db_check.execute(f"""
|
||||||
|
SELECT order_number, id_comanda FROM orders
|
||||||
|
WHERE order_number IN ({placeholders})
|
||||||
|
AND id_comanda IS NOT NULL
|
||||||
|
AND status = 'CANCELLED'
|
||||||
|
""", cancelled_numbers)
|
||||||
|
previously_imported = [dict(r) for r in await cursor.fetchall()]
|
||||||
|
finally:
|
||||||
|
await db_check.close()
|
||||||
|
|
||||||
|
if previously_imported:
|
||||||
|
_log_line(run_id, f"Verificare {len(previously_imported)} comenzi anulate care erau importate in Oracle...")
|
||||||
|
# Check which have invoices
|
||||||
|
id_comanda_list = [o["id_comanda"] for o in previously_imported]
|
||||||
|
invoice_data = await asyncio.to_thread(
|
||||||
|
invoice_service.check_invoices_for_orders, id_comanda_list
|
||||||
|
)
|
||||||
|
|
||||||
|
for o in previously_imported:
|
||||||
|
idc = o["id_comanda"]
|
||||||
|
order_num = o["order_number"]
|
||||||
|
if idc in invoice_data:
|
||||||
|
# Invoiced — keep in Oracle, just log warning
|
||||||
|
_log_line(run_id,
|
||||||
|
f"#{order_num} → ANULAT dar FACTURAT (factura {invoice_data[idc].get('serie_act', '')}"
|
||||||
|
f"{invoice_data[idc].get('numar_act', '')}) — NU se sterge din Oracle")
|
||||||
|
# Update web_status but keep CANCELLED status (already set by batch above)
|
||||||
|
else:
|
||||||
|
# Not invoiced — soft-delete in Oracle
|
||||||
|
del_result = await asyncio.to_thread(
|
||||||
|
import_service.soft_delete_order_in_roa, idc
|
||||||
|
)
|
||||||
|
if del_result["success"]:
|
||||||
|
# Clear id_comanda via mark_order_cancelled
|
||||||
|
await sqlite_service.mark_order_cancelled(order_num, "Anulata")
|
||||||
|
_log_line(run_id,
|
||||||
|
f"#{order_num} → ANULAT + STERS din Oracle (ID: {idc}, "
|
||||||
|
f"{del_result['details_deleted']} detalii)")
|
||||||
|
else:
|
||||||
|
_log_line(run_id,
|
||||||
|
f"#{order_num} → ANULAT dar EROARE la stergere Oracle: {del_result['error']}")
|
||||||
|
|
||||||
|
orders = active_orders
|
||||||
|
|
||||||
|
if not orders:
|
||||||
|
_log_line(run_id, "Nicio comanda activa dupa filtrare anulate.")
|
||||||
|
await sqlite_service.update_sync_run(run_id, "completed", cancelled_count, 0, 0, 0)
|
||||||
|
_update_progress("completed", f"No active orders ({cancelled_count} cancelled)")
|
||||||
|
summary = {"run_id": run_id, "status": "completed",
|
||||||
|
"message": f"No active orders ({cancelled_count} cancelled)",
|
||||||
|
"json_files": json_count, "cancelled": cancelled_count}
|
||||||
|
return summary
|
||||||
|
|
||||||
_update_progress("validation", f"Validating {len(orders)} orders...", 0, len(orders))
|
_update_progress("validation", f"Validating {len(orders)} orders...", 0, len(orders))
|
||||||
|
|
||||||
# ── Single Oracle connection for entire validation phase ──
|
# ── Single Oracle connection for entire validation phase ──
|
||||||
@@ -351,6 +449,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
"order_total": order.total or None,
|
"order_total": order.total or None,
|
||||||
"delivery_cost": order.delivery_cost or None,
|
"delivery_cost": order.delivery_cost or None,
|
||||||
"discount_total": order.discount_total or None,
|
"discount_total": order.discount_total or None,
|
||||||
|
"web_status": order.status or None,
|
||||||
"items": order_items_data,
|
"items": order_items_data,
|
||||||
})
|
})
|
||||||
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → DEJA IMPORTAT (ID: {id_comanda_roa})")
|
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → DEJA IMPORTAT (ID: {id_comanda_roa})")
|
||||||
@@ -381,6 +480,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
"order_total": order.total or None,
|
"order_total": order.total or None,
|
||||||
"delivery_cost": order.delivery_cost or None,
|
"delivery_cost": order.delivery_cost or None,
|
||||||
"discount_total": order.discount_total or None,
|
"discount_total": order.discount_total or None,
|
||||||
|
"web_status": order.status or None,
|
||||||
"items": order_items_data,
|
"items": order_items_data,
|
||||||
})
|
})
|
||||||
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → OMIS (lipsa: {', '.join(missing_skus)})")
|
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → OMIS (lipsa: {', '.join(missing_skus)})")
|
||||||
@@ -437,6 +537,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
order_total=order.total or None,
|
order_total=order.total or None,
|
||||||
delivery_cost=order.delivery_cost or None,
|
delivery_cost=order.delivery_cost or None,
|
||||||
discount_total=order.discount_total or None,
|
discount_total=order.discount_total or None,
|
||||||
|
web_status=order.status or None,
|
||||||
)
|
)
|
||||||
await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED")
|
await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED")
|
||||||
# Store ROA address IDs (R9)
|
# Store ROA address IDs (R9)
|
||||||
@@ -465,6 +566,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
order_total=order.total or None,
|
order_total=order.total or None,
|
||||||
delivery_cost=order.delivery_cost or None,
|
delivery_cost=order.delivery_cost or None,
|
||||||
discount_total=order.discount_total or None,
|
discount_total=order.discount_total or None,
|
||||||
|
web_status=order.status or None,
|
||||||
)
|
)
|
||||||
await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR")
|
await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR")
|
||||||
await sqlite_service.add_order_items(order.number, order_items_data)
|
await sqlite_service.add_order_items(order.number, order_items_data)
|
||||||
@@ -548,13 +650,14 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
"run_id": run_id,
|
"run_id": run_id,
|
||||||
"status": status,
|
"status": status,
|
||||||
"json_files": json_count,
|
"json_files": json_count,
|
||||||
"total_orders": len(orders),
|
"total_orders": len(orders) + cancelled_count,
|
||||||
"new_orders": len(truly_importable),
|
"new_orders": len(truly_importable),
|
||||||
"imported": total_imported,
|
"imported": total_imported,
|
||||||
"new_imported": imported_count,
|
"new_imported": imported_count,
|
||||||
"already_imported": already_imported_count,
|
"already_imported": already_imported_count,
|
||||||
"skipped": len(skipped),
|
"skipped": len(skipped),
|
||||||
"errors": error_count,
|
"errors": error_count,
|
||||||
|
"cancelled": cancelled_count,
|
||||||
"missing_skus": len(validation["missing"]),
|
"missing_skus": len(validation["missing"]),
|
||||||
"invoices_updated": invoices_updated,
|
"invoices_updated": invoices_updated,
|
||||||
"invoices_cleared": invoices_cleared,
|
"invoices_cleared": invoices_cleared,
|
||||||
@@ -562,24 +665,25 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
}
|
}
|
||||||
|
|
||||||
_update_progress("completed",
|
_update_progress("completed",
|
||||||
f"Completed: {imported_count} new, {already_imported_count} already, {len(skipped)} skipped, {error_count} errors",
|
f"Completed: {imported_count} new, {already_imported_count} already, {len(skipped)} skipped, {error_count} errors, {cancelled_count} cancelled",
|
||||||
len(truly_importable), len(truly_importable),
|
len(truly_importable), len(truly_importable),
|
||||||
{"imported": imported_count, "skipped": len(skipped), "errors": error_count,
|
{"imported": imported_count, "skipped": len(skipped), "errors": error_count,
|
||||||
"already_imported": already_imported_count})
|
"already_imported": already_imported_count, "cancelled": cancelled_count})
|
||||||
if _current_sync:
|
if _current_sync:
|
||||||
_current_sync["status"] = status
|
_current_sync["status"] = status
|
||||||
_current_sync["finished_at"] = datetime.now().isoformat()
|
_current_sync["finished_at"] = datetime.now().isoformat()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Sync {run_id} completed: {imported_count} new, {already_imported_count} already imported, "
|
f"Sync {run_id} completed: {imported_count} new, {already_imported_count} already imported, "
|
||||||
f"{len(skipped)} skipped, {error_count} errors"
|
f"{len(skipped)} skipped, {error_count} errors, {cancelled_count} cancelled"
|
||||||
)
|
)
|
||||||
|
|
||||||
duration = (datetime.now() - started_dt).total_seconds()
|
duration = (datetime.now() - started_dt).total_seconds()
|
||||||
_log_line(run_id, "")
|
_log_line(run_id, "")
|
||||||
|
cancelled_text = f", {cancelled_count} anulate" if cancelled_count else ""
|
||||||
_run_logs[run_id].append(
|
_run_logs[run_id].append(
|
||||||
f"Finalizat: {imported_count} importate, {already_imported_count} deja importate, "
|
f"Finalizat: {imported_count} importate, {already_imported_count} deja importate, "
|
||||||
f"{len(skipped)} nemapate, {error_count} erori din {len(orders)} comenzi | Durata: {int(duration)}s"
|
f"{len(skipped)} nemapate, {error_count} erori{cancelled_text} din {len(orders) + cancelled_count} comenzi | Durata: {int(duration)}s"
|
||||||
)
|
)
|
||||||
|
|
||||||
return summary
|
return summary
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ body {
|
|||||||
.fc-red { color: #dc2626; }
|
.fc-red { color: #dc2626; }
|
||||||
.fc-neutral { color: #6b7280; }
|
.fc-neutral { color: #6b7280; }
|
||||||
.fc-blue { color: #2563eb; }
|
.fc-blue { color: #2563eb; }
|
||||||
|
.fc-dark { color: #374151; }
|
||||||
|
|
||||||
/* ── Log viewer (dark theme — keep as-is) ────────── */
|
/* ── Log viewer (dark theme — keep as-is) ────────── */
|
||||||
.log-viewer {
|
.log-viewer {
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ async function loadDashOrders() {
|
|||||||
if (el('cntErr')) el('cntErr').textContent = c.error || c.errors || 0;
|
if (el('cntErr')) el('cntErr').textContent = c.error || c.errors || 0;
|
||||||
if (el('cntFact')) el('cntFact').textContent = c.facturate || 0;
|
if (el('cntFact')) el('cntFact').textContent = c.facturate || 0;
|
||||||
if (el('cntNef')) el('cntNef').textContent = c.nefacturate || c.uninvoiced || 0;
|
if (el('cntNef')) el('cntNef').textContent = c.nefacturate || c.uninvoiced || 0;
|
||||||
|
if (el('cntCanc')) el('cntCanc').textContent = c.cancelled || 0;
|
||||||
|
|
||||||
const tbody = document.getElementById('dashOrdersBody');
|
const tbody = document.getElementById('dashOrdersBody');
|
||||||
const orders = data.orders || [];
|
const orders = data.orders || [];
|
||||||
@@ -340,7 +341,8 @@ async function loadDashOrders() {
|
|||||||
{ label: 'Omise', count: c.skipped || 0, value: 'SKIPPED', active: activeStatus === 'SKIPPED', colorClass: 'fc-yellow' },
|
{ label: 'Omise', count: c.skipped || 0, value: 'SKIPPED', active: activeStatus === 'SKIPPED', colorClass: 'fc-yellow' },
|
||||||
{ label: 'Erori', count: c.error || c.errors || 0, value: 'ERROR', active: activeStatus === 'ERROR', colorClass: 'fc-red' },
|
{ label: 'Erori', count: c.error || c.errors || 0, value: 'ERROR', active: activeStatus === 'ERROR', colorClass: 'fc-red' },
|
||||||
{ label: 'Fact.', count: c.facturate || 0, value: 'INVOICED', active: activeStatus === 'INVOICED', colorClass: 'fc-green' },
|
{ label: 'Fact.', count: c.facturate || 0, value: 'INVOICED', active: activeStatus === 'INVOICED', colorClass: 'fc-green' },
|
||||||
{ label: 'Nefact.', count: c.nefacturate || c.uninvoiced || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-red' }
|
{ label: 'Nefact.', count: c.nefacturate || c.uninvoiced || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-red' },
|
||||||
|
{ label: 'Anulate', count: c.cancelled || 0, value: 'CANCELLED', active: activeStatus === 'CANCELLED', colorClass: 'fc-dark' }
|
||||||
], (val) => {
|
], (val) => {
|
||||||
document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active'));
|
||||||
const pill = document.querySelector(`.filter-pill[data-status="${val}"]`);
|
const pill = document.querySelector(`.filter-pill[data-status="${val}"]`);
|
||||||
@@ -438,6 +440,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 'CANCELLED': return '<span class="badge bg-secondary">Anulat</span>';
|
||||||
case 'DELETED_IN_ROA': return '<span class="badge bg-dark">Sters din ROA</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>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,9 @@ function statusDot(status) {
|
|||||||
case 'ERROR':
|
case 'ERROR':
|
||||||
case 'FAILED':
|
case 'FAILED':
|
||||||
return '<span class="dot dot-red"></span>';
|
return '<span class="dot dot-red"></span>';
|
||||||
|
case 'CANCELLED':
|
||||||
|
case 'DELETED_IN_ROA':
|
||||||
|
return '<span class="dot dot-gray"></span>';
|
||||||
default:
|
default:
|
||||||
return '<span class="dot dot-gray"></span>';
|
return '<span class="dot dot-gray"></span>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
{% set rp = request.scope.get('root_path', '') %}
|
{% set rp = request.scope.get('root_path', '') %}
|
||||||
<link href="{{ rp }}/static/css/style.css?v=10" rel="stylesheet">
|
<link href="{{ rp }}/static/css/style.css?v=11" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Top Navbar -->
|
<!-- Top Navbar -->
|
||||||
@@ -29,7 +29,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=10"></script>
|
<script src="{{ rp }}/static/js/shared.js?v=11"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
<button class="filter-pill d-none d-md-inline-flex" data-status="ERROR">Erori <span class="filter-count fc-red" id="cntErr">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-status="ERROR">Erori <span class="filter-count fc-red" id="cntErr">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-status="INVOICED">Facturate <span class="filter-count fc-green" id="cntFact">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-status="INVOICED">Facturate <span class="filter-count fc-green" id="cntFact">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-status="UNINVOICED">Nefacturate <span class="filter-count fc-red" id="cntNef">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-status="UNINVOICED">Nefacturate <span class="filter-count fc-red" id="cntNef">0</span></button>
|
||||||
|
<button class="filter-pill d-none d-md-inline-flex" data-status="CANCELLED">Anulate <span class="filter-count fc-dark" id="cntCanc">0</span></button>
|
||||||
<button class="btn btn-sm btn-outline-secondary d-none d-md-inline-flex align-items-center gap-1" id="btnRefreshInvoices" onclick="refreshInvoices()" title="Actualizeaza status facturi din Oracle">↻ Facturi</button>
|
<button class="btn btn-sm btn-outline-secondary d-none d-md-inline-flex align-items-center gap-1" id="btnRefreshInvoices" onclick="refreshInvoices()" title="Actualizeaza status facturi din Oracle">↻ Facturi</button>
|
||||||
<!-- Search (integrated, end of row) -->
|
<!-- Search (integrated, end of row) -->
|
||||||
<input type="search" id="orderSearch" placeholder="Cauta..." class="search-input">
|
<input type="search" id="orderSearch" placeholder="Cauta..." class="search-input">
|
||||||
@@ -199,5 +200,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=13"></script>
|
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=14"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user