feat(scripts): line-level residual check in relink_manual_invoices

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) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-26 09:46:59 +00:00
parent 698d036de9
commit ccc6a933fa
2 changed files with 104 additions and 0 deletions

View File

@@ -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 = <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).

View File

@@ -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()