feat(address): ROA address cache refresh — 8-field format + manual refresh endpoint

Phase 5 address format upgrade (pre-existing working tree changes):
- import_service: extend vadrese_parteneri query to 8 fields (strada/numar/bloc/scara/apart/etaj/localitate/judet); strip trailing city name from address string passed to Oracle
- sync_service: extend _addr_match to compare bloc/scara/apart in addition to strada/numar
- 05_pack_import_parteneri.pck: updated PL/SQL package

New: address cache refresh mechanism:
- sqlite_service: add get_order_address_ids(), update_order_address_cache() (targeted 3-column update, no ANAF fields touched), get_orders_with_address_ids()
- sync.py: POST /api/orders/{order_number}/refresh-address endpoint (404/422/503/200); batch Oracle address refresh in refresh_invoices (single IN roundtrip, per-order mismatch recomputed)
- UI: refresh button (⟳) in ADRESE modal header (base.html); refreshOrderAddress() with loading state + toast (dashboard.js v43); window._detailOrderNumber global (shared.js v32)
- tests: TestRefreshOrderAddress — 4 tests (404, 422, 503, 200 with 8-field assert)

Oracle prod fix applied directly: ADRESE_PARTENERI id_adresa=4116 STRADA VASILE→VASILE GOLDIS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-07 12:35:18 +00:00
parent a8ad54a604
commit ecde7fe440
10 changed files with 377 additions and 10 deletions

View File

@@ -57,6 +57,14 @@ def format_address_for_oracle(address: str, city: str, region: str) -> str:
region_clean = clean_web_text(region)
city_clean = clean_web_text(city)
address_clean = clean_web_text(address)
# Strip city name from end of address (users often type it)
if city_clean:
addr_upper = address_clean.upper().rstrip()
city_upper = city_clean.upper().strip()
if addr_upper.endswith(city_upper):
stripped = address_clean[:len(address_clean.rstrip()) - len(city_upper)].rstrip()
if stripped: # don't strip if nothing remains
address_clean = stripped
return f"JUD:{region_clean};{city_clean};{address_clean}"
@@ -360,13 +368,21 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
# Query address details from Oracle for sync back to SQLite
if addr_livr_id:
cur.execute("SELECT strada, numar, localitate, judet FROM vadrese_parteneri WHERE id_adresa = :1", [int(addr_livr_id)])
cur.execute("""SELECT strada, numar, bloc, scara, apart, etaj, localitate, judet
FROM vadrese_parteneri WHERE id_adresa = :1""", [int(addr_livr_id)])
row = cur.fetchone()
result["adresa_livrare_roa"] = {"strada": row[0], "numar": row[1], "localitate": row[2], "judet": row[3]} if row else None
result["adresa_livrare_roa"] = {
"strada": row[0], "numar": row[1], "bloc": row[2], "scara": row[3],
"apart": row[4], "etaj": row[5], "localitate": row[6], "judet": row[7]
} if row else None
if addr_fact_id and addr_fact_id != addr_livr_id:
cur.execute("SELECT strada, numar, localitate, judet FROM vadrese_parteneri WHERE id_adresa = :1", [int(addr_fact_id)])
cur.execute("""SELECT strada, numar, bloc, scara, apart, etaj, localitate, judet
FROM vadrese_parteneri WHERE id_adresa = :1""", [int(addr_fact_id)])
row = cur.fetchone()
result["adresa_facturare_roa"] = {"strada": row[0], "numar": row[1], "localitate": row[2], "judet": row[3]} if row else None
result["adresa_facturare_roa"] = {
"strada": row[0], "numar": row[1], "bloc": row[2], "scara": row[3],
"apart": row[4], "etaj": row[5], "localitate": row[6], "judet": row[7]
} if row else None
elif addr_fact_id and addr_fact_id == addr_livr_id:
result["adresa_facturare_roa"] = result.get("adresa_livrare_roa")

View File

@@ -1214,6 +1214,59 @@ async def update_gomag_addresses_batch(updates: list[dict]):
await db.close()
async def get_order_address_ids(order_number: str) -> dict | None:
"""Return id_adresa_livrare, id_adresa_facturare, adresa_*_gomag for an order."""
db = await get_sqlite()
try:
cursor = await db.execute("""SELECT id_adresa_livrare, id_adresa_facturare,
adresa_livrare_gomag, adresa_facturare_gomag,
adresa_livrare_roa
FROM orders WHERE order_number = ?""", [order_number])
row = await cursor.fetchone()
return dict(row) if row else None
finally:
await db.close()
async def update_order_address_cache(order_number: str, livr_roa: dict | None,
fact_roa: dict | None, mismatch: bool):
"""Update ONLY the 3 address-cache columns — does NOT touch ANAF/partner fields."""
db = await get_sqlite()
try:
await db.execute("""
UPDATE orders SET
adresa_livrare_roa = ?,
adresa_facturare_roa = ?,
address_mismatch = ?,
updated_at = datetime('now')
WHERE order_number = ?
""", (
json.dumps(livr_roa) if livr_roa else None,
json.dumps(fact_roa) if fact_roa else None,
1 if mismatch else 0,
order_number,
))
await db.commit()
finally:
await db.close()
async def get_orders_with_address_ids() -> list[dict]:
"""Get all orders that have Oracle address IDs stored (for batch refresh)."""
db = await get_sqlite()
try:
cursor = await db.execute("""
SELECT order_number, id_adresa_livrare, id_adresa_facturare,
adresa_livrare_gomag, adresa_facturare_gomag
FROM orders
WHERE id_adresa_livrare IS NOT NULL OR id_adresa_facturare IS NOT NULL
""")
rows = await cursor.fetchall()
return [dict(r) for r in rows]
finally:
await db.close()
async def get_orders_missing_anaf() -> list[dict]:
"""Get orders with cod_fiscal_roa set but no ANAF data (for backfill)."""
db = await get_sqlite()

View File

@@ -40,7 +40,7 @@ def _addr_match(gomag_json, roa_json):
s = _ADDR_WORDS.sub('', s)
return re.sub(r'[^A-Z0-9]', '', s)
g_street = norm(g.get('address') or g.get('strada') or '')
r_street = norm((r.get('strada') or '') + (r.get('numar') or ''))
r_street = norm((r.get('strada') or '') + (r.get('numar') or '') + (r.get('bloc') or '') + (r.get('scara') or '') + (r.get('apart') or ''))
g_city = norm(g.get('city') or g.get('localitate') or '')
r_city = norm(r.get('localitate') or '')
g_region = norm(g.get('region') or g.get('judet') or '')