#13: formular de facturare unificat - etapa curenta

Squash al branch-ului de lucru plan13-s2.

Cod: clasa noua ofacturare_util (geometrie si utilitare de formular), inrolata in
roafacturare.prg; meniul "Facturare (nou)" plat, cu reparatia literalului peste
limita VFP de 255 de caractere care impiedica compilarea metodei.

Livrare: changelog 2.11.19, marcajul de versiune DB pentru scripturile finale din
SCRIPTURI_CLAR\2026\09, inventarul livrarii in docs/livrare_13.md si registrul de
erori deschise. Documentatia de lucru a etapei (cercetare, rapoarte, handoff-uri,
snapshot-uri de scripturi si backup-uri de rollback) a fost scoasa din arbore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PkyjGyrV2S7932om4kfSiK
This commit is contained in:
2026-09-09 21:30:51 +03:00
parent 87063b06a8
commit 104c24ec20
80 changed files with 5585 additions and 27852 deletions

View File

@@ -1,553 +0,0 @@
# De unde vine SCC-ul pe facturile din comandă/contract — și dacă modelul se poate refolosi pentru #13
Cercetare read-only, pe cod (VFP text + export `PACK_FACTURARE` curent) și pe baza vie (`MARIUSM_AUTO`
pe `ROA_CENTRAL`, doar `SELECT`, 10.08.2026). Nu modifică nimic — nici `pack_facturare`, nici
`ofacturare_comun.vc2`, nici `ofacturare_editare.prg`.
Continuă `docs\cercetare\coresp_cont_venchelt.md` (secțiunea 9) și `docs\plan_13_unificare_formular_facturare.md`
(J-quater), care stabiliseră deja: FACT-024 verifică apartenența articolului la politica **de pe linie**,
`id_pol` e parametru per-linie trimis de VFP (`V_ID_POL`, `adauga_articol_factura`), și pe contract
articolul „liber" nu e de fapt fără politică — vine deja cu una atașată prin `cursor_contract`/`cursor_preturi`.
**Runda 2 (reformulare Marius):** prima trecere de mai jos (secțiunile D–C, păstrate ca dovadă) presupunea
că întrebarea era „de unde vine `id_pol`". Marius a corectat premisa: el chiar emite facturi pe bază de
document care **nu au** politică de preț și notă atașată **prin lanțul obișnuit** — ceea ce contrazicea
concluzia inițială („`id_pol` gol → `FACT-024`, mereu"). Secțiunea 0 de mai jos reconciliază contradicția,
pe cod și pe date reale, și e livrabilul principal al acestei runde. Sursa SQL folosită în ambele runde:
`D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql` (823.337 octeți — fișierul
cu același nume din `docs\` a fost șters în timpul sesiunii anterioare; numerele de linie de mai jos sunt
verificate direct pe fișierul din `SCRIPTURI_CLAR`, nu preluate din plan).
---
## 0. Reconcilierea contradicției — Marius are dreptate, dar mecanismul nu e cel presupus în runda 1
**Verdict, cu citat**: `id_pol` `NULL` **tot** blochează `contabilizeaza_articol` necondiționat — asta nu
s-a schimbat, e reconfirmat mai jos. Ce s-a stabilit greșit în runda 1 e presupunerea că **orice** linie de
document trece prin `contabilizeaza_articol`. **Nu e adevărat**: pachetul are cel puțin **două rute
paralele**, folosite azi în producție (confirmate pe date reale), care scriu o notă contabilă de vânzare
**fără `id_pol` și fără `CRM_POLITICI_PRETURI`** — pentru că nu apelează deloc `contabilizeaza_articol`.
### 0a. Reconfirmare: `contabilizeaza_articol` nu tolerează `id_pol NULL`, în nicio ramură
```sql
-- ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:7275-7302 (inceputul functiei, necondiționat)
BEGIN
BEGIN
SELECT COMPUS, ID_POL_ART
INTO V_COMPUS, V_ID_POL_ART
FROM VCRM_POLITICI_PRET_ART
WHERE ID_ARTICOL = detalii_articol.id_articol
AND ID_POL = detalii_articol.id_pol; -- id_pol NULL => X=NULL => NO_DATA_FOUND, mereu
EXCEPTION
WHEN NO_DATA_FOUND THEN
... SELECT NUME_LISTA_PRETURI INTO lcPolitica FROM CRM_POLITICI_PRETURI WHERE id_pol = detalii_articol.id_pol;
RAISE_APPLICATION_ERROR(-20000, '... (FACT-024)');
```
Nu există niciun `IF detalii_articol.id_pol IS NULL THEN ...` înainte de acest bloc — e primul lucru pe
care funcția îl face, necondiționat, pentru orice apel. **Bonus, deja semnalat în runda 7** (`plan_13...
md:1451-1457`, reconfirmat aici pe fișierul curent): al doilea `SELECT` din `EXCEPTION` (cel care aduce
`lcPolitica` pentru mesaj) filtrează tot pe `id_pol = detalii_articol.id_pol`, deci și el dă `NO_DATA_FOUND`
când `id_pol` e `NULL` — utilizatorul nu vede mesajul formatat „FACT-024", ci un `ORA-01403` brut,
necaptat. În ambele cazuri: **eroare, tranzacție întreruptă, nimic scris**. Deci dacă o linie chiar ajunge
la `contabilizeaza_articol` cu `id_pol` gol, nu există azi nicio cale silențioasă de succes.
### 0b. Ce am găsit real, pe date vii — 384 din 1113 linii `VANZARI_DETALII` active (34%) au `ID_POL` `NULL`
```sql
select count(*), sum(case when id_pol is null then 1 else 0 end) from vanzari_detalii where sters=0;
-- 1113 384
```
Deci liniile facturate fără `id_pol` **există masiv** în producție — nu e un caz exotic. Împărțite pe
`VANZARI.TIP`:
| `TIP` | linii fără `id_pol` | din ele, rânduri `ACT` cu `SCC` populat | interpretare |
|---|---|---|---|
| `-12` | 97 | 588/604 | **ROAAUTO** (confirmat, `tip=-12` e explicit ROAAUTO — `plan_13...md:1710,1735`) |
| `-1..-13` (restul) | ~140 | majoritar populat | variante storno/aviz ale documentelor de mai jos, nu cercetate individual |
| `2` (**contract**) | 19 | populat, vezi 0c | facturare pe bază de contract, **rată/scadențar**, nu articol |
| `1` (listă prețuri) | 1 | — | un singur caz, neexplorat separat |
| `51` | 121 | 179/275 (cumulat) | tip necunoscut în codul VFP citit — posibil retail/POS, neidentificat |
| **`3` (comandă, ROAFACTURARE)** | **0 din 66** | — | **niciun caz** — vezi 0d |
`select count(*), sum(case when id_pol is null then 1 else 0 end) from vanzari_detalii d join vanzari v on v.id_vanzare=d.id_vanzare where d.sters=0 and v.sters=0 and v.id_comanda is not null` → **66 / 0**. Pe ruta
`COMENZI` a lui ROAFACTURARE (secțiunea A de mai jos), **`id_pol` nu e niciodată gol, pe datele disponibile.**
Contradicția lui Marius nu se reproduce pe acest obiect precis.
### 0c. Mecanismul găsit: `contabilizeaza_rata` — notă contabilă direct din contract, fără politică, fără `id_pol`
Contractele cu facturare pe **rate/scadențar** (`CONTRACTE.OPT_FACTURARE IN (1,2)`) nu trec liniile prin
`contabilizeaza_articol` — trec prin o funcție **separată**, `contabilizeaza_rata`:
```sql
-- ff_...:7549-7597
FUNCTION contabilizeaza_rata(detalii_rata VANZARI_DETALII_TEMP%ROWTYPE) RETURN NUMBER IS
BEGIN
BEGIN
SELECT NVL(B.ID_VENCHELT, pack_facturare.nid_venchelt), ..., C.SCD, C.ASCD, C.SCC, C.ASCC, C.CU_TVA, C.IN_VALUTA
INTO ...
FROM CONTRACTE A
LEFT JOIN CRM_NOTE_VANZARI B ON A.ID_NOTA = B.ID_NOTA -- <- nota vine din CONTRACTE.ID_NOTA, NU din id_pol
LEFT JOIN NOTE_CONTABILE C ON B.ID_SET = C.ID_SET
WHERE A.STERS = 0 AND A.ID_CTR = detalii_rata.id_ctr AND B.STERS = 0;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20000, 'Nu sunt configurate notele de vanzare pentru acest contract!');
END;
...
V_INCASAT_CALCUL := V_INCASAT_CALCUL + pack_facturare.scrie_nota(..., V_SCD, V_ASCD, V_SCC, V_ASCC, ...);
```
`CONTRACTE` are coloană proprie `ID_NOTA` (confirmat pe schema vie: `NUMBER`, nullable) — **contractul însuși
duce nota contabilă**, ales o singură dată la configurarea lui, independent de orice `id_pol`/politică de
preț per articol. Confirmă exact tiparul din `cursor_contract` (secțiunea B mai jos, ramura „rată" a
cursorului, `ff_...:2837-2893`): `NULL as id_articol, ..., NULL AS ID_POL` — liniile de rată **nu au
niciodată `id_pol`, prin construcție**, pentru că nu sunt articole din nomenclator, sunt rate de scadențar.
**Confirmat pe o factură reală** (`MARIUSM_AUTO`, 10.08.2026):
```sql
-- id_vanzare 360, tip 2 (contract), linia din VANZARI_DETALII: id_articol NULL, id_pol NULL
-- ACT pentru id_fact=5039903:
-- ID_ACT SCD ASCD SCC ASCC SUMA EXPLICATIA
-- 102204 4111 11 704 300 RATA 1
-- 102205 4111 11 4427 57 TVA RATA 1
```
O factură reală, cu o linie fără `id_articol` și fără `id_pol`, **cu notă contabilă scrisă corect** (`SCD
4111`, `SCC 704`) — exact dovada cerută. Mecanismul: **nota nu vine prin politică deloc**, vine direct din
`CONTRACTE.ID_NOTA`, o singură dată per contract, la configurare — un tipar „un document duce o notă fixă",
nu „fiecare linie duce o politică de preț".
### 0d. De ce nu se reproduce pe `COMENZI` (ROAFACTURARE): tabelul nu are echivalentul lui `ID_NOTA`
```sql
select column_name from user_tab_columns where table_name='COMENZI';
-- ID_COMANDA, NR_COMANDA, DATA_COMANDA, ID_PART, ID_GESTIUNE, ID_SECTIE, ID_SECTIE2, ID_CTR, ... (29 coloane)
-- nicio ID_NOTA, nicio ID_POL, nicio coloana de contabilizare
```
`COMENZI` (antetul) nu duce nicio informație contabilă — singura legătură posibilă e `ID_CTR` (dacă
comanda vine dintr-un contract). `COMENZI_ELEMENTE.ID_POL` rămâne singurul canal, și e `NOT NULL` (secțiunea
A). Deci pe **acest** obiect, mecanismul „notă fără politică" descris de Marius **nu există** — dacă
experiența lui vine de aici, ar însemna fie (a) o versiune de bază/schemă diferită de `MARIUSM_AUTO`, fie
(b) o factură pe care a facturat-o efectiv prin ruta **contract cu rate** (secțiunea 0c) sau prin **ROAAUTO**
(`tip -12`, deviz — tipar deja documentat în `coresp_cont_venchelt.md` §9b: `adauga_articol_factura_deviz`
→ `scrie_in_vanzari`, fără `contabilizeaza_articol`, cu notă scrisă separat de fiecare produs), și a numit-o
generic „factură pe bază de comandă". **Nu pot decide între aceste ipoteze fără să întreb** — dovada de cod
și de date arată clar CARE mecanisme există și niciunul nu e pe `COMENZI` propriu-zis.
### 0e. Variantele din brief, verdict pe fiecare
- *„liniile de comandă primesc totuși un `id_pol` de undeva"* — **confirmat, dar nu ascuns**: da, primesc,
documentat deja în secțiunea A/D veche, e vizibil în UI (`v_articole`), nu explică „fără politică".
- *„`contabilizeaza_articol` nu e apelată pe ruta comandă, ci altă procedură"* — **fals pentru `COMENZI`**
(`ofacturare.prg:292-293` → `cursor_comanda` → `crsarticole` → `do_scrie_articole` → `adauga_articol_factura`
→ `contabilizeaza_articol`, ruta normală); **adevărat pentru contract-cu-rate** (`contabilizeaza_rata`) și
pentru ROAAUTO/ROAACNPRO (proceduri proprii, deja documentate).
- *„există o ramură de fallback în `contabilizeaza_articol` care ajunge la notă fără politică"* — **fals**,
reconfirmat la 0a; funcția nu are asemenea ramură, blochează necondiționat.
- *„FACT-024 se ridică doar când `id_pol` e nenul dar articolul nu e membru, `id_pol` nul merge pe alt
drum"* — **fals ca „alt drum în aceeași funcție"**; **adevărat ca „alt drum = altă funcție"** (0c/0d).
---
## D. Răspunsul la întrebarea lui Marius, în cinci rânduri (actualizat după reconciliere)
**Depinde care „comandă".** Pe obiectul `COMENZI`/`COMENZI_ELEMENTE` al ROAFACTURARE (facturare „pe bază
de comandă", tip 3), SCC-ul vine tot din `NOTE_CONTABILE.SCC` prin `id_pol` — **`id_pol` nu lipsește
niciodată**, e coloană `NOT NULL` (confirmat pe schemă și pe date: 0 din 6868/66 linii fără el), populată la
adăugarea articolului dintr-o listă deja restrânsă la o singură politică (`v_articole`/`com_vpreturi_utilizator`),
politică aleasă o dată pe comandă. **Dacă experiența lui Marius vine din facturarea contractelor cu rate
(scadențar, `OPT_FACTURARE IN (1,2)`) sau din ROAAUTO**, atunci da, există o rută reală, azi în producție,
care scrie nota **fără nicio politică de preț**: pe contracte-cu-rate, nota vine direct din
`CONTRACTE.ID_NOTA` (o coloană pe contract, aleasă o singură dată la configurarea lui, independentă de orice
`id_pol` per linie) — `contabilizeaza_rata`, nu `contabilizeaza_articol`. Pe ROAAUTO/ROAACNPRO, nota o scrie
o procedură proprie a produsului (`pack_acn.salveaza_regdoc` etc.), tot în afara lanțului `CRM_POLITICI_PRETURI`.
**Aceste rute nu sunt „`id_pol` gol tratat cu grijă" — sunt căi care nu ating deloc `id_pol`/`contabilizeaza_articol`.**
Pe contract-cu-**articole** (nu rate), tiparul rămâne cel din runda 1: `CTR_ARTICOLE.ID_POL_ART` → membru
al unei politici reale, ca și pe comandă.
## E. Se poate refolosi „același model" pentru articolul ad-hoc din #13?
**Parțial. Mecanismul de jos (RPC + „apartenența e garantată prin construcția listei") e direct
transplantabil — dar e deja exact ce propune rețeta în 4 pași din J-quater, nu o alternativă mai simplă
la ea. Partea care ar simplifica cu adevărat lucrurile — „ia pur și simplu politica curentă și adaugă
articolul în ea" — e decizia 24, deja retrasă de Marius, din motive care rămân valabile.**
Detaliat:
1. **Ce arată comandă/contract, tehnic**: operatorul nu alege niciodată `id_pol` pentru un articol anume —
alege **o politică** (una dintre cele la care are drept, via `UTILIZATORI_ROL_INTERN` →
`POLITICI_GRUPURI` → `CRM_POLITICI_PRETURI`), iar din acel moment orice articol afișat spre alegere e
deja membru al ei (`com_vpreturi_utilizator`/`cPol_pret_art` filtrează `id_pol_art`/`id_pol IS NOT NULL`
la sursă). Verificarea FACT-024 „trece" pe comandă/contract **nu pentru că ar exista un fallback** — ci
pentru că nu există cale, în UI, de a ajunge la un articol care nu e deja membru. E o garanție de
construcție a listei, nu o validare separată.
2. **Acest tipar exact e deja documentat, ca fezabil, în `coresp_cont_venchelt.md` secțiunea „e)" / J-quater
punctul 3** — RPC `pack_preturi.adauga_politica_pret_art` (deja folosit în producție,
`ofacturare.vc2:15551-15587`) pentru „asigură apartenența", plus trimiterea lui `id_pol` neschimbat la
`adauga_articol_factura`. Nu e nimic nou de inventat aici — comanda/contractul confirmă că tiparul
„RPC + listă pre-filtrată" funcționează în producție de multă vreme, dar **planul A din J-quater deja îl
folosește**. Refolosirea nu elimină pasul, îl confirmă.
3. **Ce NU rezolvă modelul comandă/contract e alegerea SCC-ului corect per articol** — pe comandă/contract
politica e **una singură, fixă**, aleasă pentru tot documentul/sesiunea, cu un singur `SCC` (orice ar
fi el) pentru toate articolele adăugate așa. Aplicat identic pe `caut_articol` (restricționează
rezultatele căutării din nomenclator la politica curentă a operatorului), rezultatul ar fi
**exact ceea ce există deja azi ca „Cauta in lista de preturi…"** — nu rezolvă cazul pe care S4g/#13 îl
cere explicit: un articol care **nu e membru al niciunei politici** (`plan_13...md:1711-1716`: „din
nomenclator se oferă și articole gestionabile, și negestionabile"; „un articol fără `ID_POL` nu ajunge
la cont gol, ci la eroare").
4. **„Alege o politică fixă și pune articolul în ea" e literalmente decizia 24, deja retrasă**
(`plan_13...md:926-930`): *„Decizia 24: articolul ales din nomenclator primește `id_pol`-ul unei
politici de pret implicite. RETRASĂ în runda 8... prețul lui era o întrebare de configurare cu
contabilul (care politică, cu ce `SCC`, una sau mai multe) și un cont de venit uniform pentru orice
articol adăugat așa. Marius a ales în loc derivarea contului. Nu se reintroduce."* Motivul retragerii
nu s-a schimbat: o politică unică (fie ea „a operatorului", ca pe comandă) dă un **singur** cont de
venit oricărui articol, indiferent de natura lui (marfă/producție/serviciu) — exact opusul deciziei 27,
care cere `SCC` diferențiat după clasa contului de gestiune al articolului (`707`/`711`/`702`/`703`/
`7015`/`7018`/`704`).
5. **Deci ce rămâne de făcut e exact pasul central din rețeta în 4 pași**: nu „cum trimit un `id_pol`
valid la Oracle fără să ating `pack_facturare`" (asta e deja rezolvat, și demonstrat funcțional de
comandă/contract) — ci „**care** politică, cu **care** `SCC`, pentru **acest** articol anume" — acolo
comanda/contractul nu ajută, pentru că ele nu rezolvă niciodată această întrebare: politica le vine
gata aleasă, de operator, o singură dată, nu calculată per articol.
**Verdict la „mă gândeam la același model" (dacă modelul e comandă/contract-cu-articole)**: da, la nivel de
mecanism (RPC de inserare + membership garantat prin listă filtrată) — dar acel nivel era deja acoperit de
planul A/decizia 27-bis. Nu, la nivelul la care ar simplifica ceva („nu mai calculăm SCC per articol, luăm
politica gata aleasă") — pentru că acela e exact decizia 24, respinsă cu un motiv care rămâne valabil.
### E-bis. Dar există un al treilea model, mai simplu — cel din `contabilizeaza_rata` (secțiunea 0c)
Reconcilierea de la secțiunea 0 arată un tipar pe care raportul din runda 1 nu-l luase în calcul: **un
document poate duce el însuși o notă contabilă fixă, complet în afara `CRM_POLITICI_PRETURI`, iar
`pack_facturare` are deja o funcție care face exact asta** (`contabilizeaza_rata`, sursa `CONTRACTE.ID_NOTA`).
Aplicat la #13, ideea ar fi: nu mai deriva `SCC` per articol și nu mai caut/construi o politică tehnică — scrie
nota direct, cu conturile calculate în VFP (regula deciziei 27: `CORESP_CONT_VENCHELT`/`NOM_ARTICOLE.CONT`/
`704`), printr-o cale de scriere **paralelă**, ca și pentru rate.
**De ce nu se poate fără să ating `pack_facturare`, și asta contează, dat fiind decizia 27-bis**:
- `contabilizeaza_rata` există deja, dar e legată strict de `VANZARI_DETALII_TEMP` cu semantică de „rată de
contract" (`detalii_rata.id_ctr` obligatoriu în interogare) — nu e apelabilă pentru un rând de articol
obișnuit (`id_articol` populat, `id_pol` gol) fără o funcție **nouă**, analogă, în `pack_facturare`.
- Sursa notei la rată e `CONTRACTE.ID_NOTA` — o coloană pe un document care **există deja** și **se
configurează o dată** (la crearea contractului). Pentru articolul ad-hoc de pe formularul unificat nu
există azi un asemenea „document-sursă cu notă fixă" — ar trebui inventat (ex. o coloană similară pe
antetul facturii, sau parametru nou la `adauga_articol_factura`) — tot cod nou în `PACK_FACTURARE`.
- **Constrângerea „pack_facturare rămâne neatins" (decizia 27-bis) exclude explicit acest drum.** Dacă
Marius e dispus s-o relaxeze, tiparul `contabilizeaza_rata` e un precedent real, mai simplu decât rețeta
în 4 pași — pentru că elimină complet nevoia unei „politici tehnice" și a RPC-ului de apartenență
(`pack_preturi.adauga_politica_pret_art`): SCC-ul ar merge direct în `scrie_nota`, fără ocolul prin
`CRM_POLITICI_PRET_ART`. Dacă nu, rămâne exact ce spunea verdictul din runda 1: rețeta în 4 pași (planul
A, tot în VFP) rămâne singura variantă compatibilă cu constrângerea.
**Verdict final, actualizat**: modelul comandă/contract (runda 1) nu ajută, motivele rămân cele deja arătate.
Dar există un al doilea model real, comandă**-rate**/contract-rate, care **ar** ajuta — cu prețul de a
renunța la „`pack_facturare` neatins". E o decizie de produs pentru Marius, nu o concluzie tehnică unică —
raportul pune ambele opțiuni pe masă, cu costul lor exact.
**Corecție importantă, din verificarea independentă de mai jos („Completare de la sesiunea principală"):**
politica `7 DISCOUNT` (870 linii de comandă, `SCD=667`/`SCC=4111`, notă `2 DISCOUNT`) e deja, în producție,
o **politică tehnică folosită doar ca rutare contabilă**, nu ca listă comercială — exact tiparul „o politică
per scop contabil" pe care planul A/decizia 27-bis îl propune (nu decizia 24, care era o politică unică
pentru *orice* articol ad-hoc, indiferent de natură). Deci precedentul operațional pentru „politică tehnică,
una per destinație contabilă" **există deja pe ruta comandă** — mai puternic decât `gnId_pol_pret_stoc`
(care e un singur cont pentru tot stocul). **Dar** aceeași verificare arată că garanția „apartenență prin
construcția listei" (punctul 1 de mai sus) **nu e etanșă pe date**: 37 din 6868 linii active de comandă au
un articol care nu (mai) e membru al politicii înregistrate pe linie — verificare, cauză și comportament la
facturare, deschise, vezi secțiunea finală.
---
## A. Ruta COMANDĂ
### A.1 — `cursor_comanda` selectează `ID_POL`? Da, direct de pe linia comenzii
```sql
-- D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:2996-3054 (ramura factură)
OPEN V_CURSOR FOR
SELECT ROWNUM as id_c,
A.ID_ARTICOL,
NULL AS LOT,
NULL as SERIE,
A.ID_POL, -- <- direct din COMENZI_ELEMENTE, nu derivat
A.ID_VALUTA, ...
FROM COMENZI_ELEMENTE A
LEFT JOIN CRM_POLITICI_PRET_ART B
ON A.ID_POL = B.ID_POL AND A.ID_ARTICOL = B.ID_ARTICOL
...
WHERE A.STERS = 0 AND A.ID_COMANDA = V_ID_COMANDA ...
```
(identic la `:3084-3140` pentru ramura aviz, `V_TIP > 20`). `A.ID_POL` e coloană directă pe
`COMENZI_ELEMENTE` — `LEFT JOIN CRM_POLITICI_PRET_ART B` de mai jos **nu** derivă `id_pol`, doar aduce
`DISCOUNT_UNITAR`/`PROC_TVAV` pentru acea combinație `(id_pol, id_articol)`.
### A.2 — Unde e stocat: `COMENZI_ELEMENTE.ID_POL`, coloană `NOT NULL`
Confirmat pe schema vie (`MARIUSM_AUTO`, `ROA_CENTRAL`, 10.08.2026):
```sql
select column_name, data_type, nullable from user_tab_columns where table_name='COMENZI_ELEMENTE';
-- ID_POL NUMBER N <- NOT NULL
select count(*), sum(case when id_pol is null then 1 else 0 end) from comenzi_elemente where sters=0;
-- 6868 0
```
**`ID_POL` e obligatoriu la nivel de constrângere de tabel**, nu doar „de obicei populat" — și cele 6868
linii active din baza vie confirmă 0 excepții. E o coloană de **linie**, nu de antet (nu există `ID_POL`
pe `COMENZI`).
### A.3 — Cine îl pune acolo la crearea comenzii
Nu e o alegere liberă per articol — e moștenit dintr-o **politică aleasă o singură dată pe comandă**:
```
COMUN\clase\ocomenzi.vc2:1849-1870 (do_modifica, la deschiderea/crearea comenzii)
Do Case
Case Inlist(loRec.interna,2,5)
If lnTip = 0 And Reccount('crscomanda_curenta')>0
lnIdPol = id_pol && preia politica de pe comanda existenta
Else
loCauta = caut_politici_curente_utilizator() && cautare manuala, comanda noua
If !Empty(Nvl(loCauta.id_pol,0))
lnIdPol = loCauta.id_pol
Else
Return
Endif
Endif
update_articole_politica(lnIdPol)
Case loRec.interna = 3
update_articole_politica_comenzi([IDPOLITICAPRET]) && optiune implicita globala
Otherwise
update_articole_politica_comenzi([ID_LISTA_PRETURI_PV]) && optiune implicita globala
Endcase
```
Deci: pentru un tip de comandă (`interna` 2/5) operatorul **caută explicit** o politică
(`caut_politici_curente_utilizator`); pentru celelalte tipuri, politica vine dintr-o **opțiune globală**
(`gnIdPoliticaPret`/`gnId_lista_preturi_PV`, citite în `extrage_optiuni_firma`,
`update_comenzi.prg:16-29`). Nu se moștenește de la client.
Politica aleasă filtrează lista de articole disponibile de adăugat:
```
COMUN\programe\update_comenzi.prg:31-60 (update_articole_politica)
select p.id_articol,p.id_pol,p.pret,... from com_vpreturi_utilizator p ...
where p.id_util = <<gnIdUtil>> and p.id_pol = <<tnIdPol>>
```
```sql
-- D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\01\ff_2026_01_21_01_FACTURARE.sql:3-30
create or replace view com_vpreturi_utilizator as
select a.id_util, b.id_politica as id_pol, ..., d.id_articol, d.pret, ...
from utilizatori_rol_intern a
left join politici_grupuri b on a.id_grup = b.id_grup
left join crm_politici_preturi c on b.id_politica = c.id_pol
left join crm_politici_pret_art d on c.id_pol = d.id_pol
left join nom_articole e on d.id_articol = e.id_articol
where a.sters = 0 and b.sters = 0 and ... and d.id_pol is not null and ...
```
`v_articole` (grid-ul de adăugare articole pe comandă, `ocomenzi.vc2:4005-4014`,
`RecordSource = "v_articole"`) e populat strict din acest view, **filtrat `d.id_pol IS NOT NULL`** — deci
orice articol afișat spre alegere e deja membru garantat. La alegere (`do_adauga`,
`ocomenzi.vc2:4645-4702`) și la salvare (`do_scrie_articole`, `:4864-4899`):
```
ocomenzi.vc2:4885-4893
Scatter Name poArticol
...
lcSql = [begin pack_comenzi.adauga_articol_comanda(] + Alltrim(Str(This.nId)) + [,] + ;
Alltrim(Str(poArticol.id_articol)) + [,] + Alltrim(Str(poArticol.id_pol)) + [,] + ...
```
`poArticol.id_pol` (scatter din `v_articole`) merge neschimbat în `COMENZI_ELEMENTE.ID_POL`. Nu există în
`ocomenzi.vc2` niciun al doilea punct de adăugare a articolelor pe comandă (nicio cale către `caut_articol`
de nomenclator liber) — `v_articole` e singura sursă.
### A.4 — Ce se întâmplă când linia de comandă are `ID_POL` gol la facturare
**Nu se poate întâmpla, prin construcție dublă**: (a) `COMENZI_ELEMENTE.ID_POL` e `NOT NULL` la nivel de
Oracle — un `INSERT` cu `id_pol` gol ar da `ORA-01400`, nu ajunge niciodată să fie facturat; (b) singurul
punct de inserare din VFP (`pack_comenzi.adauga_articol_comanda`, apelat cu `poArticol.id_pol` din
`v_articole`) nu poate produce `id_pol` gol, pentru că sursa (`com_vpreturi_utilizator`) filtrează deja
`id_pol IS NOT NULL`. Verificat pe date reale: 0 din 6868 linii active. Spre deosebire de nomenclator
(`caut_articol`, unde coloana `id_pol` **nu există deloc** în SELECT), pe comandă întrebarea „ce se
întâmplă la gol" nu are un răspuns de cod (fallback/eroare) pentru că premisa nu apare niciodată.
---
## B. Ruta CONTRACT
Produsul e separat, **`D:\ROA\ROACONTRACTE`** (există în arbore, alături de `ROAFACTURARE`).
### B.1 — Cursorul de facturare selectează `ID_POL`? Da, dar prin `ID_POL_ART`, nu direct
```sql
-- D:\ROA\DATABASE\SCRIPTURI_CLAR\2026\08\ff_2026_08_09_01_COMUN_PACK_FACTURARE.sql:2718-2836 (cursor_contract, V_CURSOR2 = grd_contracte)
SELECT ..., id_pol, ...
FROM (SELECT A.ID_CTR, C.ID_ARTICOL, NULL as id_rata, C.ID_POL, ...
FROM CONTRACTE A
LEFT JOIN CTR_ARTICOLE B ON A.ID_CTR = B.ID_CTR
LEFT JOIN CRM_POLITICI_PRET_ART C ON B.ID_POL_ART = C.ID_POL_ART -- <- cheia stocata e ID_POL_ART
LEFT JOIN NOM_ARTICOLE E ON C.ID_ARTICOL = E.ID_ARTICOL ...
```
Grid-ul principal de adăugare articole pe contract (`crsarticole`, folosit pentru „adăugare liberă") **nu**
vine din acest cursor — vine dintr-un apel separat, în interiorul aceleiași proceduri:
```sql
-- ff_...:2940-2948
pack_facturare.cursor_preturi(V_DATA_CURS, V_TIP, V_ID_VALUTA, V_ID_GESTIUNE_INIT,
V_LUNA, V_AN, V_ID_UTIL, V_ID_SUCURSALA, V_CURSOR);
```
adică **exact aceeași sursă ca „lista de prețuri" (secțiunea C)** — confirmă din nou concluzia din runda
anterioară (`retur_si_lista_preturi.md` B7): grid-ul „liber" de pe contract nu e liber de politică, e lista
de prețuri normală a operatorului.
### B.2 — Unde e stocat pe contract: `CTR_ARTICOLE.ID_POL_ART` (linie, nullable)
```sql
select column_name, nullable from user_tab_columns where table_name='CTR_ARTICOLE';
-- ID_POL_ART Y <- nullable, spre deosebire de COMENZI_ELEMENTE.ID_POL
select count(*), sum(case when id_pol_art is null then 1 else 0 end) from ctr_articole;
-- 27 6
```
Spre deosebire de comandă, contractul stochează **`ID_POL_ART`** — FK direct la rândul din
`CRM_POLITICI_PRET_ART` (articol+politică), nu la politică ca atare — și coloana **e nullable**, fără
constrângere de tabel. Pe datele reale (bază de dezvoltare, eșantion mic — 27 rânduri în total, deci
**nu extrapolez procentul la producție**), 6 din 27 rânduri nu au `id_pol_art`. Dacă un asemenea rând ar
ajunge selectat spre facturare prin `V_CURSOR2`/`grd_contracte` (nu prin `crsarticole`, care e mereu lista
de prețuri), `LEFT JOIN ... ON B.ID_POL_ART = C.ID_POL_ART` nu găsește nimic, `id_pol` iese `NULL` în
cursor — același drum spre FACT-024 ca la nomenclator. **Neverificat**: dacă UI-ul (`grd_contracte`) permite
efectiv selecția unei asemenea linii spre facturare sau o filtrează/dezactivează — nu am urmărit acel cod
de grid, în afara bugetului acestei runde.
### B.3 — Cine îl pune acolo la crearea contractului
`D:\ROA\ROACONTRACTE\Clase\ferestre_contracte.vc2`, `PROCEDURE do_adauga_obiectul` (`:9454+`):
```
:9464-9470
Case gnParametru_prog = 1 && clienti
lcSel = [{call pack_crm.get_politici_grup(] + Alltrim(Str(gnIdUtil)) + [,] + ;
Alltrim(Str(goContract.id_valuta)) + [,?gnIdSucursala)}]
... INTO Cursor crsPoliticiGrup1 ...
```
```
:9486-9500
fpp = Createobject("frm_politica_preturi") && operatorul alege O politica dintre cele la care are drept
fpp.Show(1)
...
Select *, PRETFTVA As pret_unitar, 1 As cant, ... FROM cPol_pret_art With (Buffering = .T.) ;
WHERE ales = 1 INTO Cursor cAles && articolele bifate, deja membre ale politicii alese
```
`pack_crm.get_politici_grup` e echivalentul ROACONTRACTE al lanțului
`UTILIZATORI_ROL_INTERN → POLITICI_GRUPURI → CRM_POLITICI_PRETURI` folosit și de comandă/factură normală —
operatorul primește lista politicilor lui, alege una, `cPol_pret_art` (bind pe `CRM_POLITICI_PRET_ART`
pentru politica aleasă) afișează articolele ei, iar rândurile bifate (`ales=1`) devin linii `CTR_ARTICOLE`
cu `id_pol_art` = `ID_POL_ART`-ul din acel rând. Din nou: **nu se alege un articol liber, se alege dintr-o
listă deja restrânsă la politică.**
### B.4 — Ce se întâmplă cu `ID_POL_ART` gol la facturare
Vezi B.2 — spre deosebire de comandă, **nu e imposibil prin constrângere de schemă** (coloana e nullable),
și există cazuri reale (6/27) în baza de dezvoltare. Comportamentul la facturare (dacă acea linie ajunge
selectată) urmează același `NULL → FACT-024` ca la nomenclator — **neconfirmat direct pe o factură reală**,
dedus din structura JOIN a cursorului (B.1).
---
## C. Ruta LISTĂ DE PREȚURI (tip 1/5/7/10/22/29/45), ca termen de comparație
**Corectare față de premisa din brief**: `id_pol` pe această rută **nu vine din alegerea operatorului pe
document** pentru majoritatea tipurilor — vine automat din rolul operatorului logat, exact ca pe comandă,
dar fără nicio interacțiune UI.
`ofacturare.prg:279-282` (apelul cursorului pentru tip 1,5,7,10,22,23,29):
```
lcSqlCursor = [{call pack_facturare.cursor_preturi(?poDate.zi_curs,?poDate.tip,?poDate.id_valuta,] + ;
[?poDate.id_gestiune_init,?gnLuna,?gnAn,?gnIdUtil,?gnIdSucursala)}]
```
`poDate.id_pol` **nu e printre parametri**. `cursor_preturi` (`ff_...:2138-2354`) derivă `A.ID_POL` intern:
```sql
-- ff_...:2222-2251 (V_TIP IN (1,2)) / 2330-2354 identic ca structura, tot fara parametru id_pol
FROM (select a1.id_util, a3.id_pol, ...
from utilizatori_rol_intern a1
left join politici_grupuri a2 on a1.id_grup = a2.id_grup
left join crm_politici_preturi a3 on a2.id_politica = a3.id_pol
...
where a1.id_util = V_ID_UTIL
and NVL(a1.ID_SUCURSALA,-99) = NVL(V_ID_SUCURSALA,-99)
and <data curentă între valabilitatea politicii>) A
LEFT JOIN CRM_POLITICI_PRET_ART B ON A.ID_POL = B.ID_POL
```
Adică `id_pol` per linie vine din rolul de utilizator (`UTILIZATORI_ROL_INTERN.ID_UTIL = V_ID_UTIL`) și
sucursală — **niciodată dintr-o alegere pe documentul curent**, pentru tip 1/2/5/7/10/22/29/45.
Câmpul de căutare vizibil pe antet există, dar e **inert pentru aceste tipuri**:
```
COMUN\clase\ofacturare.vc2:6822-6825
ADD OBJECT 'Ct_clb_politici_preturi' AS ct_clb_cautare WITH ;
cconditie = ISNULL(poDate.id_pol) or EMPTY(poDate.id_pol), ;
cprocedura = thisform.do_cauta_politica, ...
```
`poDate.id_pol` e citit efectiv (trimis la Oracle) **doar** pentru `cursor_gestiune`, tip 41/23
(`ofacturare.prg:297,777`); e validat ca obligatoriu **doar** pentru aceleași două tipuri
(`ofacturare.vc2:7334-7337`: *„Nu ati ales politica de preturi!"*). Pe factura normală din listă de
prețuri, câmpul poate rămâne gol fără nicio consecință — nu alimentează nimic.
**Concluzie C**: „lista de preț pe document" nu există ca mecanism activ pentru rutele principale — e „lista
de preț a operatorului logat", determinată automat de rolul lui în `UTILIZATORI_ROL_INTERN`. Fiecare linie
din `crsarticole` vine deja cu propriul `A.ID_POL` din acest join; dacă operatorul are mai multe grupuri de
politici active simultan, teoretic același articol ar putea apărea de mai multe ori cu `id_pol` diferit —
**neverificat pe date reale**, în afara bugetului acestei runde.
---
## Ce nu s-a putut stabili și de ce
- **B.4, comportamentul exact la facturare a unei linii de contract cu `ID_POL_ART` gol** — dedus din
structura JOIN a `cursor_contract`, nu confirmat pe o factură reală emisă din contract cu o asemenea
linie (ar necesita un contract de test cu o linie fără politică, apoi o încercare de facturare — în
afara bugetului read-only al acestei runde).
- **Dacă `grd_contracte` (UI) permite selecția/afișarea unei linii cu `id_pol_art` gol** — nu am urmărit
codul de grid din `ROACONTRACTE`/`ofacturare.vc2` pentru acest caz specific.
- **Dacă un operator cu mai multe grupuri de politici active poate primi articole duplicate cu `id_pol`
diferit în `crsarticole`** (secțiunea C) — plauzibil din structura JOIN-ului (fără `DISTINCT` pe
`a1.id_util`), neverificat pe date reale.
- **Eșantionul `CTR_ARTICOLE` e mic (27 rânduri) în baza de dezvoltare** — raportul 6/27 fără
`id_pol_art` e o dovadă reală, dar nu extrapolabilă direct la volumul de producție; merită o verificare
separată pe o bază cu volum de producție dacă decizia depinde de frecvența cazului.
- **Cele 37 de linii de comandă active cu articol ne-membru al politicii de pe linie** (semnalate în
„Completare de la sesiunea principală") — cauza (import? editare ulterioară a politicii? ștergere din
`CRM_POLITICI_PRET_ART` după ce comanda a fost creată?) nu e stabilită, și nici comportamentul real la
facturare (`FACT-024`/`ORA-01403` așteptat pe cod, neconfirmat pe o încercare reală). E cea mai
importantă verificare rămasă înainte de S4g — dacă garanția „membership prin construcția listei" se sparge
și pe alte linii similare, tot raționamentul din secțiunea E (paragraful 1) trebuie recalibrat.
- **`VANZARI.TIP = 51`** — 121 de linii fără `id_pol`, cu 179/275 rânduri `ACT` cu `SCC` populat cumulat pe
documentele aferente — nu am identificat ce tip de document e (nu apare în `Do Case` din
`ofacturare.prg:266-308`); posibil vânzare retail/POS sau un tip specific altui produs ROA. Neexplorat.
- **Restul tipurilor negative (`-1`…`-13`, în afară de `-12`=ROAAUTO)** — nu au fost identificate individual;
presupun variante storno/aviz ale rutelor deja documentate, dar nu s-a verificat pe cod care produs le
generează.
- **Dacă experiența lui Marius vine efectiv din contract-cu-rate, din ROAAUTO, sau din altă rută
neidentificată (`51`, negative)** — nu s-a putut stabili fără să-l întrebăm direct; secțiunea 0d
enumeră ipotezele plauzibile, cu dovezi pentru fiecare, dar nu poate alege între ele.
---
## Completare de la sesiunea principala (confirmare pe schema, 10.08.2026)
Verdictul de mai sus a fost verificat independent, pentru ca **contrazice o afirmatie a lui Marius**
(„fac facturi pe baza de comanda care nu au politica de pret"). Confirmat:
- `COMENZI_ELEMENTE.ID_POL` — `nullable = N`, constrangerea `SYS_C0015376` (`"ID_POL" IS NOT NULL`) plus
`FK_COMENZI_ELEMENTE_002`. In date: **7108 randuri total, 0 cu `ID_POL` nul, 0 cu `ID_POL` = 0**, 9
politici distincte in uz. Pe liniile active (`STERS = 0`): 6868, tot 0 nule.
- `CTR_ARTICOLE.ID_POL_ART` — `nullable = Y`. Confirmata asimetria semnalata in raport.
- Toate cele 9 politici folosite pe linii de comanda au `ID_NOTA` si ajung la un `SCC`.
**Doua observatii noi, care nu erau in raport:**
1. **Politica `7 DISCOUNT` e o politica tehnica de rutare contabila, folosita in producție.** Apare pe
**870 de linii de comanda** si trimite la nota `2 DISCOUNT` (`SCD = 667`, `SCC = 4111`). Nu e o lista
comerciala de preturi — e o politica existenta doar ca sa duca linia la o nota contabila anume. **E
precedentul de care are nevoie decizia 27**, si e mai apropiat de ce cere #13 decat
`gnId_pol_pret_stoc` (care e o singura politica pentru tot stocul). Tiparul „politica tehnica per scop
contabil" nu trebuie inventat: exista.
2. **Garantia „apartenenta prin construcția listei" nu e etanșă in date.** Din 6868 de linii active de
comanda, **37 au un articol care nu e membru al politicii de pe linie**
(`NOT EXISTS (SELECT 1 FROM crm_politici_pret_art WHERE id_pol = e.id_pol AND id_articol = e.id_articol)`).
Adica 37 de linii care, la facturare, ar trebui sa cada cu `FACT-024`. Cauza nu e stabilita — import,
sau editarea / stergerea articolului din politica dupa introducerea comenzii. **Punctul 1 al secțiunii E
(„nu exista cale, in UI, de a ajunge la un articol care nu e deja membru") e corect despre UI, dar nu
despre starea datelor.** De verificat inainte de S4g: daca acele 37 de linii se factureaza fara eroare,
exista o cale nedescoperita si intrebarea deciziei 32 se redeschide.
Interogarile: schema `MARIUSM_AUTO` pe `ROA_CENTRAL`, doar `SELECT`.