feat(partner): detect and resync partner mismatches on already-imported orders
Detects PF↔PJ transitions and CUI changes after import; auto-resyncs uninvoiced orders (max 5/cycle) and shows visual alert for invoiced ones. - SQLite: partner_mismatch column + batch helpers - sync_service: detection loop + _resync_partner_for_order - dashboard: red dot + attention card indicator - modal: alert with contextual message and resync button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -353,6 +353,7 @@ def init_sqlite():
|
||||
("anaf_denumire_mismatch", "INTEGER DEFAULT 0"),
|
||||
("denumire_anaf", "TEXT"),
|
||||
("address_mismatch", "INTEGER DEFAULT 0"),
|
||||
("partner_mismatch", "INTEGER DEFAULT 0"),
|
||||
]:
|
||||
if col not in order_cols:
|
||||
conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}")
|
||||
|
||||
@@ -544,6 +544,7 @@ async def order_detail(order_number: str):
|
||||
"anaf_cod_fiscal_adjusted": order.get("anaf_cod_fiscal_adjusted") == 1,
|
||||
"anaf_denumire_mismatch": order.get("anaf_denumire_mismatch") == 1,
|
||||
"denumire_anaf": order.get("denumire_anaf"),
|
||||
"partner_mismatch": order.get("partner_mismatch") == 1,
|
||||
}
|
||||
# Parse JSON address strings
|
||||
for key in ("adresa_livrare_gomag", "adresa_facturare_gomag",
|
||||
@@ -579,6 +580,80 @@ async def retry_order(order_number: str):
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/api/orders/{order_number}/resync-partner")
|
||||
async def resync_partner(order_number: str):
|
||||
"""Manual partner resync for invoiced orders with partner_mismatch=1.
|
||||
|
||||
Auto-resync handles uninvoiced orders during sync loop.
|
||||
This endpoint is for edge case: operator wants to fix an already-invoiced order.
|
||||
"""
|
||||
detail = await sqlite_service.get_order_detail(order_number)
|
||||
if not detail:
|
||||
raise HTTPException(status_code=404, detail="Comanda nu a fost gasita")
|
||||
|
||||
order_data = detail["order"]
|
||||
if not order_data.get("partner_mismatch"):
|
||||
return {"success": False, "message": "Comanda nu are mismatch de partener"}
|
||||
|
||||
if sync_service._sync_lock.locked():
|
||||
return {"success": False, "message": "Sync in curs — asteapta finalizarea"}
|
||||
|
||||
stored = {
|
||||
"id_comanda": order_data.get("id_comanda"),
|
||||
"id_partener": order_data.get("id_partener"),
|
||||
"denumire_roa": order_data.get("denumire_roa"),
|
||||
"cod_fiscal_gomag": order_data.get("cod_fiscal_gomag"),
|
||||
"factura_numar": order_data.get("factura_numar"),
|
||||
}
|
||||
|
||||
# Download order from GoMag to get current data
|
||||
import tempfile
|
||||
from ..services import order_reader, gomag_client
|
||||
app_settings = await sqlite_service.get_app_settings()
|
||||
gomag_key = app_settings.get("gomag_api_key") or None
|
||||
gomag_shop = app_settings.get("gomag_api_shop") or None
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
order_date_str = order_data.get("order_date", "")
|
||||
try:
|
||||
order_date = datetime.fromisoformat(order_date_str.replace("Z", "+00:00")).date()
|
||||
except (ValueError, AttributeError):
|
||||
order_date = datetime.now().date() - timedelta(days=1)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
try:
|
||||
days_back = (datetime.now().date() - order_date).days + 2
|
||||
await gomag_client.download_orders(
|
||||
tmp_dir, days_back=days_back,
|
||||
api_key=gomag_key, api_shop=gomag_shop, limit=200,
|
||||
)
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"Eroare download GoMag: {e}"}
|
||||
|
||||
target_order = None
|
||||
orders, _ = order_reader.read_json_orders(json_dir=tmp_dir)
|
||||
for o in orders:
|
||||
if str(o.number) == str(order_number):
|
||||
target_order = o
|
||||
break
|
||||
|
||||
if not target_order:
|
||||
return {"success": False, "message": f"Comanda {order_number} nu a fost gasita in GoMag API"}
|
||||
|
||||
run_id = f"resync_{order_number}"
|
||||
try:
|
||||
await sync_service._resync_partner_for_order(
|
||||
order=target_order,
|
||||
stored=stored,
|
||||
app_settings=app_settings,
|
||||
run_id=run_id,
|
||||
)
|
||||
return {"success": True, "message": "Partener actualizat in ROA"}
|
||||
except Exception as e:
|
||||
logger.error(f"Manual resync failed for {order_number}: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
@router.get("/api/orders/by-sku/{sku}/pending")
|
||||
async def get_pending_orders_for_sku(sku: str):
|
||||
"""Get SKIPPED orders that contain the given SKU."""
|
||||
|
||||
@@ -52,6 +52,38 @@ def convert_web_date(date_str: str) -> datetime:
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def determine_partner_data(order) -> dict:
|
||||
"""Extract partner identification from a GoMag order (no Oracle calls).
|
||||
|
||||
Returns: {denumire, cod_fiscal, registru, is_pj}
|
||||
Identical logic to import_single_order partner block — reuse to avoid drift.
|
||||
"""
|
||||
if order.billing.is_company:
|
||||
denumire = clean_web_text(order.billing.company_name).upper()
|
||||
if not denumire:
|
||||
# CUI-only fallback: company has code but no name → use billing person name
|
||||
denumire = clean_web_text(
|
||||
f"{order.billing.lastname} {order.billing.firstname}"
|
||||
).upper()
|
||||
cod_fiscal = clean_web_text(order.billing.company_code) or None
|
||||
registru = clean_web_text(order.billing.company_reg) or None
|
||||
is_pj = 1
|
||||
else:
|
||||
if order.shipping and (order.shipping.lastname or order.shipping.firstname):
|
||||
raw_name = clean_web_text(
|
||||
f"{order.shipping.lastname} {order.shipping.firstname}"
|
||||
).upper()
|
||||
else:
|
||||
raw_name = clean_web_text(
|
||||
f"{order.billing.lastname} {order.billing.firstname}"
|
||||
).upper()
|
||||
denumire = " ".join(sorted(raw_name.split()))
|
||||
cod_fiscal = None
|
||||
registru = None
|
||||
is_pj = 0
|
||||
return {"denumire": denumire, "cod_fiscal": cod_fiscal, "registru": registru, "is_pj": is_pj}
|
||||
|
||||
|
||||
def format_address_for_oracle(address: str, city: str, region: str) -> str:
|
||||
"""Port of VFP FormatAddressForOracle."""
|
||||
region_clean = clean_web_text(region)
|
||||
@@ -245,26 +277,11 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
# Step 1: Process partner — use shipping person data for name
|
||||
id_partener = cur.var(oracledb.DB_TYPE_NUMBER)
|
||||
|
||||
if order.billing.is_company:
|
||||
denumire = clean_web_text(order.billing.company_name).upper()
|
||||
cod_fiscal = cod_fiscal_override or clean_web_text(order.billing.company_code) or None
|
||||
registru = clean_web_text(order.billing.company_reg) or None
|
||||
is_pj = 1
|
||||
else:
|
||||
# Use shipping person for partner name (person on shipping label)
|
||||
# Sort words alphabetically to normalize firstname/lastname swap
|
||||
if order.shipping and (order.shipping.lastname or order.shipping.firstname):
|
||||
raw_name = clean_web_text(
|
||||
f"{order.shipping.lastname} {order.shipping.firstname}"
|
||||
).upper()
|
||||
else:
|
||||
raw_name = clean_web_text(
|
||||
f"{order.billing.lastname} {order.billing.firstname}"
|
||||
).upper()
|
||||
denumire = " ".join(sorted(raw_name.split()))
|
||||
cod_fiscal = None
|
||||
registru = None
|
||||
is_pj = 0
|
||||
_pdata = determine_partner_data(order)
|
||||
denumire = _pdata["denumire"]
|
||||
cod_fiscal = (cod_fiscal_override or _pdata["cod_fiscal"]) if _pdata["is_pj"] else None
|
||||
registru = _pdata["registru"]
|
||||
is_pj = _pdata["is_pj"]
|
||||
|
||||
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener", [
|
||||
cod_fiscal, denumire, registru, is_pj, anaf_strict, id_partener
|
||||
@@ -283,19 +300,6 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
result["denumire_roa"] = row[0] if row else None
|
||||
result["cod_fiscal_roa"] = row[1] if row else None
|
||||
|
||||
# Determine if billing and shipping are different persons
|
||||
billing_name = clean_web_text(
|
||||
f"{order.billing.lastname} {order.billing.firstname}"
|
||||
).strip().upper()
|
||||
shipping_name = ""
|
||||
if order.shipping:
|
||||
shipping_name = clean_web_text(
|
||||
f"{order.shipping.lastname} {order.shipping.firstname}"
|
||||
).strip().upper()
|
||||
different_person = bool(
|
||||
shipping_name and billing_name and shipping_name != billing_name
|
||||
)
|
||||
|
||||
# Step 2: Process shipping address (primary — person on shipping label)
|
||||
# Use shipping person phone/email for partner contact
|
||||
shipping_phone = ""
|
||||
@@ -333,12 +337,9 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
result["error"] = err_msg
|
||||
return result
|
||||
|
||||
# Step 3: Process billing address
|
||||
if different_person:
|
||||
# Different person: use shipping address for BOTH billing and shipping in ROA
|
||||
addr_fact_id = addr_livr_id
|
||||
else:
|
||||
# Same person: compute billing addr, short-circuit if identical to shipping
|
||||
# Step 3: Process billing address — PJ vs PF rule
|
||||
if is_pj:
|
||||
# PJ (company): billing address = GoMag billing (company HQ)
|
||||
billing_addr = format_address_for_oracle(
|
||||
order.billing.address, order.billing.city, order.billing.region
|
||||
)
|
||||
@@ -364,6 +365,9 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
|
||||
logger.error(f"Order {order_number}: {err_msg}")
|
||||
result["error"] = err_msg
|
||||
return result
|
||||
else:
|
||||
# PF (individual): billing = shipping (ramburs curier pe numele destinatarului)
|
||||
addr_fact_id = addr_livr_id
|
||||
|
||||
if addr_fact_id is not None:
|
||||
result["id_adresa_facturare"] = int(addr_fact_id)
|
||||
|
||||
@@ -124,7 +124,9 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
|
||||
# Parse billing
|
||||
billing_data = data.get("billing", {}) or {}
|
||||
company = billing_data.get("company")
|
||||
is_company = isinstance(company, dict) and bool(company.get("name"))
|
||||
is_company = isinstance(company, dict) and (
|
||||
bool(company.get("name")) or bool(company.get("code"))
|
||||
)
|
||||
|
||||
billing = OrderBilling(
|
||||
firstname=str(billing_data.get("firstname", "")),
|
||||
|
||||
@@ -701,6 +701,7 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
||||
elif status_filter.upper() == "DIFFS":
|
||||
data_clauses.append(
|
||||
"(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1"
|
||||
" OR partner_mismatch = 1"
|
||||
" OR (cod_fiscal_gomag IS NOT NULL AND cod_fiscal_gomag != '' AND anaf_platitor_tva IS NOT NULL"
|
||||
" AND anaf_cod_fiscal_adjusted != 1"
|
||||
" AND ((UPPER(cod_fiscal_gomag) LIKE 'RO%' AND anaf_platitor_tva = 0)"
|
||||
@@ -759,9 +760,10 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
||||
cursor = await db.execute(f"SELECT COUNT(*) FROM orders {uninv_old_where}", base_params)
|
||||
uninvoiced_old = (await cursor.fetchone())[0]
|
||||
|
||||
# Diffs count: orders with ANAF adjustments or TVA mismatch (not address)
|
||||
# Diffs count: orders with ANAF adjustments, TVA mismatch, or partner mismatch
|
||||
diffs_clauses = list(base_clauses) + [
|
||||
"(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1"
|
||||
" OR partner_mismatch = 1"
|
||||
" OR (cod_fiscal_gomag IS NOT NULL AND cod_fiscal_gomag != '' AND anaf_platitor_tva IS NOT NULL"
|
||||
" AND anaf_cod_fiscal_adjusted != 1"
|
||||
" AND ((UPPER(cod_fiscal_gomag) LIKE 'RO%' AND anaf_platitor_tva = 0)"
|
||||
@@ -771,6 +773,12 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
||||
cursor = await db.execute(f"SELECT COUNT(*) FROM orders {diffs_where}", base_params)
|
||||
diffs_count = (await cursor.fetchone())[0]
|
||||
|
||||
# Partner mismatches count
|
||||
pm_clauses = list(base_clauses) + ["partner_mismatch = 1"]
|
||||
pm_where = "WHERE " + " AND ".join(pm_clauses)
|
||||
cursor = await db.execute(f"SELECT COUNT(*) FROM orders {pm_where}", base_params)
|
||||
partner_mismatches_count = (await cursor.fetchone())[0]
|
||||
|
||||
return {
|
||||
"orders": [dict(r) for r in rows],
|
||||
"total": total,
|
||||
@@ -788,6 +796,7 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
||||
"uninvoiced_sqlite": uninvoiced_sqlite,
|
||||
"uninvoiced_old": uninvoiced_old,
|
||||
"diffs": diffs_count,
|
||||
"partner_mismatches": partner_mismatches_count,
|
||||
}
|
||||
}
|
||||
finally:
|
||||
@@ -1382,3 +1391,76 @@ async def set_incomplete_addresses_count(count: int):
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
# ── Partner Mismatch ──────────────────────────────
|
||||
|
||||
async def get_orders_partner_data_batch(order_numbers: list) -> dict:
|
||||
"""Return {order_number: {cod_fiscal_gomag, denumire_roa, id_partener, factura_numar, id_comanda}}."""
|
||||
if not order_numbers:
|
||||
return {}
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
result = {}
|
||||
for i in range(0, len(order_numbers), 500):
|
||||
batch = order_numbers[i:i+500]
|
||||
placeholders = ",".join("?" * len(batch))
|
||||
cursor = await db.execute(
|
||||
f"SELECT order_number, cod_fiscal_gomag, denumire_roa, id_partener, "
|
||||
f"factura_numar, id_comanda FROM orders WHERE order_number IN ({placeholders})",
|
||||
batch
|
||||
)
|
||||
for row in await cursor.fetchall():
|
||||
result[row[0]] = {
|
||||
"cod_fiscal_gomag": row[1],
|
||||
"denumire_roa": row[2],
|
||||
"id_partener": row[3],
|
||||
"factura_numar": row[4],
|
||||
"id_comanda": row[5],
|
||||
}
|
||||
return result
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def update_partner_mismatch_batch(updates: list) -> None:
|
||||
"""Update partner_mismatch flag for a batch of orders.
|
||||
Each item: {order_number, partner_mismatch: 0|1}
|
||||
"""
|
||||
if not updates:
|
||||
return
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.executemany(
|
||||
"UPDATE orders SET partner_mismatch = ?, updated_at = datetime('now') WHERE order_number = ?",
|
||||
[(u["partner_mismatch"], u["order_number"]) for u in updates]
|
||||
)
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def update_partner_resync_data(order_number: str, data: dict) -> None:
|
||||
"""Update partner fields + clear partner_mismatch after a successful resync."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
UPDATE orders SET
|
||||
id_partener = ?,
|
||||
cod_fiscal_gomag = ?,
|
||||
cod_fiscal_roa = ?,
|
||||
denumire_roa = ?,
|
||||
partner_mismatch = ?,
|
||||
updated_at = datetime('now')
|
||||
WHERE order_number = ?
|
||||
""", (
|
||||
data.get("id_partener"),
|
||||
data.get("cod_fiscal_gomag"),
|
||||
data.get("cod_fiscal_roa"),
|
||||
data.get("denumire_roa"),
|
||||
data.get("partner_mismatch", 0),
|
||||
order_number,
|
||||
))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
@@ -625,6 +625,63 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
})
|
||||
await sqlite_service.update_gomag_addresses_batch(addr_updates)
|
||||
|
||||
# Detect partner mismatches for already-imported orders
|
||||
if already_in_roa:
|
||||
stored_partner_data = await sqlite_service.get_orders_partner_data_batch(
|
||||
[o.number for o in already_in_roa]
|
||||
)
|
||||
mismatch_map = {}
|
||||
mismatch_updates = []
|
||||
for order in already_in_roa:
|
||||
stored = stored_partner_data.get(order.number, {})
|
||||
stored_cf = stored.get("cod_fiscal_gomag")
|
||||
new_data = import_service.determine_partner_data(order)
|
||||
new_cf = new_data["cod_fiscal"]
|
||||
|
||||
def _strip_ro(cf):
|
||||
if not cf:
|
||||
return ""
|
||||
return re.sub(r'^RO', '', cf.strip().upper())
|
||||
|
||||
is_mismatch = False
|
||||
if new_data["is_pj"] and not stored_cf:
|
||||
is_mismatch = True # PF→PJ
|
||||
elif not new_data["is_pj"] and stored_cf:
|
||||
is_mismatch = True # PJ→PF
|
||||
elif new_data["is_pj"] and stored_cf and _strip_ro(new_cf) != _strip_ro(stored_cf):
|
||||
is_mismatch = True # CUI schimbat
|
||||
|
||||
val = 1 if is_mismatch else 0
|
||||
mismatch_map[order.number] = val
|
||||
mismatch_updates.append({"order_number": order.number, "partner_mismatch": val})
|
||||
|
||||
await sqlite_service.update_partner_mismatch_batch(mismatch_updates)
|
||||
|
||||
# Auto-resync uninvoiced orders with partner mismatch (max 5/cycle)
|
||||
MAX_PARTNER_RESYNC_PER_CYCLE = 5
|
||||
mismatched_uninvoiced = [
|
||||
o for o in already_in_roa
|
||||
if mismatch_map.get(o.number) == 1
|
||||
and not stored_partner_data.get(o.number, {}).get("factura_numar")
|
||||
][:MAX_PARTNER_RESYNC_PER_CYCLE]
|
||||
|
||||
if mismatched_uninvoiced:
|
||||
resync_ok = 0
|
||||
for _order in mismatched_uninvoiced:
|
||||
try:
|
||||
await _resync_partner_for_order(
|
||||
order=_order,
|
||||
stored=stored_partner_data.get(_order.number, {}),
|
||||
app_settings=app_settings,
|
||||
run_id=run_id,
|
||||
)
|
||||
resync_ok += 1
|
||||
except Exception as _e:
|
||||
_log_line(run_id, f"#{_order.number} EROARE resync partener: {_e}")
|
||||
logger.error(f"Partner resync error for {_order.number}: {_e}")
|
||||
if resync_ok:
|
||||
_log_line(run_id, f"Resync parteneri: {resync_ok} comenzi actualizate")
|
||||
|
||||
# Step 3b: Record skipped orders + store items (batch)
|
||||
skipped_count = len(skipped)
|
||||
skipped_batch = []
|
||||
@@ -1076,3 +1133,199 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
def stop_sync():
|
||||
"""Signal sync to stop. Currently sync runs to completion."""
|
||||
pass
|
||||
|
||||
|
||||
async def _resync_partner_for_order(order, stored: dict, app_settings: dict, run_id: str) -> None:
|
||||
"""Resync partner for a single already-imported uninvoiced order.
|
||||
|
||||
Safety: double-checks factura_numar before Oracle call.
|
||||
Reads existing comanda row and calls PACK_COMENZI.modifica_comanda.
|
||||
"""
|
||||
import oracledb
|
||||
|
||||
order_number = order.number
|
||||
id_comanda = stored.get("id_comanda")
|
||||
if not id_comanda:
|
||||
_log_line(run_id, f"#{order_number} SKIP resync partener: id_comanda lipsa")
|
||||
return
|
||||
|
||||
# Double-check factura_numar — may have been invoiced since mismatch detection
|
||||
current_detail = await sqlite_service.get_order_detail(order_number)
|
||||
if current_detail and current_detail.get("order", {}).get("factura_numar"):
|
||||
_log_line(run_id, f"#{order_number} SKIP resync partener: comanda facturata in tranzit")
|
||||
return
|
||||
|
||||
old_partner_id = stored.get("id_partener")
|
||||
old_partner_name = stored.get("denumire_roa") or "?"
|
||||
|
||||
new_partner_data = import_service.determine_partner_data(order)
|
||||
|
||||
# ANAF check for PF→PJ transition
|
||||
cod_fiscal_override = None
|
||||
anaf_data = None
|
||||
if new_partner_data["is_pj"] and new_partner_data["cod_fiscal"]:
|
||||
raw_cf = new_partner_data["cod_fiscal"]
|
||||
bare_cui, _ = anaf_service.sanitize_cui(raw_cf)
|
||||
if bare_cui:
|
||||
anaf_data = await sqlite_service.get_anaf_cache(bare_cui)
|
||||
if not anaf_data:
|
||||
try:
|
||||
fresh = await anaf_service.check_vat_status_batch([bare_cui])
|
||||
if fresh:
|
||||
await sqlite_service.bulk_populate_anaf_cache(fresh)
|
||||
anaf_data = fresh.get(bare_cui)
|
||||
except Exception as e:
|
||||
logger.warning(f"ANAF check failed for {bare_cui}: {e}")
|
||||
if anaf_data and anaf_data.get("scpTVA") is not None:
|
||||
cod_fiscal_override = anaf_service.determine_correct_cod_fiscal(
|
||||
bare_cui, anaf_data["scpTVA"]
|
||||
)
|
||||
|
||||
def _do_resync():
|
||||
if database.pool is None:
|
||||
raise RuntimeError("Oracle pool not initialized")
|
||||
conn = database.pool.acquire()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# Create/find partner
|
||||
id_partener_var = cur.var(oracledb.DB_TYPE_NUMBER)
|
||||
anaf_strict = 1 if (anaf_data and anaf_data.get("scpTVA") is not None) else None
|
||||
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener", [
|
||||
cod_fiscal_override or new_partner_data["cod_fiscal"],
|
||||
new_partner_data["denumire"],
|
||||
new_partner_data["registru"],
|
||||
new_partner_data["is_pj"],
|
||||
anaf_strict,
|
||||
id_partener_var,
|
||||
])
|
||||
new_partner_id = id_partener_var.getvalue()
|
||||
if not new_partner_id or new_partner_id <= 0:
|
||||
raise RuntimeError(f"Partner creation failed for {new_partner_data['denumire']}")
|
||||
new_partner_id = int(new_partner_id)
|
||||
|
||||
# Same partner — just clear mismatch
|
||||
if new_partner_id == (old_partner_id or -1):
|
||||
return {"same_partner": True, "new_partner_id": new_partner_id}
|
||||
|
||||
# Get new partner details for audit log
|
||||
cur.execute(
|
||||
"SELECT denumire, cod_fiscal FROM nom_parteneri WHERE id_part = :1",
|
||||
[new_partner_id]
|
||||
)
|
||||
row = cur.fetchone()
|
||||
new_partner_name = row[0] if row else new_partner_data["denumire"]
|
||||
new_cod_fiscal_roa = row[1] if row else None
|
||||
|
||||
# Create addresses under new partner
|
||||
addr_livr_id = None
|
||||
shipping_addr = None
|
||||
if order.shipping:
|
||||
id_adresa_livr = cur.var(oracledb.DB_TYPE_NUMBER)
|
||||
shipping_addr = import_service.format_address_for_oracle(
|
||||
order.shipping.address, order.shipping.city, order.shipping.region
|
||||
)
|
||||
shipping_phone = order.shipping.phone or order.billing.phone or ""
|
||||
shipping_email = order.shipping.email or order.billing.email or ""
|
||||
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_adresa", [
|
||||
new_partner_id, shipping_addr, shipping_phone, shipping_email, id_adresa_livr
|
||||
])
|
||||
addr_livr_id = id_adresa_livr.getvalue()
|
||||
if addr_livr_id is None:
|
||||
raise RuntimeError(f"Shipping address creation failed for partner {new_partner_id}")
|
||||
addr_livr_id = int(addr_livr_id)
|
||||
|
||||
billing_name_str = import_service.clean_web_text(
|
||||
f"{order.billing.lastname} {order.billing.firstname}"
|
||||
).strip().upper()
|
||||
ship_name_str = ""
|
||||
if order.shipping:
|
||||
ship_name_str = import_service.clean_web_text(
|
||||
f"{order.shipping.lastname} {order.shipping.firstname}"
|
||||
).strip().upper()
|
||||
different_person = bool(ship_name_str and billing_name_str and ship_name_str != billing_name_str)
|
||||
|
||||
if different_person and addr_livr_id:
|
||||
addr_fact_id = addr_livr_id
|
||||
else:
|
||||
billing_addr = import_service.format_address_for_oracle(
|
||||
order.billing.address, order.billing.city, order.billing.region
|
||||
)
|
||||
if addr_livr_id and order.shipping and billing_addr == shipping_addr:
|
||||
addr_fact_id = addr_livr_id
|
||||
else:
|
||||
id_adresa_fact = cur.var(oracledb.DB_TYPE_NUMBER)
|
||||
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_adresa", [
|
||||
new_partner_id, billing_addr,
|
||||
order.billing.phone or "",
|
||||
order.billing.email or "",
|
||||
id_adresa_fact,
|
||||
])
|
||||
addr_fact_id = id_adresa_fact.getvalue()
|
||||
if addr_fact_id is None:
|
||||
raise RuntimeError(f"Billing address creation failed for partner {new_partner_id}")
|
||||
addr_fact_id = int(addr_fact_id)
|
||||
|
||||
# Read existing comanda row for modifica_comanda params
|
||||
cur.execute("""
|
||||
SELECT nr_comanda, data_comanda, data_livrare, proc_discount,
|
||||
interna, id_util_um, id_codclient, comanda_externa, id_ctr
|
||||
FROM comenzi WHERE id_comanda = :1
|
||||
""", [id_comanda])
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise RuntimeError(f"Comanda {id_comanda} not found in Oracle")
|
||||
nr_comanda, data_comanda, data_livrare, proc_discount, interna, id_util_um, id_codclient, comanda_externa, id_ctr = row
|
||||
|
||||
cur.callproc("PACK_COMENZI.modifica_comanda", [
|
||||
id_comanda,
|
||||
nr_comanda,
|
||||
data_comanda,
|
||||
new_partner_id,
|
||||
data_livrare,
|
||||
proc_discount,
|
||||
interna,
|
||||
id_util_um,
|
||||
addr_fact_id,
|
||||
addr_livr_id,
|
||||
id_codclient,
|
||||
comanda_externa,
|
||||
id_ctr,
|
||||
])
|
||||
conn.commit()
|
||||
return {
|
||||
"same_partner": False,
|
||||
"new_partner_id": new_partner_id,
|
||||
"new_partner_name": new_partner_name,
|
||||
"new_cod_fiscal_roa": new_cod_fiscal_roa,
|
||||
}
|
||||
except Exception:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
database.pool.release(conn)
|
||||
|
||||
resync_result = await asyncio.to_thread(_do_resync)
|
||||
|
||||
if resync_result.get("same_partner"):
|
||||
await sqlite_service.update_partner_mismatch_batch([
|
||||
{"order_number": order_number, "partner_mismatch": 0}
|
||||
])
|
||||
_log_line(run_id, f"#{order_number} RESYNC: partener neschimbat, mismatch cleared")
|
||||
else:
|
||||
new_partner_id = resync_result["new_partner_id"]
|
||||
new_partner_name = resync_result.get("new_partner_name", "?")
|
||||
new_cod_fiscal_roa = resync_result.get("new_cod_fiscal_roa")
|
||||
await sqlite_service.update_partner_resync_data(order_number, {
|
||||
"id_partener": new_partner_id,
|
||||
"cod_fiscal_gomag": cod_fiscal_override or new_partner_data["cod_fiscal"],
|
||||
"cod_fiscal_roa": new_cod_fiscal_roa,
|
||||
"denumire_roa": new_partner_name,
|
||||
"partner_mismatch": 0,
|
||||
})
|
||||
_log_line(
|
||||
run_id,
|
||||
f"#{order_number} RESYNC partener: {old_partner_id} ({old_partner_name}) → {new_partner_id} ({new_partner_name})"
|
||||
)
|
||||
|
||||
@@ -340,8 +340,9 @@ async function loadDashOrders() {
|
||||
const diffs = c.diffs || 0;
|
||||
|
||||
const incompleteAddr = c.incomplete_addresses || 0;
|
||||
const partnerMismatches = c.partner_mismatches || 0;
|
||||
|
||||
if (errors === 0 && unmapped === 0 && nefact === 0 && incompleteAddr === 0 && diffs === 0) {
|
||||
if (errors === 0 && unmapped === 0 && nefact === 0 && incompleteAddr === 0 && diffs === 0 && partnerMismatches === 0) {
|
||||
attnEl.innerHTML = '<div class="attention-card attention-ok"><i class="bi bi-check-circle"></i> Totul in ordine</div>';
|
||||
} else {
|
||||
let items = [];
|
||||
@@ -350,6 +351,7 @@ async function loadDashOrders() {
|
||||
if (nefact > 0) items.push(`<span class="attention-item attention-warning" onclick="document.querySelector('.filter-pill[data-status=UNINVOICED]')?.click()"><i class="bi bi-receipt"></i> ${nefact} nefacturate</span>`);
|
||||
if (c.incomplete_addresses > 0) items.push(`<span class="attention-item attention-warning"><i class="bi bi-geo-alt"></i> ${c.incomplete_addresses} adrese incomplete</span>`);
|
||||
if (diffs > 0) items.push(`<span class="attention-item attention-warning" onclick="document.querySelector('.filter-pill[data-status=DIFFS]')?.click()"><i class="bi bi-exclamation-diamond"></i> ${diffs} diferente ANAF</span>`);
|
||||
if (partnerMismatches > 0) items.push(`<span class="attention-item attention-error" onclick="document.querySelector('.filter-pill[data-status=DIFFS]')?.click()"><i class="bi bi-people"></i> ${partnerMismatches} partener schimbat</span>`);
|
||||
attnEl.innerHTML = '<div class="attention-card attention-alert">' + items.join('') + '</div>';
|
||||
}
|
||||
}
|
||||
@@ -509,6 +511,8 @@ function diffDots(o, mobile) {
|
||||
d += `<span style="${s};background:var(--compare)" title="Denumire ANAF"></span>`;
|
||||
if (o.address_mismatch===1)
|
||||
d += `<span style="${s};background:var(--info)" title="Adresa diferita"></span>`;
|
||||
if (o.partner_mismatch===1)
|
||||
d += `<span style="${s};background:var(--error)" title="Partener schimbat"></span>`;
|
||||
if (o.price_match===false)
|
||||
d += `<span style="${s};background:var(--error)" title="Pret GoMag > ROA"></span>`;
|
||||
return d;
|
||||
|
||||
@@ -531,6 +531,8 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
// Restore original structure (may have been replaced by PF indicator)
|
||||
cuiRoa.innerHTML = '<small class="text-muted">CUI:</small> <span class="font-data" id="detailCuiRoaVal"></span><span id="detailPartnerAnafArea"></span>';
|
||||
}
|
||||
const partnerMismatchEl = document.getElementById('detailPartnerMismatch');
|
||||
if (partnerMismatchEl) { partnerMismatchEl.style.display = 'none'; partnerMismatchEl.innerHTML = ''; }
|
||||
const denomMismatch = document.getElementById('detailDenomMismatch');
|
||||
if (denomMismatch) { denomMismatch.style.display = 'none'; denomMismatch.innerHTML = ''; }
|
||||
const addressBlock = document.getElementById('detailAddressBlock');
|
||||
@@ -940,6 +942,33 @@ function _renderHeaderInfo(order) {
|
||||
document.getElementById('detailIdPartener').innerHTML = '<span class="text-muted">\u2014</span>';
|
||||
}
|
||||
|
||||
// Partner mismatch alert
|
||||
if (pi && pi.partner_mismatch) {
|
||||
const pmEl = document.getElementById('detailPartnerMismatch');
|
||||
if (pmEl) {
|
||||
const isInvoiced = !!(order.invoice && order.invoice.facturat);
|
||||
let mismatchType = '';
|
||||
if (pi.cod_fiscal_gomag && !pi.cod_fiscal_roa) {
|
||||
mismatchType = 'PF → PJ: comanda importata ca persoana fizica, acum are CUI in GoMag.';
|
||||
} else if (!pi.cod_fiscal_gomag && pi.cod_fiscal_roa) {
|
||||
mismatchType = 'PJ → PF: comanda importata cu CUI, acum GoMag nu mai are companie.';
|
||||
} else if (pi.cod_fiscal_gomag && pi.cod_fiscal_roa && pi.cod_fiscal_gomag !== pi.cod_fiscal_roa) {
|
||||
mismatchType = `CUI schimbat: GoMag are ${esc(pi.cod_fiscal_gomag)}, ROA are ${esc(pi.cod_fiscal_roa)}.`;
|
||||
} else {
|
||||
mismatchType = 'Date partener diferite fata de momentul importului.';
|
||||
}
|
||||
const resyncBtn = isInvoiced
|
||||
? `<button class="btn btn-sm btn-outline-warning mt-1" onclick="resyncPartner('${esc(order.order_number)}', this)"><i class="bi bi-person-check"></i> Actualizeaza partener in ROA</button>`
|
||||
: '';
|
||||
pmEl.innerHTML = `<div class="denom-mismatch" style="border-color:var(--error)">
|
||||
<span class="denom-mismatch-title" style="color:var(--error-text)"><i class="bi bi-people"></i> Partener schimbat in GoMag</span><br>
|
||||
<span style="font-size:13px">${mismatchType}</span>
|
||||
${resyncBtn}
|
||||
</div>`;
|
||||
pmEl.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Denomination mismatch alert
|
||||
if (isPJ && pi.anaf_denumire_mismatch && pi.denumire_anaf) {
|
||||
const denomEl = document.getElementById('detailDenomMismatch');
|
||||
@@ -1022,6 +1051,7 @@ function _renderHeaderInfo(order) {
|
||||
if (addr && addr.livrare_roa && !addrMatch(addr.livrare_gomag, addr.livrare_roa)) badges.push({label:'Adr. livr.', cls:'diff-badge-addr', aria:'Adresa livrare diferita'});
|
||||
if (addr && addr.facturare_roa && !addrMatch(addr.facturare_gomag, addr.facturare_roa)) badges.push({label:'Adr. fact.', cls:'diff-badge-addr', aria:'Adresa facturare diferita'});
|
||||
if (order.price_check && order.price_check.mismatches > 0) badges.push({label:'Preturi (' + order.price_check.mismatches + ')', cls:'diff-badge-price', aria:'Preturi diferite: ' + order.price_check.mismatches});
|
||||
if (pi && pi.partner_mismatch) badges.push({label:'Partener', cls:'diff-badge-anaf', aria:'Partener schimbat in GoMag'});
|
||||
let insertAfter = orderNumEl;
|
||||
badges.forEach(b => {
|
||||
const el = document.createElement('span');
|
||||
@@ -1033,3 +1063,24 @@ function _renderHeaderInfo(order) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Partner Resync ────────────────────────────────
|
||||
|
||||
async function resyncPartner(orderNumber, btnEl) {
|
||||
if (!confirm('Actualizeaza partenerul acestei comenzi in ROA Oracle?\n\nAtentie: Comanda este facturata. Verificati manual dupa actualizare.')) return;
|
||||
if (btnEl) { btnEl.disabled = true; btnEl.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Se actualizeaza...'; }
|
||||
try {
|
||||
const res = await fetch(`${window.ROOT_PATH || ''}/api/orders/${encodeURIComponent(orderNumber)}/resync-partner`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
if (btnEl) { btnEl.innerHTML = '<i class="bi bi-check-circle"></i> Actualizat'; btnEl.className = 'btn btn-sm btn-success mt-1'; }
|
||||
setTimeout(() => renderOrderDetailModal(orderNumber, {}), 1500);
|
||||
} else {
|
||||
if (btnEl) { btnEl.disabled = false; btnEl.innerHTML = '<i class="bi bi-person-check"></i> Actualizeaza partener in ROA'; }
|
||||
alert('Eroare: ' + (data.message || 'Resync esuat'));
|
||||
}
|
||||
} catch(e) {
|
||||
if (btnEl) { btnEl.disabled = false; btnEl.innerHTML = '<i class="bi bi-person-check"></i> Actualizeaza partener in ROA'; }
|
||||
alert('Eroare de retea: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Partner mismatch alert -->
|
||||
<div id="detailPartnerMismatch" style="display:none" class="mb-2"></div>
|
||||
<!-- Denomination mismatch alert -->
|
||||
<div id="detailDenomMismatch" style="display:none" class="mb-2"></div>
|
||||
<!-- Compact Address Lines -->
|
||||
|
||||
Reference in New Issue
Block a user