diff --git a/scripts/analyze_billing.py b/scripts/analyze_billing.py new file mode 100644 index 0000000..3e046c1 --- /dev/null +++ b/scripts/analyze_billing.py @@ -0,0 +1,179 @@ +"""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 new file mode 100644 index 0000000..678f88c --- /dev/null +++ b/scripts/check_deploy.py @@ -0,0 +1,31 @@ +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 new file mode 100644 index 0000000..84e245a --- /dev/null +++ b/scripts/check_imported.py @@ -0,0 +1,61 @@ +"""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_detail.py b/scripts/compare_detail.py new file mode 100644 index 0000000..306f3f4 --- /dev/null +++ b/scripts/compare_detail.py @@ -0,0 +1,325 @@ +""" +Generate detailed comparison CSV: GoMag orders vs Oracle invoices +Side-by-side view for manual analysis. +""" +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() + +# --- Load GoMag 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, 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()] + +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() +print(f"Loaded {len(orders)} GoMag orders") + +# --- Load Oracle invoices --- +conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') +cur = conn.cursor() + +min_date = min(str(o['order_date'])[:10] for o in orders) +max_date = max(str(o['order_date'])[:10] for o in orders) + +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') - 3 + AND v.data_act <= TO_DATE(:2, 'YYYY-MM-DD') + 3 + AND v.total_cu_tva > 0 + ORDER BY v.data_act DESC +""", [min_date, max_date]) + +invoices = [] +inv_map = {} +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(), + 'items': [], + } + invoices.append(inv) + inv_map[inv['id_vanzare']] = inv + +# Batch fetch details +inv_ids = [inv['id_vanzare'] for inv in invoices] +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() +print(f"Loaded {len(invoices)} Oracle invoices") + +# --- Fuzzy matching orders → invoices --- +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 + 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 = [] +used_invoices = set() +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 + try: + od = int(order_date.replace('-','')) + id_ = int(inv['data_act'].replace('-','')) + date_diff = abs(od - id_) + except: + continue + if date_diff > 3: + continue + + 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 + + sim = name_similarity(order_name, inv['partener']) + 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) + + date_score = 1 if date_diff == 0 else (0.7 if date_diff == 1 else (0.4 if date_diff == 2 else 0.2)) + 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: + matches.append({'order': order, 'invoice': None, 'score': 0}) + +# Sort by order date +matches.sort(key=lambda m: str(m['order']['order_date']), reverse=True) + +# --- Write detailed comparison CSV --- +out_dir = r'C:\gomag-vending\scripts\output' +os.makedirs(out_dir, exist_ok=True) + +with open(os.path.join(out_dir, 'comparatie_detaliata.csv'), 'w', newline='', encoding='utf-8-sig') as f: + w = csv.writer(f, delimiter=';') + + w.writerow([ + 'NR_COMANDA_GOMAG', 'DATA_COMANDA', 'CLIENT_GOMAG', 'STATUS_IMPORT', + 'TOTAL_COMANDA_GOMAG', + 'NR_ARTICOLE_GOMAG', 'SKU_GOMAG', 'PRODUS_GOMAG', 'QTY_GOMAG', 'PRET_GOMAG', 'TVA_GOMAG', + 'LINIE_TOTAL_GOMAG', + '|', + 'FACTURA_ROA', 'DATA_FACTURA', 'CLIENT_ROA', 'TOTAL_FACTURA_ROA', + 'NR_ARTICOLE_ROA', 'CODMAT_ROA', 'PRODUS_ROA', 'QTY_ROA', 'PRET_ROA', 'TVA_ROA', + 'LINIE_TOTAL_ROA', + '|', + 'MATCH_SCORE', 'DIFF_TOTAL', 'SKU_EQ_CODMAT', + ]) + + for m in matches: + o = m['order'] + inv = m['invoice'] + go_items = o['items'] + roa_items = inv['items'] if inv else [] + + # Filter out transport/discount for comparison count + roa_real = [ri for ri in roa_items if ri['codmat'] not in ('TRANSPORT', 'DISCOUNT', None, '') and ri['cantitate'] > 0] + roa_extra = [ri for ri in roa_items if ri['codmat'] in ('TRANSPORT', 'DISCOUNT') or ri['cantitate'] < 0] + + max_lines = max(len(go_items), len(roa_items), 1) + + order_total = o['order_total'] or 0 + inv_total = inv['total_cu_tva'] if inv else 0 + diff_total = round(order_total - inv_total, 2) if inv else '' + + for idx in range(max_lines): + gi = go_items[idx] if idx < len(go_items) else None + ri = roa_items[idx] if idx < len(roa_items) else None + + # GoMag side + if idx == 0: + go_order = o['order_number'] + go_date = str(o['order_date'])[:10] + go_client = o['customer_name'] or '' + go_status = o['status'] + go_total = order_total + go_nr_art = len(go_items) + else: + go_order = '' + go_date = '' + go_client = '' + go_status = '' + go_total = '' + go_nr_art = '' + + if gi: + go_sku = gi['sku'] or '' + go_prod = gi['product_name'] or '' + go_qty = gi['quantity'] + go_price = gi['price'] + go_vat = gi['vat'] + go_line_total = round(gi['quantity'] * gi['price'], 2) + else: + go_sku = go_prod = go_qty = go_price = go_vat = go_line_total = '' + + # ROA side + if idx == 0 and inv: + roa_fact = f"{inv['serie_act']}{inv['numar_act']}" + roa_date = inv['data_act'] + roa_client = inv['partener'] + roa_total = inv_total + roa_nr_art = len(roa_items) + else: + roa_fact = '' + roa_date = '' + roa_client = '' + roa_total = '' + roa_nr_art = '' + + if ri: + roa_codmat = ri['codmat'] or '' + roa_prod = ri['denumire'] or '' + roa_qty = ri['cantitate'] + roa_price = ri['pret'] + roa_vat = ri['tva_pct'] + roa_line_total = round(ri['cantitate'] * ri['pret'], 2) if ri['cantitate'] > 0 else round(-ri['cantitate'] * ri['pret'], 2) + else: + roa_codmat = roa_prod = roa_qty = roa_price = roa_vat = roa_line_total = '' + + # Match indicators + if idx == 0: + score = round(m['score'], 2) if m['score'] else '' + diff = diff_total + else: + score = '' + diff = '' + + # Check SKU == CODMAT + sku_eq = '' + if gi and ri and go_sku and roa_codmat: + if go_sku == roa_codmat: + sku_eq = 'DA' + else: + sku_eq = '' + + w.writerow([ + go_order, go_date, go_client, go_status, + go_total, + go_nr_art, go_sku, go_prod, go_qty, go_price, go_vat, + go_line_total, + '|', + roa_fact, roa_date, roa_client, roa_total, + roa_nr_art, roa_codmat, roa_prod, roa_qty, roa_price, roa_vat, + roa_line_total, + '|', + score, diff, sku_eq, + ]) + + # Empty separator row before unmatched invoice summary + w.writerow([]) + w.writerow(['--- FACTURI ROA FARA COMANDA GOMAG ---']) + w.writerow([]) + + unmatched_inv = [inv for inv in invoices if inv['id_vanzare'] not in used_invoices] + unmatched_inv.sort(key=lambda x: x['data_act'], reverse=True) + + for inv in unmatched_inv: + for idx, ri in enumerate(inv['items']): + if idx == 0: + w.writerow([ + '', '', '', '', '', '', '', '', '', '', '', '', + '|', + f"{inv['serie_act']}{inv['numar_act']}", inv['data_act'], + inv['partener'], inv['total_cu_tva'], + len(inv['items']), + ri['codmat'] or '', ri['denumire'] or '', + ri['cantitate'], ri['pret'], ri['tva_pct'], + round(ri['cantitate'] * ri['pret'], 2), + '|', '', '', '', + ]) + else: + w.writerow([ + '', '', '', '', '', '', '', '', '', '', '', '', + '|', + '', '', '', '', + '', + ri['codmat'] or '', ri['denumire'] or '', + ri['cantitate'], ri['pret'], ri['tva_pct'], + round(ri['cantitate'] * ri['pret'], 2), + '|', '', '', '', + ]) + +print(f"\nDone!") +print(f"Matched: {sum(1 for m in matches if m['invoice'])} / {len(orders)} orders") +print(f"Unmatched invoices: {len(unmatched_inv)}") +print(f"\nOutput: {os.path.join(out_dir, 'comparatie_detaliata.csv')}") +print(f"Open in Excel (separator: ;, encoding: UTF-8)") diff --git a/scripts/compare_order.py b/scripts/compare_order.py new file mode 100644 index 0000000..ceb449e --- /dev/null +++ b/scripts/compare_order.py @@ -0,0 +1,76 @@ +"""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 new file mode 100644 index 0000000..8e54131 --- /dev/null +++ b/scripts/count_orders.py @@ -0,0 +1,30 @@ +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/debug_match.py b/scripts/debug_match.py new file mode 100644 index 0000000..2d52496 --- /dev/null +++ b/scripts/debug_match.py @@ -0,0 +1,112 @@ +"""Debug matching for specific order 480102897""" +import oracledb +import os +import sys +import sqlite3 +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() + +# Get GoMag order +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 = '480102897'") +order = dict(c.fetchone()) +c.execute("SELECT * FROM order_items WHERE order_number = '480102897'") +items = [dict(r) for r in c.fetchall()] +db.close() + +print(f"=== GoMag Order 480102897 ===") +print(f" Client: {order['customer_name']}") +print(f" Billing: {order['billing_name']}") +print(f" Shipping: {order['shipping_name']}") +print(f" Date: {order['order_date']}") +print(f" Total: {order['order_total']}") +print(f" Status: {order['status']}") +print(f" Items ({len(items)}):") +for it in items: + print(f" SKU={it['sku']:20s} qty={it['quantity']:6.1f} price={it['price']:8.2f} {it['product_name']}") + +# Now search Oracle for ALL invoices around that date with similar total +conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') +cur = conn.cursor() + +order_date = str(order['order_date'])[:10] +order_total = order['order_total'] + +print(f"\n=== Oracle invoices near date {order_date}, total ~{order_total} ===") +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_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') - 3 + AND v.data_act <= TO_DATE(:2, 'YYYY-MM-DD') + 3 + AND v.total_cu_tva > 0 + ORDER BY ABS(v.total_cu_tva - :3), v.data_act +""", [order_date, order_date, order_total]) + +print(f"{'NR_FACT':>8s} {'SERIE':>5s} {'DATA':>12s} {'TOTAL_CU':>10s} {'DIFF':>10s} {'PARTENER':40s}") +candidates = [] +for r in cur: + total = float(r[4] or 0) + diff = total - order_total + partener = ((r[6] or '') + ' ' + (r[7] or '')).strip() + print(f"{str(r[1]):>8s} {str(r[2] or ''):>5s} {r[3]:>12s} {total:10.2f} {diff:+10.2f} {partener:40s}") + candidates.append({'numar_act': r[1], 'serie_act': r[2], 'data_act': r[3], + 'total': total, 'partener': partener, 'id_vanzare': r[0]}) + if len(candidates) >= 20: + break + +# Also search by client name +print(f"\n=== Oracle invoices by name 'STOICA' or 'LIVIU' near that date ===") +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_cu_tva, + 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') - 5 + AND v.data_act <= TO_DATE(:2, 'YYYY-MM-DD') + 5 + AND (UPPER(p.denumire) LIKE '%STOICA%' OR UPPER(p.prenume) LIKE '%STOICA%' + OR UPPER(p.denumire) LIKE '%LIVIU%' OR UPPER(p.prenume) LIKE '%LIVIU%' + OR UPPER(p.denumire) LIKE '%SLM%') + ORDER BY v.data_act DESC +""", [order_date, order_date]) + +for r in cur: + total = float(r[4] or 0) + partener = ((r[5] or '') + ' ' + (r[6] or '')).strip() + print(f" {str(r[1]):>8s} {str(r[2] or ''):>5s} {r[3]:>12s} {total:10.2f} {partener}") + +# Show details of invoice 4035 +print(f"\n=== Details of invoice VM4035 ===") +cur.execute(""" + SELECT v.id_vanzare, TO_CHAR(v.data_act, 'YYYY-MM-DD'), 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 = 4035 AND v.serie_act = 'VM' AND v.sters = 0 +""") +row = cur.fetchone() +if row: + print(f" Date: {row[1]}, Total: {float(row[2]):.2f}, Client: {row[3]} {row[4]}") + cur.execute(""" + SELECT a.codmat, a.denumire, vd.cantitate, vd.pret, vd.pret_cu_tva + 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 + """, [row[0]]) + for r in cur: + print(f" COD={r[0] or '':20s} qty={float(r[2]):6.1f} pret={float(r[3]):8.2f} pretcu={float(r[4]):8.2f} {r[1]}") + +conn.close() diff --git a/scripts/delete_imported.py b/scripts/delete_imported.py new file mode 100644 index 0000000..f54d13c --- /dev/null +++ b/scripts/delete_imported.py @@ -0,0 +1,98 @@ +""" +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 new file mode 100644 index 0000000..bc206bc --- /dev/null +++ b/scripts/explore_oracle.py @@ -0,0 +1,78 @@ +"""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 new file mode 100644 index 0000000..ace7808 --- /dev/null +++ b/scripts/fetch_one_order.py @@ -0,0 +1,30 @@ +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 new file mode 100644 index 0000000..93ee010 --- /dev/null +++ b/scripts/match_all.py @@ -0,0 +1,533 @@ +""" +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/match_by_price.py b/scripts/match_by_price.py new file mode 100644 index 0000000..e2ff6f4 --- /dev/null +++ b/scripts/match_by_price.py @@ -0,0 +1,414 @@ +""" +Match GoMag SKUs → ROA id_articol by matching order lines on unit price. +For each matched order-invoice pair, compare lines by price to discover mappings. +Output: SQL for nom_articole codmat updates + CSV for ARTICOLE_TERTI mappings. +""" +import oracledb +import os +import sys +import sqlite3 +import csv +from collections import defaultdict +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() + +# --- Load GoMag 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, order_date, customer_name, status, order_total FROM orders ORDER BY order_date DESC") +orders = [dict(r) for r in c.fetchall()] +for order in orders: + c.execute("SELECT sku, product_name, quantity, price, vat FROM order_items WHERE order_number = ? ORDER BY sku", (order['order_number'],)) + order['items'] = [dict(r) for r in c.fetchall()] +db.close() +print(f"Loaded {len(orders)} GoMag orders") + +# --- Load Oracle invoices --- +conn = oracledb.connect(user='VENDING', password='ROMFASTSOFT', dsn='ROA') +cur = conn.cursor() +min_date = min(str(o['order_date'])[:10] for o in orders) +max_date = max(str(o['order_date'])[:10] for o in orders) + +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') - 3 + AND v.data_act <= TO_DATE(:2, 'YYYY-MM-DD') + 3 AND v.total_cu_tva > 0 + ORDER BY v.data_act DESC +""", [min_date, max_date]) + +invoices = [] +inv_map = {} +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(), + 'items': [], + } + invoices.append(inv) + inv_map[inv['id_vanzare']] = inv + +inv_ids = [inv['id_vanzare'] for inv in invoices] +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), + }) + +print(f"Loaded {len(invoices)} Oracle invoices") + +# --- Match orders → invoices (same as before) --- +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 + sim1 = SequenceMatcher(None, nn1, nn2).ratio() + words1 = nn1.split() + if len(words1) >= 2: + sim2 = SequenceMatcher(None, ' '.join(reversed(words1)), nn2).ratio() + return max(sim1, sim2) + return sim1 + +matches = [] +used_invoices = set() +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 + + for inv in invoices: + if inv['id_vanzare'] in used_invoices: + continue + try: + date_diff = abs(int(order_date.replace('-','')) - int(inv['data_act'].replace('-',''))) + except: + continue + if date_diff > 3: + continue + 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 + sim = name_similarity(order['customer_name'] or '', inv['partener']) + date_score = 1 if date_diff == 0 else (0.7 if date_diff == 1 else (0.4 if date_diff == 2 else 0.2)) + 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']) + +print(f"Matched: {len(matches)} orders → invoices") + +# --- Match line items by PRICE --- +# For each matched pair, match GoMag items → ROA items by line total (qty * price) +# Discovery: SKU → (id_articol, codmat, denumire, qty_ratio) + +# Collect all discovered mappings: sku → list of observations +sku_observations = defaultdict(list) + +for m in matches: + o = m['order'] + inv = m['invoice'] + go_items = o['items'] + # Exclude transport/discount from ROA + roa_items = [ri for ri in inv['items'] if ri['cantitate'] > 0 + and ri['codmat'] not in ('TRANSPORT', 'DISCOUNT')] + roa_transport = [ri for ri in inv['items'] + if ri['codmat'] in ('TRANSPORT', 'DISCOUNT') or ri['cantitate'] < 0] + + go_remaining = list(range(len(go_items))) + roa_remaining = list(range(len(roa_items))) + item_matches = [] + + # Pass 1: match by line total (qty * unit_price_fara_tva) + for gi_idx in list(go_remaining): + gi = go_items[gi_idx] + go_line = gi['quantity'] * gi['price'] # cu TVA + go_line_fara = go_line / (1 + gi['vat']/100) if gi['vat'] else go_line + + for ri_idx in list(roa_remaining): + ri = roa_items[ri_idx] + roa_line = ri['cantitate'] * ri['pret'] # fara TVA + + if abs(go_line_fara - roa_line) < 0.50: + item_matches.append((gi_idx, [ri_idx])) + go_remaining.remove(gi_idx) + roa_remaining.remove(ri_idx) + break + + # Pass 2: match by unit price (for items where qty might differ but price is same) + for gi_idx in list(go_remaining): + gi = go_items[gi_idx] + go_price_fara = gi['price'] / (1 + gi['vat']/100) if gi['vat'] else gi['price'] + + for ri_idx in list(roa_remaining): + ri = roa_items[ri_idx] + if abs(go_price_fara - ri['pret']) < 0.02: + item_matches.append((gi_idx, [ri_idx])) + go_remaining.remove(gi_idx) + roa_remaining.remove(ri_idx) + break + + # Pass 3: 1:1 positional if same count remaining + if len(go_remaining) == 1 and len(roa_remaining) == 1: + item_matches.append((go_remaining[0], [roa_remaining[0]])) + go_remaining = [] + roa_remaining = [] + + # Pass 4: 1:N — one GoMag item matches multiple ROA items by combined total + for gi_idx in list(go_remaining): + gi = go_items[gi_idx] + go_line_fara = (gi['quantity'] * gi['price']) / (1 + gi['vat']/100) if gi['vat'] else gi['quantity'] * gi['price'] + + if len(roa_remaining) >= 2: + 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 = ri1['cantitate'] * ri1['pret'] + ri2['cantitate'] * ri2['pret'] + if abs(go_line_fara - combined) < 1.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) + break + else: + continue + break + + # Record observations + 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] + qty_ratio = ri['cantitate'] / gi['quantity'] if gi['quantity'] else 1 + sku_observations[gi['sku']].append({ + 'type': 'simple' if abs(qty_ratio - round(qty_ratio)) < 0.01 and abs(qty_ratio - 1) < 0.01 else 'repack', + 'id_articol': ri['id_articol'], + 'codmat': ri['codmat'], + 'denumire': ri['denumire'], + 'go_qty': gi['quantity'], + 'roa_qty': ri['cantitate'], + 'qty_ratio': round(qty_ratio, 4), + 'go_price': gi['price'], + 'roa_pret': ri['pret'], + 'product_name': gi['product_name'], + 'order': o['order_number'], + 'factura': f"VM{inv['numar_act']}", + }) + else: + # Complex set + go_line_fara = (gi['quantity'] * gi['price']) / (1 + gi['vat']/100) if gi['vat'] else gi['quantity'] * gi['price'] + for ri in ris: + ri_line = ri['cantitate'] * ri['pret'] + pct = round(ri_line / go_line_fara * 100, 2) if go_line_fara else 0 + qty_ratio = ri['cantitate'] / gi['quantity'] if gi['quantity'] else 1 + sku_observations[gi['sku']].append({ + 'type': 'set', + 'id_articol': ri['id_articol'], + 'codmat': ri['codmat'], + 'denumire': ri['denumire'], + 'go_qty': gi['quantity'], + 'roa_qty': ri['cantitate'], + 'qty_ratio': round(qty_ratio, 4), + 'procent_pret': pct, + 'go_price': gi['price'], + 'roa_pret': ri['pret'], + 'product_name': gi['product_name'], + 'order': o['order_number'], + 'factura': f"VM{inv['numar_act']}", + }) + +conn.close() + +# --- Analyze observations: find consistent mappings --- +print(f"\n{'='*80}") +print(f"ANALYSIS: {len(sku_observations)} unique SKUs with observations") +print(f"{'='*80}") + +# For each SKU, check if all observations agree on the same id_articol +simple_update = {} # SKU → {id_articol, codmat, denumire} — for nom_articole UPDATE +repack_csv = {} # (SKU, codmat) → {cantitate_roa} — for ARTICOLE_TERTI +set_csv = {} # (SKU, codmat) → {cantitate_roa, procent_pret} +inconsistent = {} # SKU → list of conflicting observations +already_has_codmat = {} # SKU already equals codmat + +for sku, obs_list in sorted(sku_observations.items()): + # Group by id_articol + by_articol = defaultdict(list) + for obs in obs_list: + by_articol[obs['id_articol']].append(obs) + + # Check if any observation shows SKU == CODMAT already + if any(obs.get('codmat') == sku for obs in obs_list): + already_has_codmat[sku] = obs_list[0] + continue + + # Filter to types + types = set(obs['type'] for obs in obs_list) + + if 'set' in types: + # Complex set — collect all components + components = {} + for obs in obs_list: + if obs['type'] == 'set': + key = obs['id_articol'] + if key not in components: + components[key] = obs + # Check consistency across observations + if len(components) >= 2: + for art_id, obs in components.items(): + codmat = obs['codmat'] or f"ID:{art_id}" + set_csv[(sku, codmat)] = { + 'id_articol': art_id, + 'cantitate_roa': obs['qty_ratio'], + 'procent_pret': obs['procent_pret'], + 'denumire': obs['denumire'], + 'product_name': obs['product_name'], + } + continue + + if len(by_articol) == 1: + # All observations point to same article + art_id = list(by_articol.keys())[0] + obs = by_articol[art_id][0] + + # Check qty ratios are consistent + ratios = [o['qty_ratio'] for o in by_articol[art_id]] + avg_ratio = sum(ratios) / len(ratios) + + if all(abs(r - avg_ratio) < 0.01 for r in ratios): + if abs(avg_ratio - 1.0) < 0.01: + # Simple 1:1 + simple_update[sku] = { + 'id_articol': art_id, + 'codmat_actual': obs['codmat'], + 'denumire': obs['denumire'], + 'product_name': obs['product_name'], + 'observations': len(by_articol[art_id]), + } + else: + # Repackaging + codmat = obs['codmat'] or f"ID:{art_id}" + repack_csv[(sku, codmat)] = { + 'id_articol': art_id, + 'cantitate_roa': round(avg_ratio, 3), + 'denumire': obs['denumire'], + 'product_name': obs['product_name'], + 'observations': len(by_articol[art_id]), + } + else: + inconsistent[sku] = obs_list + else: + # Multiple different articles for same SKU across orders + if len(by_articol) == 1: + pass # handled above + else: + inconsistent[sku] = obs_list + +# --- Output --- +out_dir = r'C:\gomag-vending\scripts\output' +os.makedirs(out_dir, exist_ok=True) + +print(f"\n{'='*80}") +print(f"RESULTS") +print(f"{'='*80}") + +print(f"\n--- Already mapped (SKU == CODMAT): {len(already_has_codmat)} ---") + +print(f"\n--- Simple 1:1 → UPDATE nom_articole SET codmat = SKU: {len(simple_update)} ---") +for sku, info in sorted(simple_update.items()): + print(f" {sku:25s} → id_articol={info['id_articol']:6d} codmat_actual='{info['codmat_actual'] or ''}' [{info['denumire'][:40]}] ({info['observations']} obs)") + +print(f"\n--- Repackaging → ARTICOLE_TERTI: {len(repack_csv)} ---") +for (sku, codmat), info in sorted(repack_csv.items()): + print(f" {sku:25s} → {codmat:15s} x{info['cantitate_roa']} id_art={info['id_articol']} [{info['denumire'][:35]}] ({info['observations']} obs)") + +print(f"\n--- Complex sets → ARTICOLE_TERTI: {len(set_csv)} ---") +for (sku, codmat), info in sorted(set_csv.items()): + print(f" {sku:25s} → {codmat:15s} {info['procent_pret']:6.2f}% x{info['cantitate_roa']} [{info['denumire'][:35]}]") + +print(f"\n--- Inconsistent (different articles across orders): {len(inconsistent)} ---") +for sku, obs_list in sorted(inconsistent.items()): + arts = set((o['id_articol'], o['denumire'][:30]) for o in obs_list) + print(f" {sku:25s} → {len(arts)} different articles: {'; '.join(f'id={a[0]}({a[1]})' for a in arts)}") + +# Write SQL for simple updates +with open(os.path.join(out_dir, 'update_codmat.sql'), 'w', encoding='utf-8') as f: + f.write("-- UPDATE nom_articole: set codmat = GoMag SKU for 1:1 mappings\n") + f.write("-- Generated from invoice-order matching\n") + f.write("-- VERIFY BEFORE RUNNING!\n\n") + for sku, info in sorted(simple_update.items()): + f.write(f"-- {info['product_name'][:60]} → {info['denumire'][:60]}\n") + f.write(f"-- Current codmat: '{info['codmat_actual'] or ''}' | {info['observations']} order matches\n") + f.write(f"UPDATE nom_articole SET codmat = '{sku}' WHERE id_articol = {info['id_articol']} AND sters = 0;\n\n") + +# Write CSV for repackaging (ARTICOLE_TERTI format) +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', 'id_articol', 'product_name_gomag', 'denumire_roa', 'observations']) + for (sku, codmat), info in sorted(repack_csv.items()): + w.writerow([sku, codmat, info['cantitate_roa'], 100, info['id_articol'], info['product_name'], info['denumire'], info['observations']]) + +# Write CSV for sets +with open(os.path.join(out_dir, 'set_mappings.csv'), 'w', newline='', encoding='utf-8') as f: + w = csv.writer(f) + w.writerow(['sku', 'codmat', 'cantitate_roa', 'procent_pret', 'id_articol', 'product_name_gomag', 'denumire_roa']) + for (sku, codmat), info in sorted(set_csv.items()): + w.writerow([sku, codmat, info['cantitate_roa'], info['procent_pret'], info['id_articol'], info['product_name'], info['denumire']]) + +# Write inconsistent for manual review +with open(os.path.join(out_dir, 'inconsistent_skus.csv'), 'w', newline='', encoding='utf-8') as f: + w = csv.writer(f) + w.writerow(['sku', 'product_name', 'id_articol', 'codmat', 'denumire_roa', 'qty_ratio', 'type', 'order', 'factura']) + for sku, obs_list in sorted(inconsistent.items()): + for obs in obs_list: + w.writerow([sku, obs['product_name'], obs['id_articol'], obs['codmat'] or '', + obs['denumire'], obs['qty_ratio'], obs['type'], obs['order'], obs['factura']]) + +print(f"\nOutput written to {out_dir}:") +print(f" update_codmat.sql - {len(simple_update)} SQL updates for nom_articole") +print(f" repack_mappings.csv - {len(repack_csv)} repackaging mappings") +print(f" set_mappings.csv - {len(set_csv)} complex set mappings") +print(f" inconsistent_skus.csv - {len(inconsistent)} SKUs needing manual review") diff --git a/scripts/match_invoices.py b/scripts/match_invoices.py new file mode 100644 index 0000000..86f7420 --- /dev/null +++ b/scripts/match_invoices.py @@ -0,0 +1,442 @@ +""" +Match 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 + +# Fix Windows console encoding +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 GoMag orders from SQLite --- +print("=" * 80) +print("STEP 1: Loading 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() + +# Get orders with status IMPORTED or ALREADY_IMPORTED +c.execute(""" + SELECT order_number, order_date, customer_name, status, + id_comanda, order_total, billing_name, shipping_name + FROM orders + WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED') + AND order_date >= date('now', '-10 days') + 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() +print(f"Loaded {len(orders)} GoMag orders") +for o in orders: + print(f" {o['order_number']:>10s} | {str(o['order_date'])[:10]} | {(o['customer_name'] or '')[:35]:35s} | {o['order_total'] or 0:10.2f} | {len(o['items'])} items") + +# --- Step 2: Get Oracle invoices (vanzari) with detail lines --- +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 vanzari header + partner name +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 >= SYSDATE - 10 + ORDER BY v.data_act DESC +""") + +invoices = [] +for r in cur: + inv = { + 'id_vanzare': r[0], + 'numar_act': r[1], + 'serie_act': r[2], + '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] if r[8] else ''), + } + invoices.append(inv) + +# Get detail lines for each invoice +for inv in invoices: + 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 + """, [inv['id_vanzare']]) + inv['items'] = [] + for r in cur: + inv['items'].append({ + 'id_articol': r[0], + 'codmat': r[1], + 'denumire': r[2], + 'cantitate': float(r[3] or 0), + 'pret': float(r[4] or 0), + 'pret_cu_tva': float(r[5] or 0), + 'tva_pct': float(r[6] or 0), + }) + +conn.close() +print(f"Loaded {len(invoices)} Oracle invoices") +for inv in invoices: + print(f" {inv['serie_act']}{str(inv['numar_act']):>6s} | {inv['data_act']} | {inv['partener'][:35]:35s} | {inv['total_cu_tva']:10.2f} | {len(inv['items'])} items") + +# --- 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 '' + return name.strip().upper().replace('S.R.L.', 'SRL').replace('S.R.L', 'SRL') + +def name_similarity(n1, n2): + return SequenceMatcher(None, normalize_name(n1), normalize_name(n2)).ratio() + +matches = [] +unmatched_orders = [] +used_invoices = set() + +for order in orders: + 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 same day or +/- 1 day) + inv_date = inv['data_act'] + date_diff = abs( + (int(order_date.replace('-','')) - int(inv_date.replace('-',''))) + ) + if date_diff > 1: + continue + + # Total match (within 5% or 5 lei) + total_diff = abs(order_total - inv['total_cu_tva']) + total_pct = total_diff / max(order_total, 0.01) * 100 + if total_pct > 5 and total_diff > 5: + continue + + # Name similarity + sim = name_similarity(order_name, inv['partener']) + + # Score: name similarity (0-1) + total closeness (0-1) + date match (0-1) + total_score = sim * 0.5 + (1 - min(total_pct/100, 1)) * 0.4 + (1 if date_diff == 0 else 0.5) * 0.1 + + if total_score > best_score: + best_score = total_score + best_match = inv + + if best_match and best_score > 0.3: + matches.append({ + 'order': order, + 'invoice': best_match, + 'score': best_score, + }) + used_invoices.add(best_match['id_vanzare']) + else: + unmatched_orders.append(order) + +print(f"\nMatched: {len(matches)} | Unmatched orders: {len(unmatched_orders)}") +print() + +for m in matches: + o = m['order'] + inv = m['invoice'] + print(f" ORDER {o['order_number']} ({(o['customer_name'] or '')[:25]}, {o['order_total']:.2f})") + print(f" ↔ FACT {inv['serie_act']}{inv['numar_act']} ({inv['partener'][:25]}, {inv['total_cu_tva']:.2f}) [score={m['score']:.2f}]") + print() + +if unmatched_orders: + print("Unmatched orders:") + for o in unmatched_orders: + print(f" {o['order_number']} | {(o['customer_name'] or '')[:35]} | {o['order_total'] or 0:.2f}") + +# --- Step 4: Compare line items for matched pairs --- +print() +print("=" * 80) +print("STEP 4: Line item comparison for matched orders") +print("=" * 80) + +simple_mappings = [] # SKU → CODMAT, same qty/price → update nom_articole +repack_mappings = [] # SKU → CODMAT, different qty → ARTICOLE_TERTI +complex_mappings = [] # 1 SKU → N CODMATs → ARTICOLE_TERTI with procent_pret +unresolved = [] # Cannot determine mapping + +for m in matches: + o = m['order'] + inv = m['invoice'] + go_items = o['items'] + roa_items = inv['items'] + + print(f"\n--- ORDER {o['order_number']} ↔ FACT {inv['serie_act']}{inv['numar_act']} ---") + print(f" GoMag: {len(go_items)} items | ROA: {len(roa_items)} items") + + # Show items side by side + print(f" GoMag items:") + for gi in go_items: + print(f" SKU={gi['sku']:20s} qty={gi['quantity']:6.1f} price={gi['price']:10.2f} [{gi['product_name'][:40]}]") + print(f" ROA items:") + for ri in roa_items: + print(f" COD={str(ri['codmat'] or ''):20s} qty={ri['cantitate']:6.1f} pret={ri['pret']:10.4f} [{(ri['denumire'] or '')[:40]}]") + + # Try matching by price (unit price with TVA) + # GoMag price is usually with TVA, ROA pret can be fara TVA + # Let's try both + go_remaining = list(range(len(go_items))) + roa_remaining = list(range(len(roa_items))) + item_matches = [] + + # First pass: exact 1:1 by total value (qty * price) + for gi_idx in list(go_remaining): + gi = go_items[gi_idx] + go_total = gi['quantity'] * gi['price'] + go_total_fara = go_total / (1 + gi['vat']/100) if gi['vat'] else go_total + + for ri_idx in list(roa_remaining): + ri = roa_items[ri_idx] + roa_total = ri['cantitate'] * ri['pret'] + roa_total_cu = ri['cantitate'] * ri['pret_cu_tva'] + + # Match by total (fara TVA or cu TVA) + if (abs(go_total_fara - roa_total) < 0.5 or + abs(go_total - roa_total_cu) < 0.5 or + abs(go_total - roa_total) < 0.5): + item_matches.append((gi_idx, [ri_idx])) + go_remaining.remove(gi_idx) + roa_remaining.remove(ri_idx) + break + + # Second pass: 1:N matching (one GoMag item → multiple ROA items) + for gi_idx in list(go_remaining): + gi = go_items[gi_idx] + go_total = gi['quantity'] * gi['price'] + go_total_fara = go_total / (1 + gi['vat']/100) if gi['vat'] else go_total + + # Try combinations of remaining ROA items + if len(roa_remaining) >= 2: + # Try pairs + for i, ri_idx1 in enumerate(roa_remaining): + for ri_idx2 in roa_remaining[i+1:]: + ri1 = roa_items[ri_idx1] + ri2 = roa_items[ri_idx2] + combined_total = ri1['cantitate'] * ri1['pret'] + ri2['cantitate'] * ri2['pret'] + combined_total_cu = ri1['cantitate'] * ri1['pret_cu_tva'] + ri2['cantitate'] * ri2['pret_cu_tva'] + if (abs(go_total_fara - combined_total) < 1.0 or + abs(go_total - combined_total_cu) < 1.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) + break + else: + continue + break + + # Report 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] + # Same quantity? + if abs(gi['quantity'] - ri['cantitate']) < 0.01: + # Simple 1:1 + entry = { + 'sku': gi['sku'], + 'codmat': ri['codmat'], + 'id_articol': ri['id_articol'], + 'product_name': gi['product_name'], + 'denumire': ri['denumire'], + 'go_qty': gi['quantity'], + 'roa_qty': ri['cantitate'], + 'go_price': gi['price'], + 'roa_pret': ri['pret'], + 'order': o['order_number'], + 'factura': f"{inv['serie_act']}{inv['numar_act']}", + } + simple_mappings.append(entry) + print(f" ✓ SIMPLE: {gi['sku']} → {ri['codmat']} (qty {gi['quantity']}={ri['cantitate']})") + else: + # Repackaging + cantitate_roa = ri['cantitate'] / gi['quantity'] if gi['quantity'] else 1 + entry = { + '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'], + 'order': o['order_number'], + 'factura': f"{inv['serie_act']}{inv['numar_act']}", + } + repack_mappings.append(entry) + print(f" ✓ REPACK: {gi['sku']} → {ri['codmat']} (qty {gi['quantity']}→{ri['cantitate']}, ratio={cantitate_roa:.3f})") + else: + # Complex set + go_total = gi['quantity'] * gi['price'] + go_total_fara = go_total / (1 + gi['vat']/100) if gi['vat'] else go_total + 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 + entry = { + 'sku': gi['sku'], + 'codmat': ri['codmat'], + 'id_articol': ri['id_articol'], + 'cantitate_roa': ri['cantitate'] / gi['quantity'] if gi['quantity'] else 1, + 'procent_pret': pct, + 'product_name': gi['product_name'], + 'denumire': ri['denumire'], + 'order': o['order_number'], + 'factura': f"{inv['serie_act']}{inv['numar_act']}", + } + complex_mappings.append(entry) + print(f" ✓ SET: {gi['sku']} → {ri['codmat']} ({pct}%)") + + # Unresolved + for gi_idx in go_remaining: + gi = go_items[gi_idx] + 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': [roa_items[i] for i in roa_remaining], + }) + print(f" ? UNRESOLVED: {gi['sku']} ({gi['product_name'][:40]})") + +# --- Step 5: Summary and output --- +print() +print("=" * 80) +print("STEP 5: SUMMARY") +print("=" * 80) + +# Deduplicate mappings +seen_simple = {} +for m in simple_mappings: + key = (m['sku'], m['codmat']) + if key not in seen_simple: + seen_simple[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 + +print(f"\nSimple 1:1 (update nom_articole.codmat = SKU): {len(seen_simple)} unique") +for key, m in seen_simple.items(): + print(f" {m['sku']:25s} → {m['codmat']:15s} | {m['product_name'][:35]} ↔ {(m['denumire'] or '')[:35]}") + +print(f"\nRepackaging (ARTICOLE_TERTI with cantitate_roa): {len(seen_repack)} unique") +for key, m in seen_repack.items(): + print(f" {m['sku']:25s} → {m['codmat']:15s} x{m['cantitate_roa']} | {m['product_name'][:30]} ↔ {(m['denumire'] or '')[:30]}") + +print(f"\nComplex sets (ARTICOLE_TERTI with procent_pret): {len(seen_complex)} unique") +for key, m in seen_complex.items(): + print(f" {m['sku']:25s} → {m['codmat']:15s} {m['procent_pret']}% | {m['product_name'][:30]} ↔ {(m['denumire'] or '')[:30]}") + +print(f"\nUnresolved: {len(unresolved)}") +for u in unresolved: + print(f" {u['sku']:25s} | {u['product_name'][:40]} | order={u['order']}") + +# --- Write CSVs --- +out_dir = r'C:\gomag-vending\scripts\output' +os.makedirs(out_dir, exist_ok=True) + +# Simple mappings CSV (for verification before SQL update) +with open(os.path.join(out_dir, 'simple_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', 'order', 'factura']) + for m in seen_simple.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'], m['order'], m['factura']]) + +# Repackaging CSV (for ARTICOLE_TERTI import) +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 (for ARTICOLE_TERTI import) +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 CSV +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: + roa_str = '; '.join([f"{r['codmat']}({r['cantitate']}x{r['pret']:.2f})" for r in u['roa_remaining']]) + w.writerow([u['sku'], u['product_name'], u['quantity'], u['price'], u['order'], u['factura'], roa_str]) + +# SQL script for simple mappings (update nom_articole) +with open(os.path.join(out_dir, 'update_codmat.sql'), 'w', encoding='utf-8') as f: + f.write("-- Simple SKU → CODMAT: set SKU as CODMAT in nom_articole\n") + f.write("-- VERIFY BEFORE RUNNING!\n\n") + for m in seen_simple.values(): + codmat = m['codmat'] + sku = m['sku'] + f.write(f"-- {m['product_name'][:50]} → {m['denumire'][:50]}\n") + f.write(f"UPDATE nom_articole SET codmat = '{sku}' WHERE codmat = '{codmat}' AND sters = 0;\n\n") + +print(f"\nOutput written to {out_dir}:") +print(f" simple_mappings.csv - {len(seen_simple)} rows (verify, then run update_codmat.sql)") +print(f" repack_mappings.csv - {len(seen_repack)} rows (import via /api/mappings/import-csv)") +print(f" complex_mappings.csv - {len(seen_complex)} rows (import via /api/mappings/import-csv)") +print(f" unresolved.csv - {len(unresolved)} rows (manual review needed)") +print(f" update_codmat.sql - SQL for simple mappings") diff --git a/scripts/reset_sqlite.py b/scripts/reset_sqlite.py new file mode 100644 index 0000000..0d9d852 --- /dev/null +++ b/scripts/reset_sqlite.py @@ -0,0 +1,42 @@ +"""Reset imported orders in SQLite back to SKIPPED after Oracle deletion""" +import sys, sqlite3 +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +db = sqlite3.connect(r'C:\gomag-vending\api\data\import.db') +c = db.cursor() + +# Show before +c.execute("SELECT order_number, customer_name, id_comanda, status FROM orders WHERE status = 'IMPORTED'") +rows = c.fetchall() +print(f"Orders to reset: {len(rows)}") +for r in rows: + print(f" {r[0]} | {r[1]} | id_comanda={r[2]} | {r[3]}") + +# Reset +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' +""") +print(f"\nReset: {c.rowcount} orders → SKIPPED") +db.commit() + +# Verify +c.execute("SELECT status, COUNT(*) FROM orders GROUP BY status") +print("\nStatus after reset:") +for r in c.fetchall(): + print(f" {r[0]:20s} {r[1]}") + +db.close()