- Add SSE event bus in sync_service (subscribe/unsubscribe/_emit) - Add GET /api/sync/stream SSE endpoint for real-time sync progress - Rewrite logs.html: unified runs table + live feed + summary + filters - Rewrite logs.js: SSE EventSource client, run selection, pagination - Dashboard: clickable runs navigate to /logs?run=, sync started banner - Remove "Import Comenzi" nav item, delete sync_detail.html - Add error_message column to sync_runs table with migration - Fix: export TNS_ADMIN as OS env var so oracledb finds tnsnames.ora - Fix: use get_oracle_connection() instead of direct pool.acquire() - Fix: CRM_POLITICI_PRET_ART INSERT to match actual table schema Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
204 lines
7.8 KiB
Python
204 lines
7.8 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)
|
|
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
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])
|
|
finally:
|
|
database.pool.release(conn)
|
|
|
|
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
|
|
|
|
def find_new_orders(order_numbers: list[str]) -> set[str]:
|
|
"""Check which order numbers do NOT already exist in Oracle COMENZI.
|
|
Returns: set of order numbers that are truly new (not yet imported).
|
|
"""
|
|
if not order_numbers:
|
|
return set()
|
|
|
|
existing = set()
|
|
num_list = list(order_numbers)
|
|
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
for i in range(0, len(num_list), 500):
|
|
batch = num_list[i:i+500]
|
|
placeholders = ",".join([f":o{j}" for j in range(len(batch))])
|
|
params = {f"o{j}": num for j, num in enumerate(batch)}
|
|
|
|
cur.execute(f"""
|
|
SELECT DISTINCT comanda_externa FROM COMENZI
|
|
WHERE comanda_externa IN ({placeholders}) AND sters = 0
|
|
""", params)
|
|
for row in cur:
|
|
existing.add(row[0])
|
|
finally:
|
|
database.pool.release(conn)
|
|
|
|
new_orders = set(order_numbers) - existing
|
|
logger.info(f"Order check: {len(new_orders)} new, {len(existing)} already exist out of {len(order_numbers)} total")
|
|
return new_orders
|
|
|
|
def validate_prices(codmats: set[str], id_pol: int) -> dict:
|
|
"""Check which CODMATs have a price entry in CRM_POLITICI_PRET_ART for the given policy.
|
|
Returns: {"has_price": set_of_codmats, "missing_price": set_of_codmats}
|
|
"""
|
|
if not codmats:
|
|
return {"has_price": set(), "missing_price": set()}
|
|
|
|
codmat_to_id = {}
|
|
ids_with_price = set()
|
|
codmat_list = list(codmats)
|
|
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
# Step 1: Get ID_ARTICOL for each CODMAT
|
|
for i in range(0, len(codmat_list), 500):
|
|
batch = codmat_list[i:i+500]
|
|
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
|
|
params = {f"c{j}": cm for j, cm in enumerate(batch)}
|
|
|
|
cur.execute(f"""
|
|
SELECT id_articol, codmat FROM NOM_ARTICOLE
|
|
WHERE codmat IN ({placeholders})
|
|
""", params)
|
|
for row in cur:
|
|
codmat_to_id[row[1]] = row[0]
|
|
|
|
# Step 2: Check which ID_ARTICOLs have a price in the policy
|
|
id_list = list(codmat_to_id.values())
|
|
for i in range(0, len(id_list), 500):
|
|
batch = id_list[i:i+500]
|
|
placeholders = ",".join([f":a{j}" for j in range(len(batch))])
|
|
params = {f"a{j}": aid for j, aid in enumerate(batch)}
|
|
params["id_pol"] = id_pol
|
|
|
|
cur.execute(f"""
|
|
SELECT DISTINCT pa.ID_ARTICOL FROM CRM_POLITICI_PRET_ART pa
|
|
WHERE pa.ID_POL = :id_pol AND pa.ID_ARTICOL IN ({placeholders})
|
|
""", params)
|
|
for row in cur:
|
|
ids_with_price.add(row[0])
|
|
finally:
|
|
database.pool.release(conn)
|
|
|
|
# Map back to CODMATs
|
|
has_price = {cm for cm, aid in codmat_to_id.items() if aid in ids_with_price}
|
|
missing_price = codmats - has_price
|
|
|
|
logger.info(f"Price validation (policy {id_pol}): {len(has_price)} have price, {len(missing_price)} missing price")
|
|
return {"has_price": has_price, "missing_price": missing_price}
|
|
|
|
def ensure_prices(codmats: set[str], id_pol: int):
|
|
"""Insert price 0 entries for CODMATs missing from the given price policy."""
|
|
if not codmats:
|
|
return
|
|
|
|
conn = database.get_oracle_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
# Get ID_VALUTA for this policy
|
|
cur.execute("""
|
|
SELECT ID_VALUTA FROM CRM_POLITICI_PRETURI WHERE ID_POL = :id_pol
|
|
""", {"id_pol": id_pol})
|
|
row = cur.fetchone()
|
|
if not row:
|
|
logger.error(f"Price policy {id_pol} not found in CRM_POLITICI_PRETURI")
|
|
return
|
|
id_valuta = row[0]
|
|
|
|
for codmat in codmats:
|
|
# Get ID_ARTICOL
|
|
cur.execute("""
|
|
SELECT id_articol FROM NOM_ARTICOLE WHERE codmat = :codmat
|
|
""", {"codmat": codmat})
|
|
row = cur.fetchone()
|
|
if not row:
|
|
logger.warning(f"CODMAT {codmat} not found in NOM_ARTICOLE, skipping price insert")
|
|
continue
|
|
id_articol = row[0]
|
|
|
|
cur.execute("""
|
|
INSERT INTO CRM_POLITICI_PRET_ART
|
|
(ID_POL_ART, ID_POL, ID_ARTICOL, PRET, ID_VALUTA,
|
|
ID_UTIL, DATAORA, PROC_TVAV,
|
|
PRETFTVA, PRETCTVA)
|
|
VALUES
|
|
(SEQ_CRM_POLITICI_PRET_ART.NEXTVAL, :id_pol, :id_articol, 0, :id_valuta,
|
|
-3, SYSDATE, 1.19,
|
|
0, 0)
|
|
""", {"id_pol": id_pol, "id_articol": id_articol, "id_valuta": id_valuta})
|
|
logger.info(f"Pret 0 adaugat pentru CODMAT {codmat} in politica {id_pol}")
|
|
|
|
conn.commit()
|
|
finally:
|
|
database.pool.release(conn)
|
|
|
|
logger.info(f"Ensure prices done: {len(codmats)} CODMATs processed for policy {id_pol}")
|