feat(anaf-dedup): ANAF partner dedup + address fix + UI enrichment

Prevent partner duplicates via ANAF CUI verification and dual PL/SQL
search. Fix address matching with street-level comparison and diacritics
normalization. Show partner/address comparison in order detail modal.

- New anaf_service.py: batch ANAF API client with chunking, retry, cache
- PL/SQL: dual CUI search (bare/RO+bare/RO space+bare), 3-tier address
  search (street+city+id_loc → city+id_loc → create), strip_diacritics
  at storage for addresses and partner names
- SQLite: anaf_cache table, 12 new order columns for partner/address data
- import_service: cod_fiscal_override param, return partner/address from Oracle
- sync_service: ANAF batch integration, denomination mismatch detection,
  cache pre-population trigger
- Router: enriched order_detail with partner_info + addresses JSON
- UI: collapsible Detalii Partener + Adrese Comparativ sections in modal,
  auto-expand on mismatch, ANAF badges, mobile address cards
- Dashboard: address quality attention indicator
- New scan_duplicate_partners.py script for one-time duplicate audit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-01 14:36:52 +00:00
parent 3b9198d742
commit 2f593c30f6
12 changed files with 925 additions and 64 deletions

View File

@@ -12,7 +12,7 @@ def _now():
"""Return current time in Bucharest timezone (naive, for display/storage)."""
return datetime.now(_tz_bucharest).replace(tzinfo=None)
from . import order_reader, validation_service, import_service, sqlite_service, invoice_service, gomag_client
from . import order_reader, validation_service, import_service, sqlite_service, invoice_service, gomag_client, anaf_service
from ..config import settings
from .. import database
@@ -638,7 +638,51 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
0, len(truly_importable),
{"imported": 0, "skipped": skipped_count, "errors": 0, "already_imported": already_imported_count})
# Step 4: Import only truly new orders
# ANAF cache pre-population check
try:
db_check = await sqlite_service.get_sqlite()
try:
cursor = await db_check.execute("SELECT COUNT(*) FROM anaf_cache WHERE checked_at > datetime('now', '-7 days')")
row = await cursor.fetchone()
cache_count = row[0] if row else 0
finally:
await db_check.close()
if cache_count < 10:
_log_line(run_id, "ANAF pre-populare cache...")
except Exception as e:
logger.warning(f"ANAF cache pre-population check failed: {e}")
# Step 4: ANAF batch verification for company CUIs
company_cuis = set()
for order in truly_importable:
if order.billing.is_company and order.billing.company_code:
raw_cf = import_service.clean_web_text(order.billing.company_code) or ""
bare = anaf_service.strip_ro_prefix(raw_cf)
if anaf_service.validate_cui(bare):
company_cuis.add(bare)
# Check anaf_cache for already-known CUIs (7-day validity)
uncached_cuis = []
cached_results = {}
for cui in company_cuis:
cached = await sqlite_service.get_anaf_cache(cui)
if cached:
cached_results[cui] = cached
else:
uncached_cuis.append(cui)
# Batch ANAF call for uncached CUIs only
if uncached_cuis:
_log_line(run_id, f"ANAF: verificare {len(uncached_cuis)} CUI-uri noi...")
anaf_results = await anaf_service.check_vat_status_batch(uncached_cuis)
if anaf_results:
await sqlite_service.bulk_populate_anaf_cache(anaf_results)
cached_results.update(anaf_results)
else:
_log_line(run_id, "ANAF: batch call esuat, continua fara corectie CUI")
# Step 5: Import only truly new orders
imported_count = 0
error_count = 0
@@ -651,10 +695,25 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
{"imported": imported_count, "skipped": len(skipped), "errors": error_count,
"already_imported": already_imported_count})
# Determine cod_fiscal override from ANAF data
cod_fiscal_override = None
anaf_data_for_order = None
raw_cf = ""
if order.billing.is_company and order.billing.company_code:
raw_cf = import_service.clean_web_text(order.billing.company_code) or ""
bare_cui = anaf_service.strip_ro_prefix(raw_cf)
anaf_data_for_order = cached_results.get(bare_cui)
if anaf_data_for_order and anaf_data_for_order.get("scpTVA") is not None:
correct_cf = anaf_service.determine_correct_cod_fiscal(bare_cui, anaf_data_for_order["scpTVA"])
if correct_cf != raw_cf:
_log_line(run_id, f"#{order.number} CUI corectat: {raw_cf}{correct_cf}")
cod_fiscal_override = correct_cf
result = await asyncio.to_thread(
import_service.import_single_order,
order, id_pol=id_pol, id_sectie=id_sectie,
app_settings=app_settings, id_gestiuni=id_gestiuni
app_settings=app_settings, id_gestiuni=id_gestiuni,
cod_fiscal_override=cod_fiscal_override
)
# Build order items data for storage (R9)
@@ -702,7 +761,34 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
)
await sqlite_service.add_order_items(order.number, order_items_data)
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → IMPORTAT (ID: {result['id_comanda']})")
else:
# Save partner + ANAF + address data to SQLite
if result["success"] or result.get("id_partener"):
partner_data = {
"cod_fiscal_gomag": raw_cf if order.billing.is_company else None,
"cod_fiscal_roa": result.get("cod_fiscal_roa"),
"denumire_roa": result.get("denumire_roa"),
"anaf_platitor_tva": (1 if anaf_data_for_order.get("scpTVA") else 0) if anaf_data_for_order and anaf_data_for_order.get("scpTVA") is not None else None,
"anaf_checked_at": anaf_data_for_order.get("checked_at") if anaf_data_for_order else None,
"anaf_cod_fiscal_adjusted": 1 if cod_fiscal_override and cod_fiscal_override != raw_cf else 0,
"adresa_livrare_gomag": json.dumps({"address": order.shipping.address, "city": order.shipping.city, "region": order.shipping.region}) if order.shipping else None,
"adresa_facturare_gomag": json.dumps({"address": order.billing.address, "city": order.billing.city, "region": order.billing.region}),
"adresa_livrare_roa": json.dumps(result.get("adresa_livrare_roa")) if result.get("adresa_livrare_roa") else None,
"adresa_facturare_roa": json.dumps(result.get("adresa_facturare_roa")) if result.get("adresa_facturare_roa") else None,
"anaf_denumire_mismatch": 0,
"denumire_anaf": None,
}
# Denomination mismatch check
if anaf_data_for_order and anaf_data_for_order.get("denumire_anaf") and order.billing.is_company:
norm_gomag = anaf_service.normalize_company_name(order.billing.company_name or "")
norm_anaf = anaf_service.normalize_company_name(anaf_data_for_order["denumire_anaf"])
if norm_gomag and norm_anaf and norm_gomag != norm_anaf:
partner_data["anaf_denumire_mismatch"] = 1
partner_data["denumire_anaf"] = anaf_data_for_order["denumire_anaf"]
await sqlite_service.update_order_partner_data(order.number, partner_data)
if not result["success"]:
error_count += 1
await sqlite_service.upsert_order(
sync_run_id=run_id,