refactor(status): introduce OrderStatus enum, replace string literals
Centralized order status values in api/app/constants.py via a str-valued Enum so comparisons keep working. Replaced literals in: - services: sync_service, sqlite_service, retry_service - routers: sync, dashboard - templates: dashboard.html, logs.html - static JS: shared (ORDER_STATUS mirror), dashboard, logs - tests: requirements, order_items_overwrite, business_rules MALFORMED intentionally NOT added — introduced in follow-up PR2 (per-order failure isolation). Full test suite: 231 unit + 33 e2e pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
api/app/constants.py
Normal file
17
api/app/constants.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"""Application-wide constants shared across services, routers, and tests."""
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class OrderStatus(str, Enum):
|
||||||
|
"""Order status values stored in SQLite `orders.status` column.
|
||||||
|
|
||||||
|
Inherits from `str` so existing string comparisons (==, in, dict.get)
|
||||||
|
keep working. Always use `.value` when passing to SQL queries or JSON
|
||||||
|
payloads to avoid Python-version-specific str(enum) surprises.
|
||||||
|
"""
|
||||||
|
IMPORTED = "IMPORTED"
|
||||||
|
ALREADY_IMPORTED = "ALREADY_IMPORTED"
|
||||||
|
SKIPPED = "SKIPPED"
|
||||||
|
ERROR = "ERROR"
|
||||||
|
CANCELLED = "CANCELLED"
|
||||||
|
DELETED_IN_ROA = "DELETED_IN_ROA"
|
||||||
@@ -4,9 +4,11 @@ from fastapi.responses import HTMLResponse
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..services import sqlite_service
|
from ..services import sqlite_service
|
||||||
|
from ..constants import OrderStatus
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||||
|
templates.env.globals["OrderStatus"] = OrderStatus
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
async def dashboard(request: Request):
|
async def dashboard(request: Request):
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ from typing import Optional
|
|||||||
|
|
||||||
from ..services import sync_service, scheduler_service, sqlite_service, invoice_service
|
from ..services import sync_service, scheduler_service, sqlite_service, invoice_service
|
||||||
from .. import database
|
from .. import database
|
||||||
|
from ..constants import OrderStatus
|
||||||
|
|
||||||
router = APIRouter(tags=["sync"])
|
router = APIRouter(tags=["sync"])
|
||||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||||
|
templates.env.globals["OrderStatus"] = OrderStatus
|
||||||
|
|
||||||
|
|
||||||
async def _enrich_items_with_codmat(items: list) -> None:
|
async def _enrich_items_with_codmat(items: list) -> None:
|
||||||
@@ -231,13 +233,13 @@ def _format_text_log_from_detail(detail: dict) -> str:
|
|||||||
customer = o.get("customer_name", "?")
|
customer = o.get("customer_name", "?")
|
||||||
order_date = o.get("order_date") or "?"
|
order_date = o.get("order_date") or "?"
|
||||||
|
|
||||||
if status == "IMPORTED":
|
if status == OrderStatus.IMPORTED.value:
|
||||||
id_cmd = o.get("id_comanda", "?")
|
id_cmd = o.get("id_comanda", "?")
|
||||||
lines.append(f"#{number} [{order_date}] {customer} → IMPORTAT (ID: {id_cmd})")
|
lines.append(f"#{number} [{order_date}] {customer} → IMPORTAT (ID: {id_cmd})")
|
||||||
elif status == "ALREADY_IMPORTED":
|
elif status == OrderStatus.ALREADY_IMPORTED.value:
|
||||||
id_cmd = o.get("id_comanda", "?")
|
id_cmd = o.get("id_comanda", "?")
|
||||||
lines.append(f"#{number} [{order_date}] {customer} → DEJA IMPORTAT (ID: {id_cmd})")
|
lines.append(f"#{number} [{order_date}] {customer} → DEJA IMPORTAT (ID: {id_cmd})")
|
||||||
elif status == "SKIPPED":
|
elif status == OrderStatus.SKIPPED.value:
|
||||||
missing = o.get("missing_skus", "")
|
missing = o.get("missing_skus", "")
|
||||||
if isinstance(missing, str):
|
if isinstance(missing, str):
|
||||||
try:
|
try:
|
||||||
@@ -246,7 +248,7 @@ def _format_text_log_from_detail(detail: dict) -> str:
|
|||||||
missing = [missing] if missing else []
|
missing = [missing] if missing else []
|
||||||
skus_str = ", ".join(missing) if isinstance(missing, list) else str(missing)
|
skus_str = ", ".join(missing) if isinstance(missing, list) else str(missing)
|
||||||
lines.append(f"#{number} [{order_date}] {customer} → OMIS (lipsa: {skus_str})")
|
lines.append(f"#{number} [{order_date}] {customer} → OMIS (lipsa: {skus_str})")
|
||||||
elif status == "ERROR":
|
elif status == OrderStatus.ERROR.value:
|
||||||
err = o.get("error_message", "necunoscuta")
|
err = o.get("error_message", "necunoscuta")
|
||||||
lines.append(f"#{number} [{order_date}] {customer} → EROARE: {err}")
|
lines.append(f"#{number} [{order_date}] {customer} → EROARE: {err}")
|
||||||
|
|
||||||
@@ -618,7 +620,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
|||||||
is_invoiced_filter = (status == "INVOICED")
|
is_invoiced_filter = (status == "INVOICED")
|
||||||
|
|
||||||
# For UNINVOICED/INVOICED: fetch all IMPORTED orders, then filter post-invoice-check
|
# For UNINVOICED/INVOICED: fetch all IMPORTED orders, then filter post-invoice-check
|
||||||
fetch_status = "IMPORTED" if (is_uninvoiced_filter or is_invoiced_filter) else status
|
fetch_status = OrderStatus.IMPORTED.value if (is_uninvoiced_filter or is_invoiced_filter) else status
|
||||||
fetch_per_page = 10000 if (is_uninvoiced_filter or is_invoiced_filter) else per_page
|
fetch_per_page = 10000 if (is_uninvoiced_filter or is_invoiced_filter) else per_page
|
||||||
fetch_page = 1 if (is_uninvoiced_filter or is_invoiced_filter) else page
|
fetch_page = 1 if (is_uninvoiced_filter or is_invoiced_filter) else page
|
||||||
|
|
||||||
@@ -687,7 +689,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
|||||||
newly_invoiced = sum(1 for o in uncached_orders if o.get("invoice") and o["invoice"].get("facturat"))
|
newly_invoiced = sum(1 for o in uncached_orders if o.get("invoice") and o["invoice"].get("facturat"))
|
||||||
uninvoiced_base = counts.get("uninvoiced_sqlite", sum(
|
uninvoiced_base = counts.get("uninvoiced_sqlite", sum(
|
||||||
1 for o in all_orders
|
1 for o in all_orders
|
||||||
if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice")
|
if o.get("status") in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) and not o.get("invoice")
|
||||||
))
|
))
|
||||||
counts["nefacturate"] = max(0, uninvoiced_base - newly_invoiced)
|
counts["nefacturate"] = max(0, uninvoiced_base - newly_invoiced)
|
||||||
imported_total = counts.get("imported_all") or counts.get("imported", 0)
|
imported_total = counts.get("imported_all") or counts.get("imported", 0)
|
||||||
@@ -713,7 +715,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
|||||||
|
|
||||||
# For UNINVOICED filter: apply server-side filtering + pagination
|
# For UNINVOICED filter: apply server-side filtering + pagination
|
||||||
if is_uninvoiced_filter:
|
if is_uninvoiced_filter:
|
||||||
filtered = [o for o in all_orders if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice")]
|
filtered = [o for o in all_orders if o.get("status") in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) and not o.get("invoice")]
|
||||||
total = len(filtered)
|
total = len(filtered)
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
result["orders"] = filtered[offset:offset + per_page]
|
result["orders"] = filtered[offset:offset + per_page]
|
||||||
@@ -722,7 +724,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
|||||||
result["per_page"] = per_page
|
result["per_page"] = per_page
|
||||||
result["pages"] = (total + per_page - 1) // per_page if total > 0 else 0
|
result["pages"] = (total + per_page - 1) // per_page if total > 0 else 0
|
||||||
elif is_invoiced_filter:
|
elif is_invoiced_filter:
|
||||||
filtered = [o for o in all_orders if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and o.get("invoice")]
|
filtered = [o for o in all_orders if o.get("status") in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) and o.get("invoice")]
|
||||||
total = len(filtered)
|
total = len(filtered)
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
result["orders"] = filtered[offset:offset + per_page]
|
result["orders"] = filtered[offset:offset + per_page]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import logging
|
|||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from ..constants import OrderStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome
|
|||||||
order_number=order_number,
|
order_number=order_number,
|
||||||
order_date=order_date_str,
|
order_date=order_date_str,
|
||||||
customer_name=customer_name,
|
customer_name=customer_name,
|
||||||
status="ERROR",
|
status=OrderStatus.ERROR.value,
|
||||||
error_message=f"Retry failed: {e}",
|
error_message=f"Retry failed: {e}",
|
||||||
)
|
)
|
||||||
return {"success": False, "message": f"Eroare import: {e}"}
|
return {"success": False, "message": f"Eroare import: {e}"}
|
||||||
@@ -103,7 +105,7 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome
|
|||||||
order_number=order_number,
|
order_number=order_number,
|
||||||
order_date=order_date_str,
|
order_date=order_date_str,
|
||||||
customer_name=customer_name,
|
customer_name=customer_name,
|
||||||
status="IMPORTED",
|
status=OrderStatus.IMPORTED.value,
|
||||||
id_comanda=result.get("id_comanda"),
|
id_comanda=result.get("id_comanda"),
|
||||||
id_partener=result.get("id_partener"),
|
id_partener=result.get("id_partener"),
|
||||||
error_message=None,
|
error_message=None,
|
||||||
@@ -116,7 +118,7 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome
|
|||||||
)
|
)
|
||||||
await sqlite_service.add_order_items(order_number, order_items_data)
|
await sqlite_service.add_order_items(order_number, order_items_data)
|
||||||
logger.info(f"Retry successful for order {order_number} → IMPORTED ({len(order_items_data)} items)")
|
logger.info(f"Retry successful for order {order_number} → IMPORTED ({len(order_items_data)} items)")
|
||||||
return {"success": True, "message": "Comanda reimportata cu succes", "status": "IMPORTED"}
|
return {"success": True, "message": "Comanda reimportata cu succes", "status": OrderStatus.IMPORTED.value}
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "Unknown error")
|
error = result.get("error", "Unknown error")
|
||||||
await sqlite_service.upsert_order(
|
await sqlite_service.upsert_order(
|
||||||
@@ -124,11 +126,11 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome
|
|||||||
order_number=order_number,
|
order_number=order_number,
|
||||||
order_date=order_date_str,
|
order_date=order_date_str,
|
||||||
customer_name=customer_name,
|
customer_name=customer_name,
|
||||||
status="ERROR",
|
status=OrderStatus.ERROR.value,
|
||||||
error_message=f"Retry: {error}",
|
error_message=f"Retry: {error}",
|
||||||
)
|
)
|
||||||
await sqlite_service.add_order_items(order_number, order_items_data)
|
await sqlite_service.add_order_items(order_number, order_items_data)
|
||||||
return {"success": False, "message": f"Import esuat: {error}", "status": "ERROR"}
|
return {"success": False, "message": f"Import esuat: {error}", "status": OrderStatus.ERROR.value}
|
||||||
|
|
||||||
|
|
||||||
async def retry_single_order(order_number: str, app_settings: dict) -> dict:
|
async def retry_single_order(order_number: str, app_settings: dict) -> dict:
|
||||||
@@ -157,7 +159,7 @@ async def retry_single_order(order_number: str, app_settings: dict) -> dict:
|
|||||||
|
|
||||||
order_data = detail["order"]
|
order_data = detail["order"]
|
||||||
status = order_data.get("status", "")
|
status = order_data.get("status", "")
|
||||||
if status not in ("ERROR", "SKIPPED", "DELETED_IN_ROA"):
|
if status not in (OrderStatus.ERROR.value, OrderStatus.SKIPPED.value, OrderStatus.DELETED_IN_ROA.value):
|
||||||
return {"success": False, "message": f"Retry permis doar pentru ERROR/SKIPPED/DELETED_IN_ROA (status actual: {status})"}
|
return {"success": False, "message": f"Retry permis doar pentru ERROR/SKIPPED/DELETED_IN_ROA (status actual: {status})"}
|
||||||
|
|
||||||
order_date_str = order_data.get("order_date", "")
|
order_date_str = order_data.get("order_date", "")
|
||||||
@@ -196,7 +198,7 @@ async def resync_single_order(order_number: str, app_settings: dict) -> dict:
|
|||||||
status = order_data.get("status", "")
|
status = order_data.get("status", "")
|
||||||
id_comanda = order_data.get("id_comanda")
|
id_comanda = order_data.get("id_comanda")
|
||||||
|
|
||||||
if status not in ("IMPORTED", "ALREADY_IMPORTED") or not id_comanda:
|
if status not in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) or not id_comanda:
|
||||||
return {"success": False, "message": f"Resync permis doar pentru IMPORTED/ALREADY_IMPORTED cu id_comanda (status actual: {status})"}
|
return {"success": False, "message": f"Resync permis doar pentru IMPORTED/ALREADY_IMPORTED cu id_comanda (status actual: {status})"}
|
||||||
|
|
||||||
# Invoice safety gate
|
# Invoice safety gate
|
||||||
@@ -269,7 +271,7 @@ async def delete_single_order(order_number: str) -> dict:
|
|||||||
status = order_data.get("status", "")
|
status = order_data.get("status", "")
|
||||||
id_comanda = order_data.get("id_comanda")
|
id_comanda = order_data.get("id_comanda")
|
||||||
|
|
||||||
if status not in ("IMPORTED", "ALREADY_IMPORTED") or not id_comanda:
|
if status not in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) or not id_comanda:
|
||||||
return {"success": False, "message": f"Stergere permisa doar pentru IMPORTED/ALREADY_IMPORTED cu id_comanda (status actual: {status})"}
|
return {"success": False, "message": f"Stergere permisa doar pentru IMPORTED/ALREADY_IMPORTED cu id_comanda (status actual: {status})"}
|
||||||
|
|
||||||
# Invoice safety gate
|
# Invoice safety gate
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from ..database import get_sqlite, get_sqlite_sync
|
from ..database import get_sqlite, get_sqlite_sync
|
||||||
|
from ..constants import OrderStatus
|
||||||
|
|
||||||
# Re-export so other services can import get_sqlite from sqlite_service
|
# Re-export so other services can import get_sqlite from sqlite_service
|
||||||
__all__ = ["get_sqlite", "get_sqlite_sync"]
|
__all__ = ["get_sqlite", "get_sqlite_sync"]
|
||||||
@@ -68,7 +69,7 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
|||||||
"""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:
|
||||||
await db.execute("""
|
await db.execute(f"""
|
||||||
INSERT INTO orders
|
INSERT INTO orders
|
||||||
(order_number, order_date, customer_name, status,
|
(order_number, order_date, customer_name, status,
|
||||||
id_comanda, id_partener, error_message, missing_skus, items_count,
|
id_comanda, id_partener, error_message, missing_skus, items_count,
|
||||||
@@ -79,7 +80,7 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
|||||||
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
|
||||||
WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED'
|
WHEN orders.status = '{OrderStatus.IMPORTED.value}' AND excluded.status = '{OrderStatus.ALREADY_IMPORTED.value}'
|
||||||
THEN orders.status
|
THEN orders.status
|
||||||
ELSE excluded.status
|
ELSE excluded.status
|
||||||
END,
|
END,
|
||||||
@@ -88,7 +89,7 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
|||||||
items_count = excluded.items_count,
|
items_count = excluded.items_count,
|
||||||
id_comanda = COALESCE(excluded.id_comanda, orders.id_comanda),
|
id_comanda = COALESCE(excluded.id_comanda, orders.id_comanda),
|
||||||
id_partener = COALESCE(excluded.id_partener, orders.id_partener),
|
id_partener = COALESCE(excluded.id_partener, orders.id_partener),
|
||||||
times_skipped = CASE WHEN excluded.status = 'SKIPPED'
|
times_skipped = CASE WHEN excluded.status = '{OrderStatus.SKIPPED.value}'
|
||||||
THEN orders.times_skipped + 1
|
THEN orders.times_skipped + 1
|
||||||
ELSE orders.times_skipped END,
|
ELSE orders.times_skipped END,
|
||||||
last_sync_run_id = excluded.last_sync_run_id,
|
last_sync_run_id = excluded.last_sync_run_id,
|
||||||
@@ -140,7 +141,7 @@ async def save_orders_batch(orders_data: list[dict]):
|
|||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
# 1. Upsert orders
|
# 1. Upsert orders
|
||||||
await db.executemany("""
|
await db.executemany(f"""
|
||||||
INSERT INTO orders
|
INSERT INTO orders
|
||||||
(order_number, order_date, customer_name, status,
|
(order_number, order_date, customer_name, status,
|
||||||
id_comanda, id_partener, error_message, missing_skus, items_count,
|
id_comanda, id_partener, error_message, missing_skus, items_count,
|
||||||
@@ -151,7 +152,7 @@ async def save_orders_batch(orders_data: list[dict]):
|
|||||||
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
|
||||||
WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED'
|
WHEN orders.status = '{OrderStatus.IMPORTED.value}' AND excluded.status = '{OrderStatus.ALREADY_IMPORTED.value}'
|
||||||
THEN orders.status
|
THEN orders.status
|
||||||
ELSE excluded.status
|
ELSE excluded.status
|
||||||
END,
|
END,
|
||||||
@@ -160,7 +161,7 @@ async def save_orders_batch(orders_data: list[dict]):
|
|||||||
items_count = excluded.items_count,
|
items_count = excluded.items_count,
|
||||||
id_comanda = COALESCE(excluded.id_comanda, orders.id_comanda),
|
id_comanda = COALESCE(excluded.id_comanda, orders.id_comanda),
|
||||||
id_partener = COALESCE(excluded.id_partener, orders.id_partener),
|
id_partener = COALESCE(excluded.id_partener, orders.id_partener),
|
||||||
times_skipped = CASE WHEN excluded.status = 'SKIPPED'
|
times_skipped = CASE WHEN excluded.status = '{OrderStatus.SKIPPED.value}'
|
||||||
THEN orders.times_skipped + 1
|
THEN orders.times_skipped + 1
|
||||||
ELSE orders.times_skipped END,
|
ELSE orders.times_skipped END,
|
||||||
last_sync_run_id = excluded.last_sync_run_id,
|
last_sync_run_id = excluded.last_sync_run_id,
|
||||||
@@ -400,17 +401,17 @@ async def get_dashboard_stats():
|
|||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT COUNT(*) FROM orders WHERE status = 'IMPORTED'"
|
f"SELECT COUNT(*) FROM orders WHERE status = '{OrderStatus.IMPORTED.value}'"
|
||||||
)
|
)
|
||||||
imported = (await cursor.fetchone())[0]
|
imported = (await cursor.fetchone())[0]
|
||||||
|
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT COUNT(*) FROM orders WHERE status = 'SKIPPED'"
|
f"SELECT COUNT(*) FROM orders WHERE status = '{OrderStatus.SKIPPED.value}'"
|
||||||
)
|
)
|
||||||
skipped = (await cursor.fetchone())[0]
|
skipped = (await cursor.fetchone())[0]
|
||||||
|
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT COUNT(*) FROM orders WHERE status = 'ERROR'"
|
f"SELECT COUNT(*) FROM orders WHERE status = '{OrderStatus.ERROR.value}'"
|
||||||
)
|
)
|
||||||
errors = (await cursor.fetchone())[0]
|
errors = (await cursor.fetchone())[0]
|
||||||
|
|
||||||
@@ -694,11 +695,11 @@ async def get_run_orders_filtered(run_id: str, status_filter: str = "all",
|
|||||||
"per_page": per_page,
|
"per_page": per_page,
|
||||||
"pages": (total + per_page - 1) // per_page if total > 0 else 0,
|
"pages": (total + per_page - 1) // per_page if total > 0 else 0,
|
||||||
"counts": {
|
"counts": {
|
||||||
"imported": status_counts.get("IMPORTED", 0),
|
"imported": status_counts.get(OrderStatus.IMPORTED.value, 0),
|
||||||
"skipped": status_counts.get("SKIPPED", 0),
|
"skipped": status_counts.get(OrderStatus.SKIPPED.value, 0),
|
||||||
"error": status_counts.get("ERROR", 0),
|
"error": status_counts.get(OrderStatus.ERROR.value, 0),
|
||||||
"already_imported": status_counts.get("ALREADY_IMPORTED", 0),
|
"already_imported": status_counts.get(OrderStatus.ALREADY_IMPORTED.value, 0),
|
||||||
"cancelled": status_counts.get("CANCELLED", 0),
|
"cancelled": status_counts.get(OrderStatus.CANCELLED.value, 0),
|
||||||
"total": sum(status_counts.values())
|
"total": sum(status_counts.values())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -738,8 +739,8 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
|||||||
data_params = list(base_params)
|
data_params = list(base_params)
|
||||||
|
|
||||||
if status_filter and status_filter not in ("all", "UNINVOICED"):
|
if status_filter and status_filter not in ("all", "UNINVOICED"):
|
||||||
if status_filter.upper() == "IMPORTED":
|
if status_filter.upper() == OrderStatus.IMPORTED.value:
|
||||||
data_clauses.append("UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')")
|
data_clauses.append(f"UPPER(status) IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')")
|
||||||
elif status_filter.upper() == "DIFFS":
|
elif status_filter.upper() == "DIFFS":
|
||||||
data_clauses.append(
|
data_clauses.append(
|
||||||
"(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1"
|
"(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1"
|
||||||
@@ -785,7 +786,7 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
|||||||
|
|
||||||
# Uninvoiced count: IMPORTED/ALREADY_IMPORTED with no cached invoice, same period+search
|
# Uninvoiced count: IMPORTED/ALREADY_IMPORTED with no cached invoice, same period+search
|
||||||
uninv_clauses = list(base_clauses) + [
|
uninv_clauses = list(base_clauses) + [
|
||||||
"UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')",
|
f"UPPER(status) IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')",
|
||||||
"(factura_numar IS NULL OR factura_numar = '')",
|
"(factura_numar IS NULL OR factura_numar = '')",
|
||||||
]
|
]
|
||||||
uninv_where = "WHERE " + " AND ".join(uninv_clauses)
|
uninv_where = "WHERE " + " AND ".join(uninv_clauses)
|
||||||
@@ -794,7 +795,7 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
|||||||
|
|
||||||
# Uninvoiced > 3 days old
|
# Uninvoiced > 3 days old
|
||||||
uninv_old_clauses = list(base_clauses) + [
|
uninv_old_clauses = list(base_clauses) + [
|
||||||
"UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')",
|
f"UPPER(status) IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')",
|
||||||
"(factura_numar IS NULL OR factura_numar = '')",
|
"(factura_numar IS NULL OR factura_numar = '')",
|
||||||
"order_date < datetime('now', '-3 days')",
|
"order_date < datetime('now', '-3 days')",
|
||||||
]
|
]
|
||||||
@@ -828,12 +829,12 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
|||||||
"per_page": per_page,
|
"per_page": per_page,
|
||||||
"pages": (total + per_page - 1) // per_page if total > 0 else 0,
|
"pages": (total + per_page - 1) // per_page if total > 0 else 0,
|
||||||
"counts": {
|
"counts": {
|
||||||
"imported": status_counts.get("IMPORTED", 0),
|
"imported": status_counts.get(OrderStatus.IMPORTED.value, 0),
|
||||||
"already_imported": status_counts.get("ALREADY_IMPORTED", 0),
|
"already_imported": status_counts.get(OrderStatus.ALREADY_IMPORTED.value, 0),
|
||||||
"imported_all": status_counts.get("IMPORTED", 0) + status_counts.get("ALREADY_IMPORTED", 0),
|
"imported_all": status_counts.get(OrderStatus.IMPORTED.value, 0) + status_counts.get(OrderStatus.ALREADY_IMPORTED.value, 0),
|
||||||
"skipped": status_counts.get("SKIPPED", 0),
|
"skipped": status_counts.get(OrderStatus.SKIPPED.value, 0),
|
||||||
"error": status_counts.get("ERROR", 0),
|
"error": status_counts.get(OrderStatus.ERROR.value, 0),
|
||||||
"cancelled": status_counts.get("CANCELLED", 0),
|
"cancelled": status_counts.get(OrderStatus.CANCELLED.value, 0),
|
||||||
"total": sum(status_counts.values()),
|
"total": sum(status_counts.values()),
|
||||||
"uninvoiced_sqlite": uninvoiced_sqlite,
|
"uninvoiced_sqlite": uninvoiced_sqlite,
|
||||||
"uninvoiced_old": uninvoiced_old,
|
"uninvoiced_old": uninvoiced_old,
|
||||||
@@ -869,9 +870,9 @@ async def get_uninvoiced_imported_orders() -> list:
|
|||||||
"""Get all imported orders that don't yet have invoice data cached."""
|
"""Get all imported orders that don't yet have invoice data cached."""
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(f"""
|
||||||
SELECT order_number, id_comanda FROM orders
|
SELECT order_number, id_comanda FROM orders
|
||||||
WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED')
|
WHERE status IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')
|
||||||
AND id_comanda IS NOT NULL
|
AND id_comanda IS NOT NULL
|
||||||
AND factura_numar IS NULL
|
AND factura_numar IS NULL
|
||||||
""")
|
""")
|
||||||
@@ -923,9 +924,9 @@ async def get_invoiced_imported_orders() -> list:
|
|||||||
"""Get imported orders that HAVE cached invoice data (for re-verification)."""
|
"""Get imported orders that HAVE cached invoice data (for re-verification)."""
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(f"""
|
||||||
SELECT order_number, id_comanda FROM orders
|
SELECT order_number, id_comanda FROM orders
|
||||||
WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED')
|
WHERE status IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')
|
||||||
AND id_comanda IS NOT NULL
|
AND id_comanda IS NOT NULL
|
||||||
AND factura_numar IS NOT NULL AND factura_numar != ''
|
AND factura_numar IS NOT NULL AND factura_numar != ''
|
||||||
""")
|
""")
|
||||||
@@ -939,9 +940,9 @@ async def get_all_imported_orders() -> list:
|
|||||||
"""Get ALL imported orders with id_comanda (for checking if deleted in ROA)."""
|
"""Get ALL imported orders with id_comanda (for checking if deleted in ROA)."""
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(f"""
|
||||||
SELECT order_number, id_comanda FROM orders
|
SELECT order_number, id_comanda FROM orders
|
||||||
WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED')
|
WHERE status IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')
|
||||||
AND id_comanda IS NOT NULL
|
AND id_comanda IS NOT NULL
|
||||||
""")
|
""")
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
@@ -976,9 +977,9 @@ async def mark_order_deleted_in_roa(order_number: str):
|
|||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
await db.execute("DELETE FROM order_items WHERE order_number = ?", (order_number,))
|
await db.execute("DELETE FROM order_items WHERE order_number = ?", (order_number,))
|
||||||
await db.execute("""
|
await db.execute(f"""
|
||||||
UPDATE orders SET
|
UPDATE orders SET
|
||||||
status = 'DELETED_IN_ROA',
|
status = '{OrderStatus.DELETED_IN_ROA.value}',
|
||||||
id_comanda = NULL,
|
id_comanda = NULL,
|
||||||
id_partener = NULL,
|
id_partener = NULL,
|
||||||
factura_serie = NULL,
|
factura_serie = NULL,
|
||||||
@@ -1001,9 +1002,9 @@ 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."""
|
"""Mark an order as cancelled from GoMag. Clears id_comanda and invoice cache."""
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
await db.execute("""
|
await db.execute(f"""
|
||||||
UPDATE orders SET
|
UPDATE orders SET
|
||||||
status = 'CANCELLED',
|
status = '{OrderStatus.CANCELLED.value}',
|
||||||
id_comanda = NULL,
|
id_comanda = NULL,
|
||||||
id_partener = NULL,
|
id_partener = NULL,
|
||||||
factura_serie = NULL,
|
factura_serie = NULL,
|
||||||
@@ -1055,11 +1056,11 @@ async def get_skipped_orders_with_sku(sku: str) -> list[str]:
|
|||||||
"""Get order_numbers of SKIPPED orders that contain the given SKU."""
|
"""Get order_numbers of SKIPPED orders that contain the given SKU."""
|
||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(f"""
|
||||||
SELECT DISTINCT oi.order_number
|
SELECT DISTINCT oi.order_number
|
||||||
FROM order_items oi
|
FROM order_items oi
|
||||||
JOIN orders o ON o.order_number = oi.order_number
|
JOIN orders o ON o.order_number = oi.order_number
|
||||||
WHERE oi.sku = ? AND o.status = 'SKIPPED'
|
WHERE oi.sku = ? AND o.status = '{OrderStatus.SKIPPED.value}'
|
||||||
""", (sku,))
|
""", (sku,))
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
return [row[0] for row in rows]
|
return [row[0] for row in rows]
|
||||||
@@ -1314,7 +1315,7 @@ async def get_orders_missing_anaf() -> list[dict]:
|
|||||||
WHERE cod_fiscal_roa IS NOT NULL
|
WHERE cod_fiscal_roa IS NOT NULL
|
||||||
AND cod_fiscal_roa != ''
|
AND cod_fiscal_roa != ''
|
||||||
AND anaf_platitor_tva IS NULL
|
AND anaf_platitor_tva IS NULL
|
||||||
AND status IN ('IMPORTED', 'ALREADY_IMPORTED')
|
AND status IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')
|
||||||
""")
|
""")
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ def _now():
|
|||||||
from . import order_reader, validation_service, import_service, sqlite_service, invoice_service, gomag_client, anaf_service
|
from . import order_reader, validation_service, import_service, sqlite_service, invoice_service, gomag_client, anaf_service
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from .. import database
|
from .. import database
|
||||||
|
from ..constants import OrderStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -166,20 +167,20 @@ async def _fix_stale_error_orders(existing_map: dict, run_id: str):
|
|||||||
db = await get_sqlite()
|
db = await get_sqlite()
|
||||||
try:
|
try:
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT order_number FROM orders WHERE status = 'ERROR'"
|
f"SELECT order_number FROM orders WHERE status = '{OrderStatus.ERROR.value}'"
|
||||||
)
|
)
|
||||||
error_orders = [row["order_number"] for row in await cursor.fetchall()]
|
error_orders = [row["order_number"] for row in await cursor.fetchall()]
|
||||||
fixed = 0
|
fixed = 0
|
||||||
for order_number in error_orders:
|
for order_number in error_orders:
|
||||||
if order_number in existing_map:
|
if order_number in existing_map:
|
||||||
id_comanda = existing_map[order_number]
|
id_comanda = existing_map[order_number]
|
||||||
await db.execute("""
|
await db.execute(f"""
|
||||||
UPDATE orders SET
|
UPDATE orders SET
|
||||||
status = 'ALREADY_IMPORTED',
|
status = '{OrderStatus.ALREADY_IMPORTED.value}',
|
||||||
id_comanda = ?,
|
id_comanda = ?,
|
||||||
error_message = NULL,
|
error_message = NULL,
|
||||||
updated_at = datetime('now')
|
updated_at = datetime('now')
|
||||||
WHERE order_number = ? AND status = 'ERROR'
|
WHERE order_number = ? AND status = '{OrderStatus.ERROR.value}'
|
||||||
""", (id_comanda, order_number))
|
""", (id_comanda, order_number))
|
||||||
fixed += 1
|
fixed += 1
|
||||||
_log_line(run_id, f"#{order_number} → status corectat ERROR → ALREADY_IMPORTED (ID: {id_comanda})")
|
_log_line(run_id, f"#{order_number} → status corectat ERROR → ALREADY_IMPORTED (ID: {id_comanda})")
|
||||||
@@ -293,7 +294,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
cancelled_batch.append({
|
cancelled_batch.append({
|
||||||
"sync_run_id": run_id, "order_number": order.number,
|
"sync_run_id": run_id, "order_number": order.number,
|
||||||
"order_date": order.date, "customer_name": customer,
|
"order_date": order.date, "customer_name": customer,
|
||||||
"status": "CANCELLED", "status_at_run": "CANCELLED",
|
"status": OrderStatus.CANCELLED.value, "status_at_run": OrderStatus.CANCELLED.value,
|
||||||
"id_comanda": None, "id_partener": None,
|
"id_comanda": None, "id_partener": None,
|
||||||
"error_message": "Comanda anulata in GoMag",
|
"error_message": "Comanda anulata in GoMag",
|
||||||
"missing_skus": None,
|
"missing_skus": None,
|
||||||
@@ -320,7 +321,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
SELECT order_number, id_comanda FROM orders
|
SELECT order_number, id_comanda FROM orders
|
||||||
WHERE order_number IN ({placeholders})
|
WHERE order_number IN ({placeholders})
|
||||||
AND id_comanda IS NOT NULL
|
AND id_comanda IS NOT NULL
|
||||||
AND status = 'CANCELLED'
|
AND status = '{OrderStatus.CANCELLED.value}'
|
||||||
""", cancelled_numbers)
|
""", cancelled_numbers)
|
||||||
previously_imported = [dict(r) for r in await cursor.fetchall()]
|
previously_imported = [dict(r) for r in await cursor.fetchall()]
|
||||||
finally:
|
finally:
|
||||||
@@ -624,7 +625,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
already_batch.append({
|
already_batch.append({
|
||||||
"sync_run_id": run_id, "order_number": order.number,
|
"sync_run_id": run_id, "order_number": order.number,
|
||||||
"order_date": order.date, "customer_name": customer,
|
"order_date": order.date, "customer_name": customer,
|
||||||
"status": "ALREADY_IMPORTED", "status_at_run": "ALREADY_IMPORTED",
|
"status": OrderStatus.ALREADY_IMPORTED.value, "status_at_run": OrderStatus.ALREADY_IMPORTED.value,
|
||||||
"id_comanda": id_comanda_roa, "id_partener": None,
|
"id_comanda": id_comanda_roa, "id_partener": None,
|
||||||
"error_message": None, "missing_skus": None,
|
"error_message": None, "missing_skus": None,
|
||||||
"items_count": len(order.items),
|
"items_count": len(order.items),
|
||||||
@@ -736,7 +737,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
skipped_batch.append({
|
skipped_batch.append({
|
||||||
"sync_run_id": run_id, "order_number": order.number,
|
"sync_run_id": run_id, "order_number": order.number,
|
||||||
"order_date": order.date, "customer_name": customer,
|
"order_date": order.date, "customer_name": customer,
|
||||||
"status": "SKIPPED", "status_at_run": "SKIPPED",
|
"status": OrderStatus.SKIPPED.value, "status_at_run": OrderStatus.SKIPPED.value,
|
||||||
"id_comanda": None, "id_partener": None,
|
"id_comanda": None, "id_partener": None,
|
||||||
"error_message": None, "missing_skus": missing_skus,
|
"error_message": None, "missing_skus": missing_skus,
|
||||||
"items_count": len(order.items),
|
"items_count": len(order.items),
|
||||||
@@ -901,7 +902,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
order_number=order.number,
|
order_number=order.number,
|
||||||
order_date=order.date,
|
order_date=order.date,
|
||||||
customer_name=customer,
|
customer_name=customer,
|
||||||
status="IMPORTED",
|
status=OrderStatus.IMPORTED.value,
|
||||||
id_comanda=result["id_comanda"],
|
id_comanda=result["id_comanda"],
|
||||||
id_partener=result["id_partener"],
|
id_partener=result["id_partener"],
|
||||||
items_count=len(order.items),
|
items_count=len(order.items),
|
||||||
@@ -915,7 +916,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
web_status=order.status or None,
|
web_status=order.status or None,
|
||||||
discount_split=discount_split_json,
|
discount_split=discount_split_json,
|
||||||
)
|
)
|
||||||
await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED")
|
await sqlite_service.add_sync_run_order(run_id, order.number, OrderStatus.IMPORTED.value)
|
||||||
# Store ROA address IDs (R9)
|
# Store ROA address IDs (R9)
|
||||||
await sqlite_service.update_import_order_addresses(
|
await sqlite_service.update_import_order_addresses(
|
||||||
order.number,
|
order.number,
|
||||||
@@ -968,7 +969,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
order_number=order.number,
|
order_number=order.number,
|
||||||
order_date=order.date,
|
order_date=order.date,
|
||||||
customer_name=customer,
|
customer_name=customer,
|
||||||
status="ERROR",
|
status=OrderStatus.ERROR.value,
|
||||||
id_partener=result.get("id_partener"),
|
id_partener=result.get("id_partener"),
|
||||||
error_message=result["error"],
|
error_message=result["error"],
|
||||||
items_count=len(order.items),
|
items_count=len(order.items),
|
||||||
@@ -982,7 +983,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
web_status=order.status or None,
|
web_status=order.status or None,
|
||||||
discount_split=discount_split_json,
|
discount_split=discount_split_json,
|
||||||
)
|
)
|
||||||
await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR")
|
await sqlite_service.add_sync_run_order(run_id, order.number, OrderStatus.ERROR.value)
|
||||||
await sqlite_service.add_order_items(order.number, order_items_data)
|
await sqlite_service.add_order_items(order.number, order_items_data)
|
||||||
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → EROARE: {result['error']}")
|
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → EROARE: {result['error']}")
|
||||||
|
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ async function loadDashOrders() {
|
|||||||
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
|
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
|
||||||
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
|
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
|
||||||
<td class="text-end fw-bold">${orderTotal}</td>
|
<td class="text-end fw-bold">${orderTotal}</td>
|
||||||
<td class="kebab-dropdown" onclick="event.stopPropagation()">${(o.status === 'IMPORTED' || o.status === 'ALREADY_IMPORTED') && !(o.invoice && o.invoice.facturat) ? '<div class="dropdown"><button class="btn btn-sm border-0" aria-label="Actiuni comanda" data-bs-toggle="dropdown"><i class="bi bi-three-dots-vertical"></i></button><ul class="dropdown-menu dropdown-menu-end"><li><button class="dropdown-item" onclick="dashResyncOrder(\'' + esc(o.order_number) + '\', this)"><i class="bi bi-arrow-repeat me-2"></i>Resync</button></li><li><button class="dropdown-item text-danger" onclick="dashDeleteOrder(\'' + esc(o.order_number) + '\', this)"><i class="bi bi-trash me-2"></i>Sterge din ROA</button></li></ul></div>' : ''}</td>
|
<td class="kebab-dropdown" onclick="event.stopPropagation()">${(o.status === ORDER_STATUS.IMPORTED || o.status === ORDER_STATUS.ALREADY_IMPORTED) && !(o.invoice && o.invoice.facturat) ? '<div class="dropdown"><button class="btn btn-sm border-0" aria-label="Actiuni comanda" data-bs-toggle="dropdown"><i class="bi bi-three-dots-vertical"></i></button><ul class="dropdown-menu dropdown-menu-end"><li><button class="dropdown-item" onclick="dashResyncOrder(\'' + esc(o.order_number) + '\', this)"><i class="bi bi-arrow-repeat me-2"></i>Resync</button></li><li><button class="dropdown-item text-danger" onclick="dashDeleteOrder(\'' + esc(o.order_number) + '\', this)"><i class="bi bi-trash me-2"></i>Sterge din ROA</button></li></ul></div>' : ''}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -409,12 +409,12 @@ async function loadDashOrders() {
|
|||||||
// Mobile segmented control
|
// Mobile segmented control
|
||||||
renderMobileSegmented('dashMobileSeg', [
|
renderMobileSegmented('dashMobileSeg', [
|
||||||
{ label: 'Toate', count: c.total || 0, value: 'all', active: (activeStatus || 'all') === 'all', colorClass: 'fc-neutral' },
|
{ label: 'Toate', count: c.total || 0, value: 'all', active: (activeStatus || 'all') === 'all', colorClass: 'fc-neutral' },
|
||||||
{ label: 'Imp.', count: c.imported_all || c.imported || 0, value: 'IMPORTED', active: activeStatus === 'IMPORTED', colorClass: 'fc-green' },
|
{ label: 'Imp.', count: c.imported_all || c.imported || 0, value: ORDER_STATUS.IMPORTED, active: activeStatus === ORDER_STATUS.IMPORTED, colorClass: 'fc-green' },
|
||||||
{ label: 'Omise', count: c.skipped || 0, value: 'SKIPPED', active: activeStatus === 'SKIPPED', colorClass: 'fc-yellow' },
|
{ label: 'Omise', count: c.skipped || 0, value: ORDER_STATUS.SKIPPED, active: activeStatus === ORDER_STATUS.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: ORDER_STATUS.ERROR, active: activeStatus === ORDER_STATUS.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' },
|
{ label: 'Anulate', count: c.cancelled || 0, value: ORDER_STATUS.CANCELLED, active: activeStatus === ORDER_STATUS.CANCELLED, colorClass: 'fc-dark' },
|
||||||
{ label: 'Dif.', count: c.diffs || 0, value: 'DIFFS', active: activeStatus === 'DIFFS', colorClass: 'fc-orange' }
|
{ label: 'Dif.', count: c.diffs || 0, value: 'DIFFS', active: activeStatus === 'DIFFS', colorClass: 'fc-orange' }
|
||||||
], (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'));
|
||||||
@@ -496,10 +496,10 @@ function escHtml(s) {
|
|||||||
|
|
||||||
function statusLabelText(status) {
|
function statusLabelText(status) {
|
||||||
switch ((status || '').toUpperCase()) {
|
switch ((status || '').toUpperCase()) {
|
||||||
case 'IMPORTED': return 'Importat';
|
case ORDER_STATUS.IMPORTED: return 'Importat';
|
||||||
case 'ALREADY_IMPORTED': return 'Deja imp.';
|
case ORDER_STATUS.ALREADY_IMPORTED: return 'Deja imp.';
|
||||||
case 'SKIPPED': return 'Omis';
|
case ORDER_STATUS.SKIPPED: return 'Omis';
|
||||||
case 'ERROR': return 'Eroare';
|
case ORDER_STATUS.ERROR: return 'Eroare';
|
||||||
default: return esc(status);
|
default: return esc(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -523,7 +523,7 @@ function diffDots(o, mobile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function invoiceDot(order) {
|
function invoiceDot(order) {
|
||||||
if (order.status !== 'IMPORTED' && order.status !== 'ALREADY_IMPORTED') return '–';
|
if (order.status !== ORDER_STATUS.IMPORTED && order.status !== ORDER_STATUS.ALREADY_IMPORTED) return '–';
|
||||||
if (order.invoice && order.invoice.facturat) return '<span class="dot dot-green" style="box-shadow:none" title="Facturat"></span>';
|
if (order.invoice && order.invoice.facturat) return '<span class="dot dot-green" style="box-shadow:none" title="Facturat"></span>';
|
||||||
return '<span class="dot dot-red" style="box-shadow:none" title="Nefacturat"></span>';
|
return '<span class="dot dot-red" style="box-shadow:none" title="Nefacturat"></span>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ function runStatusBadge(status) {
|
|||||||
|
|
||||||
function logStatusText(status) {
|
function logStatusText(status) {
|
||||||
switch ((status || '').toUpperCase()) {
|
switch ((status || '').toUpperCase()) {
|
||||||
case 'IMPORTED': return 'Importat';
|
case ORDER_STATUS.IMPORTED: return 'Importat';
|
||||||
case 'ALREADY_IMPORTED': return 'Deja imp.';
|
case ORDER_STATUS.ALREADY_IMPORTED: return 'Deja imp.';
|
||||||
case 'SKIPPED': return 'Omis';
|
case ORDER_STATUS.SKIPPED: return 'Omis';
|
||||||
case 'ERROR': return 'Eroare';
|
case ORDER_STATUS.ERROR: return 'Eroare';
|
||||||
default: return esc(status);
|
default: return esc(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,9 +144,9 @@ async function loadRunOrders(runId, statusFilter, page) {
|
|||||||
if (orders.length === 0) {
|
if (orders.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
const problemOrders = orders.filter(o => ['ERROR', 'SKIPPED'].includes(o.status));
|
const problemOrders = orders.filter(o => [ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED].includes(o.status));
|
||||||
const okOrders = orders.filter(o => ['IMPORTED', 'ALREADY_IMPORTED'].includes(o.status));
|
const okOrders = orders.filter(o => [ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes(o.status));
|
||||||
const otherOrders = orders.filter(o => !['ERROR', 'SKIPPED', 'IMPORTED', 'ALREADY_IMPORTED'].includes(o.status));
|
const otherOrders = orders.filter(o => ![ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED, ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes(o.status));
|
||||||
|
|
||||||
function orderRow(o, i) {
|
function orderRow(o, i) {
|
||||||
const dateStr = fmtDate(o.order_date);
|
const dateStr = fmtDate(o.order_date);
|
||||||
@@ -195,9 +195,9 @@ async function loadRunOrders(runId, statusFilter, page) {
|
|||||||
if (orders.length === 0) {
|
if (orders.length === 0) {
|
||||||
mobileList.innerHTML = '<div class="flat-row text-muted py-3 justify-content-center">Nicio comanda</div>';
|
mobileList.innerHTML = '<div class="flat-row text-muted py-3 justify-content-center">Nicio comanda</div>';
|
||||||
} else {
|
} else {
|
||||||
const problemOrders = orders.filter(o => ['ERROR', 'SKIPPED'].includes(o.status));
|
const problemOrders = orders.filter(o => [ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED].includes(o.status));
|
||||||
const okOrders = orders.filter(o => ['IMPORTED', 'ALREADY_IMPORTED'].includes(o.status));
|
const okOrders = orders.filter(o => [ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes(o.status));
|
||||||
const otherOrders = orders.filter(o => !['ERROR', 'SKIPPED', 'IMPORTED', 'ALREADY_IMPORTED'].includes(o.status));
|
const otherOrders = orders.filter(o => ![ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED, ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes(o.status));
|
||||||
|
|
||||||
function mobileRow(o) {
|
function mobileRow(o) {
|
||||||
const d = o.order_date || '';
|
const d = o.order_date || '';
|
||||||
@@ -235,10 +235,10 @@ async function loadRunOrders(runId, statusFilter, page) {
|
|||||||
// Mobile segmented control
|
// Mobile segmented control
|
||||||
renderMobileSegmented('logsMobileSeg', [
|
renderMobileSegmented('logsMobileSeg', [
|
||||||
{ label: 'Toate', count: counts.total || 0, value: 'all', active: currentFilter === 'all', colorClass: 'fc-neutral' },
|
{ label: 'Toate', count: counts.total || 0, value: 'all', active: currentFilter === 'all', colorClass: 'fc-neutral' },
|
||||||
{ label: 'Imp.', count: counts.imported || 0, value: 'IMPORTED', active: currentFilter === 'IMPORTED', colorClass: 'fc-green' },
|
{ label: 'Imp.', count: counts.imported || 0, value: ORDER_STATUS.IMPORTED, active: currentFilter === ORDER_STATUS.IMPORTED, colorClass: 'fc-green' },
|
||||||
{ label: 'Deja', count: counts.already_imported || 0, value: 'ALREADY_IMPORTED', active: currentFilter === 'ALREADY_IMPORTED', colorClass: 'fc-blue' },
|
{ label: 'Deja', count: counts.already_imported || 0, value: ORDER_STATUS.ALREADY_IMPORTED, active: currentFilter === ORDER_STATUS.ALREADY_IMPORTED, colorClass: 'fc-blue' },
|
||||||
{ label: 'Omise', count: counts.skipped || 0, value: 'SKIPPED', active: currentFilter === 'SKIPPED', colorClass: 'fc-yellow' },
|
{ label: 'Omise', count: counts.skipped || 0, value: ORDER_STATUS.SKIPPED, active: currentFilter === ORDER_STATUS.SKIPPED, colorClass: 'fc-yellow' },
|
||||||
{ label: 'Erori', count: counts.error || 0, value: 'ERROR', active: currentFilter === 'ERROR', colorClass: 'fc-red' }
|
{ label: 'Erori', count: counts.error || 0, value: ORDER_STATUS.ERROR, active: currentFilter === ORDER_STATUS.ERROR, colorClass: 'fc-red' }
|
||||||
], (val) => filterOrders(val));
|
], (val) => filterOrders(val));
|
||||||
|
|
||||||
// Orders pagination
|
// Orders pagination
|
||||||
|
|||||||
@@ -11,6 +11,16 @@
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// ── Order status constants (mirror of Python OrderStatus enum) ────────────
|
||||||
|
const ORDER_STATUS = Object.freeze({
|
||||||
|
IMPORTED: 'IMPORTED',
|
||||||
|
ALREADY_IMPORTED: 'ALREADY_IMPORTED',
|
||||||
|
SKIPPED: 'SKIPPED',
|
||||||
|
ERROR: 'ERROR',
|
||||||
|
CANCELLED: 'CANCELLED',
|
||||||
|
DELETED_IN_ROA: 'DELETED_IN_ROA',
|
||||||
|
});
|
||||||
|
|
||||||
// ── HTML escaping ─────────────────────────────────
|
// ── HTML escaping ─────────────────────────────────
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
if (s == null) return '';
|
if (s == null) return '';
|
||||||
@@ -503,12 +513,12 @@ function fmtNum(v) {
|
|||||||
|
|
||||||
function orderStatusBadge(status) {
|
function orderStatusBadge(status) {
|
||||||
switch ((status || '').toUpperCase()) {
|
switch ((status || '').toUpperCase()) {
|
||||||
case 'IMPORTED': return '<span class="badge bg-success">Importat</span>';
|
case ORDER_STATUS.IMPORTED: return '<span class="badge bg-success">Importat</span>';
|
||||||
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
case ORDER_STATUS.ALREADY_IMPORTED: return '<span class="badge bg-info">Deja importat</span>';
|
||||||
case 'SKIPPED': return '<span class="badge bg-warning">Omis</span>';
|
case ORDER_STATUS.SKIPPED: return '<span class="badge bg-warning">Omis</span>';
|
||||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
case ORDER_STATUS.ERROR: return '<span class="badge bg-danger">Eroare</span>';
|
||||||
case 'CANCELLED': return '<span class="badge bg-secondary">Anulat</span>';
|
case ORDER_STATUS.CANCELLED: return '<span class="badge bg-secondary">Anulat</span>';
|
||||||
case 'DELETED_IN_ROA': return '<span class="badge bg-dark">Sters din ROA</span>';
|
case ORDER_STATUS.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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -844,7 +854,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
|||||||
// Retry button (only for ERROR/SKIPPED orders)
|
// Retry button (only for ERROR/SKIPPED orders)
|
||||||
const retryBtn = document.getElementById('detailRetryBtn');
|
const retryBtn = document.getElementById('detailRetryBtn');
|
||||||
if (retryBtn) {
|
if (retryBtn) {
|
||||||
const canRetry = ['ERROR', 'SKIPPED', 'DELETED_IN_ROA'].includes((order.status || '').toUpperCase());
|
const canRetry = [ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED, ORDER_STATUS.DELETED_IN_ROA].includes((order.status || '').toUpperCase());
|
||||||
retryBtn.style.display = canRetry ? '' : 'none';
|
retryBtn.style.display = canRetry ? '' : 'none';
|
||||||
if (canRetry) {
|
if (canRetry) {
|
||||||
retryBtn.onclick = async () => {
|
retryBtn.onclick = async () => {
|
||||||
@@ -879,7 +889,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
|||||||
// Resync button (IMPORTED/ALREADY_IMPORTED only)
|
// Resync button (IMPORTED/ALREADY_IMPORTED only)
|
||||||
const resyncBtn = document.getElementById('detailResyncBtn');
|
const resyncBtn = document.getElementById('detailResyncBtn');
|
||||||
if (resyncBtn) {
|
if (resyncBtn) {
|
||||||
const canResync = ['IMPORTED', 'ALREADY_IMPORTED'].includes((order.status || '').toUpperCase());
|
const canResync = [ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes((order.status || '').toUpperCase());
|
||||||
resyncBtn.style.display = canResync ? '' : 'none';
|
resyncBtn.style.display = canResync ? '' : 'none';
|
||||||
if (canResync) {
|
if (canResync) {
|
||||||
const isInvoiced = !!(order.factura_numar);
|
const isInvoiced = !!(order.factura_numar);
|
||||||
@@ -930,7 +940,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
|||||||
// Delete button (IMPORTED/ALREADY_IMPORTED only)
|
// Delete button (IMPORTED/ALREADY_IMPORTED only)
|
||||||
const deleteBtn = document.getElementById('detailDeleteBtn');
|
const deleteBtn = document.getElementById('detailDeleteBtn');
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
const canDelete = ['IMPORTED', 'ALREADY_IMPORTED'].includes((order.status || '').toUpperCase());
|
const canDelete = [ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes((order.status || '').toUpperCase());
|
||||||
deleteBtn.style.display = canDelete ? '' : 'none';
|
deleteBtn.style.display = canDelete ? '' : 'none';
|
||||||
if (canDelete) {
|
if (canDelete) {
|
||||||
const isInvoiced = !!(order.factura_numar);
|
const isInvoiced = !!(order.factura_numar);
|
||||||
@@ -1015,20 +1025,20 @@ function inlineConfirmAction(btn, confirmText, actionFn, opts) {
|
|||||||
// ── Dot helper ────────────────────────────────────
|
// ── Dot helper ────────────────────────────────────
|
||||||
function statusDot(status) {
|
function statusDot(status) {
|
||||||
switch ((status || '').toUpperCase()) {
|
switch ((status || '').toUpperCase()) {
|
||||||
case 'IMPORTED':
|
case ORDER_STATUS.IMPORTED:
|
||||||
case 'ALREADY_IMPORTED':
|
case ORDER_STATUS.ALREADY_IMPORTED:
|
||||||
case 'COMPLETED':
|
case 'COMPLETED':
|
||||||
case 'RESOLVED':
|
case 'RESOLVED':
|
||||||
return '<span class="dot dot-green"></span>';
|
return '<span class="dot dot-green"></span>';
|
||||||
case 'SKIPPED':
|
case ORDER_STATUS.SKIPPED:
|
||||||
case 'UNRESOLVED':
|
case 'UNRESOLVED':
|
||||||
case 'INCOMPLETE':
|
case 'INCOMPLETE':
|
||||||
return '<span class="dot dot-yellow"></span>';
|
return '<span class="dot dot-yellow"></span>';
|
||||||
case 'ERROR':
|
case ORDER_STATUS.ERROR:
|
||||||
case 'FAILED':
|
case 'FAILED':
|
||||||
return '<span class="dot dot-red"></span>';
|
return '<span class="dot dot-red"></span>';
|
||||||
case 'CANCELLED':
|
case ORDER_STATUS.CANCELLED:
|
||||||
case 'DELETED_IN_ROA':
|
case ORDER_STATUS.DELETED_IN_ROA:
|
||||||
return '<span class="dot dot-gray"></span>';
|
return '<span class="dot dot-gray"></span>';
|
||||||
default:
|
default:
|
||||||
return '<span class="dot dot-gray"></span>';
|
return '<span class="dot dot-gray"></span>';
|
||||||
@@ -1168,7 +1178,7 @@ function _renderHeaderInfo(order) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ERROR orders: muted dashes for ROA fields
|
// ERROR orders: muted dashes for ROA fields
|
||||||
if (order.status === 'ERROR' && !order.id_comanda) {
|
if (order.status === ORDER_STATUS.ERROR && !order.id_comanda) {
|
||||||
document.getElementById('detailIdComanda').innerHTML = '<span class="text-muted">\u2014</span>';
|
document.getElementById('detailIdComanda').innerHTML = '<span class="text-muted">\u2014</span>';
|
||||||
document.getElementById('detailIdPartener').innerHTML = '<span class="text-muted">\u2014</span>';
|
document.getElementById('detailIdPartener').innerHTML = '<span class="text-muted">\u2014</span>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,12 +70,12 @@
|
|||||||
<input type="search" id="orderSearch" placeholder="Cauta comanda, client..." class="search-input">
|
<input type="search" id="orderSearch" placeholder="Cauta comanda, client..." class="search-input">
|
||||||
<!-- Status pills -->
|
<!-- Status pills -->
|
||||||
<button class="filter-pill active d-none d-md-inline-flex" data-status="all">Toate <span class="filter-count fc-neutral" id="cntAll">0</span></button>
|
<button class="filter-pill active d-none d-md-inline-flex" data-status="all">Toate <span class="filter-count fc-neutral" id="cntAll">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-status="IMPORTED">Importat <span class="filter-count fc-green" id="cntImp">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-status="{{ OrderStatus.IMPORTED.value }}">Importat <span class="filter-count fc-green" id="cntImp">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-status="SKIPPED">Omise <span class="filter-count fc-yellow" id="cntSkip">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-status="{{ OrderStatus.SKIPPED.value }}">Omise <span class="filter-count fc-yellow" id="cntSkip">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="{{ OrderStatus.ERROR.value }}">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="filter-pill d-none d-md-inline-flex" data-status="{{ OrderStatus.CANCELLED.value }}">Anulate <span class="filter-count fc-dark" id="cntCanc">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-status="DIFFS">Diferente <span class="filter-count fc-orange" id="cntDiff">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-status="DIFFS">Diferente <span class="filter-count fc-orange" id="cntDiff">0</span></button>
|
||||||
<button class="btn btn-sm btn-outline-secondary d-none d-md-inline-flex" id="btnRefreshInvoices" onclick="refreshInvoices()" title="Actualizeaza status facturi din Oracle">↻</button>
|
<button class="btn btn-sm btn-outline-secondary d-none d-md-inline-flex" id="btnRefreshInvoices" onclick="refreshInvoices()" title="Actualizeaza status facturi din Oracle">↻</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,10 +59,10 @@
|
|||||||
<!-- Filter pills -->
|
<!-- Filter pills -->
|
||||||
<div class="filter-bar mb-3" id="orderFilterPills">
|
<div class="filter-bar mb-3" id="orderFilterPills">
|
||||||
<button class="filter-pill active d-none d-md-inline-flex" data-log-status="all">Toate <span class="filter-count fc-neutral" id="countAll">0</span></button>
|
<button class="filter-pill active d-none d-md-inline-flex" data-log-status="all">Toate <span class="filter-count fc-neutral" id="countAll">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-log-status="IMPORTED">Importate <span class="filter-count fc-green" id="countImported">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-log-status="{{ OrderStatus.IMPORTED.value }}">Importate <span class="filter-count fc-green" id="countImported">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-log-status="ALREADY_IMPORTED">Deja imp. <span class="filter-count fc-blue" id="countAlreadyImported">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-log-status="{{ OrderStatus.ALREADY_IMPORTED.value }}">Deja imp. <span class="filter-count fc-blue" id="countAlreadyImported">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-log-status="SKIPPED">Omise <span class="filter-count fc-yellow" id="countSkipped">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-log-status="{{ OrderStatus.SKIPPED.value }}">Omise <span class="filter-count fc-yellow" id="countSkipped">0</span></button>
|
||||||
<button class="filter-pill d-none d-md-inline-flex" data-log-status="ERROR">Erori <span class="filter-count fc-red" id="countError">0</span></button>
|
<button class="filter-pill d-none d-md-inline-flex" data-log-status="{{ OrderStatus.ERROR.value }}">Erori <span class="filter-count fc-red" id="countError">0</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-md-none mb-2" id="logsMobileSeg" style="overflow-x:auto"></div>
|
<div class="d-md-none mb-2" id="logsMobileSeg" style="overflow-x:auto"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
from app.services.import_service import build_articles_json, compute_discount_split
|
from app.services.import_service import build_articles_json, compute_discount_split
|
||||||
from app.services.order_reader import OrderData, OrderItem
|
from app.services.order_reader import OrderData, OrderItem
|
||||||
|
from app.constants import OrderStatus
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -857,14 +858,14 @@ class TestRefreshOrderAddress:
|
|||||||
|
|
||||||
def test_null_address_ids_returns_422(self, client, db):
|
def test_null_address_ids_returns_422(self, client, db):
|
||||||
"""Orders without Oracle address IDs return 422."""
|
"""Orders without Oracle address IDs return 422."""
|
||||||
db.execute("INSERT OR IGNORE INTO orders (order_number, status) VALUES ('test-no-addr', 'IMPORTED')")
|
db.execute(f"INSERT OR IGNORE INTO orders (order_number, status) VALUES ('test-no-addr', '{OrderStatus.IMPORTED.value}')")
|
||||||
db.commit()
|
db.commit()
|
||||||
res = client.post("/api/orders/test-no-addr/refresh-address")
|
res = client.post("/api/orders/test-no-addr/refresh-address")
|
||||||
assert res.status_code == 422
|
assert res.status_code == 422
|
||||||
|
|
||||||
def test_oracle_unavailable_returns_503(self, client, db, monkeypatch):
|
def test_oracle_unavailable_returns_503(self, client, db, monkeypatch):
|
||||||
"""Oracle connection failure returns 503."""
|
"""Oracle connection failure returns 503."""
|
||||||
db.execute("INSERT OR IGNORE INTO orders (order_number, status, id_adresa_livrare) VALUES ('test-oracle-fail', 'IMPORTED', 4116)")
|
db.execute(f"INSERT OR IGNORE INTO orders (order_number, status, id_adresa_livrare) VALUES ('test-oracle-fail', '{OrderStatus.IMPORTED.value}', 4116)")
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
import asyncio as _asyncio
|
import asyncio as _asyncio
|
||||||
@@ -878,7 +879,7 @@ class TestRefreshOrderAddress:
|
|||||||
|
|
||||||
def test_refresh_returns_8_fields(self, client, db, monkeypatch):
|
def test_refresh_returns_8_fields(self, client, db, monkeypatch):
|
||||||
"""Successful refresh returns 8-field address dict."""
|
"""Successful refresh returns 8-field address dict."""
|
||||||
db.execute("INSERT OR IGNORE INTO orders (order_number, status, id_adresa_livrare) VALUES ('test-refresh-ok', 'IMPORTED', 4116)")
|
db.execute(f"INSERT OR IGNORE INTO orders (order_number, status, id_adresa_livrare) VALUES ('test-refresh-ok', '{OrderStatus.IMPORTED.value}', 4116)")
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
mock_result = (
|
mock_result = (
|
||||||
@@ -908,7 +909,7 @@ class TestRefreshOrderAddress:
|
|||||||
from unittest.mock import AsyncMock # noqa: E402 (already imported MagicMock/patch above)
|
from unittest.mock import AsyncMock # noqa: E402 (already imported MagicMock/patch above)
|
||||||
|
|
||||||
|
|
||||||
def _make_order_detail(status='IMPORTED', id_comanda=12345, factura_numar=None):
|
def _make_order_detail(status=OrderStatus.IMPORTED.value, id_comanda=12345, factura_numar=None):
|
||||||
return {
|
return {
|
||||||
"order": {
|
"order": {
|
||||||
"order_number": "1001",
|
"order_number": "1001",
|
||||||
@@ -983,7 +984,7 @@ class TestResyncDeleteSafetyGates:
|
|||||||
from app.services import retry_service
|
from app.services import retry_service
|
||||||
|
|
||||||
with patch('app.services.sqlite_service.get_order_detail',
|
with patch('app.services.sqlite_service.get_order_detail',
|
||||||
new=AsyncMock(return_value=_make_order_detail(status='ERROR'))), \
|
new=AsyncMock(return_value=_make_order_detail(status=OrderStatus.ERROR.value))), \
|
||||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()):
|
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()):
|
||||||
result = await retry_service.resync_single_order("1001", {})
|
result = await retry_service.resync_single_order("1001", {})
|
||||||
|
|
||||||
@@ -1051,7 +1052,7 @@ class TestResyncDeleteHappyPaths:
|
|||||||
from app.services import retry_service
|
from app.services import retry_service
|
||||||
|
|
||||||
with patch('app.services.sqlite_service.get_order_detail',
|
with patch('app.services.sqlite_service.get_order_detail',
|
||||||
new=AsyncMock(return_value=_make_order_detail(status='DELETED_IN_ROA'))), \
|
new=AsyncMock(return_value=_make_order_detail(status=OrderStatus.DELETED_IN_ROA.value))), \
|
||||||
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \
|
||||||
patch('app.services.retry_service._download_and_reimport',
|
patch('app.services.retry_service._download_and_reimport',
|
||||||
new=AsyncMock(return_value={"success": True, "message": "ok"})):
|
new=AsyncMock(return_value={"success": True, "message": "ok"})):
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ if _api_dir not in sys.path:
|
|||||||
|
|
||||||
from app import database
|
from app import database
|
||||||
from app.services import sqlite_service
|
from app.services import sqlite_service
|
||||||
|
from app.constants import OrderStatus
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@@ -69,7 +70,7 @@ async def _seed_order(order_number="TEST-001"):
|
|||||||
order_number=order_number,
|
order_number=order_number,
|
||||||
order_date="2026-01-01",
|
order_date="2026-01-01",
|
||||||
customer_name="Test",
|
customer_name="Test",
|
||||||
status="IMPORTED",
|
status=OrderStatus.IMPORTED.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -192,5 +193,5 @@ async def test_mark_order_deleted_removes_items():
|
|||||||
finally:
|
finally:
|
||||||
await db.close()
|
await db.close()
|
||||||
assert row is not None
|
assert row is not None
|
||||||
assert row["status"] == "DELETED_IN_ROA"
|
assert row["status"] == OrderStatus.DELETED_IN_ROA.value
|
||||||
assert row["id_comanda"] is None
|
assert row["id_comanda"] is None
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import pytest_asyncio
|
|||||||
|
|
||||||
from app.database import init_sqlite
|
from app.database import init_sqlite
|
||||||
from app.services import sqlite_service
|
from app.services import sqlite_service
|
||||||
|
from app.constants import OrderStatus
|
||||||
|
|
||||||
# Initialize SQLite once before any tests run
|
# Initialize SQLite once before any tests run
|
||||||
init_sqlite()
|
init_sqlite()
|
||||||
@@ -70,10 +71,10 @@ def seed_baseline_data():
|
|||||||
|
|
||||||
# Add the first order (IMPORTED) with items
|
# Add the first order (IMPORTED) with items
|
||||||
await sqlite_service.upsert_order(
|
await sqlite_service.upsert_order(
|
||||||
"RUN001", "ORD001", "2025-01-15", "Test Client", "IMPORTED",
|
"RUN001", "ORD001", "2025-01-15", "Test Client", OrderStatus.IMPORTED.value,
|
||||||
id_comanda=100, id_partener=200, items_count=2
|
id_comanda=100, id_partener=200, items_count=2
|
||||||
)
|
)
|
||||||
await sqlite_service.add_sync_run_order("RUN001", "ORD001", "IMPORTED")
|
await sqlite_service.add_sync_run_order("RUN001", "ORD001", OrderStatus.IMPORTED.value)
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
@@ -103,15 +104,15 @@ def seed_baseline_data():
|
|||||||
|
|
||||||
# Add more orders for filter tests
|
# Add more orders for filter tests
|
||||||
await sqlite_service.upsert_order(
|
await sqlite_service.upsert_order(
|
||||||
"RUN001", "ORD002", "2025-01-16", "Client 2", "SKIPPED",
|
"RUN001", "ORD002", "2025-01-16", "Client 2", OrderStatus.SKIPPED.value,
|
||||||
missing_skus=["SKU99"], items_count=1
|
missing_skus=["SKU99"], items_count=1
|
||||||
)
|
)
|
||||||
await sqlite_service.add_sync_run_order("RUN001", "ORD002", "SKIPPED")
|
await sqlite_service.add_sync_run_order("RUN001", "ORD002", OrderStatus.SKIPPED.value)
|
||||||
await sqlite_service.upsert_order(
|
await sqlite_service.upsert_order(
|
||||||
"RUN001", "ORD003", "2025-01-17", "Client 3", "ERROR",
|
"RUN001", "ORD003", "2025-01-17", "Client 3", OrderStatus.ERROR.value,
|
||||||
error_message="Test error", items_count=3
|
error_message="Test error", items_count=3
|
||||||
)
|
)
|
||||||
await sqlite_service.add_sync_run_order("RUN001", "ORD003", "ERROR")
|
await sqlite_service.add_sync_run_order("RUN001", "ORD003", OrderStatus.ERROR.value)
|
||||||
|
|
||||||
asyncio.run(_seed())
|
asyncio.run(_seed())
|
||||||
yield
|
yield
|
||||||
@@ -212,7 +213,7 @@ async def test_get_order_detail_not_found():
|
|||||||
async def test_get_order_detail_status():
|
async def test_get_order_detail_status():
|
||||||
"""Seeded ORD001 should have IMPORTED status."""
|
"""Seeded ORD001 should have IMPORTED status."""
|
||||||
detail = await sqlite_service.get_order_detail("ORD001")
|
detail = await sqlite_service.get_order_detail("ORD001")
|
||||||
assert detail["order"]["status"] == "IMPORTED"
|
assert detail["order"]["status"] == OrderStatus.IMPORTED.value
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -232,7 +233,7 @@ async def test_get_run_orders_filtered_all():
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_run_orders_filtered_imported():
|
async def test_get_run_orders_filtered_imported():
|
||||||
"""Filter IMPORTED should return only ORD001."""
|
"""Filter IMPORTED should return only ORD001."""
|
||||||
result = await sqlite_service.get_run_orders_filtered("RUN001", "IMPORTED", 1, 50)
|
result = await sqlite_service.get_run_orders_filtered("RUN001", OrderStatus.IMPORTED.value, 1, 50)
|
||||||
assert result["total"] == 1
|
assert result["total"] == 1
|
||||||
assert result["orders"][0]["order_number"] == "ORD001"
|
assert result["orders"][0]["order_number"] == "ORD001"
|
||||||
|
|
||||||
@@ -240,7 +241,7 @@ async def test_get_run_orders_filtered_imported():
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_run_orders_filtered_skipped():
|
async def test_get_run_orders_filtered_skipped():
|
||||||
"""Filter SKIPPED should return only ORD002."""
|
"""Filter SKIPPED should return only ORD002."""
|
||||||
result = await sqlite_service.get_run_orders_filtered("RUN001", "SKIPPED", 1, 50)
|
result = await sqlite_service.get_run_orders_filtered("RUN001", OrderStatus.SKIPPED.value, 1, 50)
|
||||||
assert result["total"] == 1
|
assert result["total"] == 1
|
||||||
assert result["orders"][0]["order_number"] == "ORD002"
|
assert result["orders"][0]["order_number"] == "ORD002"
|
||||||
|
|
||||||
@@ -248,7 +249,7 @@ async def test_get_run_orders_filtered_skipped():
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_run_orders_filtered_error():
|
async def test_get_run_orders_filtered_error():
|
||||||
"""Filter ERROR should return only ORD003."""
|
"""Filter ERROR should return only ORD003."""
|
||||||
result = await sqlite_service.get_run_orders_filtered("RUN001", "ERROR", 1, 50)
|
result = await sqlite_service.get_run_orders_filtered("RUN001", OrderStatus.ERROR.value, 1, 50)
|
||||||
assert result["total"] == 1
|
assert result["total"] == 1
|
||||||
assert result["orders"][0]["order_number"] == "ORD003"
|
assert result["orders"][0]["order_number"] == "ORD003"
|
||||||
|
|
||||||
@@ -360,10 +361,10 @@ def test_api_sync_run_orders(client):
|
|||||||
|
|
||||||
def test_api_sync_run_orders_filtered(client):
|
def test_api_sync_run_orders_filtered(client):
|
||||||
"""R1: Filtering by status=IMPORTED returns only IMPORTED orders."""
|
"""R1: Filtering by status=IMPORTED returns only IMPORTED orders."""
|
||||||
resp = client.get("/api/sync/run/RUN001/orders?status=IMPORTED")
|
resp = client.get(f"/api/sync/run/RUN001/orders?status={OrderStatus.IMPORTED.value}")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
assert all(o["status"] == "IMPORTED" for o in data["orders"])
|
assert all(o["status"] == OrderStatus.IMPORTED.value for o in data["orders"])
|
||||||
|
|
||||||
|
|
||||||
def test_api_sync_run_orders_pagination_fields(client):
|
def test_api_sync_run_orders_pagination_fields(client):
|
||||||
|
|||||||
Reference in New Issue
Block a user