diff --git a/.gitignore b/.gitignore index 42e70c8..dcb150e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ output/ vfp/*.json *.~pck .claude/HANDOFF.md +scripts/work/ # Virtual environments venv/ diff --git a/scripts/analyze_billing.py b/scripts/analyze_billing.py deleted file mode 100644 index 3e046c1..0000000 --- a/scripts/analyze_billing.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Analyze billing vs shipping patterns across all GoMag orders""" -import sys, json, httpx -sys.stdout.reconfigure(encoding='utf-8', errors='replace') - -API_KEY = '4c5e46df8f6c4f054fe2787de7a13d4a' -API_SHOP = 'https://coffeepoint.ro' -API_URL = 'https://api.gomag.ro/api/v1/order/read/json' - -headers = { - 'Apikey': API_KEY, - 'ApiShop': API_SHOP, - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/json', -} - -params = {'startDate': '2026-03-08', 'page': 1, 'limit': 250} - -all_orders = [] -for page in range(1, 20): - params['page'] = page - resp = httpx.get(API_URL, headers=headers, params=params, timeout=60) - data = resp.json() - pages = data.get('pages', 1) - orders_raw = data.get('orders', {}) - if isinstance(orders_raw, dict): - for key, order in orders_raw.items(): - if isinstance(order, dict): - all_orders.append(order) - print(f"Page {page}/{pages}: {len(orders_raw)} orders") - if page >= pages: - break - -print(f"\nTotal orders: {len(all_orders)}") - -# Analyze patterns -company_orders = [] -person_orders = [] -diff_person_orders = [] # shipping != billing person - -for order in all_orders: - billing = order.get('billing', {}) or {} - shipping = order.get('shipping', {}) or {} - company = billing.get('company', {}) - is_company = isinstance(company, dict) and bool(company.get('name')) - - b_first = billing.get('firstname', '') or '' - b_last = billing.get('lastname', '') or '' - s_first = shipping.get('firstname', '') or '' - s_last = shipping.get('lastname', '') or '' - - billing_person = f"{b_first} {b_last}".strip() - shipping_person = f"{s_first} {s_last}".strip() - - entry = { - 'number': order.get('number', ''), - 'total': order.get('total', ''), - 'billing_person': billing_person, - 'shipping_person': shipping_person, - 'company_name': company.get('name', '') if is_company else '', - 'company_code': company.get('code', '') if is_company else '', - 'is_company': is_company, - 'same_person': billing_person.upper() == shipping_person.upper(), - } - - if is_company: - company_orders.append(entry) - else: - person_orders.append(entry) - if not entry['same_person'] and shipping_person and billing_person: - diff_person_orders.append(entry) - -print(f"\n{'='*80}") -print(f"COMPANY orders (billing has company): {len(company_orders)}") -print(f"PERSON orders (no company): {len(person_orders)}") -print(f" - same billing/shipping person: {len(person_orders) - len(diff_person_orders)}") -print(f" - DIFFERENT billing/shipping person: {len(diff_person_orders)}") - -# Show company examples -print(f"\n{'='*80}") -print(f"COMPANY ORDERS — first 20 examples") -print(f"{'ORDER':>12s} {'COMPANY':35s} {'CUI':20s} {'BILLING_PERSON':25s} {'SHIPPING_PERSON':25s} {'SAME':5s}") -for e in company_orders[:20]: - same = 'DA' if e['same_person'] else 'NU' - print(f"{e['number']:>12s} {e['company_name'][:35]:35s} {e['company_code'][:20]:20s} {e['billing_person'][:25]:25s} {e['shipping_person'][:25]:25s} {same:5s}") - -# Show different person examples -print(f"\n{'='*80}") -print(f"DIFFERENT PERSON (no company, billing != shipping) — ALL {len(diff_person_orders)} examples") -print(f"{'ORDER':>12s} {'BILLING_PERSON':30s} {'SHIPPING_PERSON':30s}") -for e in diff_person_orders: - print(f"{e['number']:>12s} {e['billing_person'][:30]:30s} {e['shipping_person'][:30]:30s}") - -# Show person orders with same name -print(f"\n{'='*80}") -print(f"PERSON ORDERS (same billing/shipping) — first 10 examples") -print(f"{'ORDER':>12s} {'BILLING_PERSON':30s} {'SHIPPING_PERSON':30s} {'TOTAL':>10s}") -same_person = [e for e in person_orders if e['same_person']] -for e in same_person[:10]: - print(f"{e['number']:>12s} {e['billing_person'][:30]:30s} {e['shipping_person'][:30]:30s} {e['total']:>10s}") - -# Now cross-reference with Oracle to verify import logic -# For company orders, check what partner name was created in ROA -import oracledb, os, sqlite3 -os.environ['PATH'] = r'C:\app\Server\product\18.0.0\dbhomeXE\bin' + ';' + os.environ.get('PATH','') -oracledb.init_oracle_client() - -db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') -db.row_factory = sqlite3.Row -c = db.cursor() - -conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') -cur = conn.cursor() - -print(f"\n{'='*80}") -print(f"VERIFICATION: Company orders — GoMag vs SQLite vs Oracle ROA") -print(f"{'ORDER':>12s} | {'GOMAG_COMPANY':30s} | {'SQLITE_CUSTOMER':30s} | {'ROA_PARTNER':30s} | MATCH?") - -# Build lookup from GoMag orders -gomag_lookup = {} -for order in all_orders: - num = order.get('number', '') - gomag_lookup[num] = order - -# Get all imported orders from SQLite with id_partener -c.execute(""" - SELECT order_number, customer_name, id_partener, billing_name, shipping_name - FROM orders WHERE id_partener IS NOT NULL -""") -for row in c.fetchall(): - on = row['order_number'] - gomag = gomag_lookup.get(on) - if not gomag: - continue - - billing = gomag.get('billing', {}) or {} - company = billing.get('company', {}) - is_company = isinstance(company, dict) and bool(company.get('name')) - company_name = company.get('name', '') if is_company else '' - - # Get ROA partner name - roa_partner = '' - if row['id_partener']: - cur.execute("SELECT denumire, prenume FROM nom_parteneri WHERE id_part = :1", [row['id_partener']]) - r = cur.fetchone() - if r: - roa_partner = ((r[0] or '') + ' ' + (r[1] or '')).strip() - - gomag_label = company_name if is_company else f"{billing.get('firstname','')} {billing.get('lastname','')}" - match = 'OK' if company_name and company_name.upper()[:15] in roa_partner.upper() else ('OK' if not company_name else 'DIFF') - - print(f"{on:>12s} | {gomag_label[:30]:30s} | {(row['customer_name'] or '')[:30]:30s} | {roa_partner[:30]:30s} | {match}") - -# Also show some SKIPPED company orders to see what customer_name we stored -print(f"\n{'='*80}") -print(f"SKIPPED company orders — GoMag company vs SQLite customer_name") -print(f"{'ORDER':>12s} | {'GOMAG_COMPANY':30s} | {'SQLITE_CUSTOMER':30s} | {'BILLING_PERSON':25s} | {'SHIPPING_PERSON':25s}") - -c.execute("SELECT order_number, customer_name, billing_name, shipping_name FROM orders WHERE status = 'SKIPPED' LIMIT 200") -for row in c.fetchall(): - on = row['order_number'] - gomag = gomag_lookup.get(on) - if not gomag: - continue - - billing = gomag.get('billing', {}) or {} - company = billing.get('company', {}) - is_company = isinstance(company, dict) and bool(company.get('name')) - if not is_company: - continue - - company_name = company.get('name', '') - b_person = f"{billing.get('firstname','')} {billing.get('lastname','')}".strip() - shipping = gomag.get('shipping', {}) or {} - s_person = f"{shipping.get('firstname','')} {shipping.get('lastname','')}".strip() - - print(f"{on:>12s} | {company_name[:30]:30s} | {(row['customer_name'] or '')[:30]:30s} | {b_person[:25]:25s} | {s_person[:25]:25s}") - -db.close() -conn.close() diff --git a/scripts/check_deploy.py b/scripts/check_deploy.py deleted file mode 100644 index 678f88c..0000000 --- a/scripts/check_deploy.py +++ /dev/null @@ -1,31 +0,0 @@ -import sqlite3, sys, importlib -sys.stdout.reconfigure(encoding='utf-8', errors='replace') - -# Check SQLite current state -db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') -c = db.cursor() -c.execute("SELECT order_number, customer_name, shipping_name, billing_name FROM orders WHERE order_number='480102897'") -r = c.fetchone() -print(f"SQLite: customer={r[1]}, shipping={r[2]}, billing={r[3]}") -db.close() - -# Check deployed code version -sys.path.insert(0, r'C:\gomag-vending\api') -from app.services.sync_service import _derive_customer_info -from app.services.order_reader import OrderData, OrderBilling, OrderShipping - -# Simulate the order -billing = OrderBilling(firstname='Liviu', lastname='Stoica', is_company=True, company_name='SLM COMERCE SRL') -shipping = OrderShipping(firstname='Liviu', lastname='Stoica') -order = OrderData(id='1', number='480102897', date='2026-03-09', billing=billing, shipping=shipping) -s, b, customer, _, _ = _derive_customer_info(order) -print(f"Code: _derive_customer_info returns customer={customer!r}") - -# Check if the sqlite_service has the fix -import inspect -from app.services.sqlite_service import upsert_order -source = inspect.getsource(upsert_order) -if 'customer_name = excluded.customer_name' in source: - print("sqlite_service: upsert has customer_name update ✓") -else: - print("sqlite_service: upsert MISSING customer_name update ✗") diff --git a/scripts/check_imported.py b/scripts/check_imported.py deleted file mode 100644 index 84e245a..0000000 --- a/scripts/check_imported.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Check imported orders in SQLite and Oracle — what needs to be deleted""" -import sys, sqlite3, oracledb, os -sys.stdout.reconfigure(encoding='utf-8', errors='replace') - -os.environ['PATH'] = r'C:\app\Server\product\18.0.0\dbhomeXE\bin' + ';' + os.environ.get('PATH','') -oracledb.init_oracle_client() - -db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') -db.row_factory = sqlite3.Row -c = db.cursor() - -# Get imported orders with id_comanda -c.execute(""" - SELECT order_number, customer_name, id_comanda, id_partener, order_date, order_total - FROM orders - WHERE status = 'IMPORTED' AND id_comanda IS NOT NULL - ORDER BY order_date -""") -imported = [dict(r) for r in c.fetchall()] -db.close() - -print(f"Imported orders in SQLite: {len(imported)}") - -# Check Oracle status -conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') -cur = conn.cursor() - -print(f"\n{'ORDER_NR':>12s} {'ID_CMD':>8s} {'SQLITE_CLIENT':30s} {'ROA_PARTNER':30s} {'ROA_STERS':>9s} {'FACTURAT':>8s} {'DATA':>12s}") -for o in imported: - id_cmd = o['id_comanda'] - - # Check COMENZI - cur.execute(""" - SELECT c.sters, p.denumire, p.prenume, - (SELECT COUNT(*) FROM vanzari v WHERE v.id_comanda = c.id_comanda AND v.sters = 0) as nr_facturi - FROM comenzi c - LEFT JOIN nom_parteneri p ON c.id_part = p.id_part - WHERE c.id_comanda = :1 - """, [id_cmd]) - row = cur.fetchone() - if row: - sters = row[0] - partner = ((row[1] or '') + ' ' + (row[2] or '')).strip() - nr_fact = row[3] - print(f"{o['order_number']:>12s} {id_cmd:>8d} {(o['customer_name'] or '')[:30]:30s} {partner[:30]:30s} {'DA' if sters else 'NU':>9s} {nr_fact:>8d} {str(o['order_date'])[:10]:>12s}") - else: - print(f"{o['order_number']:>12s} {id_cmd:>8d} {(o['customer_name'] or '')[:30]:30s} {'NOT FOUND':30s}") - -# Check if there's a delete/sters mechanism -print(f"\n--- COMENZI table columns for delete ---") -cur.execute("SELECT column_name FROM all_tab_columns WHERE table_name='COMENZI' AND owner='VENDING' AND column_name IN ('STERS','ID_UTILS','DATAORAS') ORDER BY column_id") -for r in cur: - print(f" {r[0]}") - -# Check comenzi_detalii -cur.execute("SELECT column_name FROM all_tab_columns WHERE table_name='COMENZI_DETALII' AND owner='VENDING' ORDER BY column_id") -print(f"\n--- COMENZI_DETALII columns ---") -for r in cur: - print(f" {r[0]}") - -conn.close() diff --git a/scripts/compare_order.py b/scripts/compare_order.py deleted file mode 100644 index ceb449e..0000000 --- a/scripts/compare_order.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Compare specific GoMag order vs Oracle invoice""" -import sys, sqlite3, oracledb, os -sys.stdout.reconfigure(encoding='utf-8', errors='replace') - -os.environ['PATH'] = r'C:\app\Server\product\18.0.0\dbhomeXE\bin' + ';' + os.environ.get('PATH','') -oracledb.init_oracle_client() - -ORDER = sys.argv[1] if len(sys.argv) > 1 else '480104185' -FACT_NR = sys.argv[2] if len(sys.argv) > 2 else '4105' - -# GoMag order from SQLite -db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') -db.row_factory = sqlite3.Row -c = db.cursor() - -c.execute("SELECT * FROM orders WHERE order_number = ?", (ORDER,)) -order = dict(c.fetchone()) -c.execute("SELECT * FROM order_items WHERE order_number = ? ORDER BY sku", (ORDER,)) -items = [dict(r) for r in c.fetchall()] -db.close() - -print(f"=== GoMag Order {ORDER} ===") -print(f" Client: {order['customer_name']}") -print(f" Shipping: {order['shipping_name']}") -print(f" Billing: {order['billing_name']}") -print(f" Date: {order['order_date']}") -print(f" Total: {order['order_total']}") -print(f" Status: {order['status']}") -print(f" Items ({len(items)}):") -go_total = 0 -for it in items: - line = it['quantity'] * it['price'] - go_total += line - print(f" SKU={it['sku']:25s} qty={it['quantity']:6.1f} x {it['price']:8.2f} = {line:8.2f} {it['product_name']}") -print(f" Sum lines: {go_total:.2f}") - -# Oracle invoice -conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') -cur = conn.cursor() - -cur.execute(""" - SELECT v.id_vanzare, TO_CHAR(v.data_act, 'YYYY-MM-DD'), v.total_fara_tva, v.total_cu_tva, - p.denumire, p.prenume - FROM vanzari v - LEFT JOIN nom_parteneri p ON v.id_part = p.id_part - WHERE v.numar_act = :1 AND v.serie_act = 'VM' AND v.sters = 0 - AND v.data_act >= TO_DATE('2026-03-01','YYYY-MM-DD') -""", [int(FACT_NR)]) -rows = cur.fetchall() - -for row in rows: - id_vanz = row[0] - print(f"\n=== Oracle Invoice VM{FACT_NR} (id_vanzare={id_vanz}) ===") - print(f" Client: {row[4]} {row[5] or ''}") - print(f" Date: {row[1]}") - print(f" Total fara TVA: {float(row[2]):.2f}") - print(f" Total cu TVA: {float(row[3]):.2f}") - - cur.execute(""" - SELECT vd.id_articol, a.codmat, a.denumire, - vd.cantitate, vd.pret, vd.pret_cu_tva, vd.proc_tvav - FROM vanzari_detalii vd - LEFT JOIN nom_articole a ON vd.id_articol = a.id_articol - WHERE vd.id_vanzare = :1 AND vd.sters = 0 - ORDER BY vd.id_articol - """, [id_vanz]) - det = cur.fetchall() - print(f" Items ({len(det)}):") - roa_total = 0 - for d in det: - line = float(d[3]) * float(d[4]) - roa_total += line - print(f" COD={str(d[1] or ''):25s} qty={float(d[3]):6.1f} x {float(d[4]):8.2f} = {line:8.2f} TVA={float(d[6]):.0f}% {d[2]}") - print(f" Sum lines (fara TVA): {roa_total:.2f}") - -conn.close() diff --git a/scripts/count_orders.py b/scripts/count_orders.py deleted file mode 100644 index 8e54131..0000000 --- a/scripts/count_orders.py +++ /dev/null @@ -1,30 +0,0 @@ -import sqlite3, sys -sys.stdout.reconfigure(encoding='utf-8', errors='replace') - -db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') -c = db.cursor() - -c.execute("SELECT COUNT(*), MIN(order_date), MAX(order_date) FROM orders") -total, min_d, max_d = c.fetchone() -print(f"Total orders: {total} (from {min_d} to {max_d})") - -c.execute("SELECT status, COUNT(*) FROM orders GROUP BY status ORDER BY COUNT(*) DESC") -print("\nBy status:") -for r in c: - print(f" {r[0]:20s} {r[1]:5d}") - -c.execute("SELECT COUNT(*) FROM orders WHERE status IN ('IMPORTED','ALREADY_IMPORTED')") -print(f"\nImported (matchable): {c.fetchone()[0]}") - -c.execute("SELECT COUNT(*) FROM order_items") -print(f"Total order_items: {c.fetchone()[0]}") - -c.execute(""" - SELECT COUNT(DISTINCT oi.sku) - FROM order_items oi - JOIN orders o ON oi.order_number = o.order_number - WHERE o.status IN ('IMPORTED','ALREADY_IMPORTED') -""") -print(f"Unique SKUs in imported orders: {c.fetchone()[0]}") - -db.close() diff --git a/scripts/delete_imported.py b/scripts/delete_imported.py deleted file mode 100644 index f54d13c..0000000 --- a/scripts/delete_imported.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Delete all imported orders from Oracle ROA and reset SQLite status. -Soft-delete: SET sters=1 on comenzi + comenzi_detalii. -Reset SQLite: clear id_comanda, id_partener, set status back to allow re-import. -""" -import sys, sqlite3, oracledb, os -sys.stdout.reconfigure(encoding='utf-8', errors='replace') - -DRY_RUN = '--execute' not in sys.argv - -os.environ['PATH'] = r'C:\app\Server\product\18.0.0\dbhomeXE\bin' + ';' + os.environ.get('PATH','') -oracledb.init_oracle_client() - -# Get imported orders -db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') -db.row_factory = sqlite3.Row -c = db.cursor() - -c.execute(""" - SELECT order_number, id_comanda, id_partener, customer_name - FROM orders - WHERE status = 'IMPORTED' AND id_comanda IS NOT NULL -""") -imported = [dict(r) for r in c.fetchall()] - -print(f"Orders to delete: {len(imported)}") -if DRY_RUN: - print("*** DRY RUN — add --execute to actually delete ***\n") - -# Step 1: Soft-delete in Oracle -conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') -cur = conn.cursor() - -id_comandas = [o['id_comanda'] for o in imported] - -# Verify none are invoiced -for id_cmd in id_comandas: - cur.execute("SELECT COUNT(*) FROM vanzari WHERE id_comanda = :1 AND sters = 0", [id_cmd]) - cnt = cur.fetchone()[0] - if cnt > 0: - print(f" ERROR: comanda {id_cmd} has {cnt} active invoices! Aborting.") - sys.exit(1) - -print("Oracle: no invoices found on any order — safe to delete") - -for id_cmd in id_comandas: - order_num = [o['order_number'] for o in imported if o['id_comanda'] == id_cmd][0] - - if DRY_RUN: - # Just show what would happen - cur.execute("SELECT COUNT(*) FROM comenzi_detalii WHERE id_comanda = :1 AND sters = 0", [id_cmd]) - det_cnt = cur.fetchone()[0] - print(f" Would delete: comanda {id_cmd} (order {order_num}) + {det_cnt} detail lines") - else: - # Soft-delete detail lines - cur.execute("UPDATE comenzi_detalii SET sters = 1 WHERE id_comanda = :1 AND sters = 0", [id_cmd]) - det_deleted = cur.rowcount - - # Soft-delete order header - cur.execute("UPDATE comenzi SET sters = 1 WHERE id_comanda = :1 AND sters = 0", [id_cmd]) - hdr_deleted = cur.rowcount - - print(f" Deleted: comanda {id_cmd} (order {order_num}): header={hdr_deleted}, details={det_deleted}") - -if not DRY_RUN: - conn.commit() - print(f"\nOracle: {len(id_comandas)} orders soft-deleted (sters=1)") -else: - print(f"\nOracle: DRY RUN — nothing changed") - -conn.close() - -# Step 2: Reset SQLite -if not DRY_RUN: - c.execute(""" - UPDATE orders SET - status = 'SKIPPED', - id_comanda = NULL, - id_partener = NULL, - id_adresa_facturare = NULL, - id_adresa_livrare = NULL, - error_message = NULL, - factura_serie = NULL, - factura_numar = NULL, - factura_total_fara_tva = NULL, - factura_total_tva = NULL, - factura_total_cu_tva = NULL, - factura_data = NULL, - invoice_checked_at = NULL - WHERE status = 'IMPORTED' AND id_comanda IS NOT NULL - """) - db.commit() - print(f"SQLite: {c.rowcount} orders reset to SKIPPED (id_comanda/id_partener cleared)") -else: - print(f"SQLite: DRY RUN — would reset {len(imported)} orders to SKIPPED") - -db.close() -print("\nDone!" if not DRY_RUN else "\nDone (dry run). Run with --execute to apply.") diff --git a/scripts/explore_oracle.py b/scripts/explore_oracle.py deleted file mode 100644 index bc206bc..0000000 --- a/scripts/explore_oracle.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Explore Oracle structure for invoice matching.""" -import oracledb -import os - -os.environ['PATH'] = r'C:\app\Server\product\18.0.0\dbhomeXE\bin' + ';' + os.environ.get('PATH','') -oracledb.init_oracle_client() -conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') -cur = conn.cursor() - -# Recent vanzari (last 10 days) -cur.execute(""" - SELECT v.id_vanzare, v.numar_act, v.serie_act, - TO_CHAR(v.data_act, 'YYYY-MM-DD') as data_act, - v.total_fara_tva, v.total_cu_tva, v.id_part, v.id_comanda, - p.denumire as partener - FROM vanzari v - LEFT JOIN nom_parteneri p ON v.id_part = p.id_part - WHERE v.sters = 0 AND v.data_act >= SYSDATE - 10 - ORDER BY v.data_act DESC -""") -print('=== Recent VANZARI (last 10 days) ===') -print(f'{"ID_VANZ":>8s} {"NR_ACT":>8s} {"SERIE":>6s} {"DATA":>12s} {"TOTAL_FARA":>12s} {"TOTAL_CU":>12s} {"ID_PART":>8s} {"ID_CMD":>8s} PARTENER') -for r in cur: - print(f'{r[0]:8d} {str(r[1] or ""):>8s} {str(r[2] or ""):>6s} {str(r[3]):>12s} {float(r[4] or 0):12.2f} {float(r[5] or 0):12.2f} {r[6] or 0:8d} {str(r[7] or ""):>8s} {r[8] or ""}') - -print() - -# Vanzari_detalii for those invoices -cur.execute(""" - SELECT vd.id_vanzare, vd.id_articol, a.codmat, a.denumire, - vd.cantitate, vd.pret, vd.pret_cu_tva, vd.proc_tvav - FROM vanzari_detalii vd - JOIN vanzari v ON vd.id_vanzare = v.id_vanzare - LEFT JOIN nom_articole a ON vd.id_articol = a.id_articol - WHERE v.sters = 0 AND vd.sters = 0 AND v.data_act >= SYSDATE - 10 - ORDER BY vd.id_vanzare, vd.id_articol -""") -print('=== Recent VANZARI_DETALII (last 10 days) ===') -print(f'{"ID_VANZ":>8s} {"ID_ART":>8s} {"CODMAT":>15s} {"DENUMIRE":>40s} {"QTY":>8s} {"PRET":>10s} {"PRET_CU":>10s} {"TVA%":>6s}') -for r in cur: - print(f'{r[0]:8d} {r[1]:8d} {str(r[2] or ""):>15s} {str((r[3] or "")[:40]):>40s} {float(r[4] or 0):8.2f} {float(r[5] or 0):10.4f} {float(r[6] or 0):10.4f} {float(r[7] or 0):6.1f}') - -print() - -# Also get SQLite orders for comparison -print('=== SQLite orders (imported, last 10 days) ===') -import sqlite3 -db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') -c = db.cursor() -c.execute(""" - SELECT o.order_number, o.order_date, o.customer_name, o.status, - o.id_comanda, o.order_total, - o.factura_serie, o.factura_numar, o.factura_data - FROM orders o - WHERE o.order_date >= date('now', '-10 days') - ORDER BY o.order_date DESC -""") -print(f'{"ORDER_NR":>10s} {"DATE":>12s} {"CLIENT":>30s} {"STATUS":>10s} {"ID_CMD":>8s} {"TOTAL":>10s} {"F_SERIE":>8s} {"F_NR":>8s} {"F_DATA":>12s}') -for r in c: - print(f'{str(r[0]):>10s} {str(r[1])[:10]:>12s} {str((r[2] or "")[:30]):>30s} {str(r[3]):>10s} {str(r[4] or ""):>8s} {float(r[5] or 0):10.2f} {str(r[6] or ""):>8s} {str(r[7] or ""):>8s} {str(r[8] or ""):>12s}') - -print() - -# Order items -c.execute(""" - SELECT oi.order_number, oi.sku, oi.product_name, oi.quantity, oi.price, oi.vat, oi.mapping_status - FROM order_items oi - JOIN orders o ON oi.order_number = o.order_number - WHERE o.order_date >= date('now', '-10 days') - ORDER BY oi.order_number, oi.sku -""") -print('=== SQLite order_items (last 10 days) ===') -print(f'{"ORDER_NR":>10s} {"SKU":>20s} {"PRODUCT":>40s} {"QTY":>6s} {"PRICE":>10s} {"VAT":>6s} {"MAP":>8s}') -for r in c: - print(f'{str(r[0]):>10s} {str(r[1] or ""):>20s} {str((r[2] or "")[:40]):>40s} {float(r[3] or 0):6.1f} {float(r[4] or 0):10.2f} {float(r[5] or 0):6.1f} {str(r[6] or ""):>8s}') - -db.close() -conn.close() diff --git a/scripts/fetch_one_order.py b/scripts/fetch_one_order.py deleted file mode 100644 index ace7808..0000000 --- a/scripts/fetch_one_order.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys, json, httpx -sys.stdout.reconfigure(encoding='utf-8', errors='replace') - -API_URL = 'https://api.gomag.ro/api/v1/order/read/json' -headers = { - 'Apikey': '4c5e46df8f6c4f054fe2787de7a13d4a', - 'ApiShop': 'https://coffeepoint.ro', - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/json', -} - -target = sys.argv[1] if len(sys.argv) > 1 else '480700091' - -for page in range(1, 20): - resp = httpx.get(API_URL, headers=headers, params={'startDate': '2026-03-08', 'page': page, 'limit': 250}, timeout=60) - data = resp.json() - orders = data.get('orders', {}) - if isinstance(orders, dict) and target in orders: - print(json.dumps(orders[target], indent=2, ensure_ascii=False)) - sys.exit(0) - # Also check by 'number' field - if isinstance(orders, dict): - for key, order in orders.items(): - if isinstance(order, dict) and str(order.get('number', '')) == target: - print(json.dumps(order, indent=2, ensure_ascii=False)) - sys.exit(0) - if page >= data.get('pages', 1): - break - -print(f"Order {target} not found") diff --git a/scripts/match_all.py b/scripts/match_all.py deleted file mode 100644 index 93ee010..0000000 --- a/scripts/match_all.py +++ /dev/null @@ -1,533 +0,0 @@ -""" -Match ALL GoMag orders (SQLite) with manual invoices (Oracle vanzari) -by date + client name + total value. -Then compare line items to discover SKU → CODMAT mappings. -""" -import oracledb -import os -import sys -import sqlite3 -import csv -from difflib import SequenceMatcher - -sys.stdout.reconfigure(encoding='utf-8', errors='replace') -os.environ['PATH'] = r'C:\app\Server\product\18.0.0\dbhomeXE\bin' + ';' + os.environ.get('PATH','') -oracledb.init_oracle_client() - -# --- Step 1: Get ALL GoMag orders from SQLite --- -print("=" * 80) -print("STEP 1: Loading ALL GoMag orders from SQLite") -print("=" * 80) - -db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') -db.row_factory = sqlite3.Row -c = db.cursor() - -# ALL orders, not just IMPORTED -c.execute(""" - SELECT order_number, order_date, customer_name, status, - id_comanda, order_total, billing_name, shipping_name - FROM orders - ORDER BY order_date DESC -""") -orders = [dict(r) for r in c.fetchall()] - -# Get order items -for order in orders: - c.execute(""" - SELECT sku, product_name, quantity, price, vat, mapping_status - FROM order_items - WHERE order_number = ? - ORDER BY sku - """, (order['order_number'],)) - order['items'] = [dict(r) for r in c.fetchall()] - -db.close() - -by_status = {} -for o in orders: - by_status.setdefault(o['status'], 0) - by_status[o['status']] += 1 -print(f"Loaded {len(orders)} GoMag orders: {by_status}") - -# --- Step 2: Get Oracle invoices with date range matching orders --- -print() -print("=" * 80) -print("STEP 2: Loading Oracle invoices (vanzari + detalii)") -print("=" * 80) - -conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') -cur = conn.cursor() - -# Get date range from orders -min_date = min(str(o['order_date'])[:10] for o in orders) -max_date = max(str(o['order_date'])[:10] for o in orders) -print(f"Order date range: {min_date} to {max_date}") - -# Get vanzari in that range (with some margin) -cur.execute(""" - SELECT v.id_vanzare, v.numar_act, v.serie_act, - TO_CHAR(v.data_act, 'YYYY-MM-DD') as data_act, - v.total_fara_tva, v.total_cu_tva, v.id_part, - p.denumire as partener, p.prenume - FROM vanzari v - LEFT JOIN nom_parteneri p ON v.id_part = p.id_part - WHERE v.sters = 0 - AND v.data_act >= TO_DATE(:1, 'YYYY-MM-DD') - 2 - AND v.data_act <= TO_DATE(:2, 'YYYY-MM-DD') + 2 - AND v.total_cu_tva > 0 - ORDER BY v.data_act DESC -""", [min_date, max_date]) - -invoices = [] -for r in cur: - inv = { - 'id_vanzare': r[0], - 'numar_act': r[1], - 'serie_act': r[2] or '', - 'data_act': r[3], - 'total_fara_tva': float(r[4] or 0), - 'total_cu_tva': float(r[5] or 0), - 'id_part': r[6], - 'partener': ((r[7] or '') + ' ' + (r[8] or '')).strip(), - } - invoices.append(inv) - -print(f"Loaded {len(invoices)} Oracle invoices in range {min_date} - {max_date}") - -# Get detail lines for ALL invoices in one batch -inv_ids = [inv['id_vanzare'] for inv in invoices] -inv_map = {inv['id_vanzare']: inv for inv in invoices} -for inv in invoices: - inv['items'] = [] - -# Batch fetch details -for i in range(0, len(inv_ids), 500): - batch = inv_ids[i:i+500] - placeholders = ",".join([f":d{j}" for j in range(len(batch))]) - params = {f"d{j}": did for j, did in enumerate(batch)} - cur.execute(f""" - SELECT vd.id_vanzare, vd.id_articol, a.codmat, a.denumire, - vd.cantitate, vd.pret, vd.pret_cu_tva, vd.proc_tvav - FROM vanzari_detalii vd - LEFT JOIN nom_articole a ON vd.id_articol = a.id_articol - WHERE vd.id_vanzare IN ({placeholders}) AND vd.sters = 0 - ORDER BY vd.id_vanzare, vd.id_articol - """, params) - for r in cur: - inv_map[r[0]]['items'].append({ - 'id_articol': r[1], - 'codmat': r[2], - 'denumire': r[3], - 'cantitate': float(r[4] or 0), - 'pret': float(r[5] or 0), - 'pret_cu_tva': float(r[6] or 0), - 'tva_pct': float(r[7] or 0), - }) - -conn.close() - -# --- Step 3: Fuzzy matching --- -print() -print("=" * 80) -print("STEP 3: Matching orders → invoices (date + name + total)") -print("=" * 80) - -def normalize_name(name): - if not name: - return '' - n = name.strip().upper() - for old, new in [('S.R.L.', 'SRL'), ('S.R.L', 'SRL'), ('SC ', ''), ('PFA ', ''), ('PF ', '')]: - n = n.replace(old, new) - return n - -def name_similarity(n1, n2): - nn1 = normalize_name(n1) - nn2 = normalize_name(n2) - if not nn1 or not nn2: - return 0 - # Also try reversed word order (GoMag: "Popescu Ion", ROA: "ION POPESCU") - sim1 = SequenceMatcher(None, nn1, nn2).ratio() - words1 = nn1.split() - if len(words1) >= 2: - reversed1 = ' '.join(reversed(words1)) - sim2 = SequenceMatcher(None, reversed1, nn2).ratio() - return max(sim1, sim2) - return sim1 - -matches = [] -unmatched_orders = [] -used_invoices = set() - -# Sort orders by total descending (match big orders first - more unique) -orders_sorted = sorted(orders, key=lambda o: -(o['order_total'] or 0)) - -for order in orders_sorted: - best_match = None - best_score = 0 - - order_date = str(order['order_date'])[:10] - order_total = order['order_total'] or 0 - order_name = order['customer_name'] or '' - - for inv in invoices: - if inv['id_vanzare'] in used_invoices: - continue - - # Date match (must be within +/- 2 days) - try: - od = int(order_date.replace('-','')) - id_ = int(inv['data_act'].replace('-','')) - date_diff = abs(od - id_) - except: - continue - if date_diff > 2: - continue - - # Total match (within 10% or 10 lei — more lenient for transport/discount) - total_diff = abs(order_total - inv['total_cu_tva']) - total_pct = total_diff / max(order_total, 0.01) * 100 - if total_pct > 15 and total_diff > 15: - continue - - # Name similarity - sim = name_similarity(order_name, inv['partener']) - - # Also check billing_name/shipping_name - sim2 = name_similarity(order.get('billing_name') or '', inv['partener']) - sim3 = name_similarity(order.get('shipping_name') or '', inv['partener']) - sim = max(sim, sim2, sim3) - - # Score - date_score = 1 if date_diff == 0 else (0.7 if date_diff == 1 else 0.3) - total_score = 1 - min(total_pct / 100, 1) - score = sim * 0.45 + total_score * 0.40 + date_score * 0.15 - - if score > best_score: - best_score = score - best_match = inv - - if best_match and best_score > 0.45: - matches.append({ - 'order': order, - 'invoice': best_match, - 'score': best_score, - }) - used_invoices.add(best_match['id_vanzare']) - else: - unmatched_orders.append(order) - -print(f"Matched: {len(matches)} | Unmatched orders: {len(unmatched_orders)}") -matched_statuses = {} -for m in matches: - s = m['order']['status'] - matched_statuses.setdefault(s, 0) - matched_statuses[s] += 1 -print(f"Matched by status: {matched_statuses}") - -# --- Step 4: Compare line items --- -print() -print("=" * 80) -print("STEP 4: Line item comparison") -print("=" * 80) - -simple_mappings = [] -repack_mappings = [] -complex_mappings = [] -unresolved = [] -match_details = [] - -for m in matches: - o = m['order'] - inv = m['invoice'] - go_items = o['items'] - # Filter out TRANSPORT and DISCOUNT from ROA items - roa_items = [ri for ri in inv['items'] - if ri['codmat'] not in ('TRANSPORT', 'DISCOUNT', None, '') - and ri['cantitate'] > 0] - roa_transport = [ri for ri in inv['items'] - if ri['codmat'] in ('TRANSPORT', 'DISCOUNT') or ri['cantitate'] < 0] - - detail = { - 'order_number': o['order_number'], - 'customer': o['customer_name'], - 'order_total': o['order_total'], - 'factura': f"{inv['serie_act']}{inv['numar_act']}", - 'inv_total': inv['total_cu_tva'], - 'score': m['score'], - 'go_items': len(go_items), - 'roa_items': len(roa_items), - 'matched_items': [], - 'unresolved_items': [], - } - - go_remaining = list(range(len(go_items))) - roa_remaining = list(range(len(roa_items))) - item_matches = [] - - # Pass 1: exact match by codmat (SKU == CODMAT) - for gi_idx in list(go_remaining): - gi = go_items[gi_idx] - for ri_idx in list(roa_remaining): - ri = roa_items[ri_idx] - if ri['codmat'] and gi['sku'] == ri['codmat']: - item_matches.append((gi_idx, [ri_idx])) - go_remaining.remove(gi_idx) - roa_remaining.remove(ri_idx) - break - - # Pass 2: match by total value (qty * price) - for gi_idx in list(go_remaining): - gi = go_items[gi_idx] - go_total_cu = gi['quantity'] * gi['price'] - go_total_fara = go_total_cu / (1 + gi['vat']/100) if gi['vat'] else go_total_cu - - for ri_idx in list(roa_remaining): - ri = roa_items[ri_idx] - roa_total_fara = ri['cantitate'] * ri['pret'] - roa_total_cu = ri['cantitate'] * ri['pret_cu_tva'] - - if (abs(go_total_fara - roa_total_fara) < 1.0 or - abs(go_total_cu - roa_total_cu) < 1.0 or - abs(go_total_cu - roa_total_fara) < 1.0): - item_matches.append((gi_idx, [ri_idx])) - go_remaining.remove(gi_idx) - roa_remaining.remove(ri_idx) - break - - # Pass 3: 1:1 positional match (if same count remaining) - if len(go_remaining) == len(roa_remaining) == 1: - item_matches.append((go_remaining[0], [roa_remaining[0]])) - go_remaining = [] - roa_remaining = [] - - # Pass 4: 1:N by combined total - for gi_idx in list(go_remaining): - gi = go_items[gi_idx] - go_total_cu = gi['quantity'] * gi['price'] - go_total_fara = go_total_cu / (1 + gi['vat']/100) if gi['vat'] else go_total_cu - - if len(roa_remaining) >= 2: - # Try all pairs - found = False - for i_pos, ri_idx1 in enumerate(roa_remaining): - for ri_idx2 in roa_remaining[i_pos+1:]: - ri1 = roa_items[ri_idx1] - ri2 = roa_items[ri_idx2] - combined_fara = ri1['cantitate'] * ri1['pret'] + ri2['cantitate'] * ri2['pret'] - combined_cu = ri1['cantitate'] * ri1['pret_cu_tva'] + ri2['cantitate'] * ri2['pret_cu_tva'] - if (abs(go_total_fara - combined_fara) < 2.0 or - abs(go_total_cu - combined_cu) < 2.0): - item_matches.append((gi_idx, [ri_idx1, ri_idx2])) - go_remaining.remove(gi_idx) - roa_remaining.remove(ri_idx1) - roa_remaining.remove(ri_idx2) - found = True - break - if found: - break - - # Classify matches - for gi_idx, ri_indices in item_matches: - gi = go_items[gi_idx] - ris = [roa_items[i] for i in ri_indices] - - if len(ris) == 1: - ri = ris[0] - if gi['sku'] == ri['codmat']: - # Already mapped (SKU == CODMAT) - detail['matched_items'].append(f"ALREADY: {gi['sku']} == {ri['codmat']}") - simple_mappings.append({ - 'sku': gi['sku'], 'codmat': ri['codmat'], - 'id_articol': ri['id_articol'], - 'type': 'already_equal', - 'product_name': gi['product_name'], 'denumire': ri['denumire'], - 'go_qty': gi['quantity'], 'roa_qty': ri['cantitate'], - 'go_price': gi['price'], 'roa_pret': ri['pret'], - }) - elif abs(gi['quantity'] - ri['cantitate']) < 0.01: - # Simple 1:1 different codmat - detail['matched_items'].append(f"SIMPLE: {gi['sku']} → {ri['codmat']}") - simple_mappings.append({ - 'sku': gi['sku'], 'codmat': ri['codmat'], - 'id_articol': ri['id_articol'], - 'type': 'simple', - 'product_name': gi['product_name'], 'denumire': ri['denumire'], - 'go_qty': gi['quantity'], 'roa_qty': ri['cantitate'], - 'go_price': gi['price'], 'roa_pret': ri['pret'], - }) - else: - # Repackaging - cantitate_roa = ri['cantitate'] / gi['quantity'] if gi['quantity'] else 1 - detail['matched_items'].append(f"REPACK: {gi['sku']} → {ri['codmat']} x{cantitate_roa:.3f}") - repack_mappings.append({ - 'sku': gi['sku'], 'codmat': ri['codmat'], - 'id_articol': ri['id_articol'], - 'cantitate_roa': round(cantitate_roa, 3), - 'product_name': gi['product_name'], 'denumire': ri['denumire'], - 'go_qty': gi['quantity'], 'roa_qty': ri['cantitate'], - }) - else: - # Complex set - go_total_cu = gi['quantity'] * gi['price'] - go_total_fara = go_total_cu / (1 + gi['vat']/100) if gi['vat'] else go_total_cu - for ri in ris: - ri_total = ri['cantitate'] * ri['pret'] - pct = round(ri_total / go_total_fara * 100, 2) if go_total_fara else 0 - cantitate_roa = ri['cantitate'] / gi['quantity'] if gi['quantity'] else 1 - detail['matched_items'].append(f"SET: {gi['sku']} → {ri['codmat']} {pct}%") - complex_mappings.append({ - 'sku': gi['sku'], 'codmat': ri['codmat'], - 'id_articol': ri['id_articol'], - 'cantitate_roa': round(cantitate_roa, 3), - 'procent_pret': pct, - 'product_name': gi['product_name'], 'denumire': ri['denumire'], - }) - - for gi_idx in go_remaining: - gi = go_items[gi_idx] - remaining_roa = [roa_items[i] for i in roa_remaining] - detail['unresolved_items'].append(gi['sku']) - unresolved.append({ - 'sku': gi['sku'], - 'product_name': gi['product_name'], - 'quantity': gi['quantity'], - 'price': gi['price'], - 'order': o['order_number'], - 'factura': f"{inv['serie_act']}{inv['numar_act']}", - 'roa_remaining': '; '.join([f"{r['codmat'] or '?'}({r['cantitate']}x{r['pret']:.2f}={r['denumire'][:30]})" - for r in remaining_roa]), - }) - - match_details.append(detail) - -# --- Step 5: Deduplicate and summarize --- -print() -print("=" * 80) -print("STEP 5: SUMMARY") -print("=" * 80) - -# Deduplicate simple -seen_simple_equal = {} -seen_simple_new = {} -for m in simple_mappings: - key = (m['sku'], m['codmat']) - if m['type'] == 'already_equal': - seen_simple_equal[key] = m - else: - seen_simple_new[key] = m - -seen_repack = {} -for m in repack_mappings: - key = (m['sku'], m['codmat']) - if key not in seen_repack: - seen_repack[key] = m - -seen_complex = {} -for m in complex_mappings: - key = (m['sku'], m['codmat']) - if key not in seen_complex: - seen_complex[key] = m - -# Deduplicate unresolved SKUs -seen_unresolved_skus = {} -for u in unresolved: - if u['sku'] not in seen_unresolved_skus: - seen_unresolved_skus[u['sku']] = u - -print(f"\n--- Already mapped (SKU == CODMAT in nom_articole): {len(seen_simple_equal)} unique ---") -for key, m in sorted(seen_simple_equal.items()): - print(f" {m['sku']:25s} = {m['codmat']:15s} | {(m['product_name'] or '')[:40]}") - -print(f"\n--- NEW simple 1:1 (SKU != CODMAT, same qty): {len(seen_simple_new)} unique ---") -for key, m in sorted(seen_simple_new.items()): - print(f" {m['sku']:25s} → {m['codmat']:15s} | GoMag: {(m['product_name'] or '')[:30]} → ROA: {(m['denumire'] or '')[:30]}") - -print(f"\n--- Repackaging (different qty): {len(seen_repack)} unique ---") -for key, m in sorted(seen_repack.items()): - print(f" {m['sku']:25s} → {m['codmat']:15s} x{m['cantitate_roa']} | {(m['product_name'] or '')[:30]} → {(m['denumire'] or '')[:30]}") - -print(f"\n--- Complex sets (1 SKU → N CODMATs): {len(seen_complex)} unique ---") -for key, m in sorted(seen_complex.items()): - print(f" {m['sku']:25s} → {m['codmat']:15s} {m['procent_pret']:6.2f}% | {(m['product_name'] or '')[:30]} → {(m['denumire'] or '')[:30]}") - -print(f"\n--- Unresolved (unique SKUs): {len(seen_unresolved_skus)} ---") -for sku, u in sorted(seen_unresolved_skus.items()): - print(f" {sku:25s} | {(u['product_name'] or '')[:40]} | example: order={u['order']}") - -print(f"\n--- Unmatched orders (no invoice found): {len(unmatched_orders)} ---") -for o in unmatched_orders[:20]: - print(f" {o['order_number']:>12s} | {str(o['order_date'])[:10]} | {(o['customer_name'] or '')[:30]:30s} | {o['order_total'] or 0:10.2f} | {o['status']}") -if len(unmatched_orders) > 20: - print(f" ... and {len(unmatched_orders) - 20} more") - -# --- Write output files --- -out_dir = r'C:\gomag-vending\scripts\output' -os.makedirs(out_dir, exist_ok=True) - -# Full match report -with open(os.path.join(out_dir, 'match_report.csv'), 'w', newline='', encoding='utf-8') as f: - w = csv.writer(f) - w.writerow(['order_number', 'customer', 'order_total', 'factura', 'inv_total', 'score', - 'go_items', 'roa_items', 'matched', 'unresolved']) - for d in match_details: - w.writerow([d['order_number'], d['customer'], d['order_total'], - d['factura'], d['inv_total'], f"{d['score']:.2f}", - d['go_items'], d['roa_items'], - '; '.join(d['matched_items']), - '; '.join(d['unresolved_items'])]) - -# New simple mappings (SKU → CODMAT where SKU != CODMAT) -with open(os.path.join(out_dir, 'simple_new_mappings.csv'), 'w', newline='', encoding='utf-8') as f: - w = csv.writer(f) - w.writerow(['sku', 'codmat', 'id_articol', 'product_name_gomag', 'denumire_roa', 'go_qty', 'roa_qty', 'go_price', 'roa_pret']) - for m in seen_simple_new.values(): - w.writerow([m['sku'], m['codmat'], m['id_articol'], m['product_name'], m['denumire'], - m['go_qty'], m['roa_qty'], m['go_price'], m['roa_pret']]) - -# Repackaging CSV -with open(os.path.join(out_dir, 'repack_mappings.csv'), 'w', newline='', encoding='utf-8') as f: - w = csv.writer(f) - w.writerow(['sku', 'codmat', 'cantitate_roa', 'procent_pret', 'product_name_gomag', 'denumire_roa']) - for m in seen_repack.values(): - w.writerow([m['sku'], m['codmat'], m['cantitate_roa'], 100, m['product_name'], m['denumire']]) - -# Complex sets CSV -with open(os.path.join(out_dir, 'complex_mappings.csv'), 'w', newline='', encoding='utf-8') as f: - w = csv.writer(f) - w.writerow(['sku', 'codmat', 'cantitate_roa', 'procent_pret', 'product_name_gomag', 'denumire_roa']) - for m in seen_complex.values(): - w.writerow([m['sku'], m['codmat'], round(m['cantitate_roa'], 3), m['procent_pret'], - m['product_name'], m['denumire']]) - -# Unresolved -with open(os.path.join(out_dir, 'unresolved.csv'), 'w', newline='', encoding='utf-8') as f: - w = csv.writer(f) - w.writerow(['sku', 'product_name', 'quantity', 'price', 'order', 'factura', 'roa_remaining_items']) - for u in unresolved: - w.writerow([u['sku'], u['product_name'], u['quantity'], u['price'], - u['order'], u['factura'], u['roa_remaining']]) - -# Already equal (for reference) -with open(os.path.join(out_dir, 'already_mapped.csv'), 'w', newline='', encoding='utf-8') as f: - w = csv.writer(f) - w.writerow(['sku', 'codmat', 'id_articol', 'product_name_gomag', 'denumire_roa']) - for m in seen_simple_equal.values(): - w.writerow([m['sku'], m['codmat'], m['id_articol'], m['product_name'], m['denumire']]) - -# Unmatched orders -with open(os.path.join(out_dir, 'unmatched_orders.csv'), 'w', newline='', encoding='utf-8') as f: - w = csv.writer(f) - w.writerow(['order_number', 'order_date', 'customer_name', 'status', 'order_total', 'items_count']) - for o in unmatched_orders: - w.writerow([o['order_number'], str(o['order_date'])[:10], o['customer_name'], - o['status'], o['order_total'], len(o['items'])]) - -print(f"\nOutput written to {out_dir}:") -print(f" match_report.csv - {len(match_details)} matched order-invoice pairs") -print(f" already_mapped.csv - {len(seen_simple_equal)} SKU==CODMAT (already OK)") -print(f" simple_new_mappings.csv - {len(seen_simple_new)} new SKU→CODMAT (need codmat in nom_articole or ARTICOLE_TERTI)") -print(f" repack_mappings.csv - {len(seen_repack)} repackaging") -print(f" complex_mappings.csv - {len(seen_complex)} complex sets") -print(f" unresolved.csv - {len(unresolved)} unresolved item lines") -print(f" unmatched_orders.csv - {len(unmatched_orders)} orders without invoice match") diff --git a/scripts/parse_sync_log.py b/scripts/parse_sync_log.py deleted file mode 100644 index a68943f..0000000 --- a/scripts/parse_sync_log.py +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/env python3 -""" -Parser pentru log-urile sync_comenzi_web. -Extrage comenzi esuate, SKU-uri lipsa, si genereaza un sumar. -Suporta atat formatul vechi (verbose) cat si formatul nou (compact). - -Utilizare: - python parse_sync_log.py # Ultimul log din vfp/log/ - python parse_sync_log.py # Log specific - python parse_sync_log.py --skus # Doar lista SKU-uri lipsa - python parse_sync_log.py --dir /path/to/logs # Director custom -""" - -import os -import sys -import re -import glob -import argparse - -# Regex pentru linii cu timestamp (intrare noua in log) -RE_TIMESTAMP = re.compile(r'^\[(\d{2}:\d{2}:\d{2})\]\s+\[(\w+\s*)\]\s*(.*)') - -# Regex format NOU: [N/Total] OrderNumber P:X A:Y/Z -> OK/ERR details -RE_COMPACT_OK = re.compile(r'\[(\d+)/(\d+)\]\s+(\S+)\s+.*->\s+OK\s+ID:(\S+)') -RE_COMPACT_ERR = re.compile(r'\[(\d+)/(\d+)\]\s+(\S+)\s+.*->\s+ERR\s+(.*)') - -# Regex format VECHI (backwards compat) -RE_SKU_NOT_FOUND = re.compile(r'SKU negasit.*?:\s*(\S+)') -RE_PRICE_POLICY = re.compile(r'Pretul pentru acest articol nu a fost gasit') -RE_FAILED_ORDER = re.compile(r'Import comanda esuat pentru\s+(\S+)') -RE_ARTICOL_ERR = re.compile(r'Eroare adaugare articol\s+(\S+)') -RE_ORDER_PROCESS = re.compile(r'Procesez comanda:\s+(\S+)\s+din\s+(\S+)') -RE_ORDER_SUCCESS = re.compile(r'SUCCES: Comanda importata.*?ID Oracle:\s+(\S+)') - -# Regex comune -RE_SYNC_END = re.compile(r'SYNC END\s*\|.*?(\d+)\s+processed.*?(\d+)\s+ok.*?(\d+)\s+err') -RE_STATS_LINE = re.compile(r'Duration:\s*(\S+)\s*\|\s*Orders:\s*(\S+)') -RE_STOPPED_EARLY = re.compile(r'Peste \d+.*ero|stopped early') - - -def find_latest_log(log_dir): - """Gaseste cel mai recent log sync_comenzi din directorul specificat.""" - pattern = os.path.join(log_dir, 'sync_comenzi_*.log') - files = glob.glob(pattern) - if not files: - return None - return max(files, key=os.path.getmtime) - - -def parse_log_entries(lines): - """Parseaza liniile log-ului in intrari structurate.""" - entries = [] - current = None - - for line in lines: - line = line.rstrip('\n\r') - m = RE_TIMESTAMP.match(line) - if m: - if current: - entries.append(current) - current = { - 'time': m.group(1), - 'level': m.group(2).strip(), - 'text': m.group(3), - 'full': line, - 'continuation': [] - } - elif current is not None: - current['continuation'].append(line) - current['text'] += '\n' + line - - if current: - entries.append(current) - - return entries - - -def extract_sku_from_error(err_text): - """Extrage SKU din textul erorii (diverse formate).""" - # SKU_NOT_FOUND: 8714858424056 - m = re.search(r'SKU_NOT_FOUND:\s*(\S+)', err_text) - if m: - return ('SKU_NOT_FOUND', m.group(1)) - - # PRICE_POLICY: 8000070028685 - m = re.search(r'PRICE_POLICY:\s*(\S+)', err_text) - if m: - return ('PRICE_POLICY', m.group(1)) - - # Format vechi: SKU negasit...NOM_ARTICOLE: xxx - m = RE_SKU_NOT_FOUND.search(err_text) - if m: - return ('SKU_NOT_FOUND', m.group(1)) - - # Format vechi: Eroare adaugare articol xxx - m = RE_ARTICOL_ERR.search(err_text) - if m: - return ('ARTICOL_ERROR', m.group(1)) - - # Format vechi: Pretul... - if RE_PRICE_POLICY.search(err_text): - return ('PRICE_POLICY', '(SKU necunoscut)') - - return (None, None) - - -def analyze_entries(entries): - """Analizeaza intrarile si extrage informatii relevante.""" - result = { - 'start_time': None, - 'end_time': None, - 'duration': None, - 'total_orders': 0, - 'success_orders': 0, - 'error_orders': 0, - 'stopped_early': False, - 'failed': [], - 'missing_skus': [], - } - - seen_skus = set() - current_order = None - - for entry in entries: - text = entry['text'] - level = entry['level'] - - # Start/end time - if entry['time']: - if result['start_time'] is None: - result['start_time'] = entry['time'] - result['end_time'] = entry['time'] - - # Format NOU: SYNC END line cu statistici - m = RE_SYNC_END.search(text) - if m: - result['total_orders'] = int(m.group(1)) - result['success_orders'] = int(m.group(2)) - result['error_orders'] = int(m.group(3)) - - # Format NOU: compact OK line - m = RE_COMPACT_OK.search(text) - if m: - continue - - # Format NOU: compact ERR line - m = RE_COMPACT_ERR.search(text) - if m: - order_nr = m.group(3) - err_detail = m.group(4).strip() - err_type, sku = extract_sku_from_error(err_detail) - if err_type and sku: - result['failed'].append((order_nr, err_type, sku)) - if sku not in seen_skus and sku != '(SKU necunoscut)': - seen_skus.add(sku) - result['missing_skus'].append(sku) - else: - result['failed'].append((order_nr, 'ERROR', err_detail[:60])) - continue - - # Stopped early - if RE_STOPPED_EARLY.search(text): - result['stopped_early'] = True - - # Format VECHI: statistici din sumar - if 'Total comenzi procesate:' in text: - try: - result['total_orders'] = int(text.split(':')[-1].strip()) - except ValueError: - pass - if 'Comenzi importate cu succes:' in text: - try: - result['success_orders'] = int(text.split(':')[-1].strip()) - except ValueError: - pass - if 'Comenzi cu erori:' in text: - try: - result['error_orders'] = int(text.split(':')[-1].strip()) - except ValueError: - pass - - # Format VECHI: Duration line - m = RE_STATS_LINE.search(text) - if m: - result['duration'] = m.group(1) - - # Format VECHI: erori - if level == 'ERROR': - m_fail = RE_FAILED_ORDER.search(text) - if m_fail: - current_order = m_fail.group(1) - - m = RE_ORDER_PROCESS.search(text) - if m: - current_order = m.group(1) - - err_type, sku = extract_sku_from_error(text) - if err_type and sku: - order_nr = current_order or '?' - result['failed'].append((order_nr, err_type, sku)) - if sku not in seen_skus and sku != '(SKU necunoscut)': - seen_skus.add(sku) - result['missing_skus'].append(sku) - - # Duration din SYNC END - m = re.search(r'\|\s*(\d+)s\s*$', text) - if m: - result['duration'] = m.group(1) + 's' - - return result - - -def format_report(result, log_path): - """Formateaza raportul complet.""" - lines = [] - lines.append('=== SYNC LOG REPORT ===') - lines.append(f'File: {os.path.basename(log_path)}') - - duration = result["duration"] or "?" - start = result["start_time"] or "?" - end = result["end_time"] or "?" - lines.append(f'Run: {start} - {end} ({duration})') - lines.append('') - - stopped = 'YES' if result['stopped_early'] else 'NO' - lines.append( - f'SUMMARY: {result["total_orders"]} processed, ' - f'{result["success_orders"]} success, ' - f'{result["error_orders"]} errors ' - f'(stopped early: {stopped})' - ) - lines.append('') - - if result['failed']: - lines.append('FAILED ORDERS:') - seen = set() - for order_nr, err_type, sku in result['failed']: - key = (order_nr, err_type, sku) - if key not in seen: - seen.add(key) - lines.append(f' {order_nr:<12} {err_type:<18} {sku}') - lines.append('') - - if result['missing_skus']: - lines.append(f'MISSING SKUs ({len(result["missing_skus"])} unique):') - for sku in sorted(result['missing_skus']): - lines.append(f' {sku}') - lines.append('') - - return '\n'.join(lines) - - -def main(): - parser = argparse.ArgumentParser( - description='Parser pentru log-urile sync_comenzi_web' - ) - parser.add_argument( - 'logfile', nargs='?', default=None, - help='Fisier log specific (default: ultimul din vfp/log/)' - ) - parser.add_argument( - '--skus', action='store_true', - help='Afiseaza doar lista SKU-uri lipsa (una pe linie)' - ) - parser.add_argument( - '--dir', default=None, - help='Director cu log-uri (default: vfp/log/ relativ la script)' - ) - - args = parser.parse_args() - - if args.logfile: - log_path = args.logfile - else: - if args.dir: - log_dir = args.dir - else: - script_dir = os.path.dirname(os.path.abspath(__file__)) - project_dir = os.path.dirname(script_dir) - log_dir = os.path.join(project_dir, 'vfp', 'log') - - log_path = find_latest_log(log_dir) - if not log_path: - print(f'Nu am gasit fisiere sync_comenzi_*.log in {log_dir}', - file=sys.stderr) - sys.exit(1) - - if not os.path.isfile(log_path): - print(f'Fisierul nu exista: {log_path}', file=sys.stderr) - sys.exit(1) - - with open(log_path, 'r', encoding='utf-8', errors='replace') as f: - lines = f.readlines() - - entries = parse_log_entries(lines) - result = analyze_entries(entries) - - if args.skus: - for sku in sorted(result['missing_skus']): - print(sku) - else: - print(format_report(result, log_path)) - - -if __name__ == '__main__': - main()