docs: runda 6 - cercetari, propuneri si conventia mediului Oracle
Cercetarile si propunerile rundelor 4-6 pe editarea facturii emise. Starea rundei 6 si ce s-a stabilit intra in progres.md. Nou: docs/conventii_mediu_oracle.md - pe dev/test se lucreaza numai cu ROA_CENTRAL si schemele CONTAFIN_ORACLE si MARIUSM_AUTO; schema ACN nu se foloseste. Scripturile de pachet sunt necalificate, deci schema tinta o decide sirul de conectare, nu fisierul. roafacturare.pj2 regenerat de git_sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013gYCNE26G1G7UoGu2ju7aS
This commit is contained in:
427
docs/cercetare/rec_r5_linii_fara_articol_contract.md
Normal file
427
docs/cercetare/rec_r5_linii_fara_articol_contract.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# Diagnostic — linii fara articol pe facturi emise din CONTRACT (rate)
|
||||
|
||||
**Scop.** Doua erori pe facturi emise din contract (explicatie "CONTRACT"), pe formularul unificat
|
||||
`frm_modific2024` (`COMUN\clase\omodificari.vc2`) + `COMUN\programe\ofacturare_editare.prg`:
|
||||
|
||||
- **EROAREA 1** (la deschidere): `Field ID_ARTICOL does not accept null values`,
|
||||
`CONSTRUIESTEPROPUNERESINCRONIZARE`, linia 652 (INSERT in `agg_tvd`).
|
||||
- **EROAREA 2** (la salvare): `Linia '' nu are articol asociat.`
|
||||
|
||||
Cazul de test: factura 10/08/2026, SSS 549, ROMFAST S.R.L., explicatie "CONTRACT", contract
|
||||
"1/21.07.2020" — grid cu linia "RATA 2" (fara codmat, fara pret de achizitie) si linia "A2" (cu
|
||||
codmat 8003510000203). Doar diagnostic — nu s-a atins niciun fisier de cod.
|
||||
|
||||
## Concluzia scurta
|
||||
|
||||
**Nu e un defect de generare.** Liniile de tip "rata" dintr-un contract sunt, prin proiectare, linii
|
||||
`VANZARI_DETALII` fara articol de nomenclator — `id_articol` chiar e `NULL` in Oracle pe randul
|
||||
respectiv, iar denumirea vizibila ("RATA 2") vine din `explicatie`, nu din `nom_articole.denumire`.
|
||||
Asta era deja documentat in `docs\plan_13_unificare_formular_facturare.md:2275` inainte sa apara
|
||||
eroarea curenta — planul #13 stia despre ele, dar guarda de la salvare (`omodificari.vc2:14450`) si
|
||||
cursoarele de agregare pentru sincronizare RUL<->TVD (`ofacturare_editare.prg:617,640`) au fost scrise
|
||||
pornind de la premisa contrara, ca `id_articol` "vine mereu completat din sursa"
|
||||
(`docs\progres.md:166`). Premisa aia era gresita exact pentru liniile de rata, si cele doua erori sunt
|
||||
consecinta directa.
|
||||
|
||||
---
|
||||
|
||||
## 1. De unde vine linia fara articol — proiectare, nu defect
|
||||
|
||||
Generarea facturii din contract (butonul de facturare al `ROACONTRACTE`, `goContract`, tipurile de
|
||||
document `2/6/52` — `ofacturare_comun.prg:261-297`) construieste liniile prin
|
||||
`creeaza_facturacrs` (`ofacturare_comun.prg:1793`, cursorul `crsfacturacrs`, camp
|
||||
`id_articol N(20) null` — **explicit nullable**) si le populeaza prin `prelucreaza_facturacrs`
|
||||
(`ofacturare_comun.prg:1836-1840`):
|
||||
|
||||
```
|
||||
Insert Into (tcCursorDestinatie) (id_articol, ...) ;
|
||||
SELECT CAST(IIF(TYPE(tcCursorSursa+".id_articol")='U',0,id_articol) as N(20)) as id_articol, ...
|
||||
```
|
||||
|
||||
`TYPE(...)='U'` verifica daca **exista coloana** `id_articol` in cursorul sursa (0 doar cand lipseste
|
||||
complet), nu daca valoarea e NULL — deci daca sursa are coloana `id_articol` si pe randul de rata ea
|
||||
e NULL, NULL trece mai departe neschimbat, nu devine 0.
|
||||
|
||||
Structura de rate a contractului insasi confirma asta — cursorul istoric pentru rate
|
||||
(`ofacturare_comun.prg:1905-1908`, comentat, dar pastrat ca document al formei) nu are deloc coloana
|
||||
`id_articol`:
|
||||
|
||||
```
|
||||
*!* Create Cursor crsfactura(id_rata N(20), id_temp N(20), den_rata c(100), ..., denumire c(100), ...)
|
||||
```
|
||||
|
||||
**Deja documentat inainte de eroarea curenta.** Cercetarea S4 din planul #13 a stabilit exact acelasi
|
||||
lucru cand a analizat `cursor_contract`:
|
||||
|
||||
> `docs\plan_13_unificare_formular_facturare.md:2273-2275`:
|
||||
> „`cursor_contract` produce deja doua cursoare — `V_CURSOR` (`crsarticole`, prin delegare la
|
||||
> `cursor_preturi`) si `V_CURSOR2` (`crsarticole1`, articole **sau** rate). Filtrarea se aplica curat
|
||||
> doar pe jumatatea `crsarticole`; **randurile de rata n-au `id_articol`** si raman needitate."
|
||||
|
||||
Concluzie punct 1: **legitim, prin proiectare**. Randul de rata reprezinta o transa de plata dintr-un
|
||||
contract (esalonare), nu un articol din nomenclator — nu exista ce `id_articol` sa i se puna, si codul
|
||||
de generare stie asta de multa vreme.
|
||||
|
||||
## 2. Ce accepta baza de date
|
||||
|
||||
**Confirmat pe Oracle** (`MARIUSM_AUTO@ROA_CENTRAL`, interogare proprie `ALL_TAB_COLUMNS`, SELECT
|
||||
strict, plus datele masurate de team-lead pe `VANZARI`/`VANZARI_DETALII`):
|
||||
|
||||
| Coloana | Tip | Nullable | Observatie |
|
||||
|---|---|---|---|
|
||||
| `ID_ARTICOL` | NUMBER | **Y** | are FK `FK_VANZARE_DET002 -> NOM_ARTICOLE.PK_ARTICOL`; in `NOM_ARTICOLE` **nu exista niciun articol cu `id_articol = 0`** (count = 0) |
|
||||
| `PRET` | NUMBER | **N** | singura coloana NOT NULL din setul verificat |
|
||||
| `PRET_CU_TVA` | NUMBER | Y | flag 0/1, nu pret |
|
||||
| `PRET_ACHIZITIE` | NUMBER | Y | |
|
||||
| `PROC_TVAV` | NUMBER | Y | multiplicator TVA (ex. 1.21), nu procent |
|
||||
| `DISCOUNT_UNITAR` | NUMBER | Y | |
|
||||
| `CANTITATE` | NUMBER | Y | |
|
||||
| `SERIE` / `LOT` / `EXPLICATIE` | VARCHAR2 | Y | |
|
||||
| `ID_VANZARE_SET` / `ID_GESTIUNE` | NUMBER | Y | |
|
||||
|
||||
Confirmare directa pe date reale (interogare team-lead): factura `VANZARI.id_vanzare=1055` (SSS 549,
|
||||
tip=2, `id_ctr=234`), linia `id_vanzare_det=1603` are efectiv `id_articol = NULL`,
|
||||
`id_gestiune = NULL`, `pret_achizitie = NULL`, `explicatie = 'RATA 2'`, `cantitate=1`, `pret=100`,
|
||||
`proc_tvav=1.21`, `id_valuta=3` — deja salvata asa, deci coloana accepta NULL in productie (nu doar
|
||||
teoretic in DDL). In toata baza exista deja **20 de linii active** (`sters=0`) cu `id_articol IS
|
||||
NULL`, din 1124 in total — starea nu e un caz izolat.
|
||||
|
||||
Concluzie punct 2: `ID_ARTICOL` e **nullable, cu FK** catre `NOM_ARTICOLE`. Asta e critic pentru
|
||||
reparatie (punctul 6, mai jos): daca s-ar scrie `0` in loc de `NULL` pe o linie fara articol, FK-ul ar
|
||||
respinge scrierea (`ORA-02291`), pentru ca `id_articol = 0` nu exista in `NOM_ARTICOLE`.
|
||||
|
||||
## 3. De unde se incarca `tvd` si de ce `denumire` iese goala
|
||||
|
||||
`tvd` se incarca prin `IncarcaArticoleFactura` (`ofacturare_editare.prg:305-`), din view-ul
|
||||
`VVANZARI_ARTICOLE`, definit recent chiar pentru acest formular:
|
||||
|
||||
```sql
|
||||
-- D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_02_COMUN_VVANZARI_ARTICOLE.sql:7-34
|
||||
CREATE OR REPLACE VIEW VVANZARI_ARTICOLE AS
|
||||
select vd.id_vanzare, ..., vd.id_articol, ..., vd.explicatie, ...,
|
||||
na.denumire, na.codmat, ng.nume_gestiune, nv.nume_val
|
||||
from vanzari_detalii vd
|
||||
left join nom_articole na on na.id_articol = vd.id_articol
|
||||
...
|
||||
```
|
||||
|
||||
Spre deosebire de `fact_vfacturi_detalii` (punctul 2), **acest view nou nu are fallback** pe
|
||||
`vd.explicatie` cand `vd.id_articol` e NULL — `na.denumire` vine direct dintr-un `LEFT JOIN` pe
|
||||
`nom_articole`, iar cand `id_articol` e NULL, join-ul nu gaseste nimic si `denumire` iese **NULL**.
|
||||
Cursorul `tvd` insusi e declarat cu `id_articol N(20) NULL` (`ofacturare_editare.prg:278`,
|
||||
`omodificari.vc2:14769`), asa ca incarcarea nu crapa aici — doar `denumire` ramane goala pe randul de
|
||||
rata.
|
||||
|
||||
Asta explica exact mesajul din EROAREA 2: `Alltrim(Nvl(denumire,''))` (`omodificari.vc2:14450`) da
|
||||
sir gol pentru ca `tvd.denumire` e cu adevarat NULL pe acel rand, nu pentru ca s-a gasit randul gresit.
|
||||
In grid, textul vizibil "RATA 2" **nu vine din coloana Denumire** (`Column1.ControlSource =
|
||||
"tvd.denumire"`, `omodificari.vc2:12349`), ci din coloana **Explicatie** de mai la dreapta
|
||||
(`Column13.ControlSource = "tvd.explicatie"`, `omodificari.vc2:12444`) — coloana Denumire e goala pe
|
||||
acel rand, exact ca in mesajul de eroare.
|
||||
|
||||
Concluzie punct 3: `denumire` **poate** iesi NULL pe linia de rata, si chiar iese — cauza e absenta
|
||||
fallback-ului pe `explicatie` in `VVANZARI_ARTICOLE`, spre deosebire de view-ul mai vechi
|
||||
`fact_vfacturi_detalii` care il are.
|
||||
|
||||
## 4. Cine a pus garda `Nvl(id_articol,0) = 0` si de ce
|
||||
|
||||
Garda de la `omodificari.vc2:14450` (in `frm_modific2024.inainte_de_do_termin`) a intrat in
|
||||
`COMUN` prin commit-ul `1c42ae0` — *"#6 editare factura emisa: S5 - scrierea sumelor editate in
|
||||
Oracle"*, 10.08.2026 — inainte de S4 (cautarea articolelor pe server) si inainte de decizia 18/S4b
|
||||
despre liniile sintetice. Nu exista niciun commit ulterior care sa modifice acel `IF`.
|
||||
|
||||
**Nu era gandita ca plasa pentru rate.** Documentatia proprie a proiectului o descrie explicit ca
|
||||
ramura considerata **imposibil de declansat** pe fluxul normal:
|
||||
|
||||
> `docs\progres.md:157,166`:
|
||||
> | `:14402` | `Nvl(id_articol,0) = 0` | blocheaza |
|
||||
> ...
|
||||
> „Din cele trei, `Isnull(pret)` e ramura moarta si `id_articol` vine **mereu completat din sursa**,
|
||||
> deci singura care ar fi putut pica e chiar cea de cantitate."
|
||||
|
||||
Asta e in contradictie directa cu ce arata investigatia S4 din acelasi plan (`plan_13:2275`, punctul 1
|
||||
de mai sus), scrisa in aceeasi fereastra de timp — **liniile de rata nu au `id_articol` de la sursa**.
|
||||
Contradictia nu a fost observata pentru ca cele doua fire de lucru (S5/garda de salvare, si S4/cautarea
|
||||
articolelor din `cursor_contract`) nu s-au intersectat pana acum, pe date reale de contract cu rate.
|
||||
|
||||
Concluzie punct 4: garda **nu** a fost pusa pentru ca cineva stia ca `VANZARI_DETALII.ID_ARTICOL` e
|
||||
NOT NULL in Oracle (ar fi contrazis chiar codul de generare din `ofacturare_comun.prg`, punctul 1) —
|
||||
a fost pusa ca validare generica de continut ("linia trebuie sa aiba un articol"), pe premisa (falsa
|
||||
pentru rate) ca `id_articol` vine intotdeauna populat. E o garda prea stricta pentru liniile de rata,
|
||||
nu o reflectare a unei constrangeri de baza de date.
|
||||
|
||||
## 5. Ce mai crapa in aval, cu `id_articol` NULL pe un rand `tvd`/`trul`
|
||||
|
||||
Lista completa, `fisier:linie`, a locurilor care folosesc `id_articol` fara `Nvl` sau il compara cu
|
||||
`=` (deci sensibile la NULL):
|
||||
|
||||
| Loc | Cod | Efect cu `id_articol` NULL |
|
||||
|---|---|---|
|
||||
| `ofacturare_editare.prg:617` | `CREATE CURSOR agg_rul (id_articol N(20), ...)` | camp declarat **fara** `NULL` — pe hartie acelasi tipar ca la `agg_tvd`, dar **confirmat inaccesibil in practica** (vezi punctul 7: `RUL.ID_ARTICOL` e `NOT NULL` in Oracle, 0 randuri active cu NULL) |
|
||||
| `ofacturare_editare.prg:626,630-631` | `LOCATE FOR id_articol = trul.id_articol` apoi `INSERT INTO agg_rul ... VALUES (trul.id_articol, ...)` | fara risc practic — `trul.id_articol` nu poate fi NULL (punctul 7) |
|
||||
| `ofacturare_editare.prg:640,647,651-652` | `CREATE CURSOR agg_tvd (id_articol N(20), ...)` + `INSERT INTO agg_tvd ... VALUES (tvd.id_articol, ...)` | **cauza directa a EROAREA 1** — camp NOT NULL, valoare NULL |
|
||||
| `ofacturare_editare.prg:660-663` | `SELECT id_articol FROM agg_rul UNION SELECT id_articol FROM agg_tvd INTO CURSOR crs_articole_unite` | **verificat empiric (punctul 7)**: `UNION` trateaza NULL=NULL ca echivalent la deduplicare — oricate randuri NULL ar aduce `agg_tvd`, `crs_articole_unite` primeste **un singur** rand NULL |
|
||||
| `ofacturare_editare.prg:671,687` | `LOCATE FOR id_articol = crs_articole_unite.id_articol` (o data pe `agg_rul`, o data pe `agg_tvd`) | **verificat empiric (punctul 7), corecteaza ipoteza initiala**: `LOCATE FOR camp = NULL` **nu gaseste niciodata**, in VFP, nici cand campul chiar contine NULL pe randul cautat — nu doar cand valorile difera. Efectul nu e o pereche falsa „Adaugare”+„Semnalare” (ipoteza initiala, neconfirmata) — e mai simplu si mai tacut: randul NULL din `crs_articole_unite` iese cu `llGasitSursa=.F.` **si** `llGasitTinta=.F.` pe ambele ramuri, cade in `OTHERWISE` cu 0=0, si **nu genereaza nicio linie in `propunere_sincronizare`** — dispare complet din sincronizare, fara eroare, fara semnalare |
|
||||
| `ofacturare_editare.prg:849,887,923` | `LOCATE FOR id_articol = m.tnIdArticol` in `AplicaModificareTvd`, `AplicaAdaugareTvd`, `AplicaModificareTrul` | irelevant in practica daca se aplica reparatia recomandata la punctul 7 (Varianta B) — `propunere_sincronizare` nu mai ajunge sa contina randuri cu `id_articol` NULL, deci aceste functii nu sunt niciodata chemate cu `tnIdArticol` NULL |
|
||||
| `omodificari.vc2:14450` | `IF Nvl(id_articol,0) = 0` | **cauza directa a EROAREA 2** — garda generica, prea stricta pentru rate |
|
||||
|
||||
Concluzie punct 5: reparatia nu se opreste la `agg_tvd` (linia care a crapat primul) — `agg_rul` are
|
||||
aceeasi lipsa de `NULL` pe declaratie, dar dovedit inaccesibila (punctul 7). Mecanismul de potrivire
|
||||
pe `id_articol =` din sincronizarea RUL<->TVD (`:671`, `:687`) e **verificat empiric ca nu functioneaza
|
||||
pentru NULL**, cu efect de disparitie tacuta din propunere, nu de eroare — vezi analiza completa si
|
||||
reparatia recomandata la punctul 7.
|
||||
|
||||
## 6. `Nvl(...,0)` la scriere in `ScrieArticoleFacturaEditate` — campuri care distrug informatie
|
||||
|
||||
Chiar daca EROAREA 1 si EROAREA 2 s-ar repara (cursoare + garda), `ScrieArticoleFacturaEditate`
|
||||
(`ofacturare_editare.prg`) tot ar strica un rand de rata la prima salvare reusita, pentru ca **atat
|
||||
UPDATE-ul liniilor pastrate (`:505-517`), cat si INSERT-ul liniilor noi (`:535-547`)** trec mai multe
|
||||
campuri prin `Nvl(camp,0)` inainte sa le scrie in Oracle — convertind orice NULL legitim in `0`
|
||||
literal:
|
||||
|
||||
```
|
||||
-- UPDATE (linii existente), ofacturare_editare.prg:505-510
|
||||
lcSql = [update vanzari_detalii set sters = 0, cantitate = ] + Alltrim(Str(Nvl(cantitate,0),18,3)) + ;
|
||||
[, pret = ] + Alltrim(Str(Nvl(pret,0),18,4)) + [, pret_cu_tva = ] + Alltrim(Str(Nvl(pret_cu_tva,0))) + ;
|
||||
[, pret_achizitie = ] + Alltrim(Str(Nvl(pret_achizitie,0),18,4)) + ;
|
||||
[, proc_tvav = ] + Alltrim(Str(Nvl(proc_tvav,0),18,4)) + ;
|
||||
[, discount_unitar = ] + Alltrim(Str(Nvl(discount_unitar,0),18,4)) + ;
|
||||
[, id_articol = ] + Alltrim(Str(Nvl(id_articol,0))) + ;
|
||||
[, id_gestiune = ] + Iif(Isnull(id_gestiune),[NULL],Alltrim(Str(id_gestiune))) + ;
|
||||
...
|
||||
|
||||
-- INSERT (linii noi), ofacturare_editare.prg:535-546
|
||||
lcSql = [insert into vanzari_detalii (id_vanzare, id_articol, cantitate, pret, pret_cu_tva, proc_tvav, discount_unitar, ] + ;
|
||||
[id_gestiune, cont, id_valuta, id_jtva_coloana, serie, explicatie, taxcode, lot, pret_achizitie, id_utils, dataoras) values (] + ;
|
||||
Alltrim(Str(m.tnIdVanzare)) + [,] + Alltrim(Str(Nvl(id_articol,0))) + [,] + Alltrim(Str(Nvl(cantitate,0),18,3)) + [,] + ;
|
||||
Alltrim(Str(Nvl(pret,0),18,4)) + [,] + Alltrim(Str(Nvl(pret_cu_tva,0))) + [,] + Alltrim(Str(Nvl(proc_tvav,0),18,4)) + [,] + ;
|
||||
Alltrim(Str(Nvl(discount_unitar,0),18,4)) + [,] + Iif(Isnull(id_gestiune),[NULL],Alltrim(Str(id_gestiune))) + [,] + ;
|
||||
...
|
||||
Alltrim(Str(Nvl(pret_achizitie,0),18,4)) + [,] + Alltrim(Str(gnIdUtil)) + [, sysdate)]
|
||||
```
|
||||
|
||||
Observatie de proiectare: **`id_gestiune`, `id_valuta`, `id_jtva_coloana`, `taxcode`, `cont`, `serie`,
|
||||
`explicatie`, `lot` sunt deja tratate corect**, cu `Iif(Isnull(...),[NULL],...)` — modelul corect
|
||||
exista deja in acelasi bloc de cod, doar ca nu s-a aplicat si celor sase campuri de mai jos.
|
||||
|
||||
Evaluare camp cu camp, cu nullabilitatea confirmata la punctul 2:
|
||||
|
||||
| Camp | Nullable in Oracle | `Nvl(...,0)` e corect? | Motiv |
|
||||
|---|---|---|---|
|
||||
| **`id_articol`** | Y, **cu FK** spre `NOM_ARTICOLE` | **NU — critic** | `0` nu exista in `NOM_ARTICOLE` (count=0) — pe o linie de rata cu `id_articol` real NULL, scrierea ar da **`ORA-02291`** (violare FK), nu doar ar corupe tacut o valoare. Chiar daca EROAREA 2 s-ar repara la nivel de garda VFP, salvarea tot ar crapa aici, doar cu un mesaj Oracle mai putin clar decat "nu are articol asociat". **Singurul din lista care produce eroare dura, nu doar corupere tacuta.** |
|
||||
| **`pret_achizitie`** | Y | **NU** | Pe randul real `id_vanzare_det=1603` e deja NULL in productie — "cost de achizitie necunoscut/nu se aplica" (rata n-are cost de achizitie, nu e marfa). `Nvl(...,0)` il transforma in "cost de achizitie efectiv zero", care e o valoare falsa pentru orice calcul de marja/profit facut ulterior direct din `VANZARI_DETALII` (in afara VFP) |
|
||||
| **`proc_tvav`** | Y | **Probabil nu, cu rezerva** | E un multiplicator TVA (1.21, nu 21%) — `0` nu inseamna "fara TVA" in aceasta conventie, ci ar corupe orice calcul care-l inmulteste (baza * 0 = 0). Pe randul de test `proc_tvav` e deja completat (1.21), deci `Nvl` nu se declanseaza acolo — dar daca exista/ar exista vreun rand cu `proc_tvav` NULL, scrierea lui ca `0` e o valoare periculoasa, nu neutra. **NEVERIFICAT**: daca exista azi randuri reale cu `proc_tvav` NULL (n-am interogat) |
|
||||
| **`discount_unitar`** | Y | **Risc scazut** | NULL si 0 sunt aproape echivalente semantic ("fara discount") — conversia schimba tipul valorii, nu sensul ei practic. Recomandat de aliniat la tiparul `Isnull` din acelasi bloc, mai mult pentru consistenta decat pentru un bug observat |
|
||||
| **`cantitate`** | Y | **Risc scazut, deja filtrat in amonte** | Garda `omodificari.vc2:14386` (`Nvl(cantitate,0) <= 0` -> blocheaza cu "are cantitatea 0") ruleaza **inaintea** lui `ScrieArticoleFacturaEditate` si opreste salvarea pe orice rand cu cantitate NULL sau <= 0. Pana la aceasta functie, `cantitate` e deja garantat non-NULL si non-zero pe calea normala — `Nvl(...,0)` de aici e defensiv, nu activ distructiv |
|
||||
| **`pret`** | **N (NOT NULL)** | **DA — corect** | Coloana Oracle nu accepta NULL; garda `omodificari.vc2:14394` (`Isnull(pret)` -> blocheaza) opreste deja NULL inainte de scriere. `Nvl(pret,0)` e aici plasa de siguranta potrivita pentru o coloana NOT NULL, nu o corupere |
|
||||
| **`pret_cu_tva`** | Y | **Risc scazut** | E flag boolean 0/1 (comentariu `:501` in acelasi fisier: "pret_cu_tva e flag (0/1), nu pret"), nu o valoare cu semnificatie de "lipsa" — `Nvl(...,0)` echivaleaza NULL cu "fara TVA in pret", o valoare implicita rezonabila pentru un flag |
|
||||
|
||||
Concluzie punct 6: din cele sase campuri, **doar `id_articol` produce o eroare Oracle dura** (FK) daca
|
||||
nu se repara — e blocajul real, dincolo de garda VFP. **`pret_achizitie` corupe tacut date reale deja
|
||||
existente** (randul 1603 chiar are NULL azi) fara sa arunce nicio eroare. `proc_tvav` e risc teoretic,
|
||||
neconfirmat pe date. Restul (`discount_unitar`, `cantitate`, `pret_cu_tva`) sunt scrieri defensive
|
||||
fara efect practic distructiv, iar `pret` e deja corect (coloana NOT NULL + garda VFP dedicata).
|
||||
|
||||
## 7. Sarim liniile fara articol la sursa, sau reparam matching-ul cu NULL — analiza cu dovada VFP
|
||||
|
||||
**Date suplimentare masurate de team-lead pe Oracle**: `RUL.ID_ARTICOL` e **NOT NULL** in Oracle
|
||||
(`all_tab_columns.nullable = 'N'`), si exista **0** randuri `RUL` active cu `id_articol IS NULL`.
|
||||
Documentul de test (SSS 549, cod 1140920) are **un singur rulaj**: `RUL.id_rul=10947,
|
||||
id_articol=4294507173, cant=0, cante=1, pretvtva=121, id_tip_rulaj=0` — si **doua** linii `tvd`
|
||||
(rata fara articol + articolul 4294507173). Deci pe acest document, `agg_rul` n-ar primi niciodata
|
||||
un rand NULL — doar `agg_tvd` (din `tvd`) aduce randul de rata.
|
||||
|
||||
**Concluzie directa**: rândul `ofacturare_editare.prg:617` (`agg_rul` declarat fara `NULL`) e nesigur
|
||||
pe hartie, dar **dovedit inaccesibil** — `trul`, incarcat din `RUL`, nu poate aduce NULL, constrangerea
|
||||
Oracle il blocheaza la sursa. Merita uniformizat pentru consistenta cu `tvd`/`agg_tvd` (defensiv,
|
||||
cost zero), dar nu e un bug activ.
|
||||
|
||||
### Testat empiric, nu presupus
|
||||
|
||||
Am rulat un test izolat in VFP (`vfp9.exe -A -T`, fara Oracle, fara formulare), reproducand exact
|
||||
structura din `ConstruiestePropunereSincronizare` — `test_null_locate_union.prg`, pastrat in
|
||||
scratchpad, log complet in `test_null_locate_union.log` (acelasi director). Rezultate:
|
||||
|
||||
| Test | Ce verifica | Rezultat masurat |
|
||||
|---|---|---|
|
||||
| 1 | `SELECT id_articol FROM agg_rul UNION SELECT id_articol FROM agg_tvd` cu 1 rand NULL in `agg_tvd` | **2 randuri** in rezultat: unul NULL, unul cu valoare — `UNION` pastreaza NULL-ul, nu-l pierde |
|
||||
| 2 | Acelasi UNION, dar cu **2** randuri NULL in `agg_tvd` (simuleaza doua rate pe acelasi document) | Tot **2 randuri** in rezultat — `UNION` trateaza cele doua NULL-uri **ca echivalente** la deduplicare (comportament SQL standard de grupare, diferit de semantica lui `=`) |
|
||||
| 3 | `LOCATE FOR id_articol = m.lnTest`, cu `m.lnTest = .NULL.`, pe un cursor care chiar are un rand cu `id_articol` NULL | **`FOUND() = .F.`** — nu gaseste randul, desi acesta exista |
|
||||
| 4 | `LOCATE FOR id_articol = crs_articole_unite.id_articol`, exact tiparul de la `:671`/`:687`, cu ambele parti NULL | **`FOUND() = .F.`** — confirma acelasi lucru intre doua cursoare, nu doar cu un memvar |
|
||||
|
||||
**Interpretare, aplicata pe `ConstruiestePropunereSincronizare`**: daca EROAREA 1 s-ar repara doar prin
|
||||
adaugarea `NULL` la declaratiile `agg_rul`/`agg_tvd` (Varianta A propusa initial), randul NULL din
|
||||
`crs_articole_unite` (Test 1/2 arata ca **exista**, indiferent de cate rate sunt) ar ajunge in bucla
|
||||
principala de la `:665-773`. Acolo, `LOCATE FOR id_articol = crs_articole_unite.id_articol` **pe
|
||||
`agg_rul` si pe `agg_tvd`, ambele** (Test 3/4) **nu gaseste nimic**, chiar daca `agg_tvd` chiar contine
|
||||
randul cautat. Rezultatul: `lnNrandRul=0` si `lnNrandTvd=0` raman la valorile implicite, deci
|
||||
`llGasitSursa=.F.` si `llGasitTinta=.F.` **pe ambele ramuri** — nu se potriveste niciun `CASE` din
|
||||
`DO CASE` (nici „Adaugare", nici „Semnalare"), cade in `OTHERWISE` cu `0=0`, si **liniei de rata nu i
|
||||
se genereaza nicio linie in `propunere_sincronizare`**. **Corectez aici ipoteza din punctul 5 al
|
||||
raportului initial** („pereche falsa Adaugare+Semnalare") — nu e o pereche falsa, e o **disparitie
|
||||
tacuta**, mai greu de observat: randul nu genereaza nici eroare, nici avertizare, doar lipseste din
|
||||
propunere, din `SemnaturaDivergenteSincronizare` (`omodificari.vc2:14839`, filtreaza pe aceleasi trei
|
||||
actiuni) si deci din dialogul de sincronizare.
|
||||
|
||||
### Raspunsul la intrebare: Varianta B (excludere la sursa) e cea corecta
|
||||
|
||||
Team-lead a propus alternativa: `ConstruiestePropunereSincronizare` sare complet peste liniile `tvd`
|
||||
(si, defensiv, `trul`) fara `id_articol`, cu `SCAN FOR Nvl(sters,0) <> 1 AND !Isnull(id_articol)` in
|
||||
loc de `SCAN FOR Nvl(sters,0) <> 1` la `:619` (trul->agg_rul) si `:642` (tvd->agg_tvd).
|
||||
|
||||
**E varianta corecta**, din trei motive, toate confirmate mai sus:
|
||||
|
||||
1. **`id_articol` e chiar cheia de comparatie a mecanismului** — comentariul din capul fisierului
|
||||
(`ofacturare_editare.prg:14`: "agregate pe id_articol, in ambele sensuri") o spune explicit. O
|
||||
linie fara cheie nu poate, prin definitie, sa aiba corespondent — nu e un caz special de tratat cu
|
||||
grija, e un rand care nu apartine acestui mecanism.
|
||||
2. **Rezultatul practic al Variantei B e identic cu ce se intampla azi accidental prin Varianta A**
|
||||
(randul de rata nu ajunge in `propunere_sincronizare`, deci nu apare in dialog, nu declanseaza
|
||||
„Aplica" pe el) — dar **prin filtrare explicita**, nu prin efectul secundar, neverificat de nimeni
|
||||
pana acum, al lui `LOCATE FOR ... = NULL`. Daca cineva „repara" mai tarziu comparatiile cu
|
||||
`Nvl(id_articol,-1) = Nvl(...,-1)` (Varianta A din reparatia initiala, sau orice alt developer care
|
||||
descopera si "repara" fara sa stie de aceasta analiza), comportamentul s-ar schimba brusc de la
|
||||
"dispare tacut" la "participa la matching" — cu riscul semnalat deja in reparatia initiala, ca mai
|
||||
multe rate distincte de pe acelasi document s-ar agrega/compara laolalta (confirmat acum de Testul
|
||||
2: `UNION` le trateaza deja ca un singur grup NULL). Varianta B evita complet acest risc, pentru ca
|
||||
liniile de rata nici nu ajung sa fie agregate.
|
||||
3. **Varianta B rezolva si EROAREA 1 la radacina**, fara sa mai fie nevoie sa se adauge `NULL` la
|
||||
declaratiile `agg_rul`/`agg_tvd` — daca linia cu `id_articol` NULL nu mai intra in bucla de `SCAN`
|
||||
care face `INSERT INTO agg_tvd`, nu mai exista nicio incercare de a insera NULL intr-un camp NOT
|
||||
NULL. (Adaugarea `NULL` la declaratii ramane totusi recomandata, ca plasa de siguranta ieftina,
|
||||
independent de asta.)
|
||||
|
||||
**Efect pe cazul concret SSS 549, cu Varianta B**: `agg_rul` are 1 rand (`4294507173`), `agg_tvd` are
|
||||
1 rand (`4294507173`, linia de rata fiind sarita la `SCAN`). `crs_articole_unite` are 1 rand. Randul
|
||||
de rata nu apare nicaieri in `propunere_sincronizare` — nici „Adaugare", nici „Semnalare", nici „N-A".
|
||||
La `SemnaturaDivergenteSincronizare`, semnatura nu contine nimic despre rata — deschiderea/inchiderea
|
||||
dialogului de sincronizare nu e afectata de ea, la deschidere sau la salvare. La „Aplica" (daca
|
||||
utilizatorul il apasa pentru articolul real), `AplicaModificareTrul`/`AplicaAdaugareTvd`/
|
||||
`AplicaModificareTvd` nu sunt niciodata chemate cu `tnIdArticol` NULL, pentru ca randul de rata nu
|
||||
ajunge in `propunere_sincronizare` ca sa fie scanat de `AplicaSincronizareArticole`
|
||||
(`ofacturare_editare.prg:785-822`, bucla `SCAN FOR Inlist(Alltrim(actiune), 'Modificare',
|
||||
'Adaugare')` la `:802`) — deci ingrijorarea din punctul 5 despre `:849/887/923` nu se mai
|
||||
materializeaza.
|
||||
|
||||
---
|
||||
|
||||
## Reparatie propusa (NU aplicata)
|
||||
|
||||
### EROAREA 1 — cursoarele de agregare
|
||||
|
||||
**Recomandare finala (dupa analiza si testul din punctul 7): Varianta B — exclude liniile fara
|
||||
`id_articol` la sursa**, in `ConstruiestePropunereSincronizare`:
|
||||
|
||||
```
|
||||
SELECT trul
|
||||
SCAN FOR Nvl(sters,0) <> 1 AND !Isnull(id_articol)
|
||||
...
|
||||
```
|
||||
(`ofacturare_editare.prg:619`, azi `SCAN FOR Nvl(sters,0) <> 1`)
|
||||
|
||||
```
|
||||
SELECT tvd
|
||||
SCAN FOR Nvl(sters,0) <> 1 AND !Isnull(id_articol)
|
||||
...
|
||||
```
|
||||
(`ofacturare_editare.prg:642`, azi `SCAN FOR Nvl(sters,0) <> 1`)
|
||||
|
||||
E coerent cu faptul ca `id_articol` e chiar cheia de comparatie a mecanismului (comentariul din capul
|
||||
fisierului, `:14`) — o linie fara cheie n-are cum sa aiba corespondent, la fel cum deja se trateaza
|
||||
separat cazul "Articol nestocat, fara corespondent in rulaje" (`:730-732`). Filtrul de mai sus **repara
|
||||
si EROAREA 1** — nu mai ajunge nicio valoare NULL la `INSERT INTO agg_tvd`/`agg_rul`, deci declaratiile
|
||||
cursoarelor n-ar mai avea nevoie sa accepte `NULL` ca sa nu crape. Recomandat totusi, ca plasa de
|
||||
siguranta ieftina si pentru consistenta cu `tvd`:
|
||||
|
||||
```
|
||||
CREATE CURSOR agg_rul (id_articol N(20) NULL, denumire C(100), codmat C(30), cant N(12,3), val N(16,4), nrand I)
|
||||
CREATE CURSOR agg_tvd (id_articol N(20) NULL, denumire C(100), codmat C(30), cant N(12,3), val N(16,4), nrand I, in_stoc I)
|
||||
```
|
||||
(`ofacturare_editare.prg:617`, `:640`)
|
||||
|
||||
**Varianta alternativa, respinsa**: doar adauga `NULL` la declaratii si repara comparatiile `id_articol
|
||||
= X` cu o forma NULL-safe (`Nvl(id_articol,-1) = Nvl(m.tnIdArticol,-1)`, acelasi tipar folosit in
|
||||
`ofacturare_comun.prg:1334,1349`). Tehnic ar functiona, dar **verificat empiric (punctul 7, Test 2)**:
|
||||
`UNION`-ul deja trateaza toate liniile NULL ca **un singur grup** — cu aceasta varianta, doua sau mai
|
||||
multe rate distincte de pe acelasi document s-ar agrega si compara laolalta, ca si cum ar fi acelasi
|
||||
"articol". Nu aduce niciun beneficiu fata de Varianta B (rezultatul pentru randul de rata tot nu
|
||||
trebuie sa apara in sincronizare), doar risc suplimentar — de aceea nu e recomandata.
|
||||
|
||||
### EROAREA 2 — garda de la salvare
|
||||
|
||||
Trei variante, e o decizie de produs:
|
||||
|
||||
- **Varianta A — restrange garda la liniile cu articol real posibil**: cere articol doar cand linia
|
||||
nu e de tip rata — de exemplu cand `id_vanzare_set = 0` **si** linia are `pret_achizitie` (semn ca
|
||||
vine dintr-un flux de articole reale), sau invers, sare garda cand `Isnull(id_articol) AND
|
||||
!Empty(explicatie)` (semnul unei linii "sintetice" descrise prin `explicatie`, ca ratele).
|
||||
- **Varianta B — scoate garda complet** pentru randuri cu `id_articol` NULL de la incarcare (nu
|
||||
adaugate manual in sesiunea curenta) — presupune sa distingi in `tvd` intre "NULL de la Oracle" si
|
||||
"NULL pentru ca utilizatorul a adaugat un rand si n-a ales inca un articol" (al doilea caz chiar
|
||||
trebuie blocat).
|
||||
- **Varianta C — transforma in intrebare (confirmare), nu blocaj** — la fel ca gardul de pret de
|
||||
achizitie 0 de doua linii mai jos (`:14458-14464`), las utilizatorul sa decida daca salveaza cu
|
||||
randul fara articol.
|
||||
|
||||
Recomandarea de continut (nu de aplicat acum): **Varianta A**, pentru ca pastreaza garda utila pe
|
||||
cazul ei real (rand adaugat manual din nomenclator, fara articol ales din greseala), fara sa oblige
|
||||
verificarea de tip "e linie de rata veche" peste tot.
|
||||
|
||||
### Scrierea in Oracle — `ScrieArticoleFacturaEditate` (punctul 6)
|
||||
|
||||
Minim, pe ambele blocuri (UPDATE `:505-510`, INSERT `:535-546`), aliniaza `id_articol` si
|
||||
`pret_achizitie` la tiparul `Iif(Isnull(...),[NULL],...)` deja folosit pentru `id_gestiune`/
|
||||
`id_valuta`/`id_jtva_coloana`/`taxcode` in acelasi bloc:
|
||||
|
||||
```
|
||||
[, id_articol = ] + Iif(Isnull(id_articol),[NULL],Alltrim(Str(id_articol))) + ;
|
||||
...
|
||||
[, pret_achizitie = ] + Iif(Isnull(pret_achizitie),[NULL],Alltrim(Str(pret_achizitie,18,4))) + ;
|
||||
```
|
||||
|
||||
**`id_articol` e obligatoriu de facut** — altfel, chiar dupa ce EROAREA 1 si EROAREA 2 s-ar repara,
|
||||
prima salvare a unei facturi cu linie de rata ar cadea pe `ORA-02291` (FK spre `NOM_ARTICOLE`, care
|
||||
n-are rand cu `id=0`). `pret_achizitie` e recomandat, ca sa nu se scrie tacut "cost de achizitie 0" pe
|
||||
un rand care azi are NULL. `proc_tvav` — de decis dupa ce se clarifica NEVERIFICAT-ul de mai jos (daca
|
||||
poate fi NULL pe un rand real, acelasi tipar se aplica si lui). `discount_unitar`, `cantitate`,
|
||||
`pret_cu_tva` — opional, doar pentru consistenta cu restul blocului, fara bug observat.
|
||||
|
||||
### `VVANZARI_ARTICOLE` — fallback pe `explicatie`
|
||||
|
||||
Independent de cele doua erori, view-ul (`D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_02_COMUN_VVANZARI_ARTICOLE.sql`)
|
||||
ar putea capata acelasi fallback ca `fact_vfacturi_detalii`:
|
||||
`NVL2(vd.id_articol, na.denumire, vd.explicatie) as denumire` — ca sa nu mai iasa coloana Denumire
|
||||
goala pe randurile de rata (in prezent doar coloana Explicatie arata ceva, iar Denumire ramane goala
|
||||
in grid, ceea ce probabil nu era intentionat cand s-a proiectat view-ul).
|
||||
|
||||
## NEVERIFICAT
|
||||
|
||||
- **Constrangerea DDL exacta pe `VANZARI_DETALII.ID_ARTICOL` — REZOLVAT.** Confirmat direct pe Oracle
|
||||
(`ALL_TAB_COLUMNS`, `MARIUSM_AUTO@ROA_CENTRAL`): `ID_ARTICOL` e `NULLABLE = Y`, cu FK
|
||||
`FK_VANZARE_DET002` spre `NOM_ARTICOLE.PK_ARTICOL`; `NOM_ARTICOLE` n-are niciun rand cu `id_articol =
|
||||
0`. Vezi tabelul din punctul 2. (Scriptul original de `CREATE TABLE` tot nu e in
|
||||
`D:\ROA\DATABASE\SCRIPTURI_CLAR` — dar nu mai e nevoie de el, DDL-ul curent s-a citit direct din
|
||||
dictionarul de date.)
|
||||
- **Daca exista azi randuri reale in `VANZARI_DETALII` cu `proc_tvav` NULL** — n-am interogat asta
|
||||
direct (doar am confirmat ca *poate* fi NULL, `NULLABLE=Y`). Pe randul de test `proc_tvav=1.21`, deci
|
||||
nu se declanseaza acolo. Daca nu exista niciodata NULL pe randuri reale, `Nvl(proc_tvav,0)` din
|
||||
punctul 6 e o plasa de siguranta fara efect, la fel ca la `pret`/`cantitate`; daca exista, e periculos
|
||||
(baza * 0 = 0 in orice recalcul din afara VFP). Se poate lamuri cu un `SELECT count(*) FROM
|
||||
vanzari_detalii WHERE sters=0 AND proc_tvav IS NULL`.
|
||||
- **De ce n-a crapat `agg_rul` — REZOLVAT.** `RUL.ID_ARTICOL` e **NOT NULL** in Oracle
|
||||
(`all_tab_columns.nullable = 'N'`, masurat de team-lead), si exista **0** randuri `RUL` active cu
|
||||
`id_articol IS NULL`. Documentul de test (SSS 549) are un singur rulaj, cu articolul real
|
||||
`4294507173` — rata n-are corespondent in `RUL`. Deci `trul` nu poate aduce niciodata NULL, iar
|
||||
declaratia `agg_rul (id_articol N(20), ...)` fara `NULL` (`:617`), desi nesigura pe hartie, e
|
||||
dovedit inaccesibila pe calea normala — nu doar "n-a crapat inca", ci "nu poate crapa" atat timp cat
|
||||
constrangerea Oracle ramane in vigoare. Vezi punctul 7.
|
||||
- **Comportamentul VFP la NULL in `UNION` si `LOCATE FOR` — REZOLVAT, testat empiric.** Vezi punctul 7:
|
||||
test izolat `vfp9.exe -A -T`, script pastrat in
|
||||
`C:\Users\mmari\AppData\Local\Temp\claude\D--ROA-ROAFACTURARE\81fbd7c8-41d4-4341-a83a-95736e6dc6d3\scratchpad\test_null_locate_union.prg`,
|
||||
log in acelasi director (`test_null_locate_union.log`) — fisiere temporare de sesiune, nu in
|
||||
arborele proiectului. `UNION` trateaza NULL=NULL ca echivalent la deduplicare; `LOCATE FOR camp =
|
||||
NULL` nu gaseste niciodata, nici cand campul chiar contine NULL pe randul cautat.
|
||||
- **Cate linii de rata existente in productie ar fi afectate** de reparatia recomandata (Varianta B) —
|
||||
nu am interogat Oracle pentru un numar total (team-lead a masurat deja 20 de linii active cu
|
||||
`id_articol IS NULL` in toata baza, punctul 2 — dar nu si cate din ele sunt pe facturi cu rulaje
|
||||
care ar trece prin `ConstruiestePropunereSincronizare`).
|
||||
Reference in New Issue
Block a user