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:
@@ -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})"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user