This commit is contained in:
2026-03-11 12:32:25 +02:00
parent 8e94c05901
commit 69841872d1
6 changed files with 5781 additions and 1121 deletions

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ vfp/settings.ini
vfp/output/
vfp/*.json
*.~pck
.claude/HANDOFF.md

View File

@@ -1,561 +0,0 @@
CREATE OR REPLACE PACKAGE PACK_JSON AS
-- Tipuri pentru lucrul cu JSON
TYPE t_json_array IS TABLE OF VARCHAR2(4000);
TYPE t_json_key_value IS RECORD (
key_name VARCHAR2(100),
key_value VARCHAR2(4000),
key_type VARCHAR2(20) -- 'STRING', 'NUMBER', 'BOOLEAN', 'NULL'
);
TYPE t_json_object IS TABLE OF t_json_key_value;
-- Proprietate pentru tracking erori
g_last_error VARCHAR2(4000);
-- Functie pentru accesarea ultimei erori
FUNCTION get_last_error RETURN VARCHAR2;
-- Functie pentru resetarea erorii
PROCEDURE clear_error;
-- Main parsing functions
FUNCTION parse_array(p_json_array IN CLOB) RETURN t_json_array PIPELINED; -- Parse [{"a":1},{"b":2}]
FUNCTION get_string(p_json_object IN VARCHAR2, p_key_name IN VARCHAR2) RETURN VARCHAR2; -- Get "value"
FUNCTION get_number(p_json_object IN VARCHAR2, p_key_name IN VARCHAR2) RETURN NUMBER; -- Get 123.45
FUNCTION get_boolean(p_json_object IN VARCHAR2, p_key_name IN VARCHAR2) RETURN BOOLEAN; -- Get true/false
-- Advanced functions
FUNCTION parse_object(p_json_object IN VARCHAR2) RETURN t_json_object PIPELINED; -- Parse to key-value pairs
FUNCTION clean(p_json IN CLOB) RETURN CLOB; -- Remove whitespace/formatting
-- Test functions
PROCEDURE run_tests; -- Run all built-in tests
FUNCTION test_basic_parsing RETURN VARCHAR2; -- Test basic JSON parsing
FUNCTION test_array_parsing RETURN VARCHAR2; -- Test array parsing
FUNCTION test_nested_objects RETURN VARCHAR2; -- Test nested JSON structures
FUNCTION test_error_handling RETURN VARCHAR2; -- Test error conditions
END PACK_JSON;
/
CREATE OR REPLACE PACKAGE BODY PACK_JSON AS
/*
PACK_JSON - Generic JSON Parser (Oracle 10g/11g/12c compatible)
USAGE:
-- Parse array: [{"key":"val"},{"key":"val2"}]
FOR obj IN (SELECT * FROM TABLE(PACK_JSON.parse_array(json_clob))) LOOP
v_val := PACK_JSON.get_string(obj.COLUMN_VALUE, 'key');
END LOOP;
-- Get values from object: {"name":"John","age":25,"active":true}
v_name := PACK_JSON.get_string(json_obj, 'name'); -- Returns: John
v_age := PACK_JSON.get_number(json_obj, 'age'); -- Returns: 25
v_active := PACK_JSON.get_boolean(json_obj, 'active'); -- Returns: TRUE
-- Error handling:
IF PACK_JSON.get_last_error() IS NOT NULL THEN
-- Handle error: PACK_JSON.get_last_error()
PACK_JSON.clear_error();
END IF;
FUNCTIONS:
parse_array(clob) - Parse JSON array, returns table of objects
get_string(obj,key) - Extract string value from JSON object
get_number(obj,key) - Extract number value from JSON object
get_boolean(obj,key) - Extract boolean value from JSON object
get_last_error() - Get last parsing error (NULL if no error)
clear_error() - Clear error state
*/
-- ================================================================
-- Functii pentru managementul erorilor
-- ================================================================
FUNCTION get_last_error RETURN VARCHAR2 IS
BEGIN
RETURN g_last_error;
END get_last_error;
PROCEDURE clear_error IS
BEGIN
g_last_error := NULL;
END clear_error;
-- ================================================================
-- Functie utilitara pentru curatarea JSON
-- ================================================================
FUNCTION clean(
p_json IN CLOB
) RETURN CLOB IS
v_clean CLOB;
BEGIN
-- Elimina spatii, tab-uri, newline-uri pentru parsing mai usor
v_clean := REPLACE(REPLACE(REPLACE(REPLACE(p_json,
CHR(10), ''), CHR(13), ''), CHR(9), ''), ' ', '');
RETURN v_clean;
END clean;
-- ================================================================
-- Parse JSON array si returneaza fiecare obiect
-- ================================================================
FUNCTION parse_array(
p_json_array IN CLOB
) RETURN t_json_array PIPELINED IS
v_json_clean CLOB;
v_articol_json VARCHAR2(4000);
v_start_pos NUMBER := 1;
v_end_pos NUMBER;
v_bracket_count NUMBER;
BEGIN
-- Reset error
g_last_error := NULL;
-- Curata JSON-ul
v_json_clean := clean(p_json_array);
-- Elimina bracket-urile exterioare [ ]
v_json_clean := TRIM(BOTH '[]' FROM v_json_clean);
-- Parse fiecare obiect JSON din array
LOOP
-- Gaseste inceputul obiectului JSON {
v_start_pos := INSTR(v_json_clean, '{', v_start_pos);
EXIT WHEN v_start_pos = 0;
-- Gaseste sfarsitul obiectului JSON } - ia in considerare nested objects
v_bracket_count := 1;
v_end_pos := v_start_pos;
WHILE v_bracket_count > 0 AND v_end_pos < LENGTH(v_json_clean) LOOP
v_end_pos := v_end_pos + 1;
IF SUBSTR(v_json_clean, v_end_pos, 1) = '{' THEN
v_bracket_count := v_bracket_count + 1;
ELSIF SUBSTR(v_json_clean, v_end_pos, 1) = '}' THEN
v_bracket_count := v_bracket_count - 1;
END IF;
END LOOP;
-- Extrage obiectul JSON curent
IF v_bracket_count = 0 THEN
v_articol_json := SUBSTR(v_json_clean, v_start_pos, v_end_pos - v_start_pos + 1);
PIPE ROW(v_articol_json);
-- Trece la urmatorul articol
v_start_pos := v_end_pos + 1;
ELSE
-- JSON malformat
g_last_error := 'JSON malformat - bracket-uri neechilibrate';
EXIT;
END IF;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
g_last_error := 'Eroare la parsing array: ' || SQLERRM;
END parse_array;
-- ================================================================
-- Extrage valoare string din obiect JSON
-- ================================================================
FUNCTION get_string(
p_json_object IN VARCHAR2,
p_key_name IN VARCHAR2
) RETURN VARCHAR2 IS
v_result VARCHAR2(4000);
BEGIN
-- Oracle 10g compatible: Extract string values
v_result := REGEXP_SUBSTR(p_json_object,
'"' || p_key_name || '":"[^"]*"');
IF v_result IS NOT NULL THEN
-- Remove key part and quotes manually
v_result := REGEXP_REPLACE(v_result, '^"' || p_key_name || '":"', '');
v_result := REGEXP_REPLACE(v_result, '"$', '');
END IF;
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
g_last_error := 'Eroare la extragere string pentru ' || p_key_name || ': ' || SQLERRM;
RETURN NULL;
END get_string;
-- ================================================================
-- Extrage valoare numerica din obiect JSON
-- ================================================================
FUNCTION get_number(
p_json_object IN VARCHAR2,
p_key_name IN VARCHAR2
) RETURN NUMBER IS
v_result_str VARCHAR2(100);
v_result NUMBER;
BEGIN
-- Oracle 10g compatible: Extract number values without subexpressions
-- Pattern: "key_name":123.45 (numeric value direct)
v_result_str := REGEXP_SUBSTR(p_json_object,
'"' || p_key_name || '":[0-9]+\.?[0-9]*');
IF v_result_str IS NOT NULL THEN
-- Extract just the number part after the colon
v_result_str := REGEXP_SUBSTR(v_result_str, '[0-9]+\.?[0-9]*');
END IF;
-- Daca nu gaseste, incearca cu quotes: "key_name":"123.45"
IF v_result_str IS NULL OR LENGTH(TRIM(v_result_str)) = 0 THEN
v_result_str := REGEXP_SUBSTR(p_json_object,
'"' || p_key_name || '":"[0-9]+\.?[0-9]*"');
IF v_result_str IS NOT NULL THEN
-- Extract number between quotes
v_result_str := REGEXP_SUBSTR(v_result_str, '[0-9]+\.?[0-9]*');
END IF;
END IF;
IF v_result_str IS NOT NULL AND LENGTH(TRIM(v_result_str)) > 0 THEN
BEGIN
v_result_str := TRIM(v_result_str);
-- Oracle 10g compatible conversion with NLS independence
v_result := TO_NUMBER(v_result_str, '999999999D999999999', 'NLS_NUMERIC_CHARACTERS=''.,''');
EXCEPTION
WHEN OTHERS THEN
BEGIN
-- Fallback: try with comma as decimal separator
v_result := TO_NUMBER(REPLACE(v_result_str, '.', ','));
EXCEPTION
WHEN OTHERS THEN
g_last_error := 'Cannot convert to number: "' || v_result_str || '" for key ' || p_key_name;
v_result := NULL;
END;
END;
END IF;
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
g_last_error := 'Eroare la extragere number pentru ' || p_key_name || ': ' || SQLERRM;
RETURN NULL;
END get_number;
-- ================================================================
-- Extrage valoare boolean din obiect JSON
-- ================================================================
FUNCTION get_boolean(
p_json_object IN VARCHAR2,
p_key_name IN VARCHAR2
) RETURN BOOLEAN IS
v_result_str VARCHAR2(100);
BEGIN
-- Oracle 10g compatible: Extract boolean values
v_result_str := REGEXP_SUBSTR(p_json_object,
'"' || p_key_name || '":(true|false)');
IF v_result_str IS NOT NULL THEN
-- Extract just the boolean value
v_result_str := REGEXP_REPLACE(v_result_str, '^"' || p_key_name || '":', '');
END IF;
IF v_result_str = 'true' THEN
RETURN TRUE;
ELSIF v_result_str = 'false' THEN
RETURN FALSE;
ELSE
RETURN NULL;
END IF;
EXCEPTION
WHEN OTHERS THEN
g_last_error := 'Eroare la extragere boolean pentru ' || p_key_name || ': ' || SQLERRM;
RETURN NULL;
END get_boolean;
-- ================================================================
-- Parse complet obiect JSON in structura cheie-valoare
-- ================================================================
FUNCTION parse_object(
p_json_object IN VARCHAR2
) RETURN t_json_object PIPELINED IS
v_clean_json VARCHAR2(4000);
v_key VARCHAR2(100);
v_value VARCHAR2(4000);
v_result t_json_key_value;
v_pos NUMBER := 1;
v_key_start NUMBER;
v_key_end NUMBER;
v_value_start NUMBER;
v_value_end NUMBER;
BEGIN
-- Curata JSON-ul si elimina { }
v_clean_json := TRIM(BOTH '{}' FROM REPLACE(p_json_object, ' ', ''));
-- Parse fiecare pereche key:value
WHILE v_pos < LENGTH(v_clean_json) LOOP
-- Gaseste cheia
v_key_start := INSTR(v_clean_json, '"', v_pos);
EXIT WHEN v_key_start = 0;
v_key_end := INSTR(v_clean_json, '"', v_key_start + 1);
EXIT WHEN v_key_end = 0;
v_key := SUBSTR(v_clean_json, v_key_start + 1, v_key_end - v_key_start - 1);
-- Gaseste valoarea
v_value_start := INSTR(v_clean_json, ':', v_key_end);
EXIT WHEN v_value_start = 0;
v_value_start := v_value_start + 1;
-- Determina tipul si extrage valoarea
IF SUBSTR(v_clean_json, v_value_start, 1) = '"' THEN
-- String value
v_value_end := INSTR(v_clean_json, '"', v_value_start + 1);
v_value := SUBSTR(v_clean_json, v_value_start + 1, v_value_end - v_value_start - 1);
v_result.key_type := 'STRING';
v_pos := v_value_end + 1;
ELSE
-- Number, boolean sau null
v_value_end := NVL(INSTR(v_clean_json, ',', v_value_start), LENGTH(v_clean_json) + 1);
v_value := SUBSTR(v_clean_json, v_value_start, v_value_end - v_value_start);
IF v_value IN ('true', 'false') THEN
v_result.key_type := 'BOOLEAN';
ELSIF v_value = 'null' THEN
v_result.key_type := 'NULL';
ELSIF REGEXP_LIKE(v_value, '^[0-9.]+$') THEN
v_result.key_type := 'NUMBER';
ELSE
v_result.key_type := 'UNKNOWN';
END IF;
v_pos := v_value_end + 1;
END IF;
v_result.key_name := v_key;
v_result.key_value := v_value;
PIPE ROW(v_result);
END LOOP;
EXCEPTION
WHEN OTHERS THEN
g_last_error := 'Eroare la parsing obiect: ' || SQLERRM;
END parse_object;
-- ================================================================
-- Functii de testare
-- ================================================================
FUNCTION test_basic_parsing RETURN VARCHAR2 IS
v_test_json VARCHAR2(1000) := '{"name":"John","age":25,"active":true,"score":98.5}';
v_name VARCHAR2(100);
v_age NUMBER;
v_active BOOLEAN;
v_score NUMBER;
v_result VARCHAR2(4000) := 'BASIC_PARSING: ';
BEGIN
clear_error();
v_name := get_string(v_test_json, 'name');
v_age := get_number(v_test_json, 'age');
v_active := get_boolean(v_test_json, 'active');
v_score := get_number(v_test_json, 'score');
-- Validate results
IF v_name = 'John' AND v_age = 25 AND v_active = TRUE AND v_score = 98.5 THEN
v_result := v_result || 'PASS - All values extracted correctly';
ELSE
v_result := v_result || 'FAIL - Values: name=' || v_name || ', age=' || v_age || ', score=' || v_score;
END IF;
IF get_last_error() IS NOT NULL THEN
v_result := v_result || ' ERROR: ' || get_last_error();
END IF;
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
RETURN 'BASIC_PARSING: EXCEPTION - ' || SQLERRM;
END test_basic_parsing;
FUNCTION test_array_parsing RETURN VARCHAR2 IS
v_test_array CLOB := '[{"sku":"PROD1","price":10.5},{"sku":"PROD2","price":25.0}]';
v_count NUMBER := 0;
v_sku VARCHAR2(100);
v_price NUMBER;
v_result VARCHAR2(4000) := 'ARRAY_PARSING: ';
BEGIN
clear_error();
FOR obj IN (SELECT * FROM TABLE(parse_array(v_test_array))) LOOP
v_count := v_count + 1;
v_sku := get_string(obj.COLUMN_VALUE, 'sku');
v_price := get_number(obj.COLUMN_VALUE, 'price');
IF v_count = 1 THEN
IF v_sku != 'PROD1' OR v_price != 10.5 THEN
RETURN v_result || 'FAIL - First object: sku=' || v_sku || ', price=' || v_price;
END IF;
ELSIF v_count = 2 THEN
IF v_sku != 'PROD2' OR v_price != 25.0 THEN
RETURN v_result || 'FAIL - Second object: sku=' || v_sku || ', price=' || v_price;
END IF;
END IF;
END LOOP;
IF v_count = 2 THEN
v_result := v_result || 'PASS - Parsed ' || v_count || ' objects correctly';
ELSE
v_result := v_result || 'FAIL - Expected 2 objects, got ' || v_count;
END IF;
IF get_last_error() IS NOT NULL THEN
v_result := v_result || ' ERROR: ' || get_last_error();
END IF;
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
RETURN 'ARRAY_PARSING: EXCEPTION - ' || SQLERRM;
END test_array_parsing;
FUNCTION test_nested_objects RETURN VARCHAR2 IS
v_test_nested CLOB := '[{"order":{"id":123,"items":[{"sku":"A1","qty":2}],"total":25.50}},{"order":{"id":124,"items":[{"sku":"B1","qty":1},{"sku":"C1","qty":3}],"total":45.00}}]';
v_count NUMBER := 0;
v_object VARCHAR2(4000);
v_order_id NUMBER;
v_total NUMBER;
v_result VARCHAR2(4000) := 'NESTED_OBJECTS: ';
v_order_json VARCHAR2(2000);
BEGIN
clear_error();
-- Test parsing array cu nested objects
FOR obj IN (SELECT * FROM TABLE(parse_array(v_test_nested))) LOOP
v_count := v_count + 1;
v_object := obj.COLUMN_VALUE;
-- Extrage nested object "order" (Oracle 10g compatible)
v_order_json := REGEXP_SUBSTR(v_object, '"order":\{[^}]+\}');
IF v_order_json IS NOT NULL THEN
-- Extract just the object part
v_order_json := REGEXP_REPLACE(v_order_json, '^"order":', '');
END IF;
IF v_order_json IS NULL THEN
-- Incearca sa gaseasca tot nested object-ul (mai complex)
v_order_json := REGEXP_SUBSTR(v_object, '"order":\{.*\}', 1, 1);
-- Elimina "order": din fata
v_order_json := REGEXP_REPLACE(v_order_json, '^"order":', '');
END IF;
IF v_order_json IS NOT NULL THEN
v_order_id := get_number(v_order_json, 'id');
v_total := get_number(v_order_json, 'total');
IF v_count = 1 THEN
IF v_order_id != 123 OR v_total != 25.50 THEN
RETURN v_result || 'FAIL - First nested: id=' || v_order_id || ', total=' || v_total;
END IF;
ELSIF v_count = 2 THEN
IF v_order_id != 124 OR v_total != 45.00 THEN
RETURN v_result || 'FAIL - Second nested: id=' || v_order_id || ', total=' || v_total;
END IF;
END IF;
ELSE
RETURN v_result || 'FAIL - Could not extract nested order object from: ' || SUBSTR(v_object, 1, 100);
END IF;
END LOOP;
IF v_count = 2 THEN
v_result := v_result || 'PASS - Parsed ' || v_count || ' nested objects correctly';
ELSE
v_result := v_result || 'FAIL - Expected 2 nested objects, got ' || v_count;
END IF;
IF get_last_error() IS NOT NULL THEN
v_result := v_result || ' ERROR: ' || get_last_error();
END IF;
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
RETURN 'NESTED_OBJECTS: EXCEPTION - ' || SQLERRM;
END test_nested_objects;
FUNCTION test_error_handling RETURN VARCHAR2 IS
v_result VARCHAR2(4000) := 'ERROR_HANDLING: ';
v_invalid_json VARCHAR2(1000) := '{"broken":}';
v_value VARCHAR2(100);
BEGIN
clear_error();
-- Force an error by trying to parse malformed array
BEGIN
FOR obj IN (SELECT * FROM TABLE(parse_array('[{"incomplete":"object"'))) LOOP
NULL;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
-- This should trigger parse_array to set g_last_error
NULL;
END;
-- Alternative: try to get a string from NULL object
v_value := get_string(NULL, 'test');
IF get_last_error() IS NOT NULL THEN
v_result := v_result || 'PASS - Error properly captured: ' || SUBSTR(get_last_error(), 1, 100);
clear_error();
ELSE
v_result := v_result || 'FAIL - No error captured for invalid operations';
END IF;
-- Test error clearing
IF get_last_error() IS NULL THEN
v_result := v_result || ' - Error cleared successfully';
ELSE
v_result := v_result || ' - Error not cleared properly';
END IF;
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
RETURN 'ERROR_HANDLING: EXCEPTION - ' || SQLERRM;
END test_error_handling;
PROCEDURE run_tests IS
v_test_result VARCHAR2(4000);
BEGIN
DBMS_OUTPUT.PUT_LINE('=== PACK_JSON Test Suite ===');
DBMS_OUTPUT.PUT_LINE('');
-- Test 1: Basic parsing
v_test_result := test_basic_parsing();
DBMS_OUTPUT.PUT_LINE(v_test_result);
-- Test 2: Array parsing
v_test_result := test_array_parsing();
DBMS_OUTPUT.PUT_LINE(v_test_result);
-- Test 3: Nested objects
v_test_result := test_nested_objects();
DBMS_OUTPUT.PUT_LINE(v_test_result);
-- Test 4: Error handling
v_test_result := test_error_handling();
DBMS_OUTPUT.PUT_LINE(v_test_result);
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('=== Test Suite Complete ===');
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('ERROR in run_tests: ' || SQLERRM);
END run_tests;
END PACK_JSON;
/

View File

@@ -1,28 +1,55 @@
-- ====================================================================
-- PACK_IMPORT_COMENZI
-- Package pentru importul comenzilor din platforme web (GoMag, etc.)
-- in sistemul ROA Oracle.
--
-- Dependinte:
-- Packages: PACK_COMENZI (adauga_comanda, adauga_articol_comanda)
-- pljson (pljson_list, pljson) - instalat in CONTAFIN_ORACLE,
-- accesat prin PUBLIC SYNONYM
-- Tabele: ARTICOLE_TERTI (mapari SKU -> CODMAT)
-- NOM_ARTICOLE (nomenclator articole ROA)
-- COMENZI (verificare duplicat comanda_externa)
--
-- Proceduri publice:
--
-- importa_comanda(...)
-- Importa o comanda completa: creeaza comanda + adauga articolele.
-- p_json_articole accepta:
-- - array JSON: [{"sku":"X","quantity":"1","price":"10","vat":"19"}, ...]
-- - obiect JSON: {"sku":"X","quantity":"1","price":"10","vat":"19"}
-- Valorile sku, quantity, price, vat sunt extrase ca STRING si convertite.
-- Daca comanda exista deja (comanda_externa), nu se dubleaza.
-- La eroare ridica RAISE_APPLICATION_ERROR(-20001, mesaj).
-- Returneaza v_id_comanda (OUT) = ID-ul comenzii create.
--
-- 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
-- 2. Fallback: cautare directa in NOM_ARTICOLE dupa CODMAT = SKU
--
-- get_last_error / clear_error
-- Management erori pentru orchestratorul VFP.
--
-- Exemplu utilizare:
-- DECLARE
-- v_id NUMBER;
-- BEGIN
-- PACK_IMPORT_COMENZI.importa_comanda(
-- p_nr_comanda_ext => '479317993',
-- p_data_comanda => SYSDATE,
-- p_id_partener => 1424,
-- p_json_articole => '[{"sku":"5941623003366","quantity":"1.00","price":"40.99","vat":"21"}]',
-- p_id_pol => 39,
-- v_id_comanda => v_id);
-- DBMS_OUTPUT.PUT_LINE('ID comanda: ' || v_id);
-- END;
-- ====================================================================
CREATE OR REPLACE PACKAGE PACK_IMPORT_COMENZI AS
-- Tipuri pentru returnarea rezultatelor
TYPE t_articol_result IS RECORD(
id_articol NUMBER,
codmat VARCHAR2(50),
cantitate_roa NUMBER,
pret_unitar NUMBER,
ptva NUMBER,
success NUMBER,
error_message VARCHAR2(4000));
TYPE t_articol_table IS TABLE OF t_articol_result;
-- Variabila package pentru ultima eroare (pentru orchestrator VFP)
g_last_error VARCHAR2(4000);
-- Functie pentru gasirea/maparea articolelor ROA
FUNCTION gaseste_articol_roa(p_sku IN VARCHAR2,
p_pret_web IN NUMBER DEFAULT NULL,
p_cantitate_web IN NUMBER DEFAULT 1,
p_ptva IN NUMBER)
RETURN t_articol_table
PIPELINED;
-- Procedura pentru importul complet al unei comenzi
PROCEDURE importa_comanda(p_nr_comanda_ext IN VARCHAR2,
p_data_comanda IN DATE,
@@ -34,7 +61,7 @@ CREATE OR REPLACE PACKAGE PACK_IMPORT_COMENZI AS
p_id_sectie IN NUMBER DEFAULT NULL,
v_id_comanda OUT NUMBER);
-- Functii pentru managementul erorilor (similar cu PACK_JSON)
-- Functii pentru managementul erorilor (pentru orchestrator VFP)
FUNCTION get_last_error RETURN VARCHAR2;
PROCEDURE clear_error;
@@ -43,7 +70,6 @@ END PACK_IMPORT_COMENZI;
CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
-- Constante pentru configurare
-- Nota: c_id_pol, c_id_gestiune, c_id_sectie sunt acum parametri ai procedurii importa_comanda
c_id_util CONSTANT NUMBER := -3; -- Sistem
c_interna CONSTANT NUMBER := 2; -- Comenzi de la client (web)
@@ -61,155 +87,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
END clear_error;
-- ================================================================
-- Functii interne
-- ================================================================
-- Procedura interna pentru validarea seturilor
FUNCTION valideaza_set(p_sku IN VARCHAR2) RETURN BOOLEAN IS
v_suma_procent NUMBER := 0;
v_count_articole NUMBER := 0;
BEGIN
SELECT NVL(SUM(procent_pret), 0), COUNT(*)
INTO v_suma_procent, v_count_articole
FROM articole_terti
WHERE sku = p_sku
AND activ = 1;
-- Validari logice pentru seturi
IF v_count_articole > 1 THEN
-- Set compus - suma procentelor trebuie sa fie intre 95-105% (toleranta)
IF v_suma_procent < 95 OR v_suma_procent > 105 THEN
-- pINFO('WARN VALIDEAZA_SET ' || p_sku || ': Suma procente nelogica: ' || v_suma_procent || '%', 'IMPORT_COMENZI');
RETURN FALSE;
END IF;
ELSIF v_count_articole = 1 THEN
-- Reimpachetare - procentul trebuie sa fie 100%
IF v_suma_procent != 100 THEN
-- pINFO('WARN VALIDEAZA_SET ' || p_sku || ': Reimpachetare cu procent != 100%: ' || v_suma_procent || '%', 'IMPORT_COMENZI');
RETURN FALSE;
END IF;
END IF;
RETURN TRUE;
END valideaza_set;
-- ================================================================
-- Functia principala pentru gasirea articolelor ROA
-- ================================================================
FUNCTION gaseste_articol_roa(p_sku IN VARCHAR2,
p_pret_web IN NUMBER DEFAULT NULL,
p_cantitate_web IN NUMBER DEFAULT 1,
p_ptva IN NUMBER)
RETURN t_articol_table
PIPELINED IS
v_result t_articol_result;
v_found_mapping BOOLEAN := FALSE;
-- Cursor pentru maparile din ARTICOLE_TERTI
CURSOR c_mapari IS
SELECT at.codmat, at.cantitate_roa, at.procent_pret, na.id_articol
FROM articole_terti at
JOIN nom_articole na
ON na.codmat = at.codmat
WHERE at.sku = p_sku
AND at.activ = 1
ORDER BY at.procent_pret DESC; -- Articolele principale primul
BEGIN
-- pINFO('GASESTE_ARTICOL ' || p_sku || ': Cautare articol pentru SKU: ' || p_sku, 'IMPORT_COMENZI');
-- Initializare rezultat
v_result.success := 0;
v_result.error_message := NULL;
-- STEP 1: Verifica maparile speciale din ARTICOLE_TERTI
FOR rec IN c_mapari LOOP
v_found_mapping := TRUE;
v_result.id_articol := rec.id_articol;
v_result.codmat := rec.codmat;
v_result.cantitate_roa := rec.cantitate_roa * p_cantitate_web;
-- Calculeaza pretul unitar pe baza procentului alocat
IF p_pret_web IS NOT NULL THEN
v_result.pret_unitar := (p_pret_web * rec.procent_pret / 100) /
rec.cantitate_roa;
ELSE
-- Fara pret web, setam 0 (va fi necesar sa fie furnizat)
v_result.pret_unitar := 0;
END IF;
v_result.ptva := p_ptva;
v_result.success := 1;
-- pINFO('GASESTE_ARTICOL ' || p_sku || ': Mapare gasita: ' || rec.codmat ||
-- ', Cant: ' || v_result.cantitate_roa ||
-- ', Pret: ' || v_result.pret_unitar, 'IMPORT_COMENZI');
PIPE ROW(v_result);
END LOOP;
-- STEP 2: Daca nu s-au gasit mapari speciale, cauta direct in nom_articole
IF NOT v_found_mapping THEN
BEGIN
SELECT id_articol, codmat
INTO v_result.id_articol, v_result.codmat
FROM nom_articole
WHERE codmat = p_sku;
v_result.cantitate_roa := p_cantitate_web;
-- Pentru cautare directa, foloseste pretul din web daca este furnizat
IF p_pret_web IS NOT NULL THEN
v_result.pret_unitar := p_pret_web;
END IF;
v_result.ptva := p_ptva;
v_result.success := 1;
-- pINFO('GASESTE_ARTICOL ' || p_sku || ': Gasit direct in nomenclator: ' || v_result.codmat, 'IMPORT_COMENZI');
PIPE ROW(v_result);
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_result.success := 0;
v_result.error_message := 'SKU nu a fost gasit nici in ARTICOLE_TERTI, nici in nom_articole: ' ||
p_sku;
-- pINFO('ERROR GASESTE_ARTICOL ' || p_sku || ': ' || v_result.error_message, 'IMPORT_COMENZI');
PIPE ROW(v_result);
WHEN TOO_MANY_ROWS THEN
v_result.success := 0;
v_result.error_message := 'Multiple articole gasite pentru SKU: ' ||
p_sku;
-- pINFO('ERROR GASESTE_ARTICOL ' || p_sku || ': ' || v_result.error_message, 'IMPORT_COMENZI');
PIPE ROW(v_result);
END;
ELSE
-- Valideaza seturile dupa ce au fost returnate toate maparile
IF NOT valideaza_set(p_sku) THEN
null;
-- pINFO('WARN GASESTE_ARTICOL ' || p_sku || ': Set cu configuratie suspecta - verifica procentele', 'IMPORT_COMENZI');
END IF;
END IF;
EXCEPTION
WHEN OTHERS THEN
v_result.success := 0;
v_result.error_message := 'Eroare neasteptata: ' || SQLERRM;
-- pINFO('ERROR GASESTE_ARTICOL ' || p_sku || ': Eroare neasteptata: ' || SQLERRM, 'IMPORT_COMENZI');
PIPE ROW(v_result);
END gaseste_articol_roa;
-- ================================================================
-- Functia pentru importul complet al unei comenzi
-- Procedura principala pentru importul unei comenzi
-- ================================================================
PROCEDURE importa_comanda(p_nr_comanda_ext IN VARCHAR2,
p_data_comanda IN DATE,
@@ -224,22 +102,23 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
v_sku VARCHAR2(100);
v_cantitate_web NUMBER;
v_pret_web NUMBER;
v_vat NUMBER;
v_articole_procesate NUMBER := 0;
v_articole_eroare NUMBER := 0;
v_start_time DATE;
v_vat NUMBER;
v_articol_count NUMBER := 0;
v_articol_json VARCHAR2(4000);
v_articol_count NUMBER := 0;
-- Variabile pentru cautare articol
v_found_mapping BOOLEAN;
v_id_articol NUMBER;
v_codmat VARCHAR2(50);
v_cantitate_roa NUMBER;
v_pret_unitar NUMBER;
v_articole_table t_articol_table;
v_articol_idx NUMBER;
art_rec t_articol_result;
l_json_articole CLOB := p_json_articole;
-- pljson
l_json_articole CLOB := p_json_articole;
v_json_arr pljson_list;
v_json_obj pljson;
BEGIN
v_start_time := SYSDATE;
-- Resetare eroare la inceputul procesarii
clear_error;
@@ -258,10 +137,9 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
WHERE comanda_externa = p_nr_comanda_ext
AND sters = 0;
-- pINFO('WARN IMPORTA_COMANDA ' || p_nr_comanda_ext || ': Comanda exista deja cu ID: ' || v_id_comanda, 'IMPORT_COMENZI');
if v_id_comanda is not null then
IF v_id_comanda IS NOT NULL THEN
GOTO sfarsit;
end if;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL; -- Normal, comanda nu exista
@@ -270,23 +148,21 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
-- Calculeaza data de livrare (comanda + 1 zi)
v_data_livrare := p_data_comanda + 1;
-- STEP 1: Creeaza comanda folosind versiunea overloaded cu OUT parameter
-- Apeleaza procedura adauga_comanda care returneaza ID_COMANDA prin OUT
-- STEP 1: Creeaza comanda
PACK_COMENZI.adauga_comanda(V_NR_COMANDA => p_nr_comanda_ext,
V_DATA_COMANDA => p_data_comanda,
V_ID => p_id_partener, -- ID_PART
V_ID => p_id_partener,
V_DATA_LIVRARE => v_data_livrare,
V_PROC_DISCOUNT => 0, -- Fara discount implicit
V_PROC_DISCOUNT => 0,
V_INTERNA => c_interna,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_ID_ADRESA_FACTURARE => p_id_adresa_facturare,
V_ID_ADRESA_LIVRARE => p_id_adresa_livrare,
V_ID_CODCLIENT => NULL, -- Nu folosim cod client
V_ID_CODCLIENT => NULL,
V_COMANDA_EXTERNA => p_nr_comanda_ext,
V_ID_CTR => NULL, -- Nu avem contract
V_ID_COMANDA => v_id_comanda -- OUT parameter cu ID_COMANDA
);
V_ID_CTR => NULL,
V_ID_COMANDA => v_id_comanda);
IF v_id_comanda IS NULL OR v_id_comanda <= 0 THEN
g_last_error := 'IMPORTA_COMANDA ' || p_nr_comanda_ext ||
@@ -294,90 +170,97 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
GOTO sfarsit;
END IF;
-- pINFO('IMPORTA_COMANDA ' || p_nr_comanda_ext || ': Comanda creata cu ID: ' || v_id_comanda, 'IMPORT_COMENZI');
-- STEP 2: Proceseaza articolele din JSON folosind pljson
-- Suporta atat array "[{...},{...}]" cat si obiect singular "{...}"
IF LTRIM(l_json_articole) LIKE '[%' THEN
v_json_arr := pljson_list(l_json_articole);
ELSE
v_json_arr := pljson_list('[' || l_json_articole || ']');
END IF;
-- STEP 2: Proceseaza articolele din JSON folosind PACK_JSON
-- Asteapta format JSON: [{"sku":"ABC","cantitate":1,"pret":10.5},{"sku":"DEF","cantitate":2,"pret":20.0}]
-- Parse JSON array folosind package-ul generic
FOR json_obj IN (SELECT *
FROM TABLE(PACK_JSON.parse_array(l_json_articole))) LOOP
FOR i IN 1 .. v_json_arr.count LOOP
v_articol_count := v_articol_count + 1;
v_articol_json := json_obj.COLUMN_VALUE;
v_json_obj := pljson(v_json_arr.get(i));
BEGIN
-- Extrage datele folosind functiile PACK_JSON
v_sku := PACK_JSON.get_string(v_articol_json, 'sku');
v_cantitate_web := PACK_JSON.get_number(v_articol_json, 'quantity');
v_pret_web := PACK_JSON.get_number(v_articol_json, 'price');
v_vat := PACK_JSON.get_number(v_articol_json, 'vat');
-- Extrage datele folosind pljson (valorile vin ca string din json magazin web)
v_sku := v_json_obj.get_string('sku');
v_cantitate_web := TO_NUMBER(v_json_obj.get_string('quantity'));
v_pret_web := TO_NUMBER(v_json_obj.get_string('price'));
v_vat := TO_NUMBER(v_json_obj.get_string('vat'));
-- pINFO('IMPORTA_COMANDA ' || p_nr_comanda_ext || ': Procesez articol ' || v_articol_count || ': ' || v_sku || ', cant: ' || v_cantitate_web || ', pret: ' || v_pret_web, 'IMPORT_COMENZI');
-- STEP 3: Gaseste articolele ROA pentru acest SKU
-- Cauta mai intai in ARTICOLE_TERTI (mapari speciale / seturi)
v_found_mapping := FALSE;
-- STEP 3: Gaseste maparile pentru acest SKU
-- Apeleaza functia si stocheaza rezultatele
SELECT *
BULK COLLECT
INTO v_articole_table
FROM TABLE(gaseste_articol_roa(v_sku,
v_pret_web,
v_cantitate_web,
v_vat));
FOR rec IN (SELECT at.codmat, at.cantitate_roa, at.procent_pret, na.id_articol
FROM articole_terti at
JOIN nom_articole na ON na.codmat = at.codmat
WHERE at.sku = v_sku
AND at.activ = 1
ORDER BY at.procent_pret DESC) LOOP
-- Itereaza prin rezultate
IF v_articole_table.COUNT > 0 THEN
FOR v_articol_idx IN 1 .. v_articole_table.COUNT LOOP
v_found_mapping := TRUE;
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;
art_rec := v_articole_table(v_articol_idx);
IF art_rec.success = 1 THEN
-- Adauga articolul la comanda
BEGIN
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => art_rec.id_articol,
V_ID_POL => p_id_pol,
V_CANTITATE => art_rec.cantitate_roa,
V_PRET => art_rec.pret_unitar,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_PTVA => art_rec.ptva);
v_articole_procesate := v_articole_procesate + 1;
-- pINFO('IMPORTA_COMANDA ' || p_nr_comanda_ext || ': Articol adaugat: ' || art_rec.codmat ||
-- ', cant: ' || art_rec.cantitate_roa ||
-- ', pret: ' || art_rec.pret_unitar, 'IMPORT_COMENZI');
EXCEPTION
WHEN OTHERS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'ERROR IMPORTA_COMANDA ' ||
p_nr_comanda_ext ||
': Eroare la adaugare articol ' ||
art_rec.codmat || ': ' || SQLERRM;
-- pINFO('ERROR IMPORTA_COMANDA ' || p_nr_comanda_ext || ': Eroare la adaugare articol ' || art_rec.codmat || ': ' || SQLERRM, 'IMPORT_COMENZI');
END;
ELSE
BEGIN
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => rec.id_articol,
V_ID_POL => 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
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'ERROR IMPORTA_COMANDA ' ||
p_nr_comanda_ext ||
': SKU nu a putut fi mapat: ' || v_sku ||
' - ' || art_rec.error_message;
-- pINFO('ERROR IMPORTA_COMANDA ' || p_nr_comanda_ext || ': SKU nu a putut fi mapat: ' || v_sku || ' - ' || art_rec.error_message, 'IMPORT_COMENZI');
END IF;
g_last_error := g_last_error || CHR(10) ||
'Eroare adaugare articol ' || rec.codmat || ': ' || SQLERRM;
END;
END LOOP;
END LOOP; -- End v_articol_idx loop
ELSE
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'WARN IMPORTA_COMANDA ' || p_nr_comanda_ext ||
': Niciun articol gasit pentru SKU: ' ||
v_sku;
-- pINFO('WARN IMPORTA_COMANDA ' || p_nr_comanda_ext || ': Niciun articol gasit pentru SKU: ' || v_sku, 'IMPORT_COMENZI');
-- Daca nu s-a gasit mapare, cauta direct in NOM_ARTICOLE
IF NOT v_found_mapping THEN
BEGIN
SELECT id_articol, codmat
INTO v_id_articol, v_codmat
FROM nom_articole
WHERE codmat = v_sku;
v_pret_unitar := NVL(v_pret_web, 0);
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_id_articol,
V_ID_POL => 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 NO_DATA_FOUND THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'SKU negasit in ARTICOLE_TERTI si NOM_ARTICOLE: ' || v_sku;
WHEN TOO_MANY_ROWS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Multiple articole gasite pentru SKU: ' || v_sku;
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; -- End DECLARE block pentru v_articole_table
END; -- End BEGIN block pentru articol individual
END LOOP;

File diff suppressed because it is too large Load Diff

306
scripts/parse_sync_log.py Normal file
View File

@@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""
Parser pentru log-urile sync_comenzi_web.
Extrage comenzi esuate, SKU-uri lipsa, si genereaza un sumar.
Suporta atat formatul vechi (verbose) cat si formatul nou (compact).
Utilizare:
python parse_sync_log.py # Ultimul log din vfp/log/
python parse_sync_log.py <fisier.log> # Log specific
python parse_sync_log.py --skus # Doar lista SKU-uri lipsa
python parse_sync_log.py --dir /path/to/logs # Director custom
"""
import os
import sys
import re
import glob
import argparse
# Regex pentru linii cu timestamp (intrare noua in log)
RE_TIMESTAMP = re.compile(r'^\[(\d{2}:\d{2}:\d{2})\]\s+\[(\w+\s*)\]\s*(.*)')
# Regex format NOU: [N/Total] OrderNumber P:X A:Y/Z -> OK/ERR details
RE_COMPACT_OK = re.compile(r'\[(\d+)/(\d+)\]\s+(\S+)\s+.*->\s+OK\s+ID:(\S+)')
RE_COMPACT_ERR = re.compile(r'\[(\d+)/(\d+)\]\s+(\S+)\s+.*->\s+ERR\s+(.*)')
# Regex format VECHI (backwards compat)
RE_SKU_NOT_FOUND = re.compile(r'SKU negasit.*?:\s*(\S+)')
RE_PRICE_POLICY = re.compile(r'Pretul pentru acest articol nu a fost gasit')
RE_FAILED_ORDER = re.compile(r'Import comanda esuat pentru\s+(\S+)')
RE_ARTICOL_ERR = re.compile(r'Eroare adaugare articol\s+(\S+)')
RE_ORDER_PROCESS = re.compile(r'Procesez comanda:\s+(\S+)\s+din\s+(\S+)')
RE_ORDER_SUCCESS = re.compile(r'SUCCES: Comanda importata.*?ID Oracle:\s+(\S+)')
# Regex comune
RE_SYNC_END = re.compile(r'SYNC END\s*\|.*?(\d+)\s+processed.*?(\d+)\s+ok.*?(\d+)\s+err')
RE_STATS_LINE = re.compile(r'Duration:\s*(\S+)\s*\|\s*Orders:\s*(\S+)')
RE_STOPPED_EARLY = re.compile(r'Peste \d+.*ero|stopped early')
def find_latest_log(log_dir):
"""Gaseste cel mai recent log sync_comenzi din directorul specificat."""
pattern = os.path.join(log_dir, 'sync_comenzi_*.log')
files = glob.glob(pattern)
if not files:
return None
return max(files, key=os.path.getmtime)
def parse_log_entries(lines):
"""Parseaza liniile log-ului in intrari structurate."""
entries = []
current = None
for line in lines:
line = line.rstrip('\n\r')
m = RE_TIMESTAMP.match(line)
if m:
if current:
entries.append(current)
current = {
'time': m.group(1),
'level': m.group(2).strip(),
'text': m.group(3),
'full': line,
'continuation': []
}
elif current is not None:
current['continuation'].append(line)
current['text'] += '\n' + line
if current:
entries.append(current)
return entries
def extract_sku_from_error(err_text):
"""Extrage SKU din textul erorii (diverse formate)."""
# SKU_NOT_FOUND: 8714858424056
m = re.search(r'SKU_NOT_FOUND:\s*(\S+)', err_text)
if m:
return ('SKU_NOT_FOUND', m.group(1))
# PRICE_POLICY: 8000070028685
m = re.search(r'PRICE_POLICY:\s*(\S+)', err_text)
if m:
return ('PRICE_POLICY', m.group(1))
# Format vechi: SKU negasit...NOM_ARTICOLE: xxx
m = RE_SKU_NOT_FOUND.search(err_text)
if m:
return ('SKU_NOT_FOUND', m.group(1))
# Format vechi: Eroare adaugare articol xxx
m = RE_ARTICOL_ERR.search(err_text)
if m:
return ('ARTICOL_ERROR', m.group(1))
# Format vechi: Pretul...
if RE_PRICE_POLICY.search(err_text):
return ('PRICE_POLICY', '(SKU necunoscut)')
return (None, None)
def analyze_entries(entries):
"""Analizeaza intrarile si extrage informatii relevante."""
result = {
'start_time': None,
'end_time': None,
'duration': None,
'total_orders': 0,
'success_orders': 0,
'error_orders': 0,
'stopped_early': False,
'failed': [],
'missing_skus': [],
}
seen_skus = set()
current_order = None
for entry in entries:
text = entry['text']
level = entry['level']
# Start/end time
if entry['time']:
if result['start_time'] is None:
result['start_time'] = entry['time']
result['end_time'] = entry['time']
# Format NOU: SYNC END line cu statistici
m = RE_SYNC_END.search(text)
if m:
result['total_orders'] = int(m.group(1))
result['success_orders'] = int(m.group(2))
result['error_orders'] = int(m.group(3))
# Format NOU: compact OK line
m = RE_COMPACT_OK.search(text)
if m:
continue
# Format NOU: compact ERR line
m = RE_COMPACT_ERR.search(text)
if m:
order_nr = m.group(3)
err_detail = m.group(4).strip()
err_type, sku = extract_sku_from_error(err_detail)
if err_type and sku:
result['failed'].append((order_nr, err_type, sku))
if sku not in seen_skus and sku != '(SKU necunoscut)':
seen_skus.add(sku)
result['missing_skus'].append(sku)
else:
result['failed'].append((order_nr, 'ERROR', err_detail[:60]))
continue
# Stopped early
if RE_STOPPED_EARLY.search(text):
result['stopped_early'] = True
# Format VECHI: statistici din sumar
if 'Total comenzi procesate:' in text:
try:
result['total_orders'] = int(text.split(':')[-1].strip())
except ValueError:
pass
if 'Comenzi importate cu succes:' in text:
try:
result['success_orders'] = int(text.split(':')[-1].strip())
except ValueError:
pass
if 'Comenzi cu erori:' in text:
try:
result['error_orders'] = int(text.split(':')[-1].strip())
except ValueError:
pass
# Format VECHI: Duration line
m = RE_STATS_LINE.search(text)
if m:
result['duration'] = m.group(1)
# Format VECHI: erori
if level == 'ERROR':
m_fail = RE_FAILED_ORDER.search(text)
if m_fail:
current_order = m_fail.group(1)
m = RE_ORDER_PROCESS.search(text)
if m:
current_order = m.group(1)
err_type, sku = extract_sku_from_error(text)
if err_type and sku:
order_nr = current_order or '?'
result['failed'].append((order_nr, err_type, sku))
if sku not in seen_skus and sku != '(SKU necunoscut)':
seen_skus.add(sku)
result['missing_skus'].append(sku)
# Duration din SYNC END
m = re.search(r'\|\s*(\d+)s\s*$', text)
if m:
result['duration'] = m.group(1) + 's'
return result
def format_report(result, log_path):
"""Formateaza raportul complet."""
lines = []
lines.append('=== SYNC LOG REPORT ===')
lines.append(f'File: {os.path.basename(log_path)}')
duration = result["duration"] or "?"
start = result["start_time"] or "?"
end = result["end_time"] or "?"
lines.append(f'Run: {start} - {end} ({duration})')
lines.append('')
stopped = 'YES' if result['stopped_early'] else 'NO'
lines.append(
f'SUMMARY: {result["total_orders"]} processed, '
f'{result["success_orders"]} success, '
f'{result["error_orders"]} errors '
f'(stopped early: {stopped})'
)
lines.append('')
if result['failed']:
lines.append('FAILED ORDERS:')
seen = set()
for order_nr, err_type, sku in result['failed']:
key = (order_nr, err_type, sku)
if key not in seen:
seen.add(key)
lines.append(f' {order_nr:<12} {err_type:<18} {sku}')
lines.append('')
if result['missing_skus']:
lines.append(f'MISSING SKUs ({len(result["missing_skus"])} unique):')
for sku in sorted(result['missing_skus']):
lines.append(f' {sku}')
lines.append('')
return '\n'.join(lines)
def main():
parser = argparse.ArgumentParser(
description='Parser pentru log-urile sync_comenzi_web'
)
parser.add_argument(
'logfile', nargs='?', default=None,
help='Fisier log specific (default: ultimul din vfp/log/)'
)
parser.add_argument(
'--skus', action='store_true',
help='Afiseaza doar lista SKU-uri lipsa (una pe linie)'
)
parser.add_argument(
'--dir', default=None,
help='Director cu log-uri (default: vfp/log/ relativ la script)'
)
args = parser.parse_args()
if args.logfile:
log_path = args.logfile
else:
if args.dir:
log_dir = args.dir
else:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_dir = os.path.dirname(script_dir)
log_dir = os.path.join(project_dir, 'vfp', 'log')
log_path = find_latest_log(log_dir)
if not log_path:
print(f'Nu am gasit fisiere sync_comenzi_*.log in {log_dir}',
file=sys.stderr)
sys.exit(1)
if not os.path.isfile(log_path):
print(f'Fisierul nu exista: {log_path}', file=sys.stderr)
sys.exit(1)
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
entries = parse_log_entries(lines)
result = analyze_entries(entries)
if args.skus:
for sku in sorted(result['missing_skus']):
print(sku)
else:
print(format_report(result, log_path))
if __name__ == '__main__':
main()

View File

@@ -11,13 +11,14 @@ Set Ansi On
Set Deleted On
*-- Variabile globale
Private gcAppPath, gcLogFile, gnStartTime, gnOrdersProcessed, gnOrdersSuccess, gnOrdersErrors
Private goConnectie, goSettings, goAppSetup
Private gcAppPath, gcLogFile, gnStartTime, gnOrdersProcessed, gnOrdersSuccess, gnOrdersErrors, gcFailedSKUs
Private goConnectie, goSettings, goAppSetup, gcStepError
Local lcJsonPattern, laJsonFiles[1], lnJsonFiles, lnIndex, lcJsonFile
Local loJsonData, lcJsonContent, lnOrderCount, lnOrderIndex
Local loOrder, lcResult, llProcessSuccess, lcPath
goConnectie = Null
gcStepError = ""
*-- Initializare
gcAppPath = Addbs(Justpath(Sys(16,0)))
@@ -37,100 +38,72 @@ gnStartTime = Seconds()
gnOrdersProcessed = 0
gnOrdersSuccess = 0
gnOrdersErrors = 0
gcFailedSKUs = ""
*-- Initializare logging
gcLogFile = InitLog("sync_comenzi")
LogMessage("=== SYNC COMENZI WEB > ORACLE ROA ===", "INFO", gcLogFile)
*-- Creare si initializare clasa setup aplicatie
goAppSetup = Createobject("ApplicationSetup", gcAppPath)
*-- Setup complet cu validare si afisare configuratie
If !goAppSetup.Initialize()
LogMessage("EROARE: Setup-ul aplicatiei a esuat sau necesita configurare!", "ERROR", gcLogFile)
Return .F.
Endif
*-- Obtinere setari din clasa
goSettings = goAppSetup.GetSettings()
*-- Verificare directoare necesare
If !Directory(gcAppPath + "output")
LogMessage("EROARE: Directorul output/ nu exista! Ruleaza mai intai adapter-ul web", "ERROR", gcLogFile)
LogMessage("EROARE: Directorul output/ nu exista!", "ERROR", gcLogFile)
Return .F.
Endif
*-- Rulare automata adapter pentru obtinere comenzi (daca este configurat)
*-- Rulare automata adapter pentru obtinere comenzi
If goSettings.AutoRunAdapter
LogMessage("Rulez adapter pentru obtinere comenzi: " + goSettings.AdapterProgram, "INFO", gcLogFile)
If !ExecuteAdapter()
LogMessage("EROARE la rularea adapter-ului, continuez cu fisierele JSON existente", "WARN", gcLogFile)
LogMessage("EROARE adapter, continuez cu JSON existente", "WARN", gcLogFile)
Endif
Else
LogMessage("AutoRunAdapter este dezactivat, folosesc doar fisierele JSON existente", "INFO", gcLogFile)
Endif
*-- Gasire fisiere JSON comenzi din pattern configurat
*-- Gasire fisiere JSON comenzi
lcJsonPattern = gcAppPath + "output\" + goSettings.JsonFilePattern
lnJsonFiles = Adir(laJsonFiles, lcJsonPattern)
If lnJsonFiles = 0
LogMessage("AVERTISMENT: Nu au fost gasite fisiere JSON cu comenzi web", "WARN", gcLogFile)
LogMessage("Ruleaza mai intai adapter-ul web cu GetOrders=1 in settings.ini", "INFO", gcLogFile)
LogMessage("Nu au fost gasite fisiere JSON cu comenzi web", "WARN", gcLogFile)
Return .T.
Endif
LogMessage("Gasite " + Transform(lnJsonFiles) + " fisiere JSON cu comenzi web", "INFO", gcLogFile)
*-- Incercare conectare Oracle (folosind conexiunea existenta din sistem)
*-- Conectare Oracle
If !ConnectToOracle()
LogMessage("EROARE: Nu s-a putut conecta la Oracle ROA", "ERROR", gcLogFile)
Return .F.
Endif
SET STEP ON
*-- Header compact
LogMessage("SYNC START | " + goSettings.OracleDSN + " " + goSettings.OracleUser + " | " + Transform(lnJsonFiles) + " JSON files", "INFO", gcLogFile)
*-- Procesare fiecare fisier JSON gasit
For lnIndex = 1 To lnJsonFiles
lcJsonFile = gcAppPath + "output\" + laJsonFiles[lnIndex, 1]
LogMessage("Procesez fisierul: " + laJsonFiles[lnIndex, 1], "INFO", gcLogFile)
*-- Citire si parsare JSON
Try
lcJsonContent = Filetostr(lcJsonFile)
If Empty(lcJsonContent)
LogMessage("AVERTISMENT: Fisier JSON gol - " + laJsonFiles[lnIndex, 1], "WARN", gcLogFile)
Loop
Endif
*-- Parsare JSON array cu comenzi
loJsonData = nfjsonread(lcJsonContent)
If Isnull(loJsonData)
LogMessage("EROARE: Nu s-a putut parsa JSON-ul din " + laJsonFiles[lnIndex, 1], "ERROR", gcLogFile)
If Isnull(loJsonData) Or Type('loJsonData') != 'O' Or Type('loJsonData.orders') != 'O'
LogMessage("EROARE JSON: " + laJsonFiles[lnIndex, 1], "ERROR", gcLogFile)
Loop
Endif
*-- Verificare daca este obiect JSON valid
If Type('loJsonData') != 'O'
LogMessage("EROARE: JSON-ul nu este un obiect valid - " + laJsonFiles[lnIndex, 1], "ERROR", gcLogFile)
Loop
Endif
*-- Verificare structura GoMag (cu proprietatea "orders")
If Type('loJsonData.orders') != 'O'
LogMessage("EROARE: JSON-ul nu contine proprietatea 'orders' - " + laJsonFiles[lnIndex, 1], "ERROR", gcLogFile)
Loop
Endif
*-- Obtinere numar comenzi din obiectul orders
Local Array laOrderProps[1]
lnOrderCount = Amembers(laOrderProps, loJsonData.orders, 0)
LogMessage("Gasite " + Transform(lnOrderCount) + " comenzi in " + laJsonFiles[lnIndex, 1], "INFO", gcLogFile)
*-- Log informatii pagina daca sunt disponibile
If Type('loJsonData.page') = 'C' Or Type('loJsonData.page') = 'N'
LogMessage("Pagina: " + Transform(loJsonData.Page) + " din " + Transform(loJsonData.Pages), "DEBUG", gcLogFile)
Endif
*-- Procesare fiecare comanda din obiectul orders
*-- Procesare fiecare comanda
For lnOrderIndex = 1 To lnOrderCount
Local lcOrderId, loOrder
lcOrderId = laOrderProps[lnOrderIndex]
@@ -138,35 +111,28 @@ For lnIndex = 1 To lnJsonFiles
If Type('loOrder') = 'O'
gnOrdersProcessed = gnOrdersProcessed + 1
LogMessage("Procesez comanda ID: " + lcOrderId + " (Nr: " + Iif(Type('loOrder.number') = 'C', loOrder.Number, "NECUNOSCUT") + ")", "DEBUG", gcLogFile)
llProcessSuccess = ProcessWebOrder(loOrder)
llProcessSuccess = ProcessWebOrder(loOrder, lnOrderIndex, lnOrderCount)
If llProcessSuccess
gnOrdersSuccess = gnOrdersSuccess + 1
Else
gnOrdersErrors = gnOrdersErrors + 1
Endif
Else
LogMessage("AVERTISMENT: Comanda cu ID " + lcOrderId + " nu este un obiect valid", "WARN", gcLogFile)
Endif
* Daca sunt peste 10 erori, ies din import fara sa mai import alte comenzi
* Probabil ca sunt erori in cod / baza de date
If m.gnOrdersErrors > 10
Exit
Endif
Endfor
Catch To loError
LogMessage("EROARE la procesarea fisierului " + laJsonFiles[lnIndex, 1] + ": " + loError.Message, "ERROR", gcLogFile)
LogMessage("EROARE fisier " + laJsonFiles[lnIndex, 1] + ": " + loError.Message, "ERROR", gcLogFile)
gnOrdersErrors = gnOrdersErrors + 1
Endtry
* Daca sunt peste 10 erori, ies din import fara sa mai import alte comenzi
* Probabil ca sunt erori in cod / baza de date
If m.gnOrdersErrors > 10
LogMessage("Peste 10 comenzi au dat eroare la import. Nu se mai importa restul de comenzi.", "ERROR", gcLogFile)
LogMessage("Peste 10 erori, stop import", "ERROR", gcLogFile)
Exit
Endif
Endfor
@@ -174,11 +140,26 @@ Endfor
*-- Inchidere conexiune Oracle
DisconnectFromOracle()
*-- Logging final cu statistici
LogMessage("=== PROCESARE COMPLETA ===", "INFO", gcLogFile)
LogMessage("Total comenzi procesate: " + Transform(gnOrdersProcessed), "INFO", gcLogFile)
LogMessage("Comenzi importate cu succes: " + Transform(gnOrdersSuccess), "INFO", gcLogFile)
LogMessage("Comenzi cu erori: " + Transform(gnOrdersErrors), "INFO", gcLogFile)
*-- Sumar SKU-uri lipsa
If !Empty(gcFailedSKUs)
LogMessage("=== SKU-URI LIPSA ===", "INFO", gcLogFile)
Local lnSkuCount, lnSkuIdx
Local Array laSkus[1]
lnSkuCount = Alines(laSkus, gcFailedSKUs, .T., CHR(10))
For lnSkuIdx = 1 To lnSkuCount
If !Empty(laSkus[lnSkuIdx])
LogMessage(Alltrim(laSkus[lnSkuIdx]), "INFO", gcLogFile)
Endif
Endfor
LogMessage("=== SFARSIT SKU-URI LIPSA ===", "INFO", gcLogFile)
Endif
*-- Footer compact
Local lcStopped, lnSkuTotal, lnDuration
lnDuration = Int(Seconds() - gnStartTime)
lnSkuTotal = Iif(Empty(gcFailedSKUs), 0, Occurs(CHR(10), gcFailedSKUs) + 1)
lcStopped = Iif(gnOrdersErrors > 10, " (stopped early)", "")
LogMessage("SYNC END | " + Transform(gnOrdersProcessed) + " processed: " + Transform(gnOrdersSuccess) + " ok, " + Transform(gnOrdersErrors) + " err" + lcStopped + " | " + Transform(lnSkuTotal) + " SKUs lipsa | " + Transform(lnDuration) + "s", "INFO", gcLogFile)
CloseLog(gnStartTime, 0, gnOrdersProcessed, gcLogFile)
Return .T.
@@ -187,132 +168,123 @@ Return .T.
*-- HELPER FUNCTIONS
*-- ===================================================================
*-- Functie pentru conectarea la Oracle folosind setarile din settings.ini
*-- Conectare la Oracle
Function ConnectToOracle
Local llSuccess, lcConnectionString, lnHandle
Local llSuccess, lnHandle
llSuccess = .F.
Try
*-- Conectare Oracle folosind datele din settings.ini
lnHandle = SQLConnect(goSettings.OracleDSN, goSettings.OracleUser, goSettings.OraclePassword)
If lnHandle > 0
goConnectie = lnHandle
llSuccess = .T.
LogMessage("Conectare Oracle reusita - Handle: " + Transform(lnHandle), "INFO", gcLogFile)
LogMessage("DSN: " + goSettings.OracleDSN + " | User: " + goSettings.OracleUser, "DEBUG", gcLogFile)
Else
LogMessage("EROARE: Conectare Oracle esuata - Handle: " + Transform(lnHandle), "ERROR", gcLogFile)
LogMessage("DSN: " + goSettings.OracleDSN + " | User: " + goSettings.OracleUser, "ERROR", gcLogFile)
LogMessage("EROARE conectare Oracle: Handle=" + Transform(lnHandle), "ERROR", gcLogFile)
Endif
Catch To loError
LogMessage("EROARE la conectarea Oracle: " + loError.Message, "ERROR", gcLogFile)
LogMessage("EROARE conectare Oracle: " + loError.Message, "ERROR", gcLogFile)
Endtry
Return llSuccess
Endfunc
*-- Functie pentru deconectarea de la Oracle
*-- Deconectare de la Oracle
Function DisconnectFromOracle
If Type('goConnectie') = 'N' And goConnectie > 0
SQLDisconnect(goConnectie)
LogMessage("Deconectare Oracle reusita", "INFO", gcLogFile)
Endif
Return .T.
Endfunc
*-- Functie principala de procesare comanda web
*-- Procesare comanda web - logeaza O SINGURA LINIE per comanda
*-- Format: [N/Total] OrderNumber P:PartnerID A:AddrFact/AddrLivr -> OK/ERR details
Function ProcessWebOrder
Parameters loOrder
Lparameters loOrder, tnIndex, tnTotal
Local llSuccess, lcOrderNumber, lcOrderDate, lnPartnerID, lcArticlesJSON
Local lcObservatii, lcSQL, lnResult, lcErrorDetails, lnIdComanda, llSucces
Local lcSQL, lnResult, lcErrorDetails, lnIdComanda, llSucces
Local ldOrderDate, loError
Local lnIdAdresaFacturare, lnIdAdresaLivrare, lcErrorMessage
Local lnIdAdresaFacturare, lnIdAdresaLivrare
Local lcPrefix, lcSummary, lcErrDetail
lnIdAdresaLivrare = NULL
lnIdAdresaFacturare = NULL
lnIdComanda = 0
llSucces = .T.
lnPartnerID = 0
lcOrderNumber = "?"
*-- Prefix: [N/Total] OrderNumber
lcPrefix = "[" + Transform(tnIndex) + "/" + Transform(tnTotal) + "]"
Try
*-- Validare comanda
If !ValidateWebOrder(loOrder)
LogMessage("EROARE: Comanda web invalida - lipsesc date obligatorii", "ERROR", gcLogFile)
llSucces = .F.
LogMessage(lcPrefix + " ? -> ERR VALIDARE: date obligatorii lipsa", "ERROR", gcLogFile)
Return .F.
Endif
*-- Extragere date comanda
If m.llSucces
lcOrderNumber = CleanWebText(Transform(loOrder.Number))
lcOrderDate = ConvertWebDate(loOrder.Date) && yyyymmdd
ldOrderDate = String2Date(m.lcOrderDate, 'yyyymmdd')
lcOrderNumber = CleanWebText(Transform(loOrder.Number))
lcOrderDate = ConvertWebDate(loOrder.Date)
ldOrderDate = String2Date(m.lcOrderDate, 'yyyymmdd')
lcPrefix = lcPrefix + " " + lcOrderNumber
LogMessage("Procesez comanda: " + lcOrderNumber + " din " + lcOrderDate, "INFO", gcLogFile)
*-- Procesare partener
gcStepError = ""
lnPartnerID = ProcessPartner(loOrder.billing)
If lnPartnerID <= 0
LogMessage(lcPrefix + " -> ERR PARTENER: " + Iif(Empty(gcStepError), "nu s-a putut procesa", gcStepError), "ERROR", gcLogFile)
Return .F.
Endif
*-- Procesare partener (billing address)
lnPartnerID = ProcessPartner(loOrder.billing)
If lnPartnerID <= 0
LogMessage("EROARE: Nu s-a putut procesa partenerul pentru comanda " + lcOrderNumber, "ERROR", gcLogFile)
llSucces = .F.
Else
LogMessage("Partener identificat/creat: ID=" + Transform(lnPartnerID), "INFO", gcLogFile)
*-- Adresa facturare
lnIdAdresaFacturare = ProcessAddress(m.lnPartnerID, loOrder.billing)
IF TYPE('loOrder.shipping') = 'O'
*-- Adresa livrares
lnIdAdresaLivrare = ProcessAddress(m.lnPartnerID, loOrder.shipping)
ENDIF
Endif
*-- Adrese
lnIdAdresaFacturare = ProcessAddress(m.lnPartnerID, loOrder.billing)
If Type('loOrder.shipping') = 'O'
lnIdAdresaLivrare = ProcessAddress(m.lnPartnerID, loOrder.shipping)
Endif
*-- Construire JSON articole
If m.llSucces
lcArticlesJSON = BuildArticlesJSON(loOrder.items)
If Empty(m.lcArticlesJSON)
LogMessage("EROARE: Nu s-au gasit articole valide in comanda " + lcOrderNumber, "ERROR", gcLogFile)
llSucces = .F.
Endif
lcArticlesJSON = BuildArticlesJSON(loOrder.items)
If Empty(m.lcArticlesJSON)
LogMessage(lcPrefix + " P:" + Transform(lnPartnerID) + " -> ERR JSON_ARTICOLE", "ERROR", gcLogFile)
Return .F.
Endif
*-- Construire observatii cu detalii suplimentare
*!* lcObservatii = BuildOrderObservations(loOrder)
*-- Import comanda in Oracle
lcSQL = "BEGIN PACK_IMPORT_COMENZI.importa_comanda(?lcOrderNumber, ?ldOrderDate, ?lnPartnerID, ?lcArticlesJSON, ?lnIdAdresaLivrare, ?lnIdAdresaFacturare, ?goSettings.IdPol, ?goSettings.IdSectie, ?@lnIdComanda); END;"
lnResult = SQLExec(goConnectie, lcSQL)
*-- Apel package Oracle pentru import comanda
If m.llSucces
lcSQL = "BEGIN PACK_IMPORT_COMENZI.importa_comanda(?lcOrderNumber, ?ldOrderDate, ?lnPartnerID, ?lcArticlesJSON, ?lnIdAdresaLivrare, ?lnIdAdresaFacturare, ?goSettings.IdPol, ?goSettings.IdSectie, ?@lnIdComanda); END;"
lnResult = SQLExec(goConnectie, lcSQL)
*-- Construire linie sumar cu ID-uri adrese
lcSummary = lcPrefix + " P:" + Transform(lnPartnerID) + ;
" A:" + Transform(Nvl(lnIdAdresaFacturare, 0)) + "/" + Transform(Nvl(lnIdAdresaLivrare, 0))
If lnResult > 0 And Nvl(m.lnIdComanda,0) > 0
LogMessage("SUCCES: Comanda importata - ID Oracle: " + Transform(m.lnIdComanda), "INFO", gcLogFile)
llSuccess = .T.
Else
llSuccess = .F.
*-- Obtinere detalii eroare Oracle
lcErrorDetails = GetOracleErrorDetails(m.lcSQL)
LogMessage("EROARE: Import comanda esuat pentru " + lcOrderNumber + CHR(10) + lcErrorDetails, "ERROR", gcLogFile)
Endif
If lnResult > 0 And Nvl(m.lnIdComanda, 0) > 0
LogMessage(lcSummary + " -> OK ID:" + Transform(m.lnIdComanda), "INFO", gcLogFile)
Return .T.
Else
lcErrorDetails = GetOracleErrorDetails()
lcErrDetail = ClassifyImportError(lcErrorDetails)
CollectFailedSKUs(lcErrorDetails)
LogMessage(lcSummary + " -> ERR " + lcErrDetail, "ERROR", gcLogFile)
Return .F.
Endif
Catch To loError
llSucces = .F.
LogMessage("EXCEPTIE la procesarea comenzii " + lcOrderNumber + ": " + loError.Message, "ERROR", gcLogFile)
LogMessage(lcPrefix + " -> ERR EXCEPTIE: " + loError.Message, "ERROR", gcLogFile)
Return .F.
Endtry
Return llSuccess
Endfunc
*-- Functie pentru validarea comenzii web
*-- Validare comanda web
Function ValidateWebOrder
Parameters loOrder
Local llValid
llValid = .T.
*-- Verificari obligatorii
If Type('loOrder.number') != 'C' Or Empty(loOrder.Number)
llValid = .F.
Endif
@@ -332,7 +304,7 @@ Function ValidateWebOrder
Return llValid
Endfunc
*-- Functie pentru procesarea partenerului din billing.company GoMag
*-- Procesare partener (fara logging, seteaza gcStepError la eroare)
Function ProcessPartner
Lparameters toBilling
Local lcDenumire, lcCodFiscal, lcRegistru, lcAdresa, lcTelefon, lcEmail, lcRegistru
@@ -344,147 +316,63 @@ Function ProcessPartner
lcCodFiscal = ''
lcRegistru = ''
lnIsPersoanaJuridica = 0
lcCodFiscal = Null && Persoanele fizice nu au CUI in platformele web
If .F.
TEXT TO lcExampleJsonAdresa NOSHOW
"billing": {
"address": "STR. CAMPUL LINISTII, NR. 1",
"city": "Arad",
"company": {
"bank": "",
"code": "RO30071208",
"iban": "",
"name": "BASSANO BUILDINGS SRL",
"registrationNo": ""
},
"country": "Romania",
"customerId": "13422",
"email": "office@bassano.ro",
"firstname": "Ionela",
"lastname": "Letcan",
"phone": "0728141899",
"region": "Arad"
},
ENDTEXT
ENDIF
lcCodFiscal = Null
Try
*-- Extragere date partener din datele billing
If Type('toBilling.company') = 'O' And !Empty(toBilling.company.Name)
loCompany = toBilling.company
*-- Companie - persoana juridica
lcDenumire = CleanWebText(loCompany.Name)
lcCodFiscal = Iif(Type('loCompany.code') = 'C', loCompany.Code, Null)
lcCodFiscal = CleanWebText(m.lcCodFiscal)
lcRegistru = Iif(Type('loCompany.registrationNo') = 'C', loCompany.registrationNo, Null)
lcRegistru = CleanWebText(m.lcRegistru)
lnIsPersoanaJuridica = 1 && Persoana juridica
lnIsPersoanaJuridica = 1
Else
*-- Persoana fizica
IF TYPE('toBilling.firstname') = 'C'
If Type('toBilling.firstname') = 'C'
lcDenumire = CleanWebText(Alltrim(toBilling.firstname) + " " + Alltrim(toBilling.lastname))
lnIsPersoanaJuridica = 0 && Persoana fizica
ENDIF
lnIsPersoanaJuridica = 0
Endif
Endif
LogMessage("Partener: " + lcDenumire + " | CUI: " + Iif(Isnull(lcCodFiscal), "NULL", lcCodFiscal) + " | Tip: " + Iif(lnIsPersoanaJuridica = 1, "JURIDICA", "FIZICA"), "DEBUG", gcLogFile)
* Cautare/creare client
lcSQL = "BEGIN PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener(?lcCodFiscal, ?lcDenumire, ?lcRegistru, ?lnIsPersoanaJuridica, ?@lnIdPart); END;"
lnResult = SQLExec(goConnectie, lcSQL)
If lnResult > 0
LogMessage("Partener procesat cu succes: ID=" + Transform(m.lnIdPart), "DEBUG", gcLogFile)
Else
*-- Obtinere detalii eroare Oracle
lcErrorDetails = GetOracleErrorDetails(m.lcSQL)
LogMessage("EROARE la apelul procedurii PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener pentru: " + lcDenumire + CHR(10) + lcErrorDetails, "ERROR", gcLogFile)
If lnResult <= 0
gcStepError = lcDenumire + " | " + GetOracleErrorDetails()
Endif
Catch To loError
LogMessage("EXCEPTIE la procesarea partenerului: " + loError.Message, "ERROR", gcLogFile)
gcStepError = loError.Message
Endtry
Return m.lnIdPart
ENDFUNC && ProcessPartner
Endfunc
*-- Functie pentru procesarea adresei din billing/shipping
*-- Procesare adresa (fara logging)
Function ProcessAddress
Lparameters tnIdPart, toAdresa
Local lcAdresa, lcTelefon, lcEmail, lcSQL, lnResult, lnIdAdresa
lnIdAdresa = 0
If .F.
TEXT TO lcExampleJsonAdresa NOSHOW
"billing": {
"address": "STR. CAMPUL LINISTII, NR. 1",
"city": "Arad",
"company": {
"bank": "",
"code": "RO30071208",
"iban": "",
"name": "BASSANO BUILDINGS SRL",
"registrationNo": ""
},
"country": "Romania",
"customerId": "13422",
"email": "office@bassano.ro",
"firstname": "Ionela",
"lastname": "Letcan",
"phone": "0728141899",
"region": "Arad"
},
"shipping": {
"address": "Strada Molnar Janos nr 23 bloc 37 sc B etj e ap. 16",
"city": "Bra?ov",
"company": "",
"country": "Romania",
"email": "ancamirela74@gmail.com",
"firstname": "Anca",
"lastname": "Stanciu",
"phone": "0758261492",
"region": "Brasov",
"zipcode": ""
}
ENDTEXT
Endif
Try
* Cautare/creare adresa
If !Empty(Nvl(m.tnIdPart,0))
*-- Formatare adresa pentru Oracle (format semicolon cu prefix JUD:)
If !Empty(Nvl(m.tnIdPart, 0))
lcAdresa = FormatAddressForOracle(toAdresa)
*-- Date contact
lcTelefon = Iif(Type('toAdresa.phone') = 'C', toAdresa.phone, "")
lcEmail = Iif(Type('toAdresa.email') = 'C', toAdresa.email, "")
lcSQL = "BEGIN PACK_IMPORT_PARTENERI.cauta_sau_creeaza_adresa(?tnIdPart, ?lcAdresa, ?lcTelefon, ?lcEmail, ?@lnIdAdresa); END;"
lnResult = SQLExec(goConnectie, lcSQL)
If lnResult > 0
LogMessage("Adresa procesata cu succes: ID=" + Transform(m.lnIdAdresa), "DEBUG", gcLogFile)
Else
*-- Obtinere detalii eroare Oracle
lcErrorDetails = GetOracleErrorDetails(m.lcSQL)
LogMessage("EROARE la apelul procedurii PACK_IMPORT_PARTENERI.cauta_sau_creeaza_adresa pentru PartnerId: " + ALLTRIM(TRANSFORM(m.tnIdPart)) + CHR(10) + lcErrorDetails, "ERROR", gcLogFile)
Endif
Endif
Catch To loError
LogMessage("EXCEPTIE la procesarea adresei: " + loError.Message, "ERROR", gcLogFile)
Endtry
Return m.lnIdAdresa
Endfunc && ProcessAddress
Endfunc
*-- Functie pentru construirea JSON-ului cu articole conform package Oracle
*-- Construire JSON articole
Function BuildArticlesJSON
Lparameters loItems
@@ -494,14 +382,13 @@ Function BuildArticlesJSON
Try
lcJSON = nfjsoncreate(loItems)
Catch To loError
LogMessage("EROARE la construirea JSON articole: " + loError.Message, "ERROR", gcLogFile)
lcJSON = ""
Endtry
Return lcJSON
Endfunc
*-- Functie pentru curatarea textului web (HTML entities ASCII simplu)
*-- Curatare text web (HTML entities -> ASCII simplu)
Function CleanWebText
Parameters tcText
Local lcResult
@@ -512,41 +399,36 @@ Function CleanWebText
lcResult = tcText
*-- Conversie HTML entities in caractere simple (fara diacritice)
lcResult = Strtran(lcResult, '&#259;', 'a') && ă → a
lcResult = Strtran(lcResult, '&#537;', 's') && ș → s
lcResult = Strtran(lcResult, '&#539;', 't') && ț → t
lcResult = Strtran(lcResult, '&#238;', 'i') && î → i
lcResult = Strtran(lcResult, '&#226;', 'a') && â → a
lcResult = Strtran(lcResult, '&#259;', 'a')
lcResult = Strtran(lcResult, '&#537;', 's')
lcResult = Strtran(lcResult, '&#539;', 't')
lcResult = Strtran(lcResult, '&#238;', 'i')
lcResult = Strtran(lcResult, '&#226;', 'a')
lcResult = Strtran(lcResult, '&amp;', '&')
lcResult = Strtran(lcResult, '&lt;', '<')
lcResult = Strtran(lcResult, '&gt;', '>')
lcResult = Strtran(lcResult, '&quot;', '"')
*-- Eliminare tag-uri HTML simple
lcResult = Strtran(lcResult, '<br>', ' ')
lcResult = Strtran(lcResult, '<br/>', ' ')
lcResult = Strtran(lcResult, '<br />', ' ')
*-- Eliminare Esc character
lcResult = Strtran(lcResult, '\/', '/')
Return Alltrim(lcResult)
Endfunc
*-- Functie pentru conversia datei web in format Oracle
*-- Conversie data web in format YYYYMMDD
Function ConvertWebDate
Parameters tcWebDate
Local lcResult
If Empty(tcWebDate) Or Type('tcWebDate') != 'C'
Return Dtos(Date()) && yyyymmdd
Return Dtos(Date())
Endif
*-- Web date format: "2025-08-27 16:32:43" → "20250827"
lcResult = Strtran(Left(tcWebDate, 10), "-", "",1,10,1)
*-- Validare format YYYYMMDD
If Len(lcResult) = 8
Return lcResult
Else
@@ -554,12 +436,9 @@ Function ConvertWebDate
Endif
Endfunc
*-- Functie pentru conversia datei string in Date
* ldData = String2Date('20250912', ['yyyymmdd'])
*-- Conversie string in Date
Function String2Date
Lparameters tcDate, tcFormat
* tcDate: 20250911
* tcFormat: yyyymmdd (default)
Local lcAn, lcDate, lcFormat, lcLuna, lcZi, ldData, lnAn, lnLuna, lnZi, loEx
ldData = {}
@@ -567,19 +446,17 @@ Function String2Date
lcDate = m.tcDate
lcFormat = Iif(!Empty(m.tcFormat), Alltrim(Lower(m.tcFormat)), 'yyyymmdd')
lcDate = Chrtran(m.lcDate, '-/\','...') && inlocuiesc .-/\ cu . ca sa am doar variante yyyy.mm.dd, dd.mm.yyyy
lcDate = Chrtran(m.lcDate, '-/\','...')
lcDate = Strtran(m.lcDate, '.', '', 1, 2, 1)
lcFormat = Chrtran(m.lcFormat, '.-/\','...')
lcFormat = Strtran(m.lcFormat, '.', '', 1, 2, 1)
Do Case
Case m.lcFormat = 'ddmmyyyy'
lcAn = Substr(m.tcDate, 5, 4)
lcLuna = Substr(m.tcDate, 3, 2)
lcZi = Substr(m.tcDate, 1, 2)
Otherwise
* yyyymmdd
lcAn = Substr(m.tcDate, 1, 4)
lcLuna = Substr(m.tcDate, 5, 2)
lcZi = Substr(m.tcDate, 7, 2)
@@ -597,30 +474,27 @@ Function String2Date
Return m.ldData
Endfunc
*-- Functie pentru formatarea adresei in format semicolon pentru Oracle
*-- Formatare adresa in format semicolon pentru Oracle
Function FormatAddressForOracle
Parameters loBilling
Local lcAdresa, lcJudet, lcOras, lcStrada
*-- Extragere componente adresa
lcJudet = Iif(Type('loBilling.region') = 'C', CleanWebText(loBilling.Region), "")
lcOras = Iif(Type('loBilling.city') = 'C', CleanWebText(loBilling.city), "")
lcStrada = Iif(Type('loBilling.address') = 'C', CleanWebText(loBilling.address), "")
*-- Format semicolon cu prefix JUD: conform specificatiilor Oracle
lcAdresa = "JUD:" + lcJudet + ";" + lcOras + ";" + lcStrada
Return lcAdresa
Endfunc
*-- Functie pentru construirea observatiilor comenzii
*-- Construire observatii comanda
Function BuildOrderObservations
Parameters loOrder
Local lcObservatii
lcObservatii = ""
*-- Informatii plata si livrare
If Type('loOrder.payment') = 'O' And Type('loOrder.payment.name') = 'C'
lcObservatii = lcObservatii + "Payment: " + CleanWebText(loOrder.Payment.Name) + "; "
Endif
@@ -629,7 +503,6 @@ Function BuildOrderObservations
lcObservatii = lcObservatii + "Delivery: " + CleanWebText(loOrder.delivery.Name) + "; "
Endif
*-- Status si sursa
If Type('loOrder.status') = 'C'
lcObservatii = lcObservatii + "Status: " + CleanWebText(loOrder.Status) + "; "
Endif
@@ -643,7 +516,6 @@ Function BuildOrderObservations
lcObservatii = lcObservatii + "; "
Endif
*-- Verificare adrese diferite shipping vs billing
If Type('loOrder.shipping') = 'O' And Type('loOrder.billing') = 'O'
If Type('loOrder.shipping.address') = 'C' And Type('loOrder.billing.address') = 'C'
If !Alltrim(loOrder.shipping.address) == Alltrim(loOrder.billing.address)
@@ -657,7 +529,6 @@ Function BuildOrderObservations
Endif
Endif
*-- Limitare lungime observatii pentru Oracle
If Len(lcObservatii) > 500
lcObservatii = Left(lcObservatii, 497) + "..."
Endif
@@ -665,16 +536,12 @@ Function BuildOrderObservations
Return lcObservatii
Endfunc
*-- Functie pentru obtinerea detaliilor erorii Oracle
*-- Obtinere detalii eroare Oracle (single-line, fara SQL)
Function GetOracleErrorDetails
Lparameters tcSql
* tcSql (optional) : SQL executat
Local lcError, laError[1], lnErrorLines, lnIndex
lcError = ""
*-- Obtinere eroare Oracle
lnErrorLines = Aerror(laError)
If lnErrorLines > 0
For lnIndex = 1 To lnErrorLines
@@ -689,12 +556,103 @@ Function GetOracleErrorDetails
lcError = "Eroare Oracle nedefinita"
Endif
lcError = Iif(Pcount() = 1 And !Empty(m.tcSql) And Type('tcSql') = 'C', m.tcSql + Chr(13) + Chr(10), '') + m.lcError
*-- Compact: inlocuieste newlines cu spatii
lcError = Strtran(lcError, CHR(13) + CHR(10), " ")
lcError = Strtran(lcError, CHR(10), " ")
lcError = Strtran(lcError, CHR(13), " ")
Return lcError
Endfunc
*-- Functie pentru executia adapter-ului configurat
*-- Clasifica eroarea Oracle intr-un format compact
*-- Returneaza: "SKU_NOT_FOUND: sku" / "PRICE_POLICY: sku" / eroarea bruta
Function ClassifyImportError
Lparameters tcErrorDetails
Local lcText, lcSku, lnPos, lcSearch
lcText = Iif(Empty(tcErrorDetails), "", tcErrorDetails)
*-- SKU negasit
lcSearch = "NOM_ARTICOLE: "
lnPos = Atc(lcSearch, lcText)
If lnPos > 0
lcSku = Alltrim(Getwordnum(Substr(lcText, lnPos + Len(lcSearch)), 1))
Return "SKU_NOT_FOUND: " + lcSku
Endif
*-- Eroare adaugare articol (include pretul)
lcSearch = "Eroare adaugare articol "
lnPos = Atc(lcSearch, lcText)
If lnPos > 0
lcSku = Alltrim(Getwordnum(Substr(lcText, lnPos + Len(lcSearch)), 1))
Return "PRICE_POLICY: " + lcSku
Endif
*-- Eroare pret fara SKU (inainte de fix-ul Oracle)
If Atc("Pretul pentru acest articol", lcText) > 0
Return "PRICE_POLICY: (SKU necunoscut)"
Endif
*-- Eroare generica - primele 100 caractere
Return Left(lcText, 100)
Endfunc
*-- Colectare SKU-uri lipsa din mesajele de eroare Oracle
Function CollectFailedSKUs
Lparameters tcErrorDetails
Local lcSku, lnPos, lcSearch, lcText
If Empty(tcErrorDetails)
Return
Endif
lcText = tcErrorDetails
*-- Pattern 1: "SKU negasit in ARTICOLE_TERTI si NOM_ARTICOLE: XXXXX"
lcSearch = "NOM_ARTICOLE: "
lnPos = Atc(lcSearch, lcText)
If lnPos > 0
lcSku = Alltrim(Getwordnum(Substr(lcText, lnPos + Len(lcSearch)), 1))
If !Empty(lcSku)
AddUniqueSKU(lcSku)
Endif
Endif
*-- Pattern 2: "Eroare adaugare articol XXXXX (CODMAT:" sau "Eroare adaugare articol XXXXX:"
lcSearch = "Eroare adaugare articol "
lnPos = Atc(lcSearch, lcText)
If lnPos > 0
lcSku = Alltrim(Getwordnum(Substr(lcText, lnPos + Len(lcSearch)), 1))
If !Empty(lcSku)
AddUniqueSKU(lcSku)
Endif
Endif
Return
Endfunc
*-- Adauga un SKU in gcFailedSKUs daca nu exista deja
Function AddUniqueSKU
Lparameters tcSku
Local lcSku
lcSku = Alltrim(tcSku)
If Empty(lcSku)
Return
Endif
If Empty(gcFailedSKUs)
gcFailedSKUs = lcSku
Else
If !(CHR(10) + lcSku + CHR(10)) $ (CHR(10) + gcFailedSKUs + CHR(10))
gcFailedSKUs = gcFailedSKUs + CHR(10) + lcSku
Endif
Endif
Return
Endfunc
*-- Executie adapter configurat
Function ExecuteAdapter
Local llSuccess, lcAdapterPath
@@ -704,28 +662,15 @@ Function ExecuteAdapter
lcAdapterPath = gcAppPath + goSettings.AdapterProgram
If File(lcAdapterPath)
LogMessage("Executie adapter: " + lcAdapterPath, "INFO", gcLogFile)
Do (lcAdapterPath)
llSuccess = .T.
LogMessage("Adapter executat cu succes", "INFO", gcLogFile)
Else
LogMessage("EROARE: Adapter-ul nu a fost gasit la: " + lcAdapterPath, "ERROR", gcLogFile)
LogMessage("EROARE: Adapter negasit: " + lcAdapterPath, "ERROR", gcLogFile)
Endif
Catch To loError
LogMessage("EXCEPTIE la executia adapter-ului " + goSettings.AdapterProgram + ": " + loError.Message, "ERROR", gcLogFile)
LogMessage("EROARE adapter: " + loError.Message, "ERROR", gcLogFile)
Endtry
Return llSuccess
Endfunc
*-- Orchestrator complet pentru sincronizarea comenzilor web cu Oracle ROA
*-- Caracteristici:
*-- - Citeste JSON-urile generate de gomag-vending.prg
*-- - Proceseaza comenzile cu toate helper functions necesare
*-- - Integreaza cu package-urile Oracle validate in Phase 1
*-- - Logging complet cu statistici de procesare
*-- - Error handling pentru toate situatiile
*-- - Support pentru toate formatele GoMag (billing/shipping, companii/persoane fizice)