Files
roafacturare/docs/cercetare/idpol_comanda_contract.md
2026-09-09 22:19:22 +03:00

36 KiB
Raw Permalink Blame History

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ă

-- 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

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:

-- 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):

-- 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

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

-- 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):

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>>
-- 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

-- 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:

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

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:

-- 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.