feat(pricing): kit/pachet pricing with price list lookup, replace procent_pret

- Oracle PL/SQL: kit pricing logic with Mode A (distributed discount) and
  Mode B (separate discount line), dual policy support, PRETURI_CU_TVA flag
- Eliminate procent_pret from entire stack (Oracle, Python, JS, HTML)
- New settings: kit_pricing_mode, kit_discount_codmat, price_sync_enabled
- Settings UI: cards for Kit Pricing and Price Sync configuration
- Mappings UI: kit badges with lazy-loaded component prices from price list
- Price sync from orders: auto-update ROA prices when web prices differ
- Catalog price sync: new service to sync all GoMag product prices to ROA
- Kit component price validation: pre-check prices before import
- New endpoint GET /api/mappings/{sku}/prices for component price display
- New endpoints POST /api/price-sync/start, GET status, GET history
- DDL script 07_drop_procent_pret.sql (run after deploy confirmation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-19 22:29:18 +00:00
parent bedb93affe
commit 9e5901a8fb
17 changed files with 1313 additions and 268 deletions

View File

@@ -310,6 +310,9 @@ create or replace package body PACK_COMENZI is
-- marius.mutu
-- adauga_articol_comanda, modifica_articol_comanda + se poate completa ptva (21,11) in loc sa il ia din politica de preturi
-- 19.03.2026
-- adauga_articol_comanda permite de 2 ori acelasi articol cu cote tva diferite (ex: discount 11% si discount 21%)
----------------------------------------------------------------------------------
procedure adauga_masina(V_ID_MODEL_MASINA IN NUMBER,
V_NRINMAT IN VARCHAR2,
@@ -781,6 +784,7 @@ create or replace package body PACK_COMENZI is
FROM COMENZI_ELEMENTE
WHERE ID_COMANDA = V_ID_COMANDA
AND ID_ARTICOL = V_ID_ARTICOL
AND NVL(PTVA,0) = NVL(V_PTVA,0)
AND STERS = 0;
IF V_NR_INREG > 0 THEN

View File

@@ -10,6 +10,8 @@
-- Tabele: ARTICOLE_TERTI (mapari SKU -> CODMAT)
-- NOM_ARTICOLE (nomenclator articole ROA)
-- COMENZI (verificare duplicat comanda_externa)
-- CRM_POLITICI_PRETURI (flag PRETURI_CU_TVA per politica)
-- CRM_POLITICI_PRET_ART (preturi componente kituri)
--
-- Proceduri publice:
--
@@ -25,9 +27,21 @@
-- La eroare ridica RAISE_APPLICATION_ERROR(-20001, mesaj).
-- Returneaza v_id_comanda (OUT) = ID-ul comenzii create.
--
-- Parametri kit pricing:
-- p_kit_mode — 'distributed' | 'separate_line' | NULL
-- distributed: discountul fata de suma componentelor se distribuie
-- proportional in pretul fiecarei componente
-- separate_line: componentele se insereaza la pret plin +
-- linii discount separate grupate pe cota TVA
-- p_id_pol_productie — politica de pret pentru articole de productie
-- (cont_vanzare in 341/345); NULL = nu se foloseste
-- p_kit_discount_codmat — CODMAT-ul articolului discount (Mode separate_line)
-- p_kit_discount_id_pol — id_pol pentru liniile discount (Mode separate_line)
--
-- Logica cautare articol per SKU:
-- 1. Mapari speciale din ARTICOLE_TERTI (reimpachetare, seturi compuse)
-- - un SKU poate avea mai multe randuri (set) cu procent_pret
-- - daca SKU are >1 rand si p_kit_mode IS NOT NULL: kit pricing logic
-- - altfel (1 rand sau kit_mode NULL): pret web / cantitate_roa direct
-- 2. Fallback: cautare directa in NOM_ARTICOLE dupa CODMAT = SKU
--
-- get_last_error / clear_error
@@ -57,11 +71,15 @@ CREATE OR REPLACE PACKAGE PACK_IMPORT_COMENZI AS
p_data_comanda IN DATE,
p_id_partener IN NUMBER,
p_json_articole IN CLOB,
p_id_adresa_livrare IN NUMBER DEFAULT NULL,
p_id_adresa_facturare IN NUMBER DEFAULT NULL,
p_id_pol IN NUMBER DEFAULT NULL,
p_id_sectie IN NUMBER DEFAULT NULL,
p_id_adresa_livrare IN NUMBER DEFAULT NULL,
p_id_adresa_facturare IN NUMBER DEFAULT NULL,
p_id_pol IN NUMBER DEFAULT NULL,
p_id_sectie IN NUMBER DEFAULT NULL,
p_id_gestiune IN VARCHAR2 DEFAULT NULL,
p_kit_mode IN VARCHAR2 DEFAULT NULL,
p_id_pol_productie IN NUMBER DEFAULT NULL,
p_kit_discount_codmat IN VARCHAR2 DEFAULT NULL,
p_kit_discount_id_pol IN NUMBER DEFAULT NULL,
v_id_comanda OUT NUMBER);
-- Functii pentru managementul erorilor (pentru orchestrator VFP)
@@ -76,6 +94,18 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
c_id_util CONSTANT NUMBER := -3; -- Sistem
c_interna CONSTANT NUMBER := 2; -- Comenzi de la client (web)
-- Tipuri pentru kit pricing (accesibile in toate procedurile din body)
TYPE t_kit_component IS RECORD (
codmat VARCHAR2(50),
id_articol NUMBER,
cantitate_roa NUMBER,
pret_cu_tva NUMBER,
ptva NUMBER,
id_pol_comp NUMBER,
value_total NUMBER
);
TYPE t_kit_components IS TABLE OF t_kit_component INDEX BY PLS_INTEGER;
-- ================================================================
-- Functii helper pentru managementul erorilor
-- ================================================================
@@ -150,11 +180,15 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
p_data_comanda IN DATE,
p_id_partener IN NUMBER,
p_json_articole IN CLOB,
p_id_adresa_livrare IN NUMBER DEFAULT NULL,
p_id_adresa_facturare IN NUMBER DEFAULT NULL,
p_id_pol IN NUMBER DEFAULT NULL,
p_id_sectie IN NUMBER DEFAULT NULL,
p_id_adresa_livrare IN NUMBER DEFAULT NULL,
p_id_adresa_facturare IN NUMBER DEFAULT NULL,
p_id_pol IN NUMBER DEFAULT NULL,
p_id_sectie IN NUMBER DEFAULT NULL,
p_id_gestiune IN VARCHAR2 DEFAULT NULL,
p_kit_mode IN VARCHAR2 DEFAULT NULL,
p_id_pol_productie IN NUMBER DEFAULT NULL,
p_kit_discount_codmat IN VARCHAR2 DEFAULT NULL,
p_kit_discount_id_pol IN NUMBER DEFAULT NULL,
v_id_comanda OUT NUMBER) IS
v_data_livrare DATE;
v_sku VARCHAR2(100);
@@ -173,6 +207,15 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
v_pret_unitar NUMBER;
v_id_pol_articol NUMBER; -- id_pol per articol (din JSON), prioritar fata de p_id_pol
-- Variabile kit pricing
v_kit_count NUMBER := 0;
v_kit_comps t_kit_components;
v_sum_list_prices NUMBER;
v_discount_total NUMBER;
v_discount_share NUMBER;
v_pret_ajustat NUMBER;
v_discount_allocated NUMBER;
-- pljson
l_json_articole CLOB := p_json_articole;
v_json_arr pljson_list;
@@ -256,65 +299,276 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
END;
-- STEP 3: Gaseste articolele ROA pentru acest SKU
-- Cauta mai intai in ARTICOLE_TERTI (mapari speciale / seturi)
v_found_mapping := FALSE;
FOR rec IN (SELECT at.codmat, at.cantitate_roa, at.procent_pret
FROM articole_terti at
WHERE at.sku = v_sku
AND at.activ = 1
AND at.sters = 0
ORDER BY at.procent_pret DESC) LOOP
-- Numara randurile ARTICOLE_TERTI pentru a detecta kituri (>1 rand = set compus)
SELECT COUNT(*) INTO v_kit_count
FROM articole_terti at
WHERE at.sku = v_sku
AND at.activ = 1
AND at.sters = 0;
IF v_kit_count > 1 AND p_kit_mode IS NOT NULL THEN
-- ============================================================
-- KIT PRICING: set compus cu >1 componente, mod activ
-- Prima trecere: colecteaza componente + preturi din politici
-- ============================================================
v_found_mapping := TRUE;
v_id_articol := resolve_id_articol(rec.codmat, p_id_gestiune);
IF v_id_articol IS NULL THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Articol activ negasit pentru CODMAT: ' || rec.codmat;
CONTINUE;
END IF;
v_cantitate_roa := rec.cantitate_roa * v_cantitate_web;
v_pret_unitar := CASE WHEN v_pret_web IS NOT NULL
THEN (v_pret_web * rec.procent_pret / 100) / rec.cantitate_roa
ELSE 0
END;
v_kit_comps.DELETE;
v_sum_list_prices := 0;
DECLARE
v_comp_idx PLS_INTEGER := 0;
v_cont_vanz VARCHAR2(20);
v_preturi_fl NUMBER;
v_pret_val NUMBER;
v_proc_tva NUMBER;
BEGIN
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_id_articol,
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
V_CANTITATE => v_cantitate_roa,
V_PRET => v_pret_unitar,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_PTVA => v_vat);
v_articole_procesate := v_articole_procesate + 1;
EXCEPTION
WHEN OTHERS THEN
FOR rec IN (SELECT at.codmat, at.cantitate_roa
FROM articole_terti at
WHERE at.sku = v_sku
AND at.activ = 1
AND at.sters = 0
ORDER BY at.codmat) LOOP
v_comp_idx := v_comp_idx + 1;
v_kit_comps(v_comp_idx).codmat := rec.codmat;
v_kit_comps(v_comp_idx).cantitate_roa := rec.cantitate_roa;
v_kit_comps(v_comp_idx).id_articol :=
resolve_id_articol(rec.codmat, p_id_gestiune);
IF v_kit_comps(v_comp_idx).id_articol IS NULL THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Articol activ negasit pentru CODMAT: ' || rec.codmat;
v_kit_comps(v_comp_idx).pret_cu_tva := 0;
v_kit_comps(v_comp_idx).ptva := ROUND(v_vat);
v_kit_comps(v_comp_idx).id_pol_comp := NVL(v_id_pol_articol, p_id_pol);
v_kit_comps(v_comp_idx).value_total := 0;
CONTINUE;
END IF;
-- Determina id_pol_comp: cont 341/345 → politica productie, altfel vanzare
BEGIN
SELECT NVL(na.cont_vanzare, '') INTO v_cont_vanz
FROM nom_articole na
WHERE na.id_articol = v_kit_comps(v_comp_idx).id_articol
AND ROWNUM = 1;
EXCEPTION WHEN OTHERS THEN v_cont_vanz := '';
END;
IF v_cont_vanz IN ('341', '345') AND p_id_pol_productie IS NOT NULL THEN
v_kit_comps(v_comp_idx).id_pol_comp := p_id_pol_productie;
ELSE
v_kit_comps(v_comp_idx).id_pol_comp := NVL(v_id_pol_articol, p_id_pol);
END IF;
-- Query flag PRETURI_CU_TVA pentru aceasta politica
BEGIN
SELECT NVL(pp.preturi_cu_tva, 0) INTO v_preturi_fl
FROM crm_politici_preturi pp
WHERE pp.id_pol = v_kit_comps(v_comp_idx).id_pol_comp;
EXCEPTION WHEN OTHERS THEN v_preturi_fl := 0;
END;
-- Citeste PRET si PROC_TVAV din crm_politici_pret_art
BEGIN
SELECT ppa.pret, NVL(ppa.proc_tvav, 1)
INTO v_pret_val, v_proc_tva
FROM crm_politici_pret_art ppa
WHERE ppa.id_pol = v_kit_comps(v_comp_idx).id_pol_comp
AND ppa.id_articol = v_kit_comps(v_comp_idx).id_articol
AND ROWNUM = 1;
-- V_PRET always WITH TVA
IF v_preturi_fl = 1 THEN
v_kit_comps(v_comp_idx).pret_cu_tva := v_pret_val;
ELSE
v_kit_comps(v_comp_idx).pret_cu_tva := v_pret_val * v_proc_tva;
END IF;
v_kit_comps(v_comp_idx).ptva := ROUND((v_proc_tva - 1) * 100);
EXCEPTION WHEN OTHERS THEN
v_kit_comps(v_comp_idx).pret_cu_tva := 0;
v_kit_comps(v_comp_idx).ptva := ROUND(v_vat);
END;
v_kit_comps(v_comp_idx).value_total :=
v_kit_comps(v_comp_idx).pret_cu_tva * v_kit_comps(v_comp_idx).cantitate_roa;
v_sum_list_prices := v_sum_list_prices + v_kit_comps(v_comp_idx).value_total;
END LOOP;
END; -- end prima trecere
-- Discount = suma liste - pret web (poate fi negativ = markup)
v_discount_total := v_sum_list_prices - v_pret_web;
-- ============================================================
-- A doua trecere: inserare in functie de mod
-- ============================================================
IF p_kit_mode = 'distributed' THEN
-- Mode A: distribui discountul proportional in pretul fiecarei componente
v_discount_allocated := 0;
FOR i_comp IN 1 .. v_kit_comps.COUNT LOOP
IF v_kit_comps(i_comp).id_articol IS NOT NULL THEN
-- Ultimul articol valid primeste remainder pentru precizie exacta
IF i_comp = v_kit_comps.LAST THEN
v_discount_share := v_discount_total - v_discount_allocated;
ELSE
IF v_sum_list_prices != 0 THEN
v_discount_share := v_discount_total *
(v_kit_comps(i_comp).value_total / v_sum_list_prices);
ELSE
v_discount_share := 0;
END IF;
v_discount_allocated := v_discount_allocated + v_discount_share;
END IF;
-- pret_ajustat = pret_cu_tva - discount_share / cantitate_roa
v_pret_ajustat := v_kit_comps(i_comp).pret_cu_tva -
(v_discount_share / v_kit_comps(i_comp).cantitate_roa);
BEGIN
PACK_COMENZI.adauga_articol_comanda(
V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_kit_comps(i_comp).id_articol,
V_ID_POL => v_kit_comps(i_comp).id_pol_comp,
V_CANTITATE => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web,
V_PRET => v_pret_ajustat,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_PTVA => v_kit_comps(i_comp).ptva);
v_articole_procesate := v_articole_procesate + 1;
EXCEPTION
WHEN OTHERS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Eroare adaugare kit component (A) ' ||
v_kit_comps(i_comp).codmat || ': ' || SQLERRM;
END;
END IF;
END LOOP;
ELSIF p_kit_mode = 'separate_line' THEN
-- Mode B: componente la pret plin + linii discount separate pe cota TVA
DECLARE
TYPE t_vat_discount IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
v_vat_disc t_vat_discount;
v_vat_key PLS_INTEGER;
v_disc_artid NUMBER;
v_vat_disc_alloc NUMBER;
v_disc_amt NUMBER;
BEGIN
-- Inserare componente la pret plin + acumulare discount pe cota TVA
FOR i_comp IN 1 .. v_kit_comps.COUNT LOOP
IF v_kit_comps(i_comp).id_articol IS NOT NULL THEN
BEGIN
PACK_COMENZI.adauga_articol_comanda(
V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_kit_comps(i_comp).id_articol,
V_ID_POL => v_kit_comps(i_comp).id_pol_comp,
V_CANTITATE => v_kit_comps(i_comp).cantitate_roa * v_cantitate_web,
V_PRET => v_kit_comps(i_comp).pret_cu_tva,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_PTVA => v_kit_comps(i_comp).ptva);
v_articole_procesate := v_articole_procesate + 1;
EXCEPTION
WHEN OTHERS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Eroare adaugare kit component (B) ' ||
v_kit_comps(i_comp).codmat || ': ' || SQLERRM;
END;
-- Acumuleaza discountul pe cota TVA (proportional cu valoarea componentei)
v_vat_key := v_kit_comps(i_comp).ptva;
IF v_sum_list_prices != 0 THEN
IF v_vat_disc.EXISTS(v_vat_key) THEN
v_vat_disc(v_vat_key) := v_vat_disc(v_vat_key) +
v_discount_total * (v_kit_comps(i_comp).value_total / v_sum_list_prices);
ELSE
v_vat_disc(v_vat_key) :=
v_discount_total * (v_kit_comps(i_comp).value_total / v_sum_list_prices);
END IF;
ELSE
IF NOT v_vat_disc.EXISTS(v_vat_key) THEN
v_vat_disc(v_vat_key) := 0;
END IF;
END IF;
END IF;
END LOOP;
-- Rezolva articolul discount si insereaza liniile de discount
v_disc_artid := resolve_id_articol(p_kit_discount_codmat, p_id_gestiune);
IF v_disc_artid IS NOT NULL AND v_vat_disc.COUNT > 0 THEN
v_vat_disc_alloc := 0;
v_vat_key := v_vat_disc.FIRST;
WHILE v_vat_key IS NOT NULL LOOP
-- Ultima cota TVA primeste remainder pentru precizie exacta
IF v_vat_key = v_vat_disc.LAST THEN
v_disc_amt := v_discount_total - v_vat_disc_alloc;
ELSE
v_disc_amt := v_vat_disc(v_vat_key);
v_vat_disc_alloc := v_vat_disc_alloc + v_disc_amt;
END IF;
IF v_disc_amt != 0 THEN
BEGIN
PACK_COMENZI.adauga_articol_comanda(
V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_disc_artid,
V_ID_POL => NVL(p_kit_discount_id_pol, p_id_pol),
V_CANTITATE => -1 * v_cantitate_web,
V_PRET => v_disc_amt / v_cantitate_web,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_PTVA => v_vat_key);
v_articole_procesate := v_articole_procesate + 1;
EXCEPTION
WHEN OTHERS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Eroare linie discount kit TVA=' || v_vat_key || '%: ' || SQLERRM;
END;
END IF;
v_vat_key := v_vat_disc.NEXT(v_vat_key);
END LOOP;
END IF;
END; -- end mode B block
END IF; -- end kit mode branching
ELSE
-- ============================================================
-- MAPARE SIMPLA: 1 CODMAT, sau kit fara kit_mode activ
-- Pret = pret web / cantitate_roa (fara procent_pret)
-- ============================================================
FOR rec IN (SELECT at.codmat, at.cantitate_roa
FROM articole_terti at
WHERE at.sku = v_sku
AND at.activ = 1
AND at.sters = 0
ORDER BY at.codmat) LOOP
v_found_mapping := TRUE;
v_id_articol := resolve_id_articol(rec.codmat, p_id_gestiune);
IF v_id_articol IS NULL THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Eroare adaugare articol ' || rec.codmat || ': ' || SQLERRM;
END;
END LOOP;
'Articol activ negasit pentru CODMAT: ' || rec.codmat;
CONTINUE;
END IF;
-- Daca nu s-a gasit mapare, cauta direct in NOM_ARTICOLE via resolve_id_articol
IF NOT v_found_mapping THEN
v_id_articol := resolve_id_articol(v_sku, p_id_gestiune);
IF v_id_articol IS NULL THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'SKU negasit in ARTICOLE_TERTI si NOM_ARTICOLE (activ): ' || v_sku;
ELSE
v_codmat := v_sku;
v_pret_unitar := NVL(v_pret_web, 0);
v_cantitate_roa := rec.cantitate_roa * v_cantitate_web;
v_pret_unitar := CASE WHEN v_pret_web IS NOT NULL
THEN v_pret_web / rec.cantitate_roa
ELSE 0
END;
BEGIN
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_id_articol,
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
V_CANTITATE => v_cantitate_web,
V_CANTITATE => v_cantitate_roa,
V_PRET => v_pret_unitar,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
@@ -324,10 +578,41 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
WHEN OTHERS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Eroare adaugare articol ' || v_sku || ' (CODMAT: ' || v_codmat || '): ' || SQLERRM;
'Eroare adaugare articol ' || rec.codmat || ': ' || SQLERRM;
END;
END LOOP;
-- Daca nu s-a gasit mapare in ARTICOLE_TERTI, cauta direct in NOM_ARTICOLE
IF NOT v_found_mapping THEN
v_id_articol := resolve_id_articol(v_sku, p_id_gestiune);
IF v_id_articol IS NULL THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'SKU negasit in ARTICOLE_TERTI si NOM_ARTICOLE (activ): ' || v_sku;
ELSE
v_codmat := v_sku;
v_pret_unitar := NVL(v_pret_web, 0);
BEGIN
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_id_articol,
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
V_CANTITATE => v_cantitate_web,
V_PRET => v_pret_unitar,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_PTVA => v_vat);
v_articole_procesate := v_articole_procesate + 1;
EXCEPTION
WHEN OTHERS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Eroare adaugare articol ' || v_sku ||
' (CODMAT: ' || v_codmat || '): ' || SQLERRM;
END;
END IF;
END IF;
END IF;
END IF; -- end kit vs simplu
END; -- End BEGIN block pentru articol individual

View File

@@ -0,0 +1,3 @@
-- Run AFTER deploying Python code changes and confirming new pricing works
-- Removes the deprecated procent_pret column from ARTICOLE_TERTI
ALTER TABLE ARTICOLE_TERTI DROP COLUMN procent_pret;