add: scripts for invoice-order matching and SKU discovery
Analysis scripts to match GoMag orders with Oracle invoices by date/client/total, then compare line items by price to discover SKU → id_articol mappings. Generates SQL for nom_articole codmat updates and CSV for ARTICOLE_TERTI repackaging/set mappings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
179
scripts/analyze_billing.py
Normal file
179
scripts/analyze_billing.py
Normal file
@@ -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()
|
||||
31
scripts/check_deploy.py
Normal file
31
scripts/check_deploy.py
Normal file
@@ -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 ✗")
|
||||
61
scripts/check_imported.py
Normal file
61
scripts/check_imported.py
Normal file
@@ -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()
|
||||
325
scripts/compare_detail.py
Normal file
325
scripts/compare_detail.py
Normal file
@@ -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)")
|
||||
76
scripts/compare_order.py
Normal file
76
scripts/compare_order.py
Normal file
@@ -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()
|
||||
30
scripts/count_orders.py
Normal file
30
scripts/count_orders.py
Normal file
@@ -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()
|
||||
112
scripts/debug_match.py
Normal file
112
scripts/debug_match.py
Normal file
@@ -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()
|
||||
98
scripts/delete_imported.py
Normal file
98
scripts/delete_imported.py
Normal file
@@ -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.")
|
||||
78
scripts/explore_oracle.py
Normal file
78
scripts/explore_oracle.py
Normal file
@@ -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()
|
||||
30
scripts/fetch_one_order.py
Normal file
30
scripts/fetch_one_order.py
Normal file
@@ -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")
|
||||
533
scripts/match_all.py
Normal file
533
scripts/match_all.py
Normal file
@@ -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")
|
||||
414
scripts/match_by_price.py
Normal file
414
scripts/match_by_price.py
Normal file
@@ -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")
|
||||
442
scripts/match_invoices.py
Normal file
442
scripts/match_invoices.py
Normal file
@@ -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")
|
||||
42
scripts/reset_sqlite.py
Normal file
42
scripts/reset_sqlite.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user