diff --git a/TODOS.md b/TODOS.md
index 685aff7..12b89bb 100644
--- a/TODOS.md
+++ b/TODOS.md
@@ -2,9 +2,9 @@
## P2: Refactor sync_service.py in module separate
**What:** Split sync_service.py (870 linii) in: download_service, parse_service, sync_orchestrator.
-**Why:** Faciliteza debugging si testare. Un bug in price sync nu ar trebui sa afecteze import flow.
+**Why:** Faciliteza debugging si testare.
**Effort:** M (human: ~1 sapt / CC: ~1-2h)
-**Context:** Dupa implementarea planului Command Center (retry_service deja extras). sync_service face download + parse + validate + import + price sync + invoice check — prea multe responsabilitati.
+**Context:** Dupa implementarea planului Command Center (retry_service deja extras). sync_service face download + parse + validate + import + invoice check — prea multe responsabilitati.
**Depends on:** Finalizarea planului Command Center.
## P2: Email/webhook alert pe sync esuat
diff --git a/api/app/main.py b/api/app/main.py
index 326154f..683bcc1 100644
--- a/api/app/main.py
+++ b/api/app/main.py
@@ -1,4 +1,3 @@
-import asyncio
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI
@@ -9,7 +8,6 @@ import os
from .config import settings
from .database import init_oracle, close_oracle, init_sqlite
-from .routers.sync import backfill_price_match
# Configure logging with both stream and file handlers
_log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)
@@ -58,8 +56,6 @@ async def lifespan(app: FastAPI):
except Exception:
pass
- asyncio.create_task(backfill_price_match())
-
logger.info("GoMag Import Manager started")
yield
diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py
index 0551c1a..3b702ce 100644
--- a/api/app/routers/sync.py
+++ b/api/app/routers/sync.py
@@ -12,7 +12,7 @@ from pydantic import BaseModel
from pathlib import Path
from typing import Optional
-from ..services import sync_service, scheduler_service, sqlite_service, invoice_service, validation_service
+from ..services import sync_service, scheduler_service, sqlite_service, invoice_service
from .. import database
router = APIRouter(tags=["sync"])
@@ -40,56 +40,6 @@ async def _enrich_items_with_codmat(items: list) -> None:
"denumire": nom_map[sku], "direct": True}]
-async def backfill_price_match():
- """Background task: check prices for all imported orders without cached price_match."""
- try:
- from ..database import get_sqlite
- db = await get_sqlite()
- try:
- # Reset all cached price_match to re-evaluate with current logic
- await db.execute("UPDATE orders SET price_match = NULL WHERE price_match IS NOT NULL")
- await db.commit()
- cursor = await db.execute("""
- SELECT order_number FROM orders
- WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED')
- AND price_match IS NULL
- ORDER BY order_date DESC
- """)
- rows = [r["order_number"] for r in await cursor.fetchall()]
- finally:
- await db.close()
-
- if not rows:
- logger.info("backfill_price_match: no unchecked orders")
- return
-
- logger.info(f"backfill_price_match: checking {len(rows)} orders...")
- app_settings = await sqlite_service.get_app_settings()
- checked = 0
-
- for order_number in rows:
- try:
- detail = await sqlite_service.get_order_detail(order_number)
- if not detail:
- continue
- items = detail.get("items", [])
- await _enrich_items_with_codmat(items)
- price_data = await asyncio.to_thread(
- validation_service.get_prices_for_order, items, app_settings
- )
- summary = price_data.get("summary", {})
- if summary.get("oracle_available") is not False:
- pm = summary.get("mismatches", 0) == 0
- await sqlite_service.update_order_price_match(order_number, pm)
- checked += 1
- except Exception as e:
- logger.debug(f"backfill_price_match: order {order_number} failed: {e}")
-
- logger.info(f"backfill_price_match: done, {checked}/{len(rows)} updated")
- except Exception as e:
- logger.error(f"backfill_price_match failed: {e}")
-
-
class ScheduleConfig(BaseModel):
enabled: bool
interval_minutes: int = 5
@@ -116,9 +66,6 @@ class AppSettingsUpdate(BaseModel):
kit_discount_codmat: str = ""
kit_discount_id_pol: str = ""
price_sync_enabled: str = "1"
- catalog_sync_enabled: str = "0"
- price_sync_schedule: str = ""
- gomag_products_url: str = ""
# API endpoints
@@ -217,31 +164,6 @@ async def sync_history(page: int = 1, per_page: int = 20):
return await sqlite_service.get_sync_runs(page, per_page)
-@router.post("/api/price-sync/start")
-async def start_price_sync(background_tasks: BackgroundTasks):
- """Trigger manual catalog price sync."""
- from ..services import price_sync_service
- result = await price_sync_service.prepare_price_sync()
- if result.get("error"):
- return {"error": result["error"]}
- run_id = result["run_id"]
- background_tasks.add_task(price_sync_service.run_catalog_price_sync, run_id=run_id)
- return {"message": "Price sync started", "run_id": run_id}
-
-
-@router.get("/api/price-sync/status")
-async def price_sync_status():
- """Get current price sync status."""
- from ..services import price_sync_service
- return await price_sync_service.get_price_sync_status()
-
-
-@router.get("/api/price-sync/history")
-async def price_sync_history(page: int = 1, per_page: int = 20):
- """Get price sync run history."""
- return await sqlite_service.get_price_sync_runs(page, per_page)
-
-
@router.get("/logs", response_class=HTMLResponse)
async def logs_page(request: Request, run: str = None):
return templates.TemplateResponse("logs.html", {"request": request, "selected_run": run or ""})
@@ -454,35 +376,8 @@ async def order_detail(order_number: str):
items = detail.get("items", [])
await _enrich_items_with_codmat(items)
- # Price comparison against ROA Oracle
- app_settings = await sqlite_service.get_app_settings()
- try:
- price_data = await asyncio.to_thread(
- validation_service.get_prices_for_order, items, app_settings
- )
- price_items = price_data.get("items", {})
- for idx, item in enumerate(items):
- pi = price_items.get(idx)
- if pi:
- item["pret_roa"] = pi.get("pret_roa")
- item["price_match"] = pi.get("match")
- if pi.get("kit"):
- item["kit"] = True
- order_price_check = price_data.get("summary", {})
- # Cache price_match in SQLite if changed
- if order_price_check.get("oracle_available") is not False:
- pm = order_price_check.get("mismatches", 0) == 0
- cached = detail.get("order", {}).get("price_match")
- cached_bool = True if cached == 1 else (False if cached == 0 else None)
- if cached_bool != pm:
- await sqlite_service.update_order_price_match(order_number, pm)
- except Exception as e:
- logger.warning(f"Price comparison failed for order {order_number}: {e}")
- order_price_check = {"mismatches": 0, "checked": 0, "oracle_available": False}
-
# Enrich with invoice data
order = detail.get("order", {})
- order["price_check"] = order_price_check
if order.get("factura_numar") and order.get("factura_data"):
order["invoice"] = {
"facturat": True,
@@ -562,7 +457,8 @@ async def order_detail(order_number: str):
"facturare_roa": order.get("adresa_facturare_roa"),
}
- # Add settings for receipt display (app_settings already fetched above)
+ # Add settings for receipt display
+ app_settings = await sqlite_service.get_app_settings()
order["transport_vat"] = app_settings.get("transport_vat") or "21"
order["transport_codmat"] = app_settings.get("transport_codmat") or ""
order["discount_codmat"] = app_settings.get("discount_codmat") or ""
@@ -720,9 +616,6 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
# Enrich orders with invoice data — prefer SQLite cache, fallback to Oracle
all_orders = result["orders"]
for o in all_orders:
- # price_match: 1=OK, 0=mismatch, NULL=not checked yet
- pm = o.get("price_match")
- o["price_match"] = True if pm == 1 else (False if pm == 0 else None)
if o.get("factura_numar") and o.get("factura_data"):
# Use cached invoice data from SQLite (only if complete)
o["invoice"] = {
@@ -1061,9 +954,6 @@ async def get_app_settings():
"kit_discount_codmat": s.get("kit_discount_codmat", ""),
"kit_discount_id_pol": s.get("kit_discount_id_pol", ""),
"price_sync_enabled": s.get("price_sync_enabled", "1"),
- "catalog_sync_enabled": s.get("catalog_sync_enabled", "0"),
- "price_sync_schedule": s.get("price_sync_schedule", ""),
- "gomag_products_url": s.get("gomag_products_url", ""),
}
@@ -1090,9 +980,6 @@ async def update_app_settings(config: AppSettingsUpdate):
await sqlite_service.set_app_setting("kit_discount_codmat", config.kit_discount_codmat)
await sqlite_service.set_app_setting("kit_discount_id_pol", config.kit_discount_id_pol)
await sqlite_service.set_app_setting("price_sync_enabled", config.price_sync_enabled)
- await sqlite_service.set_app_setting("catalog_sync_enabled", config.catalog_sync_enabled)
- await sqlite_service.set_app_setting("price_sync_schedule", config.price_sync_schedule)
- await sqlite_service.set_app_setting("gomag_products_url", config.gomag_products_url)
return {"success": True}
diff --git a/api/app/services/gomag_client.py b/api/app/services/gomag_client.py
index 81ef907..5fe7c4a 100644
--- a/api/app/services/gomag_client.py
+++ b/api/app/services/gomag_client.py
@@ -103,80 +103,3 @@ async def download_orders(
return {"pages": total_pages, "total": total_orders, "files": saved_files}
-async def download_products(
- api_key: str = None,
- api_shop: str = None,
- products_url: str = None,
- log_fn: Callable[[str], None] = None,
-) -> list[dict]:
- """Download all products from GoMag Products API.
- Returns list of product dicts with: sku, price, vat, vat_included, bundleItems.
- """
- def _log(msg: str):
- logger.info(msg)
- if log_fn:
- log_fn(msg)
-
- effective_key = api_key or settings.GOMAG_API_KEY
- effective_shop = api_shop or settings.GOMAG_API_SHOP
- default_url = "https://api.gomag.ro/api/v1/product/read/json"
- effective_url = products_url or default_url
-
- if not effective_key or not effective_shop:
- _log("GoMag API keys neconfigurați, skip product download")
- return []
-
- headers = {
- "Apikey": effective_key,
- "ApiShop": effective_shop,
- "User-Agent": "Mozilla/5.0",
- "Content-Type": "application/json",
- }
-
- all_products = []
- total_pages = 1
-
- async with httpx.AsyncClient(timeout=30) as client:
- page = 1
- while page <= total_pages:
- params = {"page": page, "limit": 100}
- try:
- response = await client.get(effective_url, headers=headers, params=params)
- response.raise_for_status()
- data = response.json()
- except httpx.HTTPError as e:
- _log(f"GoMag Products API eroare pagina {page}: {e}")
- break
- except Exception as e:
- _log(f"GoMag Products eroare neașteptată pagina {page}: {e}")
- break
-
- if page == 1:
- total_pages = int(data.get("pages", 1))
- _log(f"GoMag Products: {data.get('total', '?')} produse în {total_pages} pagini")
-
- products = data.get("products", [])
- if isinstance(products, dict):
- # GoMag returns products as {"1": {...}, "2": {...}} dict
- first_val = next(iter(products.values()), None) if products else None
- if isinstance(first_val, dict):
- products = list(products.values())
- else:
- products = [products]
- if isinstance(products, list):
- for p in products:
- if isinstance(p, dict) and p.get("sku"):
- all_products.append({
- "sku": p["sku"],
- "price": p.get("price", "0"),
- "vat": p.get("vat", "19"),
- "vat_included": str(p.get("vat_included", "1")),
- "bundleItems": p.get("bundleItems", []),
- })
-
- page += 1
- if page <= total_pages:
- await asyncio.sleep(1)
-
- _log(f"GoMag Products: {len(all_products)} produse cu SKU descărcate")
- return all_products
diff --git a/api/app/services/price_sync_service.py b/api/app/services/price_sync_service.py
deleted file mode 100644
index 26fe89a..0000000
--- a/api/app/services/price_sync_service.py
+++ /dev/null
@@ -1,264 +0,0 @@
-"""Catalog price sync service — syncs product prices from GoMag catalog to ROA Oracle."""
-import asyncio
-import logging
-import uuid
-from datetime import datetime
-from zoneinfo import ZoneInfo
-
-from . import gomag_client, validation_service, sqlite_service
-from .. import database
-from ..config import settings
-
-logger = logging.getLogger(__name__)
-_tz = ZoneInfo("Europe/Bucharest")
-
-_price_sync_lock = asyncio.Lock()
-_current_price_sync = None
-
-
-def _now():
- return datetime.now(_tz).replace(tzinfo=None)
-
-
-async def prepare_price_sync() -> dict:
- global _current_price_sync
- if _price_sync_lock.locked():
- return {"error": "Price sync already running"}
- run_id = _now().strftime("%Y%m%d_%H%M%S") + "_ps_" + uuid.uuid4().hex[:6]
- _current_price_sync = {
- "run_id": run_id, "status": "running",
- "started_at": _now().isoformat(), "finished_at": None,
- "phase_text": "Starting...",
- }
- # Create SQLite record
- db = await sqlite_service.get_sqlite()
- try:
- await db.execute(
- "INSERT INTO price_sync_runs (run_id, started_at, status) VALUES (?, ?, 'running')",
- (run_id, _now().strftime("%d.%m.%Y %H:%M:%S"))
- )
- await db.commit()
- finally:
- await db.close()
- return {"run_id": run_id}
-
-
-async def get_price_sync_status() -> dict:
- if _current_price_sync and _current_price_sync.get("status") == "running":
- return _current_price_sync
- # Return last run from SQLite
- db = await sqlite_service.get_sqlite()
- try:
- cursor = await db.execute(
- "SELECT * FROM price_sync_runs ORDER BY started_at DESC LIMIT 1"
- )
- row = await cursor.fetchone()
- if row:
- return {"status": "idle", "last_run": dict(row)}
- return {"status": "idle"}
- except Exception:
- return {"status": "idle"}
- finally:
- await db.close()
-
-
-async def run_catalog_price_sync(run_id: str):
- global _current_price_sync
- async with _price_sync_lock:
- log_lines = []
- def _log(msg):
- logger.info(msg)
- log_lines.append(f"[{_now().strftime('%H:%M:%S')}] {msg}")
- if _current_price_sync:
- _current_price_sync["phase_text"] = msg
-
- try:
- app_settings = await sqlite_service.get_app_settings()
- id_pol = int(app_settings.get("id_pol") or 0) or None
- id_pol_productie = int(app_settings.get("id_pol_productie") or 0) or None
-
- if not id_pol:
- _log("Politica de preț nu e configurată — skip sync")
- await _finish_run(run_id, "error", log_lines, error="No price policy")
- return
-
- # Fetch products from GoMag
- _log("Descărcare produse din GoMag API...")
- products = await gomag_client.download_products(
- api_key=app_settings.get("gomag_api_key"),
- api_shop=app_settings.get("gomag_api_shop"),
- products_url=app_settings.get("gomag_products_url") or None,
- log_fn=_log,
- )
-
- if not products:
- _log("Niciun produs descărcat")
- await _finish_run(run_id, "completed", log_lines, products_total=0)
- return
-
- # Index products by SKU for kit component lookup
- products_by_sku = {p["sku"]: p for p in products}
-
- # Connect to Oracle
- conn = await asyncio.to_thread(database.get_oracle_connection)
- try:
- # Get all mappings from ARTICOLE_TERTI
- _log("Citire mapări ARTICOLE_TERTI...")
- mapped_data = await asyncio.to_thread(
- validation_service.resolve_mapped_codmats,
- {p["sku"] for p in products}, conn
- )
-
- # Get direct articles from NOM_ARTICOLE
- _log("Identificare articole directe...")
- direct_id_map = {}
- with conn.cursor() as cur:
- all_skus = list({p["sku"] for p in products})
- for i in range(0, len(all_skus), 500):
- batch = all_skus[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)}
- cur.execute(f"""
- SELECT codmat, id_articol, cont FROM nom_articole
- WHERE codmat IN ({placeholders}) AND sters = 0 AND inactiv = 0
- """, params)
- for row in cur:
- if row[0] not in mapped_data:
- direct_id_map[row[0]] = {"id_articol": row[1], "cont": row[2]}
-
- matched = 0
- updated = 0
- errors = 0
-
- for product in products:
- sku = product["sku"]
- try:
- price_str = product.get("price", "0")
- price = float(price_str) if price_str else 0
- if price <= 0:
- continue
-
- vat = float(product.get("vat", "19"))
-
- # Calculate price with TVA (vat_included can be int 1 or str "1")
- if str(product.get("vat_included", "1")) == "1":
- price_cu_tva = price
- else:
- price_cu_tva = price * (1 + vat / 100)
-
- # For kits, sync each component individually from standalone GoMag prices
- mapped_comps = mapped_data.get(sku, [])
- is_kit = len(mapped_comps) > 1 or (
- len(mapped_comps) == 1 and (mapped_comps[0].get("cantitate_roa") or 1) > 1
- )
- if is_kit:
- for comp in mapped_data[sku]:
- comp_codmat = comp["codmat"]
-
- # Skip components that have their own ARTICOLE_TERTI mapping
- # (they'll be synced with correct cantitate_roa in individual path)
- if comp_codmat in mapped_data:
- continue
-
- comp_product = products_by_sku.get(comp_codmat)
- if not comp_product:
- continue # Component not in GoMag as standalone product
-
- comp_price_str = comp_product.get("price", "0")
- comp_price = float(comp_price_str) if comp_price_str else 0
- if comp_price <= 0:
- continue
-
- comp_vat = float(comp_product.get("vat", "19"))
-
- # vat_included can be int 1 or str "1"
- if str(comp_product.get("vat_included", "1")) == "1":
- comp_price_cu_tva = comp_price
- else:
- comp_price_cu_tva = comp_price * (1 + comp_vat / 100)
-
- comp_cont_str = str(comp.get("cont") or "").strip()
- comp_pol = id_pol_productie if (comp_cont_str in ("341", "345") and id_pol_productie) else id_pol
-
- matched += 1
- result = await asyncio.to_thread(
- validation_service.compare_and_update_price,
- comp["id_articol"], comp_pol, comp_price_cu_tva, conn
- )
- if result and result["updated"]:
- updated += 1
- _log(f" {comp_codmat}: {result['old_price']:.2f} → {result['new_price']:.2f} (kit {sku})")
- elif result is None:
- _log(f" {comp_codmat}: LIPSESTE din politica {comp_pol} — adauga manual in ROA (kit {sku})")
- continue
-
- # Determine id_articol and policy
- id_articol = None
- cantitate_roa = 1
-
- if sku in mapped_data and len(mapped_data[sku]) == 1 and (mapped_data[sku][0].get("cantitate_roa") or 1) <= 1:
- comp = mapped_data[sku][0]
- id_articol = comp["id_articol"]
- cantitate_roa = comp.get("cantitate_roa") or 1
- elif sku in direct_id_map:
- id_articol = direct_id_map[sku]["id_articol"]
- else:
- continue # SKU not in ROA
-
- matched += 1
- price_per_unit = price_cu_tva / cantitate_roa if cantitate_roa != 1 else price_cu_tva
-
- # Determine policy
- cont = None
- if sku in mapped_data and len(mapped_data[sku]) == 1 and (mapped_data[sku][0].get("cantitate_roa") or 1) <= 1:
- cont = mapped_data[sku][0].get("cont")
- elif sku in direct_id_map:
- cont = direct_id_map[sku].get("cont")
-
- cont_str = str(cont or "").strip()
- pol = id_pol_productie if (cont_str in ("341", "345") and id_pol_productie) else id_pol
-
- result = await asyncio.to_thread(
- validation_service.compare_and_update_price,
- id_articol, pol, price_per_unit, conn
- )
- if result and result["updated"]:
- updated += 1
- _log(f" {result['codmat']}: {result['old_price']:.2f} → {result['new_price']:.2f}")
-
- except Exception as e:
- errors += 1
- _log(f"Eroare produs {sku}: {e}")
-
- _log(f"Sync complet: {len(products)} produse, {matched} potrivite, {updated} actualizate, {errors} erori")
-
- finally:
- await asyncio.to_thread(database.pool.release, conn)
-
- await _finish_run(run_id, "completed", log_lines,
- products_total=len(products), matched=matched,
- updated=updated, errors=errors)
-
- except Exception as e:
- _log(f"Eroare critică: {e}")
- logger.error(f"Catalog price sync error: {e}", exc_info=True)
- await _finish_run(run_id, "error", log_lines, error=str(e))
-
-
-async def _finish_run(run_id, status, log_lines, products_total=0,
- matched=0, updated=0, errors=0, error=None):
- global _current_price_sync
- db = await sqlite_service.get_sqlite()
- try:
- await db.execute("""
- UPDATE price_sync_runs SET
- finished_at = ?, status = ?, products_total = ?,
- matched = ?, updated = ?, errors = ?,
- log_text = ?
- WHERE run_id = ?
- """, (_now().strftime("%d.%m.%Y %H:%M:%S"), status, products_total, matched, updated, errors,
- "\n".join(log_lines), run_id))
- await db.commit()
- finally:
- await db.close()
- _current_price_sync = None
diff --git a/api/app/services/sqlite_service.py b/api/app/services/sqlite_service.py
index 80067d7..8ff330b 100644
--- a/api/app/services/sqlite_service.py
+++ b/api/app/services/sqlite_service.py
@@ -1026,23 +1026,6 @@ async def get_skipped_orders_with_sku(sku: str) -> list[str]:
# ── Price Sync Runs ───────────────────────────────
-async def get_price_sync_runs(page: int = 1, per_page: int = 20):
- """Get paginated price sync run history."""
- db = await get_sqlite()
- try:
- offset = (page - 1) * per_page
- cursor = await db.execute("SELECT COUNT(*) FROM price_sync_runs")
- total = (await cursor.fetchone())[0]
- cursor = await db.execute(
- "SELECT * FROM price_sync_runs ORDER BY started_at DESC LIMIT ? OFFSET ?",
- (per_page, offset)
- )
- runs = [dict(r) for r in await cursor.fetchall()]
- return {"runs": runs, "total": total, "page": page, "pages": (total + per_page - 1) // per_page}
- finally:
- await db.close()
-
-
# ── ANAF Cache ───────────────────────────────────
async def get_anaf_cache(bare_cui: str) -> dict | None:
diff --git a/api/app/services/validation_service.py b/api/app/services/validation_service.py
index b70733d..2eee3f9 100644
--- a/api/app/services/validation_service.py
+++ b/api/app/services/validation_service.py
@@ -588,193 +588,3 @@ def sync_prices_from_order(orders, mapped_codmat_data: dict, direct_id_map: dict
return updated
-def get_prices_for_order(items: list[dict], app_settings: dict, conn=None) -> dict:
- """Compare GoMag prices with ROA prices for order items.
-
- Args:
- items: list of order items, each with 'sku', 'price', 'quantity', 'codmat_details'
- (codmat_details = [{"codmat", "cantitate_roa", "id_articol"?, "cont"?, "direct"?}])
- app_settings: dict with 'id_pol', 'id_pol_productie'
- conn: Oracle connection (optional, will acquire if None)
-
- Returns: {
- "items": {idx: {"pret_roa": float|None, "match": bool|None, "pret_gomag": float}},
- "summary": {"mismatches": int, "checked": int, "oracle_available": bool}
- }
- """
- try:
- id_pol = int(app_settings.get("id_pol", 0) or 0)
- id_pol_productie = int(app_settings.get("id_pol_productie", 0) or 0)
- except (ValueError, TypeError):
- id_pol = 0
- id_pol_productie = 0
-
- def _empty_result(oracle_available: bool) -> dict:
- return {
- "items": {
- idx: {"pret_roa": None, "match": None, "pret_gomag": float(item.get("price") or 0)}
- for idx, item in enumerate(items)
- },
- "summary": {"mismatches": 0, "checked": 0, "oracle_available": oracle_available}
- }
-
- if not items or not id_pol:
- return _empty_result(oracle_available=False)
-
- own_conn = conn is None
- try:
- if own_conn:
- conn = database.get_oracle_connection()
-
- # Step 1: Collect codmats; use id_articol/cont from codmat_details when already known
- pre_resolved = {} # {codmat: {"id_articol": int, "cont": str}}
- all_codmats = set()
- for item in items:
- for cd in (item.get("codmat_details") or []):
- codmat = cd.get("codmat")
- if not codmat:
- continue
- all_codmats.add(codmat)
- if cd.get("id_articol") and codmat not in pre_resolved:
- pre_resolved[codmat] = {
- "id_articol": cd["id_articol"],
- "cont": cd.get("cont") or "",
- }
-
- # Step 2: Resolve missing id_articols via nom_articole
- need_resolve = all_codmats - set(pre_resolved.keys())
- if need_resolve:
- db_resolved = resolve_codmat_ids(need_resolve, conn=conn)
- pre_resolved.update(db_resolved)
-
- codmat_info = pre_resolved # {codmat: {"id_articol": int, "cont": str}}
-
- # Step 3: Get PRETURI_CU_TVA flag once per policy
- policies = {id_pol}
- if id_pol_productie and id_pol_productie != id_pol:
- policies.add(id_pol_productie)
-
- pol_cu_tva = {} # {id_pol: bool}
- with conn.cursor() as cur:
- for pol in policies:
- cur.execute(
- "SELECT PRETURI_CU_TVA FROM CRM_POLITICI_PRETURI WHERE ID_POL = :pol",
- {"pol": pol},
- )
- row = cur.fetchone()
- pol_cu_tva[pol] = (int(row[0] or 0) == 1) if row else False
-
- # Step 4: Batch query PRET + PROC_TVAV for all id_articols across both policies
- all_id_articols = list({
- info["id_articol"]
- for info in codmat_info.values()
- if info.get("id_articol")
- })
- price_map = {} # {(id_pol, id_articol): (pret, proc_tvav)}
-
- if all_id_articols:
- pol_list = list(policies)
- pol_placeholders = ",".join([f":p{k}" for k in range(len(pol_list))])
- with conn.cursor() as cur:
- for i in range(0, len(all_id_articols), 500):
- batch = all_id_articols[i:i + 500]
- art_placeholders = ",".join([f":a{j}" for j in range(len(batch))])
- params = {f"a{j}": aid for j, aid in enumerate(batch)}
- for k, pol in enumerate(pol_list):
- params[f"p{k}"] = pol
- cur.execute(f"""
- SELECT ID_POL, ID_ARTICOL, PRET, PROC_TVAV
- FROM CRM_POLITICI_PRET_ART
- WHERE ID_POL IN ({pol_placeholders}) AND ID_ARTICOL IN ({art_placeholders})
- """, params)
- for row in cur:
- price_map[(row[0], row[1])] = (row[2], row[3])
-
- # Step 5: Compute pret_roa per item and compare with GoMag price
- result_items = {}
- mismatches = 0
- checked = 0
-
- for idx, item in enumerate(items):
- pret_gomag = float(item.get("price") or 0)
- result_items[idx] = {"pret_gomag": pret_gomag, "pret_roa": None, "match": None}
-
- codmat_details = item.get("codmat_details") or []
- if not codmat_details:
- continue
-
- is_kit = len(codmat_details) > 1 or (
- len(codmat_details) == 1
- and float(codmat_details[0].get("cantitate_roa") or 1) != 1
- )
-
- if is_kit:
- # Kit/pachet: prețul GoMag e comercial, ROA e suma componente din lista
- # de prețuri — diferența e gestionată de discount line
- result_items[idx]["kit"] = True
- continue
-
- pret_roa_total = 0.0
- all_resolved = True
-
- for cd in codmat_details:
- codmat = cd.get("codmat")
- if not codmat:
- all_resolved = False
- break
-
- info = codmat_info.get(codmat, {})
- id_articol = info.get("id_articol")
- if not id_articol:
- all_resolved = False
- break
-
- # Dual-policy routing: cont 341/345 → production, else → sales
- cont = str(info.get("cont") or cd.get("cont") or "").strip()
- if cont in ("341", "345") and id_pol_productie:
- pol = id_pol_productie
- else:
- pol = id_pol
-
- price_entry = price_map.get((pol, id_articol))
- if price_entry is None:
- all_resolved = False
- break
-
- pret, proc_tvav = price_entry
- proc_tvav = float(proc_tvav or 1.19)
-
- if pol_cu_tva.get(pol):
- pret_cu_tva = float(pret or 0)
- else:
- pret_cu_tva = float(pret or 0) * proc_tvav
-
- cantitate_roa = float(cd.get("cantitate_roa") or 1)
- if is_kit:
- pret_roa_total += pret_cu_tva * cantitate_roa
- else:
- pret_roa_total = pret_cu_tva # cantitate_roa==1 for simple items
-
- if not all_resolved:
- continue
-
- pret_roa = round(pret_roa_total, 4)
- match = pret_gomag <= pret_roa + 0.01
- result_items[idx]["pret_roa"] = pret_roa
- result_items[idx]["match"] = match
- checked += 1
- if not match:
- mismatches += 1
-
- logger.info(f"get_prices_for_order: {checked}/{len(items)} checked, {mismatches} mismatches")
- return {
- "items": result_items,
- "summary": {"mismatches": mismatches, "checked": checked, "oracle_available": True},
- }
-
- except Exception as e:
- logger.error(f"get_prices_for_order failed: {e}")
- return _empty_result(oracle_available=False)
- finally:
- if own_conn and conn:
- database.pool.release(conn)
diff --git a/api/app/static/css/style.css b/api/app/static/css/style.css
index 0f23445..624eee1 100644
--- a/api/app/static/css/style.css
+++ b/api/app/static/css/style.css
@@ -1189,7 +1189,6 @@ tr.mapping-deleted td {
.diff-badge-anaf { background:var(--error-light); color:var(--error-text); }
.diff-badge-denumire { background:var(--compare-light); color:var(--compare-text); }
.diff-badge-addr { background:var(--info-light); color:var(--info-text); }
-.diff-badge-price { background:var(--success-light); color:var(--success-text); }
/* ── Compact order detail layout ──────────────── */
.detail-col-label {
diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js
index 1ea1471..c1320e7 100644
--- a/api/app/static/js/dashboard.js
+++ b/api/app/static/js/dashboard.js
@@ -513,8 +513,6 @@ function diffDots(o, mobile) {
d += ``;
if (o.partner_mismatch===1)
d += ``;
- if (o.price_match===false)
- d += ``;
return d;
}
diff --git a/api/app/static/js/settings.js b/api/app/static/js/settings.js
index a869fdb..fe89e8b 100644
--- a/api/app/static/js/settings.js
+++ b/api/app/static/js/settings.js
@@ -10,8 +10,9 @@ document.addEventListener('DOMContentLoaded', async () => {
// Kit pricing mode radio toggle
document.querySelectorAll('input[name="kitPricingMode"]').forEach(r => {
r.addEventListener('change', () => {
+ const mode = document.querySelector('input[name="kitPricingMode"]:checked')?.value || '';
document.getElementById('kitModeBFields').style.display =
- document.getElementById('kitModeSeparate').checked ? '' : 'none';
+ (mode === 'separate_line' || mode === 'distributed') ? '' : 'none';
});
});
@@ -138,27 +139,12 @@ async function loadSettings() {
document.querySelectorAll('input[name="kitPricingMode"]').forEach(r => {
r.checked = r.value === kitMode;
});
- document.getElementById('kitModeBFields').style.display = kitMode === 'separate_line' ? '' : 'none';
+ document.getElementById('kitModeBFields').style.display = (kitMode === 'separate_line' || kitMode === 'distributed') ? '' : 'none';
if (el('settKitDiscountCodmat')) el('settKitDiscountCodmat').value = data.kit_discount_codmat || '';
if (el('settKitDiscountIdPol')) el('settKitDiscountIdPol').value = data.kit_discount_id_pol || '';
// Price sync
if (el('settPriceSyncEnabled')) el('settPriceSyncEnabled').checked = data.price_sync_enabled !== "0";
- if (el('settCatalogSyncEnabled')) {
- el('settCatalogSyncEnabled').checked = data.catalog_sync_enabled === "1";
- document.getElementById('catalogSyncOptions').style.display = data.catalog_sync_enabled === "1" ? '' : 'none';
- }
- if (el('settPriceSyncSchedule')) el('settPriceSyncSchedule').value = data.price_sync_schedule || '';
-
- // Load price sync status
- try {
- const psRes = await fetch('/api/price-sync/status');
- const psData = await psRes.json();
- const psEl = document.getElementById('settPriceSyncStatus');
- if (psEl && psData.last_run) {
- psEl.textContent = `Ultima: ${psData.last_run.finished_at || ''} — ${psData.last_run.updated || 0} actualizate din ${psData.last_run.matched || 0}`;
- }
- } catch {}
} catch (err) {
console.error('loadSettings error:', err);
}
@@ -187,9 +173,6 @@ async function saveSettings() {
kit_discount_codmat: el('settKitDiscountCodmat')?.value?.trim() || '',
kit_discount_id_pol: el('settKitDiscountIdPol')?.value?.trim() || '',
price_sync_enabled: el('settPriceSyncEnabled')?.checked ? "1" : "0",
- catalog_sync_enabled: el('settCatalogSyncEnabled')?.checked ? "1" : "0",
- price_sync_schedule: el('settPriceSyncSchedule')?.value || '',
- gomag_products_url: '',
};
try {
const res = await fetch('/api/settings', {
@@ -211,40 +194,6 @@ async function saveSettings() {
}
}
-async function startCatalogSync() {
- const btn = document.getElementById('btnCatalogSync');
- const status = document.getElementById('settPriceSyncStatus');
- btn.disabled = true;
- btn.innerHTML = ' Sincronizare...';
- try {
- const res = await fetch('/api/price-sync/start', { method: 'POST' });
- const data = await res.json();
- if (data.error) {
- status.innerHTML = `${escHtml(data.error)}`;
- btn.disabled = false;
- btn.textContent = 'Sincronizează acum';
- return;
- }
- // Poll status
- const pollInterval = setInterval(async () => {
- const sr = await fetch('/api/price-sync/status');
- const sd = await sr.json();
- if (sd.status === 'running') {
- status.textContent = sd.phase_text || 'Sincronizare în curs...';
- } else {
- clearInterval(pollInterval);
- btn.disabled = false;
- btn.textContent = 'Sincronizează acum';
- if (sd.last_run) status.textContent = `Ultima: ${sd.last_run.finished_at || ''} — ${sd.last_run.updated || 0} actualizate din ${sd.last_run.matched || 0}`;
- }
- }, 2000);
- } catch (err) {
- status.innerHTML = `${escHtml(err.message)}`;
- btn.disabled = false;
- btn.textContent = 'Sincronizează acum';
- }
-}
-
function wireAutocomplete(inputId, dropdownId) {
const input = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
diff --git a/api/app/static/js/shared.js b/api/app/static/js/shared.js
index fa19e46..5db75a4 100644
--- a/api/app/static/js/shared.js
+++ b/api/app/static/js/shared.js
@@ -615,10 +615,6 @@ async function renderOrderDetailModal(orderNumber, opts) {
: `${esc(item.codmat || '–')}`;
const valoare = (Number(item.price || 0) * Number(item.quantity || 0));
const clickAttr = opts.onQuickMap ? `onclick="_sharedModalQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}',${idx})"` : '';
- const priceInfo = { pret_roa: item.pret_roa, match: item.price_match };
- const priceMismatchHtml = priceInfo.match === false
- ? `
${esc(item.sku)}' + esc(tCodmat) + '' : ''}' + esc(dCodmat) + '' : ''}