Compare commits
4 Commits
bf194eb088
...
b2f1687920
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2f1687920 | ||
|
|
07df807719 | ||
|
|
d3d72032ef | ||
|
|
89c3d1d07f |
@@ -73,9 +73,10 @@ Documentatie completa: [README.md](README.md)
|
|||||||
- Recovery: la fiecare sync, comenzile ERROR sunt reverificate in Oracle
|
- Recovery: la fiecare sync, comenzile ERROR sunt reverificate in Oracle
|
||||||
|
|
||||||
### Parteneri
|
### Parteneri
|
||||||
- Prioritate: **companie** (PJ, cod_fiscal + registru) daca exista in GoMag, altfel persoana fizica cu **shipping name**
|
- Prioritate: **companie** (PJ, cod_fiscal + registru) daca exista in GoMag (name SAU code), altfel persoana fizica cu **shipping name**
|
||||||
- Adresa livrare: intotdeauna GoMag shipping
|
- Adresa livrare: intotdeauna GoMag shipping
|
||||||
- Adresa facturare: daca shipping ≠ billing person → shipping pt ambele; altfel → billing din GoMag
|
- Adresa facturare PJ: adresa billing din GoMag (sediul firmei)
|
||||||
|
- Adresa facturare PF: adresa shipping din GoMag (ramburs curier pe numele destinatarului)
|
||||||
|
|
||||||
### Preturi
|
### Preturi
|
||||||
- Dual policy: articolele sunt rutate la `id_pol_vanzare` sau `id_pol_productie` pe baza contului contabil (341/345 = productie)
|
- Dual policy: articolele sunt rutate la `id_pol_vanzare` sau `id_pol_productie` pe baza contului contabil (341/345 = productie)
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ def init_sqlite():
|
|||||||
("anaf_denumire_mismatch", "INTEGER DEFAULT 0"),
|
("anaf_denumire_mismatch", "INTEGER DEFAULT 0"),
|
||||||
("denumire_anaf", "TEXT"),
|
("denumire_anaf", "TEXT"),
|
||||||
("address_mismatch", "INTEGER DEFAULT 0"),
|
("address_mismatch", "INTEGER DEFAULT 0"),
|
||||||
|
("partner_mismatch", "INTEGER DEFAULT 0"),
|
||||||
]:
|
]:
|
||||||
if col not in order_cols:
|
if col not in order_cols:
|
||||||
conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}")
|
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_cod_fiscal_adjusted": order.get("anaf_cod_fiscal_adjusted") == 1,
|
||||||
"anaf_denumire_mismatch": order.get("anaf_denumire_mismatch") == 1,
|
"anaf_denumire_mismatch": order.get("anaf_denumire_mismatch") == 1,
|
||||||
"denumire_anaf": order.get("denumire_anaf"),
|
"denumire_anaf": order.get("denumire_anaf"),
|
||||||
|
"partner_mismatch": order.get("partner_mismatch") == 1,
|
||||||
}
|
}
|
||||||
# Parse JSON address strings
|
# Parse JSON address strings
|
||||||
for key in ("adresa_livrare_gomag", "adresa_facturare_gomag",
|
for key in ("adresa_livrare_gomag", "adresa_facturare_gomag",
|
||||||
@@ -579,6 +580,80 @@ async def retry_order(order_number: str):
|
|||||||
return result
|
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")
|
@router.get("/api/orders/by-sku/{sku}/pending")
|
||||||
async def get_pending_orders_for_sku(sku: str):
|
async def get_pending_orders_for_sku(sku: str):
|
||||||
"""Get SKIPPED orders that contain the given SKU."""
|
"""Get SKIPPED orders that contain the given SKU."""
|
||||||
|
|||||||
@@ -52,6 +52,38 @@ def convert_web_date(date_str: str) -> datetime:
|
|||||||
return datetime.now()
|
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:
|
def format_address_for_oracle(address: str, city: str, region: str) -> str:
|
||||||
"""Port of VFP FormatAddressForOracle."""
|
"""Port of VFP FormatAddressForOracle."""
|
||||||
region_clean = clean_web_text(region)
|
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
|
# Step 1: Process partner — use shipping person data for name
|
||||||
id_partener = cur.var(oracledb.DB_TYPE_NUMBER)
|
id_partener = cur.var(oracledb.DB_TYPE_NUMBER)
|
||||||
|
|
||||||
if order.billing.is_company:
|
_pdata = determine_partner_data(order)
|
||||||
denumire = clean_web_text(order.billing.company_name).upper()
|
denumire = _pdata["denumire"]
|
||||||
cod_fiscal = cod_fiscal_override or clean_web_text(order.billing.company_code) or None
|
cod_fiscal = (cod_fiscal_override or _pdata["cod_fiscal"]) if _pdata["is_pj"] else None
|
||||||
registru = clean_web_text(order.billing.company_reg) or None
|
registru = _pdata["registru"]
|
||||||
is_pj = 1
|
is_pj = _pdata["is_pj"]
|
||||||
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
|
|
||||||
|
|
||||||
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener", [
|
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener", [
|
||||||
cod_fiscal, denumire, registru, is_pj, anaf_strict, id_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["denumire_roa"] = row[0] if row else None
|
||||||
result["cod_fiscal_roa"] = row[1] 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)
|
# Step 2: Process shipping address (primary — person on shipping label)
|
||||||
# Use shipping person phone/email for partner contact
|
# Use shipping person phone/email for partner contact
|
||||||
shipping_phone = ""
|
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
|
result["error"] = err_msg
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Step 3: Process billing address
|
# Step 3: Process billing address — PJ vs PF rule
|
||||||
if different_person:
|
if is_pj:
|
||||||
# Different person: use shipping address for BOTH billing and shipping in ROA
|
# PJ (company): billing address = GoMag billing (company HQ)
|
||||||
addr_fact_id = addr_livr_id
|
|
||||||
else:
|
|
||||||
# Same person: compute billing addr, short-circuit if identical to shipping
|
|
||||||
billing_addr = format_address_for_oracle(
|
billing_addr = format_address_for_oracle(
|
||||||
order.billing.address, order.billing.city, order.billing.region
|
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}")
|
logger.error(f"Order {order_number}: {err_msg}")
|
||||||
result["error"] = err_msg
|
result["error"] = err_msg
|
||||||
return result
|
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:
|
if addr_fact_id is not None:
|
||||||
result["id_adresa_facturare"] = int(addr_fact_id)
|
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
|
# Parse billing
|
||||||
billing_data = data.get("billing", {}) or {}
|
billing_data = data.get("billing", {}) or {}
|
||||||
company = billing_data.get("company")
|
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(
|
billing = OrderBilling(
|
||||||
firstname=str(billing_data.get("firstname", "")),
|
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":
|
elif status_filter.upper() == "DIFFS":
|
||||||
data_clauses.append(
|
data_clauses.append(
|
||||||
"(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1"
|
"(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"
|
" 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 anaf_cod_fiscal_adjusted != 1"
|
||||||
" AND ((UPPER(cod_fiscal_gomag) LIKE 'RO%' AND anaf_platitor_tva = 0)"
|
" 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)
|
cursor = await db.execute(f"SELECT COUNT(*) FROM orders {uninv_old_where}", base_params)
|
||||||
uninvoiced_old = (await cursor.fetchone())[0]
|
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) + [
|
diffs_clauses = list(base_clauses) + [
|
||||||
"(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1"
|
"(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"
|
" 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 anaf_cod_fiscal_adjusted != 1"
|
||||||
" AND ((UPPER(cod_fiscal_gomag) LIKE 'RO%' AND anaf_platitor_tva = 0)"
|
" 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)
|
cursor = await db.execute(f"SELECT COUNT(*) FROM orders {diffs_where}", base_params)
|
||||||
diffs_count = (await cursor.fetchone())[0]
|
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 {
|
return {
|
||||||
"orders": [dict(r) for r in rows],
|
"orders": [dict(r) for r in rows],
|
||||||
"total": total,
|
"total": total,
|
||||||
@@ -788,6 +796,7 @@ async def get_orders(page: int = 1, per_page: int = 50,
|
|||||||
"uninvoiced_sqlite": uninvoiced_sqlite,
|
"uninvoiced_sqlite": uninvoiced_sqlite,
|
||||||
"uninvoiced_old": uninvoiced_old,
|
"uninvoiced_old": uninvoiced_old,
|
||||||
"diffs": diffs_count,
|
"diffs": diffs_count,
|
||||||
|
"partner_mismatches": partner_mismatches_count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
@@ -1382,3 +1391,76 @@ async def set_incomplete_addresses_count(count: int):
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
finally:
|
finally:
|
||||||
await db.close()
|
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)
|
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)
|
# Step 3b: Record skipped orders + store items (batch)
|
||||||
skipped_count = len(skipped)
|
skipped_count = len(skipped)
|
||||||
skipped_batch = []
|
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():
|
def stop_sync():
|
||||||
"""Signal sync to stop. Currently sync runs to completion."""
|
"""Signal sync to stop. Currently sync runs to completion."""
|
||||||
pass
|
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 diffs = c.diffs || 0;
|
||||||
|
|
||||||
const incompleteAddr = c.incomplete_addresses || 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>';
|
attnEl.innerHTML = '<div class="attention-card attention-ok"><i class="bi bi-check-circle"></i> Totul in ordine</div>';
|
||||||
} else {
|
} else {
|
||||||
let items = [];
|
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 (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 (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 (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>';
|
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>`;
|
d += `<span style="${s};background:var(--compare)" title="Denumire ANAF"></span>`;
|
||||||
if (o.address_mismatch===1)
|
if (o.address_mismatch===1)
|
||||||
d += `<span style="${s};background:var(--info)" title="Adresa diferita"></span>`;
|
d += `<span style="${s};background:var(--info)" title="Adresa diferita"></span>`;
|
||||||
|
if (o.partner_mismatch===1)
|
||||||
|
d += `<span style="${s};background:var(--warning)" title="Partener schimbat"></span>`;
|
||||||
if (o.price_match===false)
|
if (o.price_match===false)
|
||||||
d += `<span style="${s};background:var(--error)" title="Pret GoMag > ROA"></span>`;
|
d += `<span style="${s};background:var(--error)" title="Pret GoMag > ROA"></span>`;
|
||||||
return d;
|
return d;
|
||||||
|
|||||||
@@ -531,6 +531,8 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
|||||||
// Restore original structure (may have been replaced by PF indicator)
|
// 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>';
|
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');
|
const denomMismatch = document.getElementById('detailDenomMismatch');
|
||||||
if (denomMismatch) { denomMismatch.style.display = 'none'; denomMismatch.innerHTML = ''; }
|
if (denomMismatch) { denomMismatch.style.display = 'none'; denomMismatch.innerHTML = ''; }
|
||||||
const addressBlock = document.getElementById('detailAddressBlock');
|
const addressBlock = document.getElementById('detailAddressBlock');
|
||||||
@@ -883,11 +885,14 @@ function _renderHeaderInfo(order) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ROA column — show partner name for both PJ and PF
|
// ROA column — show partner name for both PJ and PF
|
||||||
if (pi && pi.denumire_roa) {
|
const partenerRoa = document.getElementById('detailPartenerRoa');
|
||||||
const partenerRoa = document.getElementById('detailPartenerRoa');
|
if (partenerRoa) {
|
||||||
if (partenerRoa) {
|
if (pi && pi.denumire_roa) {
|
||||||
partenerRoa.textContent = pi.denumire_roa;
|
partenerRoa.textContent = pi.denumire_roa;
|
||||||
partenerRoa.style.display = '';
|
partenerRoa.style.display = '';
|
||||||
|
} else if (pi && pi.partner_mismatch) {
|
||||||
|
partenerRoa.innerHTML = '<span class="text-muted" style="font-style:italic">necunoscut — se va actualiza la urmatorul sync</span>';
|
||||||
|
partenerRoa.style.display = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -940,6 +945,33 @@ function _renderHeaderInfo(order) {
|
|||||||
document.getElementById('detailIdPartener').innerHTML = '<span class="text-muted">\u2014</span>';
|
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
|
// Denomination mismatch alert
|
||||||
if (isPJ && pi.anaf_denumire_mismatch && pi.denumire_anaf) {
|
if (isPJ && pi.anaf_denumire_mismatch && pi.denumire_anaf) {
|
||||||
const denomEl = document.getElementById('detailDenomMismatch');
|
const denomEl = document.getElementById('detailDenomMismatch');
|
||||||
@@ -1022,6 +1054,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.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 (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 (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;
|
let insertAfter = orderNumEl;
|
||||||
badges.forEach(b => {
|
badges.forEach(b => {
|
||||||
const el = document.createElement('span');
|
const el = document.createElement('span');
|
||||||
@@ -1033,3 +1066,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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Partner mismatch alert -->
|
||||||
|
<div id="detailPartnerMismatch" style="display:none" class="mb-2"></div>
|
||||||
<!-- Denomination mismatch alert -->
|
<!-- Denomination mismatch alert -->
|
||||||
<div id="detailDenomMismatch" style="display:none" class="mb-2"></div>
|
<div id="detailDenomMismatch" style="display:none" class="mb-2"></div>
|
||||||
<!-- Compact Address Lines -->
|
<!-- Compact Address Lines -->
|
||||||
@@ -168,7 +170,7 @@
|
|||||||
|
|
||||||
<script>window.ROOT_PATH = "{{ rp }}";</script>
|
<script>window.ROOT_PATH = "{{ rp }}";</script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="{{ rp }}/static/js/shared.js?v=35"></script>
|
<script src="{{ rp }}/static/js/shared.js?v=36"></script>
|
||||||
<script>
|
<script>
|
||||||
// Dark mode toggle
|
// Dark mode toggle
|
||||||
function toggleDarkMode() {
|
function toggleDarkMode() {
|
||||||
|
|||||||
@@ -114,5 +114,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=44"></script>
|
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=45"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
95
api/database-scripts/cleanup_comenzi_sterse_nefacturate.sql
Normal file
95
api/database-scripts/cleanup_comenzi_sterse_nefacturate.sql
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- cleanup_comenzi_sterse_nefacturate.sql
|
||||||
|
-- 2026-04-08
|
||||||
|
--
|
||||||
|
-- Soft-delete (sters=1) comenzile din ROA care sunt:
|
||||||
|
-- 1. Active (sters=0)
|
||||||
|
-- 2. Nu au factura activa in VANZARI
|
||||||
|
-- 3. Mai vechi de 3 zile (DATA_COMANDA < SYSDATE - 3)
|
||||||
|
--
|
||||||
|
-- Motivatie: comenzi de test importate din GoMag care au fost facturate manual
|
||||||
|
-- (direct, nu factura din comanda). Raman pe veci ca active nefacturate.
|
||||||
|
--
|
||||||
|
-- IMPORTANT: Ruleaza intai SELECT-ul de preview inainte de UPDATE!
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
SET SERVEROUTPUT ON;
|
||||||
|
SET LINESIZE 200;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 1: PREVIEW — vezi ce se va marca sters
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
PROMPT;
|
||||||
|
PROMPT === PREVIEW: Comenzi active, nefacturate, mai vechi de 3 zile ===;
|
||||||
|
PROMPT;
|
||||||
|
|
||||||
|
SELECT c.id_comanda,
|
||||||
|
c.nr_comanda,
|
||||||
|
c.comanda_externa,
|
||||||
|
c.data_comanda,
|
||||||
|
c.id_part,
|
||||||
|
(SELECT COUNT(*) FROM comenzi_elemente e
|
||||||
|
WHERE e.id_comanda = c.id_comanda AND e.sters = 0) AS nr_elemente
|
||||||
|
FROM comenzi c
|
||||||
|
WHERE c.sters = 0
|
||||||
|
AND c.data_comanda < SYSDATE - 3
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM vanzari v
|
||||||
|
WHERE v.id_comanda = c.id_comanda
|
||||||
|
AND v.sters = 0
|
||||||
|
)
|
||||||
|
ORDER BY c.data_comanda;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 2: SOFT-DELETE — decomentati blocul dupa verificarea preview-ului
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
DECLARE
|
||||||
|
v_elemente_count NUMBER := 0;
|
||||||
|
v_comenzi_count NUMBER := 0;
|
||||||
|
BEGIN
|
||||||
|
-- Mai intai soft-delete pe detalii (COMENZI_ELEMENTE)
|
||||||
|
UPDATE comenzi_elemente SET sters = 1
|
||||||
|
WHERE sters = 0
|
||||||
|
AND id_comanda IN (
|
||||||
|
SELECT c.id_comanda
|
||||||
|
FROM comenzi c
|
||||||
|
WHERE c.sters = 0
|
||||||
|
AND c.data_comanda < SYSDATE - 3
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM vanzari v
|
||||||
|
WHERE v.id_comanda = c.id_comanda
|
||||||
|
AND v.sters = 0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
v_elemente_count := SQL%ROWCOUNT;
|
||||||
|
|
||||||
|
-- Apoi soft-delete pe header (COMENZI)
|
||||||
|
UPDATE comenzi SET sters = 1
|
||||||
|
WHERE sters = 0
|
||||||
|
AND data_comanda < SYSDATE - 3
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM vanzari v
|
||||||
|
WHERE v.id_comanda = comenzi.id_comanda
|
||||||
|
AND v.sters = 0
|
||||||
|
);
|
||||||
|
v_comenzi_count := SQL%ROWCOUNT;
|
||||||
|
|
||||||
|
DBMS_OUTPUT.PUT_LINE('=== REZULTAT CLEANUP ===');
|
||||||
|
DBMS_OUTPUT.PUT_LINE('Elemente marcate sters: ' || v_elemente_count);
|
||||||
|
DBMS_OUTPUT.PUT_LINE('Comenzi marcate sters: ' || v_comenzi_count);
|
||||||
|
|
||||||
|
-- COMMIT explicit — decomentati doar dupa ce sunteti siguri
|
||||||
|
-- COMMIT;
|
||||||
|
|
||||||
|
-- Sau ROLLBACK daca ceva nu arata bine:
|
||||||
|
-- ROLLBACK;
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
*/
|
||||||
|
|
||||||
|
PROMPT;
|
||||||
|
PROMPT === Pentru a executa, decomentati blocul PL/SQL si COMMIT ===;
|
||||||
|
PROMPT;
|
||||||
@@ -20,6 +20,9 @@ _SESSION_WINDOW_HOURS = 1
|
|||||||
# These are real bugs that need fixing but should not block test runs.
|
# These are real bugs that need fixing but should not block test runs.
|
||||||
_KNOWN_ISSUES = [
|
_KNOWN_ISSUES = [
|
||||||
"soft-deleting order ID=533: ORA-00942", # Pre-existing: missing table/view
|
"soft-deleting order ID=533: ORA-00942", # Pre-existing: missing table/view
|
||||||
|
"Oracle init failed: DPY-4000", # Dev env: no Oracle tnsnames
|
||||||
|
"ANAF API client error 404", # Dev env: ANAF mock returns 404
|
||||||
|
"ANAF API server error after retry: 500", # Dev env: ANAF mock returns 500
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
347
api/tests/test_address_rules_oracle.py
Normal file
347
api/tests/test_address_rules_oracle.py
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
"""
|
||||||
|
Oracle Integration Tests — Regula adrese PJ/PF
|
||||||
|
===============================================
|
||||||
|
Verifică că comenzile importate respectă regula:
|
||||||
|
PF (fără CUI): id_adresa_facturare = id_adresa_livrare
|
||||||
|
PJ (cu CUI): adresa_facturare_roa se potrivește cu adresa billing GoMag
|
||||||
|
|
||||||
|
Testele principale sunt E2E (importă comenzi sintetice în Oracle și verifică).
|
||||||
|
Testele de regresie verifică comenzile existente din SQLite.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
pytest api/tests/test_address_rules_oracle.py -v
|
||||||
|
./test.sh oracle
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.oracle
|
||||||
|
|
||||||
|
_script_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
||||||
|
_project_root = os.path.dirname(_script_dir)
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
_env_path = os.path.join(_script_dir, ".env")
|
||||||
|
load_dotenv(_env_path, override=True)
|
||||||
|
|
||||||
|
_tns_admin = os.environ.get("TNS_ADMIN", "")
|
||||||
|
if _tns_admin and os.path.isfile(_tns_admin):
|
||||||
|
os.environ["TNS_ADMIN"] = os.path.dirname(_tns_admin)
|
||||||
|
elif not _tns_admin:
|
||||||
|
os.environ["TNS_ADMIN"] = _script_dir
|
||||||
|
|
||||||
|
if _script_dir not in sys.path:
|
||||||
|
sys.path.insert(0, _script_dir)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixtures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def oracle_env():
|
||||||
|
"""Re-aplică .env și actualizează settings pentru Oracle."""
|
||||||
|
load_dotenv(_env_path, override=True)
|
||||||
|
_tns = os.environ.get("TNS_ADMIN", "")
|
||||||
|
if _tns and os.path.isfile(_tns):
|
||||||
|
os.environ["TNS_ADMIN"] = os.path.dirname(_tns)
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
settings.ORACLE_USER = os.environ.get("ORACLE_USER", "MARIUSM_AUTO")
|
||||||
|
settings.ORACLE_PASSWORD = os.environ.get("ORACLE_PASSWORD", "ROMFASTSOFT")
|
||||||
|
settings.ORACLE_DSN = os.environ.get("ORACLE_DSN", "ROA_CENTRAL")
|
||||||
|
settings.TNS_ADMIN = os.environ.get("TNS_ADMIN", _script_dir)
|
||||||
|
settings.FORCE_THIN_MODE = os.environ.get("FORCE_THIN_MODE", "") == "true"
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def client(oracle_env):
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from app.main import app
|
||||||
|
with TestClient(app) as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def oracle_pool(oracle_env):
|
||||||
|
"""Pool Oracle direct pentru verificări în DB."""
|
||||||
|
from app import database
|
||||||
|
database.init_oracle()
|
||||||
|
yield database.pool
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def real_codmat(client):
|
||||||
|
"""CODMAT real din Oracle pentru liniile comenzii sintetice."""
|
||||||
|
for term in ["01", "PH", "CA", "A"]:
|
||||||
|
resp = client.get("/api/articles/search", params={"q": term})
|
||||||
|
if resp.status_code == 200:
|
||||||
|
results = resp.json().get("results", [])
|
||||||
|
if results:
|
||||||
|
return results[0]["codmat"]
|
||||||
|
pytest.skip("Nu s-a găsit niciun CODMAT în Oracle pentru test")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def app_settings(client):
|
||||||
|
"""Setările aplicației (id_pol, id_sectie, etc.)."""
|
||||||
|
resp = client.get("/api/sync/schedule")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
import sqlite3
|
||||||
|
db_path = os.environ.get("SQLITE_DB_PATH", os.path.join(_script_dir, "orders.db"))
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
rows = conn.execute("SELECT key, value FROM app_settings").fetchall()
|
||||||
|
conn.close()
|
||||||
|
return {r["key"]: r["value"] for r in rows}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def run_id():
|
||||||
|
return f"pytest-addr-{int(time.time())}"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_pj_order(run_id, real_codmat):
|
||||||
|
"""Comandă sintetică PJ: companie cu billing ≠ shipping."""
|
||||||
|
from app.services.order_reader import OrderBilling, OrderShipping, OrderData, OrderItem
|
||||||
|
billing = OrderBilling(
|
||||||
|
firstname="Test", lastname="PJ", phone="0700000000", email="pj@pytest.local",
|
||||||
|
address="Bld Unirii 1", city="Bucuresti", region="Bucuresti", country="RO",
|
||||||
|
company_name="PYTEST COMPANY SRL", company_code="RO99000001", company_reg="J40/9999/2026",
|
||||||
|
is_company=True
|
||||||
|
)
|
||||||
|
shipping = OrderShipping(
|
||||||
|
firstname="Curier", lastname="Destinatar", phone="0799999999", email="ship@pytest.local",
|
||||||
|
address="Str Livrare 99", city="Cluj-Napoca", region="Cluj", country="RO"
|
||||||
|
)
|
||||||
|
return OrderData(
|
||||||
|
id=f"{run_id}-PJ",
|
||||||
|
number=f"{run_id}-PJ",
|
||||||
|
date="2026-01-15T10:00:00",
|
||||||
|
status="new", status_id="1",
|
||||||
|
billing=billing, shipping=shipping,
|
||||||
|
items=[OrderItem(sku="PYTEST-SKU-PJ", name="Test PJ Item",
|
||||||
|
price=10.0, quantity=1.0, vat=19.0)],
|
||||||
|
total=10.0, delivery_cost=0.0, discount_total=0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_pf_order(run_id, real_codmat):
|
||||||
|
"""Comandă sintetică PF: persoană fizică, billing ≠ shipping (dar billing ROA trebuie = shipping)."""
|
||||||
|
from app.services.order_reader import OrderBilling, OrderShipping, OrderData, OrderItem
|
||||||
|
billing = OrderBilling(
|
||||||
|
firstname="Ion", lastname="Popescu", phone="0700000001", email="pf@pytest.local",
|
||||||
|
address="Str Alta 5", city="Timisoara", region="Timis", country="RO",
|
||||||
|
company_name="", company_code="", company_reg="", is_company=False
|
||||||
|
)
|
||||||
|
shipping = OrderShipping(
|
||||||
|
firstname="Ion", lastname="Popescu", phone="0700000001", email="pf@pytest.local",
|
||||||
|
address="Str Livrare 10", city="Iasi", region="Iasi", country="RO"
|
||||||
|
)
|
||||||
|
return OrderData(
|
||||||
|
id=f"{run_id}-PF",
|
||||||
|
number=f"{run_id}-PF",
|
||||||
|
date="2026-01-15T10:00:00",
|
||||||
|
status="new", status_id="1",
|
||||||
|
billing=billing, shipping=shipping,
|
||||||
|
items=[OrderItem(sku="PYTEST-SKU-PF", name="Test PF Item",
|
||||||
|
price=10.0, quantity=1.0, vat=19.0)],
|
||||||
|
total=10.0, delivery_cost=0.0, discount_total=0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_test_orders(oracle_pool, run_id):
|
||||||
|
"""Șterge comenzile de test din Oracle."""
|
||||||
|
try:
|
||||||
|
conn = oracle_pool.acquire()
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM comenzi WHERE comanda_externa LIKE :1",
|
||||||
|
[f"{run_id}%"]
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
oracle_pool.release(conn)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Cleanup warning: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test E2E: import PJ + PF sintetice în Oracle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestAddressRulesE2E:
|
||||||
|
"""Import comenzi sintetice și verifică adresele în Oracle."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
|
def cleanup(self, oracle_pool, run_id):
|
||||||
|
yield
|
||||||
|
_cleanup_test_orders(oracle_pool, run_id)
|
||||||
|
|
||||||
|
def test_pj_billing_addr_is_gomag_billing(self, oracle_pool, real_codmat, app_settings, run_id):
|
||||||
|
"""PJ: adresa facturare în Oracle provine din GoMag billing (nu shipping)."""
|
||||||
|
from app.services.import_service import import_single_order
|
||||||
|
from app.services.order_reader import OrderItem
|
||||||
|
|
||||||
|
order = _build_pj_order(run_id, real_codmat)
|
||||||
|
# Replace test SKU with real codmat via mapping (or just use items with real SKU)
|
||||||
|
order.items = [OrderItem(sku=real_codmat, name="Test PJ",
|
||||||
|
price=10.0, quantity=1.0, vat=19.0)]
|
||||||
|
|
||||||
|
id_pol = int(app_settings.get("id_pol") or 0) or None
|
||||||
|
id_sectie = int(app_settings.get("id_sectie") or 0) or None
|
||||||
|
|
||||||
|
result = import_single_order(order, id_pol=id_pol, id_sectie=id_sectie,
|
||||||
|
app_settings=app_settings)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
pytest.skip(f"Import PJ eșuat (SKU probabil nemapat): {result.get('error')}")
|
||||||
|
|
||||||
|
id_fact = result["id_adresa_facturare"]
|
||||||
|
id_livr = result["id_adresa_livrare"]
|
||||||
|
|
||||||
|
assert id_fact is not None, "PJ: id_adresa_facturare lipsește din result"
|
||||||
|
assert id_livr is not None, "PJ: id_adresa_livrare lipsește din result"
|
||||||
|
|
||||||
|
# PJ cu billing ≠ shipping: adresele trebuie să fie DIFERITE
|
||||||
|
assert id_fact != id_livr, (
|
||||||
|
f"PJ cu billing≠shipping trebuie să aibă id_fact({id_fact}) ≠ id_livr({id_livr}). "
|
||||||
|
f"Regula veche (different_person) s-ar comporta la fel, dar acum PJ folosește billing GoMag."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verifică în Oracle că adresele există
|
||||||
|
conn = oracle_pool.acquire()
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id_livrare, id_facturare FROM comenzi WHERE comanda_externa = :1",
|
||||||
|
[order.number]
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
oracle_pool.release(conn)
|
||||||
|
|
||||||
|
assert row is not None, f"Comanda {order.number} nu s-a găsit în Oracle comenzi"
|
||||||
|
assert row[0] == id_livr, f"id_livrare Oracle ({row[0]}) ≠ result ({id_livr})"
|
||||||
|
assert row[1] == id_fact, f"id_facturare Oracle ({row[1]}) ≠ result ({id_fact})"
|
||||||
|
|
||||||
|
def test_pf_billing_addr_equals_shipping(self, oracle_pool, real_codmat, app_settings, run_id):
|
||||||
|
"""PF: adresa facturare în Oracle = adresa livrare (ramburs curier)."""
|
||||||
|
from app.services.import_service import import_single_order
|
||||||
|
from app.services.order_reader import OrderItem
|
||||||
|
|
||||||
|
order = _build_pf_order(run_id, real_codmat)
|
||||||
|
order.items = [OrderItem(sku=real_codmat, name="Test PF",
|
||||||
|
price=10.0, quantity=1.0, vat=19.0)]
|
||||||
|
|
||||||
|
id_pol = int(app_settings.get("id_pol") or 0) or None
|
||||||
|
id_sectie = int(app_settings.get("id_sectie") or 0) or None
|
||||||
|
|
||||||
|
result = import_single_order(order, id_pol=id_pol, id_sectie=id_sectie,
|
||||||
|
app_settings=app_settings)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
pytest.skip(f"Import PF eșuat: {result.get('error')}")
|
||||||
|
|
||||||
|
id_fact = result["id_adresa_facturare"]
|
||||||
|
id_livr = result["id_adresa_livrare"]
|
||||||
|
|
||||||
|
assert id_fact is not None, "PF: id_adresa_facturare lipsește din result"
|
||||||
|
assert id_livr is not None, "PF: id_adresa_livrare lipsește din result"
|
||||||
|
|
||||||
|
# PF: id_facturare TREBUIE să fie = id_livrare
|
||||||
|
assert id_fact == id_livr, (
|
||||||
|
f"PF trebuie să aibă id_fact({id_fact}) = id_livr({id_livr}) — "
|
||||||
|
f"ramburs curier pe adresa de livrare"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verifică în Oracle
|
||||||
|
conn = oracle_pool.acquire()
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id_livrare, id_facturare FROM comenzi WHERE comanda_externa = :1",
|
||||||
|
[order.number]
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
oracle_pool.release(conn)
|
||||||
|
|
||||||
|
assert row is not None, f"Comanda {order.number} nu s-a găsit în Oracle comenzi"
|
||||||
|
assert row[1] == row[0], (
|
||||||
|
f"Oracle: id_facturare({row[1]}) ≠ id_livrare({row[0]}) pentru PF"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test regresie: comenzi existente în SQLite
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestAddressRulesRegression:
|
||||||
|
"""Verifică că comenzile existente importate după fix respectă regula PJ/PF."""
|
||||||
|
|
||||||
|
FIX_DATE = "2026-04-08" # data când a fost aplicat fix-ul
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def sqlite_rows(self):
|
||||||
|
"""Comenzi cu adrese populate importate după data fix-ului."""
|
||||||
|
import sqlite3
|
||||||
|
from app.config import settings
|
||||||
|
db_path = os.environ.get("SQLITE_DB_PATH", os.path.join(_script_dir, "orders.db"))
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
pytest.skip(f"SQLite DB lipsă: {db_path}")
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT order_number, cod_fiscal_gomag,
|
||||||
|
id_adresa_facturare, id_adresa_livrare,
|
||||||
|
adresa_facturare_gomag, adresa_livrare_gomag,
|
||||||
|
adresa_facturare_roa, adresa_livrare_roa,
|
||||||
|
first_seen_at
|
||||||
|
FROM orders
|
||||||
|
WHERE id_adresa_facturare IS NOT NULL
|
||||||
|
AND id_adresa_livrare IS NOT NULL
|
||||||
|
AND first_seen_at >= ?
|
||||||
|
""", (self.FIX_DATE,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def test_pf_id_facturare_equals_id_livrare(self, sqlite_rows):
|
||||||
|
"""PF noi: id_adresa_facturare = id_adresa_livrare."""
|
||||||
|
pf_rows = [r for r in sqlite_rows if not r["cod_fiscal_gomag"]]
|
||||||
|
if not pf_rows:
|
||||||
|
pytest.skip(f"Nicio comandă PF importată după {self.FIX_DATE}")
|
||||||
|
|
||||||
|
violations = [
|
||||||
|
f"{r['order_number']}: id_fact={r['id_adresa_facturare']} id_livr={r['id_adresa_livrare']}"
|
||||||
|
for r in pf_rows
|
||||||
|
if r["id_adresa_facturare"] != r["id_adresa_livrare"]
|
||||||
|
]
|
||||||
|
assert not violations, (
|
||||||
|
f"PF comenzi cu id_fact ≠ id_livr ({len(violations)}):\n" + "\n".join(violations[:10])
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pj_billing_roa_matches_gomag_billing(self, sqlite_rows):
|
||||||
|
"""PJ noi: adresa_facturare_roa se potrivește cu GoMag billing address."""
|
||||||
|
from app.services.sync_service import _addr_match
|
||||||
|
|
||||||
|
pj_rows = [
|
||||||
|
r for r in sqlite_rows
|
||||||
|
if r["cod_fiscal_gomag"] and r["adresa_facturare_gomag"] and r["adresa_facturare_roa"]
|
||||||
|
]
|
||||||
|
if not pj_rows:
|
||||||
|
pytest.skip(f"Nicio comandă PJ cu adrese populate importată după {self.FIX_DATE}")
|
||||||
|
|
||||||
|
violations = []
|
||||||
|
for r in pj_rows:
|
||||||
|
if not _addr_match(r["adresa_facturare_gomag"], r["adresa_facturare_roa"]):
|
||||||
|
violations.append(
|
||||||
|
f"{r['order_number']}: billing_gomag={r['adresa_facturare_gomag']!r} "
|
||||||
|
f"fact_roa={r['adresa_facturare_roa']!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not violations, (
|
||||||
|
f"PJ comenzi cu adresa_facturare_roa care nu corespunde GoMag billing ({len(violations)}):\n"
|
||||||
|
+ "\n".join(violations[:10])
|
||||||
|
)
|
||||||
@@ -809,6 +809,126 @@ class TestAddrMatch:
|
|||||||
addr_livr_id = 123
|
addr_livr_id = 123
|
||||||
assert not (addr_livr_id and billing_addr == shipping_addr)
|
assert not (addr_livr_id and billing_addr == shipping_addr)
|
||||||
|
|
||||||
|
def test_pf_billing_address_equals_shipping(self):
|
||||||
|
"""PF (individual): is_pj=0 → billing address = shipping (ramburs curier)."""
|
||||||
|
from app.services.import_service import determine_partner_data
|
||||||
|
from app.services.order_reader import OrderBilling, OrderShipping, OrderData
|
||||||
|
billing = OrderBilling(
|
||||||
|
firstname="Ion", lastname="Popescu", phone="0700000000", email="ion@test.com",
|
||||||
|
address="Str Victoriei 10", city="Cluj", region="Cluj", country="RO",
|
||||||
|
company_name="", company_code="", company_reg="", is_company=False
|
||||||
|
)
|
||||||
|
shipping = OrderShipping(
|
||||||
|
firstname="Ion", lastname="Popescu", phone="0700000000", email="ion@test.com",
|
||||||
|
address="Str Victoriei 10", city="Cluj", region="Cluj", country="RO"
|
||||||
|
)
|
||||||
|
order = OrderData(id="PF001", number="PF001", date="2024-01-01T10:00:00",
|
||||||
|
billing=billing, shipping=shipping)
|
||||||
|
pdata = determine_partner_data(order)
|
||||||
|
assert pdata["is_pj"] == 0, "PF order must have is_pj=0"
|
||||||
|
|
||||||
|
def test_pj_uses_billing_from_gomag(self):
|
||||||
|
"""PJ (company): is_pj=1 → billing address from GoMag billing."""
|
||||||
|
from app.services.import_service import determine_partner_data
|
||||||
|
from app.services.order_reader import OrderBilling, OrderShipping, OrderData
|
||||||
|
billing = OrderBilling(
|
||||||
|
firstname="Ion", lastname="Popescu", phone="0700000000", email="ion@test.com",
|
||||||
|
address="Bld Unirii 5", city="Bucuresti", region="Bucuresti", country="RO",
|
||||||
|
company_name="FIRMA SRL", company_code="RO12345678", company_reg="J40/1234/2020",
|
||||||
|
is_company=True
|
||||||
|
)
|
||||||
|
shipping = OrderShipping(
|
||||||
|
firstname="Mihai", lastname="Ionescu", phone="0711111111", email="mihai@test.com",
|
||||||
|
address="Str Libertatii 20", city="Ploiesti", region="Prahova", country="RO"
|
||||||
|
)
|
||||||
|
order = OrderData(id="PJ001", number="PJ001", date="2024-01-01T10:00:00",
|
||||||
|
billing=billing, shipping=shipping)
|
||||||
|
pdata = determine_partner_data(order)
|
||||||
|
assert pdata["is_pj"] == 1, "PJ order must have is_pj=1"
|
||||||
|
assert pdata["denumire"] == "FIRMA SRL"
|
||||||
|
assert pdata["cod_fiscal"] == "RO12345678"
|
||||||
|
|
||||||
|
def test_pj_different_person_still_uses_billing(self):
|
||||||
|
"""Regression: PJ with different billing/shipping persons → still is_pj=1 (billing addr used)."""
|
||||||
|
from app.services.import_service import determine_partner_data
|
||||||
|
from app.services.order_reader import OrderBilling, OrderShipping, OrderData
|
||||||
|
billing = OrderBilling(
|
||||||
|
firstname="Secretara", lastname="Firma", phone="0700000000", email="office@firma.ro",
|
||||||
|
address="Calea Victoriei 1", city="Bucuresti", region="Bucuresti", country="RO",
|
||||||
|
company_name="FIRMA SA", company_code="RO99999999", company_reg="J40/9999/2019",
|
||||||
|
is_company=True
|
||||||
|
)
|
||||||
|
shipping = OrderShipping(
|
||||||
|
firstname="Curier", lastname="Destinatar", phone="0799999999", email="d@test.com",
|
||||||
|
address="Str Livrare 5", city="Iasi", region="Iasi", country="RO"
|
||||||
|
)
|
||||||
|
order = OrderData(id="PJ002", number="PJ002", date="2024-01-01T10:00:00",
|
||||||
|
billing=billing, shipping=shipping)
|
||||||
|
pdata = determine_partner_data(order)
|
||||||
|
assert pdata["is_pj"] == 1, "PJ with different persons must still be is_pj=1"
|
||||||
|
|
||||||
|
def test_pf_different_billing_still_uses_shipping(self):
|
||||||
|
"""Regression: PF with different billing address → still is_pj=0 (shipping addr used for billing)."""
|
||||||
|
from app.services.import_service import determine_partner_data
|
||||||
|
from app.services.order_reader import OrderBilling, OrderShipping, OrderData
|
||||||
|
billing = OrderBilling(
|
||||||
|
firstname="Ana", lastname="Gheorghe", phone="0700000000", email="ana@test.com",
|
||||||
|
address="Str Alta 99", city="Timisoara", region="Timis", country="RO",
|
||||||
|
company_name="", company_code="", company_reg="", is_company=False
|
||||||
|
)
|
||||||
|
shipping = OrderShipping(
|
||||||
|
firstname="Ana", lastname="Gheorghe", phone="0700000000", email="ana@test.com",
|
||||||
|
address="Str Livrare 7", city="Cluj", region="Cluj", country="RO"
|
||||||
|
)
|
||||||
|
order = OrderData(id="PF002", number="PF002", date="2024-01-01T10:00:00",
|
||||||
|
billing=billing, shipping=shipping)
|
||||||
|
pdata = determine_partner_data(order)
|
||||||
|
assert pdata["is_pj"] == 0, "PF must remain is_pj=0 regardless of billing address"
|
||||||
|
|
||||||
|
def test_is_company_cui_fallback(self):
|
||||||
|
"""Company with no name but CUI populated → is_company=True (order_reader parsing)."""
|
||||||
|
from app.services.order_reader import _parse_order
|
||||||
|
order_data = {
|
||||||
|
"number": "CUI001",
|
||||||
|
"date": "2024-01-01T10:00:00",
|
||||||
|
"statusId": 1,
|
||||||
|
"status": "new",
|
||||||
|
"billing": {
|
||||||
|
"firstname": "Ion",
|
||||||
|
"lastname": "Popescu",
|
||||||
|
"phone": "0700000000",
|
||||||
|
"email": "ion@test.com",
|
||||||
|
"address": "Str Test 1",
|
||||||
|
"city": "Bucuresti",
|
||||||
|
"region": "Bucuresti",
|
||||||
|
"country": "RO",
|
||||||
|
"company": {"name": "", "code": "RO12345678", "registrationNo": ""}
|
||||||
|
},
|
||||||
|
"items": [],
|
||||||
|
"total": 100.0,
|
||||||
|
"discountTotal": 0.0,
|
||||||
|
"shippingTotal": 0.0
|
||||||
|
}
|
||||||
|
order = _parse_order("CUI001", order_data, "test.json")
|
||||||
|
assert order.billing.is_company is True, "CUI-only company must be detected as is_company"
|
||||||
|
assert order.billing.company_code == "RO12345678"
|
||||||
|
|
||||||
|
def test_pj_denomination_fallback_empty_company_name(self):
|
||||||
|
"""PJ with CUI but no company_name → denumire falls back to billing person name."""
|
||||||
|
from app.services.import_service import determine_partner_data
|
||||||
|
from app.services.order_reader import OrderBilling, OrderData
|
||||||
|
billing = OrderBilling(
|
||||||
|
firstname="Ion", lastname="Popescu", phone="0700000000", email="ion@test.com",
|
||||||
|
address="Str Test 1", city="Bucuresti", region="Bucuresti", country="RO",
|
||||||
|
company_name="", company_code="RO12345678", company_reg="", is_company=True
|
||||||
|
)
|
||||||
|
order = OrderData(id="CUI002", number="CUI002", date="2024-01-01T10:00:00",
|
||||||
|
billing=billing, shipping=None)
|
||||||
|
pdata = determine_partner_data(order)
|
||||||
|
assert pdata["is_pj"] == 1
|
||||||
|
assert pdata["denumire"] == "POPESCU ION", "Fallback denumire must use billing person name"
|
||||||
|
assert pdata["cod_fiscal"] == "RO12345678"
|
||||||
|
|
||||||
|
|
||||||
class TestFormatAddressForOracle:
|
class TestFormatAddressForOracle:
|
||||||
"""Tests for format_address_for_oracle city stripping."""
|
"""Tests for format_address_for_oracle city stripping."""
|
||||||
|
|||||||
170
scripts/verify_address_rules.py
Normal file
170
scripts/verify_address_rules.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Verifică regula adrese PJ/PF pe comenzile importate din SQLite.
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
PF (cod_fiscal_gomag IS NULL): id_adresa_facturare = id_adresa_livrare
|
||||||
|
PJ (cod_fiscal_gomag IS NOT NULL): adresa_facturare_roa se potriveste cu GoMag billing
|
||||||
|
(nu cu GoMag shipping)
|
||||||
|
|
||||||
|
Rulare:
|
||||||
|
python3 scripts/verify_address_rules.py
|
||||||
|
python3 scripts/verify_address_rules.py --days 7 # ultimele 7 zile
|
||||||
|
python3 scripts/verify_address_rules.py --all # toate comenzile
|
||||||
|
python3 scripts/verify_address_rules.py --status IMPORTED
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add api/ to path for app imports
|
||||||
|
_repo_root = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(_repo_root / "api"))
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(_repo_root / "api" / ".env")
|
||||||
|
|
||||||
|
from app.services.sync_service import _addr_match
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Verifică regula adrese PJ/PF în SQLite")
|
||||||
|
parser.add_argument("--days", type=int, default=30,
|
||||||
|
help="Număr de zile în urmă (default: 30)")
|
||||||
|
parser.add_argument("--all", action="store_true",
|
||||||
|
help="Toate comenzile, indiferent de dată")
|
||||||
|
parser.add_argument("--status", default=None,
|
||||||
|
help="Filtrează după status (ex: IMPORTED)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
_raw_path = os.environ.get("SQLITE_DB_PATH", "data/import.db")
|
||||||
|
db_path = _raw_path if os.path.isabs(_raw_path) else str(_repo_root / "api" / _raw_path)
|
||||||
|
if not Path(db_path).exists():
|
||||||
|
print(f"EROARE: SQLite DB nu există: {db_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
# Build query
|
||||||
|
where_clauses = ["id_adresa_facturare IS NOT NULL", "id_adresa_livrare IS NOT NULL"]
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if not args.all:
|
||||||
|
where_clauses.append("first_seen_at >= datetime('now', ?)")
|
||||||
|
params.append(f"-{args.days} days")
|
||||||
|
|
||||||
|
if args.status:
|
||||||
|
where_clauses.append("status = ?")
|
||||||
|
params.append(args.status)
|
||||||
|
|
||||||
|
where_sql = " AND ".join(where_clauses)
|
||||||
|
rows = conn.execute(f"""
|
||||||
|
SELECT order_number, status, cod_fiscal_gomag,
|
||||||
|
id_adresa_facturare, id_adresa_livrare,
|
||||||
|
adresa_facturare_gomag, adresa_livrare_gomag,
|
||||||
|
adresa_facturare_roa, adresa_livrare_roa,
|
||||||
|
first_seen_at
|
||||||
|
FROM orders
|
||||||
|
WHERE {where_sql}
|
||||||
|
ORDER BY first_seen_at DESC
|
||||||
|
""", params).fetchall()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
scope = "toate comenzile" if args.all else f"ultimele {args.days} zile"
|
||||||
|
print(f"Nicio comandă cu adrese populate ({scope}).")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
pf_ok = pf_err = pj_ok = pj_err = pj_skip = 0
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
is_pj = bool(r["cod_fiscal_gomag"])
|
||||||
|
id_fact = r["id_adresa_facturare"]
|
||||||
|
id_livr = r["id_adresa_livrare"]
|
||||||
|
order = r["order_number"]
|
||||||
|
date = (r["first_seen_at"] or "")[:10]
|
||||||
|
|
||||||
|
if not is_pj:
|
||||||
|
# PF: id_facturare trebuie = id_livrare
|
||||||
|
if id_fact == id_livr:
|
||||||
|
pf_ok += 1
|
||||||
|
else:
|
||||||
|
pf_err += 1
|
||||||
|
violations.append({
|
||||||
|
"order": order, "date": date, "type": "PF",
|
||||||
|
"issue": f"id_fact={id_fact} != id_livr={id_livr}",
|
||||||
|
"detail": None,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# PJ: adresa_facturare_roa trebuie sa se potriveasca cu GoMag billing
|
||||||
|
fact_roa = r["adresa_facturare_roa"]
|
||||||
|
fact_gomag = r["adresa_facturare_gomag"]
|
||||||
|
livr_gomag = r["adresa_livrare_gomag"]
|
||||||
|
|
||||||
|
if not fact_roa or not fact_gomag:
|
||||||
|
pj_skip += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check 1: billing ROA matches GoMag billing
|
||||||
|
billing_match = _addr_match(fact_gomag, fact_roa)
|
||||||
|
# Check 2: billing ROA does NOT match GoMag shipping (wrong old behavior)
|
||||||
|
shipping_match = _addr_match(livr_gomag, fact_roa) if livr_gomag else False
|
||||||
|
|
||||||
|
if billing_match:
|
||||||
|
pj_ok += 1
|
||||||
|
else:
|
||||||
|
pj_err += 1
|
||||||
|
detail = "billing_ROA matches shipping GoMag" if shipping_match else "billing_ROA mismatch"
|
||||||
|
violations.append({
|
||||||
|
"order": order, "date": date, "type": "PJ",
|
||||||
|
"issue": detail,
|
||||||
|
"detail": f"billing_gomag={_short(fact_gomag)} | fact_roa={fact_roa}",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Output
|
||||||
|
total = len(rows)
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
scope = "toate" if args.all else f"ultimele {args.days} zile"
|
||||||
|
print(f" Verificare adrese PJ/PF ({scope}, {total} comenzi cu adrese)")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f" PF (fara CUI): {pf_ok:4d} OK | {pf_err:4d} ERORI")
|
||||||
|
print(f" PJ (cu CUI): {pj_ok:4d} OK | {pj_err:4d} ERORI | {pj_skip:4d} skip (date lipsa)")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
if not violations:
|
||||||
|
print(" ✓ Toate comenzile respecta regula PJ/PF.\n")
|
||||||
|
else:
|
||||||
|
print(f"\n VIOLARI ({len(violations)}):\n")
|
||||||
|
for v in violations[:20]:
|
||||||
|
print(f" [{v['date']}] {v['order']:25s} {v['type']} {v['issue']}")
|
||||||
|
if v["detail"]:
|
||||||
|
print(f" {v['detail']}")
|
||||||
|
if len(violations) > 20:
|
||||||
|
print(f" ... si inca {len(violations)-20} violari.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
sys.exit(1 if violations else 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _short(json_str):
|
||||||
|
"""Returnează un rezumat scurt al unui JSON de adresă."""
|
||||||
|
if not json_str:
|
||||||
|
return "(null)"
|
||||||
|
try:
|
||||||
|
d = json.loads(json_str)
|
||||||
|
return f"{d.get('address','?')}, {d.get('city','?')}"
|
||||||
|
except Exception:
|
||||||
|
return json_str[:40]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user