- Add SSE event bus in sync_service (subscribe/unsubscribe/_emit) - Add GET /api/sync/stream SSE endpoint for real-time sync progress - Rewrite logs.html: unified runs table + live feed + summary + filters - Rewrite logs.js: SSE EventSource client, run selection, pagination - Dashboard: clickable runs navigate to /logs?run=, sync started banner - Remove "Import Comenzi" nav item, delete sync_detail.html - Add error_message column to sync_runs table with migration - Fix: export TNS_ADMIN as OS env var so oracledb finds tnsnames.ora - Fix: use get_oracle_connection() instead of direct pool.acquire() - Fix: CRM_POLITICI_PRET_ART INSERT to match actual table schema Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
195 lines
6.9 KiB
Python
195 lines
6.9 KiB
Python
import html
|
|
import json
|
|
import logging
|
|
import oracledb
|
|
from datetime import datetime, timedelta
|
|
from .. import database
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Diacritics to ASCII mapping (Romanian)
|
|
_DIACRITICS = str.maketrans({
|
|
'\u0103': 'a', # ă
|
|
'\u00e2': 'a', # â
|
|
'\u00ee': 'i', # î
|
|
'\u0219': 's', # ș
|
|
'\u021b': 't', # ț
|
|
'\u0102': 'A', # Ă
|
|
'\u00c2': 'A', # Â
|
|
'\u00ce': 'I', # Î
|
|
'\u0218': 'S', # Ș
|
|
'\u021a': 'T', # Ț
|
|
# Older Unicode variants
|
|
'\u015f': 's', # ş (cedilla)
|
|
'\u0163': 't', # ţ (cedilla)
|
|
'\u015e': 'S', # Ş
|
|
'\u0162': 'T', # Ţ
|
|
})
|
|
|
|
|
|
def clean_web_text(text: str) -> str:
|
|
"""Port of VFP CleanWebText: unescape HTML entities + diacritics to ASCII."""
|
|
if not text:
|
|
return ""
|
|
result = html.unescape(text)
|
|
result = result.translate(_DIACRITICS)
|
|
# Remove any remaining <br> tags
|
|
for br in ('<br>', '<br/>', '<br />'):
|
|
result = result.replace(br, ' ')
|
|
return result.strip()
|
|
|
|
|
|
def convert_web_date(date_str: str) -> datetime:
|
|
"""Port of VFP ConvertWebDate: parse web date to datetime."""
|
|
if not date_str:
|
|
return datetime.now()
|
|
try:
|
|
return datetime.strptime(date_str[:10], '%Y-%m-%d')
|
|
except ValueError:
|
|
return datetime.now()
|
|
|
|
|
|
def format_address_for_oracle(address: str, city: str, region: str) -> str:
|
|
"""Port of VFP FormatAddressForOracle."""
|
|
region_clean = clean_web_text(region)
|
|
city_clean = clean_web_text(city)
|
|
address_clean = clean_web_text(address)
|
|
return f"JUD:{region_clean};{city_clean};{address_clean}"
|
|
|
|
|
|
def build_articles_json(items) -> str:
|
|
"""Build JSON string for Oracle PACK_IMPORT_COMENZI.importa_comanda."""
|
|
articles = []
|
|
for item in items:
|
|
articles.append({
|
|
"sku": item.sku,
|
|
"quantity": str(item.quantity),
|
|
"price": str(item.price),
|
|
"vat": str(item.vat),
|
|
"name": clean_web_text(item.name)
|
|
})
|
|
return json.dumps(articles)
|
|
|
|
|
|
def import_single_order(order, id_pol: int = None, id_sectie: int = None) -> dict:
|
|
"""Import a single order into Oracle ROA.
|
|
|
|
Returns dict with:
|
|
success: bool
|
|
id_comanda: int or None
|
|
id_partener: int or None
|
|
error: str or None
|
|
"""
|
|
result = {
|
|
"success": False,
|
|
"id_comanda": None,
|
|
"id_partener": None,
|
|
"error": None
|
|
}
|
|
|
|
try:
|
|
order_number = clean_web_text(order.number)
|
|
order_date = convert_web_date(order.date)
|
|
|
|
if database.pool is None:
|
|
raise RuntimeError("Oracle pool not initialized")
|
|
with database.pool.acquire() as conn:
|
|
with conn.cursor() as cur:
|
|
# Step 1: Process partner
|
|
id_partener = cur.var(oracledb.DB_TYPE_NUMBER)
|
|
|
|
if order.billing.is_company:
|
|
denumire = clean_web_text(order.billing.company_name)
|
|
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:
|
|
denumire = clean_web_text(
|
|
f"{order.billing.firstname} {order.billing.lastname}"
|
|
)
|
|
cod_fiscal = None
|
|
registru = None
|
|
is_pj = 0
|
|
|
|
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener", [
|
|
cod_fiscal, denumire, registru, is_pj, id_partener
|
|
])
|
|
|
|
partner_id = id_partener.getvalue()
|
|
if not partner_id or partner_id <= 0:
|
|
result["error"] = f"Partner creation failed for {denumire}"
|
|
return result
|
|
|
|
result["id_partener"] = int(partner_id)
|
|
|
|
# Step 2: Process billing address
|
|
id_adresa_fact = cur.var(oracledb.DB_TYPE_NUMBER)
|
|
billing_addr = format_address_for_oracle(
|
|
order.billing.address, order.billing.city, order.billing.region
|
|
)
|
|
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_adresa", [
|
|
partner_id, billing_addr,
|
|
order.billing.phone or "",
|
|
order.billing.email or "",
|
|
id_adresa_fact
|
|
])
|
|
addr_fact_id = id_adresa_fact.getvalue()
|
|
|
|
# Step 3: Process shipping address (if different)
|
|
addr_livr_id = None
|
|
if order.shipping:
|
|
id_adresa_livr = cur.var(oracledb.DB_TYPE_NUMBER)
|
|
shipping_addr = format_address_for_oracle(
|
|
order.shipping.address, order.shipping.city,
|
|
order.shipping.region
|
|
)
|
|
cur.callproc("PACK_IMPORT_PARTENERI.cauta_sau_creeaza_adresa", [
|
|
partner_id, shipping_addr,
|
|
order.shipping.phone or "",
|
|
order.shipping.email or "",
|
|
id_adresa_livr
|
|
])
|
|
addr_livr_id = id_adresa_livr.getvalue()
|
|
|
|
# Step 4: Build articles JSON and import order
|
|
articles_json = build_articles_json(order.items)
|
|
|
|
# Use CLOB for the JSON
|
|
clob_var = cur.var(oracledb.DB_TYPE_CLOB)
|
|
clob_var.setvalue(0, articles_json)
|
|
|
|
id_comanda = cur.var(oracledb.DB_TYPE_NUMBER)
|
|
|
|
cur.callproc("PACK_IMPORT_COMENZI.importa_comanda", [
|
|
order_number, # p_nr_comanda_ext
|
|
order_date, # p_data_comanda
|
|
partner_id, # p_id_partener
|
|
clob_var, # p_json_articole (CLOB)
|
|
addr_livr_id, # p_id_adresa_livrare
|
|
addr_fact_id, # p_id_adresa_facturare
|
|
id_pol, # p_id_pol
|
|
id_sectie, # p_id_sectie
|
|
id_comanda # v_id_comanda (OUT)
|
|
])
|
|
|
|
comanda_id = id_comanda.getvalue()
|
|
|
|
if comanda_id and comanda_id > 0:
|
|
conn.commit()
|
|
result["success"] = True
|
|
result["id_comanda"] = int(comanda_id)
|
|
logger.info(f"Order {order_number} imported: ID={comanda_id}")
|
|
else:
|
|
conn.rollback()
|
|
result["error"] = "importa_comanda returned invalid ID"
|
|
|
|
except oracledb.DatabaseError as e:
|
|
error_msg = str(e)
|
|
result["error"] = error_msg
|
|
logger.error(f"Oracle error importing order {order.number}: {error_msg}")
|
|
except Exception as e:
|
|
result["error"] = str(e)
|
|
logger.error(f"Error importing order {order.number}: {e}")
|
|
|
|
return result
|