From ccc6a933fad8c08fa17bcebd8b4da1512ddec734 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Fri, 26 Jun 2026 09:46:59 +0000 Subject: [PATCH] feat(scripts): line-level residual check in relink_manual_invoices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linking the VANZARI header (ID_COMANDA) makes the app dashboard show an order facturat, but ROA decides facturat at the LINE level (PACK_FACTURARE.cursor_comanda matches invoiced qty on ID_ARTICOL + exact PRET). When a manual invoice represented lines differently than the order (e.g. per-VAT-rate discounts consolidated into one 0%-TVA line), the order stays nefacturat in ROA despite the header link. Add order_line_residual(): predicts the residual before --apply (via extra_idv) and re-verifies after linking. Warns in the plan, the summary counter, and post-apply when an order will still show nefacturat. The script never touches COMENZI_ELEMENTE — those need a manual line fix. Surfaced by orders 5419/5423 (web 492710430/492710513) on 2026-06-26. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/relink-facturi-manuale.md | 27 +++++++++++ scripts/relink_manual_invoices.py | 77 +++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/docs/relink-facturi-manuale.md b/docs/relink-facturi-manuale.md index 3cdbb2d..2d95ec2 100644 --- a/docs/relink-facturi-manuale.md +++ b/docs/relink-facturi-manuale.md @@ -61,6 +61,26 @@ Verifica si ca acea comanda e activa in ROA (`sters=0`) si **nu** are deja factu La aplicare: `UPDATE VANZARI SET ID_COMANDA = ` + populeaza `orders.factura_*` in SQLite, exact ca aplicatia (`update_order_invoice`). +### ⚠ Verificare reziduu de linie (legatura header nu e mereu suficienta) + +Aplicatia / dashboard-ul marcheaza o comanda „Facturat" doar dupa **legatura header** +(`VANZARI.ID_COMANDA`). **ROA insa verifica la nivel de linie**: in +`PACK_FACTURARE.cursor_comanda`, cantitatea facturata se potriveste cu comanda pe +**`ID_ARTICOL` + `PRET` exact**, iar o linie e „de facturat" cand +`SIGN(CANTITATE) * (CANTITATE − NVL(facturat,0)) > 0`. + +Daca factura manuala a reprezentat liniile **altfel** decat comanda — tipic discounturi +comasate (ex. discounturile pe cote de TVA 11%/21% puse intr-o singura linie la 0% TVA) — +preturile nu se mai potrivesc, deci ROA arata comanda **tot nefacturata** desi headerul +e legat si dashboard-ul o vede facturata. + +Scriptul **prezice** acest reziduu inainte de `--apply` (functia `order_line_residual`, +simuland factura ce urmeaza a fi legata) si il **re-verifica** dupa legare. Cand exista, +afiseaza `!! ATENTIE ...` cu liniile reziduale (ART / cantitate comanda / pret / facturat) +si un contor in rezumat. **Scriptul NU atinge `COMENZI_ELEMENTE`** — aceste cazuri se +corecteaza **manual in ROA** (aliniezi liniile comenzii la factura, ex. comasezi liniile +de discount ca in factura, pastrand valoarea totala). + ## Utilizare Ruleaza **pe serverul de productie VENDING** (are nevoie de Oracle prod + @@ -102,5 +122,12 @@ Codifica reconcilierea din **2026-06-26** (pana de curent la VENDING): pool cazu legate; 3 facturi de depozit corect excluse (CRISS VENDING, COFEE SEVEN TO GO, PANDELE MIOARA); 2 parteneri duplicati semnalati (CERBU, MILITARU). +**Follow-up 2026-06-26 (reziduu de linie):** 2 din cele 12 comenzi (5419/web 492710430, +5423/web 492710513) au ramas nefacturate **in ROA** desi headerul era legat — factura +manuala comasase cele 2 linii de discount (ART 2077, split pe TVA 11%/21%) intr-una la +0% TVA, deci nu se potriveau pe `ID_ARTICOL+PRET`. Reparate manual prin alinierea +liniilor comenzii la factura (comasare in `COMENZI_ELEMENTE`, valoare discount pastrata). +De aici provine verificarea de reziduu de linie adaugata in script. + Vezi si: [oracle-schema-notes.md](oracle-schema-notes.md) (tabele `COMENZI`/`VANZARI`), sectiunea „Facturi & Cache" din [README](../README.md). diff --git a/scripts/relink_manual_invoices.py b/scripts/relink_manual_invoices.py index f54a0cc..7f2fa39 100644 --- a/scripts/relink_manual_invoices.py +++ b/scripts/relink_manual_invoices.py @@ -119,6 +119,45 @@ def comanda_already_invoiced(cur, id_comanda): return cur.fetchone()[0] > 0 +def order_line_residual(cur, id_comanda, extra_idv=None): + """COMENZI_ELEMENTE lines NOT covered by the linked invoice(s), per ROA's own + line-level facturat test (`PACK_FACTURARE.cursor_comanda`): invoiced quantity is + matched to the order on **ID_ARTICOL + exact PRET**, and a line is "still to + invoice" when `SIGN(CANTITATE) * (CANTITATE - NVL(facturat, 0)) > 0`. + + `extra_idv` simulates an invoice about to be linked, so the residual can be + PREDICTED before `--apply` (when VANZARI.ID_COMANDA is not set yet). + + A non-empty result means linking the VANZARI header is NOT enough — ROA will + STILL show the order *nefacturat* (even though the app dashboard, which only + checks the header link, shows it facturat). Typical cause: the manual invoice + consolidated the order's discount lines (e.g. per-VAT-rate discounts merged into + one 0%-TVA line), so the prices no longer match the order's COMENZI_ELEMENTE. + Those need a manual line fix in ROA — the script never touches order lines. + """ + cur.execute( + """ + SELECT A.ID_COMANDA_ELEMENT, A.ID_ARTICOL, A.CANTITATE, A.PRET, + NVL(D.CANTITATE, 0) AS FACTURAT + FROM COMENZI_ELEMENTE A + LEFT JOIN (SELECT B1.ID_ARTICOL, B1.PRET, SUM(B1.CANTITATE) AS CANTITATE + FROM VANZARI A1 + JOIN VANZARI_DETALII B1 + ON A1.ID_VANZARE = B1.ID_VANZARE AND B1.STERS = 0 + WHERE A1.STERS = 0 + AND (A1.ID_COMANDA = :idc OR A1.ID_VANZARE = :idv) + GROUP BY B1.ID_ARTICOL, B1.PRET) D + ON A.ID_ARTICOL = D.ID_ARTICOL AND A.PRET = D.PRET + WHERE A.STERS = 0 + AND A.ID_COMANDA = :idc + AND SIGN(A.CANTITATE) * (A.CANTITATE - NVL(D.CANTITATE, 0)) > 0 + """, + idc=id_comanda, idv=extra_idv, + ) + cols = [d[0].lower() for d in cur.description] + return [dict(zip(cols, r)) for r in cur.fetchall()] + + # ─── SQLite ────────────────────────────────────────────────────────────────── def fetch_uninvoiced_orders(db, days): @@ -275,6 +314,16 @@ def main(): if action == "LINK" and order is not None: orders = [o for o in orders if o["order_number"] != order["order_number"]] + # Predict ROA's line-level residual for each LINK. Linking the VANZARI header is + # not always enough: if the manual invoice represented the lines differently than + # the order (e.g. consolidated discounts), ROA still shows the order nefacturat. + residuals = {} + for inv, action, order, _note in plans: + if action == "LINK" and order is not None: + res = order_line_residual(ora_cur, order["id_comanda"], inv["id_vanzare"]) + if res: + residuals[order["order_number"]] = res + def show(action, detailed=True): rows = [(i, o, n) for (i, a, o, n) in plans if a == action] if not rows: @@ -287,6 +336,15 @@ def main(): tag = f"-> {order['order_number']} (idcom {order['id_comanda']})" if order else "" print(f" IDV={inv['id_vanzare']} {inv['serie_act']}{inv['numar_act']} " f"tot={inv['total_cu_tva']} [{inv['denumire']}] {tag} {note}") + res = residuals.get(order["order_number"]) if order else None + if res: + print(f" !! ATENTIE: dupa legare ROA va arata comanda tot NEFACTURATA " + f"({len(res)} linii reziduale la nivel de element — factura nu le acopera " + f"pe ID_ARTICOL+PRET; probabil discount comasat/0% TVA). Necesita corectie " + f"manuala a liniilor in ROA:") + for r in res: + print(f" ART={r['id_articol']} CANT_COMANDA={r['cantitate']} " + f"PRET={r['pret']} facturat={r['facturat']}") print() for a in ("LINK", "SKIP_AMBIGUOUS", "SKIP_ALREADY"): @@ -295,8 +353,12 @@ def main(): to_link = [(i, o) for (i, a, o, n) in plans if a == "LINK"] ambiguous = sum(1 for (_, a, _, _) in plans if a == "SKIP_AMBIGUOUS") + with_residual = sum(1 for (_, o) in to_link if o and o["order_number"] in residuals) print(f"De legat: {len(to_link)} | De verificat manual (AMBIGUOUS): {ambiguous} | " f"Neatinse (depozit): {sum(1 for (_, a, _, _) in plans if a == 'SKIP_NOMATCH')}") + if with_residual: + print(f"!! Din care {with_residual} raman NEFACTURATE in ROA dupa legare " + f"(reziduu de linie — vezi ATENTIE mai sus; necesita corectie manuala a liniilor).") if not args.apply: print("\n[DRY-RUN] nimic modificat. Reruleaza cu --apply ca sa aplici.") @@ -325,6 +387,21 @@ def main(): db.commit() print(f"\nAplicat: {linked} facturi legate + cache SQLite actualizat.") + # Verifica reziduul REAL dupa legare. Daca > 0, ROA arata comanda tot nefacturata + # desi headerul e legat (app dashboard o vede facturata). Liniile trebuie corectate + # manual in ROA — scriptul nu atinge niciodata COMENZI_ELEMENTE. + still = [] + for inv, order in to_link: + res = order_line_residual(ora_cur, order["id_comanda"]) + if res: + still.append((order, res)) + if still: + print(f"\n!! ATENTIE — {len(still)} comenzi legate dar cu reziduu de linie in ROA " + f"(raman NEFACTURATE pana corectezi liniile manual in ROA):") + for order, res in still: + print(f" {order['order_number']} (idcom {order['id_comanda']}): {len(res)} linii — " + + ", ".join(f"ART={r['id_articol']}@{r['pret']}" for r in res)) + conn.close() db.close()