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:
@@ -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
|
La aplicare: `UPDATE VANZARI SET ID_COMANDA = <comanda>` + populeaza
|
||||||
`orders.factura_*` in SQLite, exact ca aplicatia (`update_order_invoice`).
|
`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
|
## Utilizare
|
||||||
|
|
||||||
Ruleaza **pe serverul de productie VENDING** (are nevoie de Oracle prod +
|
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,
|
legate; 3 facturi de depozit corect excluse (CRISS VENDING, COFEE SEVEN TO GO,
|
||||||
PANDELE MIOARA); 2 parteneri duplicati semnalati (CERBU, MILITARU).
|
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`),
|
Vezi si: [oracle-schema-notes.md](oracle-schema-notes.md) (tabele `COMENZI`/`VANZARI`),
|
||||||
sectiunea „Facturi & Cache" din [README](../README.md).
|
sectiunea „Facturi & Cache" din [README](../README.md).
|
||||||
|
|||||||
@@ -119,6 +119,45 @@ def comanda_already_invoiced(cur, id_comanda):
|
|||||||
return cur.fetchone()[0] > 0
|
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 ──────────────────────────────────────────────────────────────────
|
# ─── SQLite ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def fetch_uninvoiced_orders(db, days):
|
def fetch_uninvoiced_orders(db, days):
|
||||||
@@ -275,6 +314,16 @@ def main():
|
|||||||
if action == "LINK" and order is not None:
|
if action == "LINK" and order is not None:
|
||||||
orders = [o for o in orders if o["order_number"] != order["order_number"]]
|
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):
|
def show(action, detailed=True):
|
||||||
rows = [(i, o, n) for (i, a, o, n) in plans if a == action]
|
rows = [(i, o, n) for (i, a, o, n) in plans if a == action]
|
||||||
if not rows:
|
if not rows:
|
||||||
@@ -287,6 +336,15 @@ def main():
|
|||||||
tag = f"-> {order['order_number']} (idcom {order['id_comanda']})" if order else ""
|
tag = f"-> {order['order_number']} (idcom {order['id_comanda']})" if order else ""
|
||||||
print(f" IDV={inv['id_vanzare']} {inv['serie_act']}{inv['numar_act']} "
|
print(f" IDV={inv['id_vanzare']} {inv['serie_act']}{inv['numar_act']} "
|
||||||
f"tot={inv['total_cu_tva']} [{inv['denumire']}] {tag} {note}")
|
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()
|
print()
|
||||||
|
|
||||||
for a in ("LINK", "SKIP_AMBIGUOUS", "SKIP_ALREADY"):
|
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"]
|
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")
|
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} | "
|
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')}")
|
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:
|
if not args.apply:
|
||||||
print("\n[DRY-RUN] nimic modificat. Reruleaza cu --apply ca sa aplici.")
|
print("\n[DRY-RUN] nimic modificat. Reruleaza cu --apply ca sa aplici.")
|
||||||
@@ -325,6 +387,21 @@ def main():
|
|||||||
db.commit()
|
db.commit()
|
||||||
print(f"\nAplicat: {linked} facturi legate + cache SQLite actualizat.")
|
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()
|
conn.close()
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user