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:
@@ -1009,3 +1009,161 @@ async def get_price_sync_runs(page: int = 1, per_page: int = 20):
|
||||
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:
|
||||
"""Get cached ANAF data for a CUI (valid for 7 days)."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
cursor = await db.execute("""
|
||||
SELECT scp_tva, denumire_anaf, checked_at
|
||||
FROM anaf_cache
|
||||
WHERE cui = ? AND checked_at > datetime('now', '-7 days')
|
||||
""", (bare_cui,))
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"scpTVA": bool(row["scp_tva"]) if row["scp_tva"] is not None else None,
|
||||
"denumire_anaf": row["denumire_anaf"] or "",
|
||||
"checked_at": row["checked_at"],
|
||||
}
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def upsert_anaf_cache(cui: str, scp_tva: int | None, denumire_anaf: str):
|
||||
"""Insert or update ANAF cache entry."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
INSERT OR REPLACE INTO anaf_cache (cui, scp_tva, denumire_anaf, checked_at)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
""", (cui, scp_tva, denumire_anaf))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def bulk_populate_anaf_cache(results: dict[str, dict]):
|
||||
"""Batch insert/update ANAF cache entries.
|
||||
results format: {cui: {"scpTVA": bool|None, "denumire_anaf": str, "checked_at": str}, ...}
|
||||
"""
|
||||
if not results:
|
||||
return
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
rows = []
|
||||
for cui, data in results.items():
|
||||
scp = None
|
||||
if data.get("scpTVA") is True:
|
||||
scp = 1
|
||||
elif data.get("scpTVA") is False:
|
||||
scp = 0
|
||||
rows.append((cui, scp, data.get("denumire_anaf", ""), data.get("checked_at", _now_str())))
|
||||
await db.executemany("""
|
||||
INSERT OR REPLACE INTO anaf_cache (cui, scp_tva, denumire_anaf, checked_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", rows)
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
# ── Partner/Address Data on Orders ─────────────────
|
||||
|
||||
async def update_order_partner_data(order_number: str, partner_data: dict):
|
||||
"""Update order with partner/ANAF/address comparison data.
|
||||
|
||||
partner_data keys: cod_fiscal_gomag, cod_fiscal_roa, denumire_roa,
|
||||
anaf_platitor_tva, anaf_checked_at, anaf_cod_fiscal_adjusted,
|
||||
adresa_livrare_gomag, adresa_facturare_gomag, adresa_livrare_roa,
|
||||
adresa_facturare_roa, anaf_denumire_mismatch, denumire_anaf
|
||||
"""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
UPDATE orders SET
|
||||
cod_fiscal_gomag = ?,
|
||||
cod_fiscal_roa = ?,
|
||||
denumire_roa = ?,
|
||||
anaf_platitor_tva = ?,
|
||||
anaf_checked_at = ?,
|
||||
anaf_cod_fiscal_adjusted = ?,
|
||||
adresa_livrare_gomag = ?,
|
||||
adresa_facturare_gomag = ?,
|
||||
adresa_livrare_roa = ?,
|
||||
adresa_facturare_roa = ?,
|
||||
anaf_denumire_mismatch = ?,
|
||||
denumire_anaf = ?,
|
||||
updated_at = datetime('now')
|
||||
WHERE order_number = ?
|
||||
""", (
|
||||
partner_data.get("cod_fiscal_gomag"),
|
||||
partner_data.get("cod_fiscal_roa"),
|
||||
partner_data.get("denumire_roa"),
|
||||
partner_data.get("anaf_platitor_tva"),
|
||||
partner_data.get("anaf_checked_at"),
|
||||
partner_data.get("anaf_cod_fiscal_adjusted", 0),
|
||||
partner_data.get("adresa_livrare_gomag"),
|
||||
partner_data.get("adresa_facturare_gomag"),
|
||||
partner_data.get("adresa_livrare_roa"),
|
||||
partner_data.get("adresa_facturare_roa"),
|
||||
partner_data.get("anaf_denumire_mismatch", 0),
|
||||
partner_data.get("denumire_anaf"),
|
||||
order_number,
|
||||
))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
# ── Address Quality Cache (via app_settings) ──────
|
||||
|
||||
async def get_incomplete_addresses_count() -> int:
|
||||
"""Get cached count of orders with incomplete ROA addresses.
|
||||
Returns -1 if cache is stale (> 1 hour old) or not set.
|
||||
"""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"SELECT value FROM app_settings WHERE key = 'incomplete_addresses_checked_at'"
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if not row or not row["value"]:
|
||||
return -1
|
||||
# Check freshness
|
||||
from datetime import datetime, timedelta
|
||||
try:
|
||||
checked_at = datetime.fromisoformat(row["value"])
|
||||
if datetime.now() - checked_at > timedelta(hours=1):
|
||||
return -1
|
||||
except (ValueError, TypeError):
|
||||
return -1
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT value FROM app_settings WHERE key = 'incomplete_addresses_count'"
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return int(row["value"]) if row and row["value"] else 0
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def set_incomplete_addresses_count(count: int):
|
||||
"""Cache incomplete addresses count in app_settings."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO app_settings (key, value) VALUES ('incomplete_addresses_count', ?)",
|
||||
(str(count),)
|
||||
)
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO app_settings (key, value) VALUES ('incomplete_addresses_checked_at', ?)",
|
||||
(_now_str(),)
|
||||
)
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
Reference in New Issue
Block a user