Replace Flask admin with FastAPI app (api/app/) featuring: - Dashboard with stat cards, sync control, and history - Mappings CRUD for ARTICOLE_TERTI with CSV import/export - Article autocomplete from NOM_ARTICOLE - SKU pre-validation before import - Sync orchestration: read JSONs -> validate -> import -> log to SQLite - APScheduler for periodic sync from UI - File logging to logs/sync_comenzi_YYYYMMDD_HHMMSS.log - Oracle pool None guard (503 vs 500 on unavailable) Test suite: - test_app_basic.py: 30 tests (imports + routes) without Oracle - test_integration.py: 9 integration tests with Oracle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
import logging
|
|
from .. import database
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def validate_skus(skus: set[str]) -> dict:
|
|
"""Validate a set of SKUs against Oracle.
|
|
Returns: {mapped: set, direct: set, missing: set}
|
|
- mapped: found in ARTICOLE_TERTI (active)
|
|
- direct: found in NOM_ARTICOLE by codmat (not in ARTICOLE_TERTI)
|
|
- missing: not found anywhere
|
|
"""
|
|
if not skus:
|
|
return {"mapped": set(), "direct": set(), "missing": set()}
|
|
|
|
mapped = set()
|
|
direct = set()
|
|
sku_list = list(skus)
|
|
|
|
with database.pool.acquire() as conn:
|
|
with conn.cursor() as cur:
|
|
# Check in batches of 500
|
|
for i in range(0, len(sku_list), 500):
|
|
batch = sku_list[i:i+500]
|
|
placeholders = ",".join([f":s{j}" for j in range(len(batch))])
|
|
params = {f"s{j}": sku for j, sku in enumerate(batch)}
|
|
|
|
# Check ARTICOLE_TERTI
|
|
cur.execute(f"""
|
|
SELECT DISTINCT sku FROM ARTICOLE_TERTI
|
|
WHERE sku IN ({placeholders}) AND activ = 1
|
|
""", params)
|
|
for row in cur:
|
|
mapped.add(row[0])
|
|
|
|
# Check NOM_ARTICOLE for remaining
|
|
remaining = [s for s in batch if s not in mapped]
|
|
if remaining:
|
|
placeholders2 = ",".join([f":n{j}" for j in range(len(remaining))])
|
|
params2 = {f"n{j}": sku for j, sku in enumerate(remaining)}
|
|
cur.execute(f"""
|
|
SELECT DISTINCT codmat FROM NOM_ARTICOLE
|
|
WHERE codmat IN ({placeholders2})
|
|
""", params2)
|
|
for row in cur:
|
|
direct.add(row[0])
|
|
|
|
missing = skus - mapped - direct
|
|
|
|
logger.info(f"SKU validation: {len(mapped)} mapped, {len(direct)} direct, {len(missing)} missing")
|
|
return {"mapped": mapped, "direct": direct, "missing": missing}
|
|
|
|
def classify_orders(orders, validation_result):
|
|
"""Classify orders as importable or skipped based on SKU validation.
|
|
Returns: (importable_orders, skipped_orders)
|
|
Each skipped entry is a tuple of (order, list_of_missing_skus).
|
|
"""
|
|
ok_skus = validation_result["mapped"] | validation_result["direct"]
|
|
importable = []
|
|
skipped = []
|
|
|
|
for order in orders:
|
|
order_skus = {item.sku for item in order.items if item.sku}
|
|
order_missing = order_skus - ok_skus
|
|
|
|
if order_missing:
|
|
skipped.append((order, list(order_missing)))
|
|
else:
|
|
importable.append(order)
|
|
|
|
return importable, skipped
|