Files
vending_data_intelligence_r…/PLAN_FIXES_2025_11_28.md
Marius Mutu 0b732f7a7a Initial commit: Data Intelligence Report Generator
- Oracle ERP ROA integration with sales analytics and margin analysis
- Excel multi-sheet reports with conditional formatting
- PDF executive summaries with charts via ReportLab
- Optimized SQL queries (no cartesian products)
- Docker support for cross-platform deployment
- Configurable alert thresholds for business intelligence

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 15:41:56 +02:00

16 KiB

Plan: Corectii Report Generator - 28.11.2025

Probleme de Rezolvat

  1. Analiza Prajitorie - intrarile si iesirile apar pe randuri diferite in loc de coloane
  2. Query-uri Financiare "No Data" - DSO/DPO, Solduri clienti/furnizori, Aging, Pozitia Cash, Ciclu Conversie Cash nu afiseaza date (user confirma ca datele EXISTA)
  3. Recomandari in Sumar Executiv - trebuie incluse sub KPIs in sheet-ul Sumar Executiv
  4. Reordonare Sheet-uri - agregatele (indicatori_agregati, portofoliu_clienti, concentrare_risc) trebuie mutate imediat dupa Sumar Executiv

ISSUE 1: Analiza Prajitorie - Restructurare din Randuri in Coloane

Fisier: queries.py liniile 450-478

Problema Curenta

Query-ul grupeaza dupa tip_miscare (Intrare/Iesire/Transformare), creand randuri separate:

luna      | tip              | tip_miscare   | cantitate_intrata | cantitate_iesita
2024-01   | Materii prime    | Intrare       | 1000              | 0
2024-01   | Materii prime    | Iesire        | 0                 | 800

Output Cerut

Un rand per Luna + Tip cu coloane separate pentru Intrari si Iesiri:

luna      | tip              | cantitate_intrari | valoare_intrari | cantitate_iesiri | valoare_iesiri | sold_net
2024-01   | Materii prime    | 1000              | 50000           | 800              | 40000          | 10000

Solutia: Inlocuieste ANALIZA_PRAJITORIE (liniile 450-478)

ANALIZA_PRAJITORIE = """
SELECT
    TO_CHAR(r.dataact, 'YYYY-MM') AS luna,
    CASE
        WHEN r.cont = '301' THEN 'Materii prime'
        WHEN r.cont = '341' THEN 'Semifabricate'
        WHEN r.cont = '345' THEN 'Produse finite'
        ELSE 'Altele'
    END AS tip,
    -- Intrari: cantitate > 0 AND cante = 0
    ROUND(SUM(CASE WHEN r.cant > 0 AND NVL(r.cante, 0) = 0 THEN r.cant ELSE 0 END), 2) AS cantitate_intrari,
    ROUND(SUM(CASE WHEN r.cant > 0 AND NVL(r.cante, 0) = 0 THEN r.cant * NVL(r.pret, 0) ELSE 0 END), 2) AS valoare_intrari,
    -- Iesiri: cant = 0 AND cante > 0
    ROUND(SUM(CASE WHEN NVL(r.cant, 0) = 0 AND r.cante > 0 THEN r.cante ELSE 0 END), 2) AS cantitate_iesiri,
    ROUND(SUM(CASE WHEN NVL(r.cant, 0) = 0 AND r.cante > 0 THEN r.cante * NVL(r.pret, 0) ELSE 0 END), 2) AS valoare_iesiri,
    -- Transformari: cant > 0 AND cante > 0 (intrare si iesire simultan)
    ROUND(SUM(CASE WHEN r.cant > 0 AND r.cante > 0 THEN r.cant ELSE 0 END), 2) AS cantitate_transformari_in,
    ROUND(SUM(CASE WHEN r.cant > 0 AND r.cante > 0 THEN r.cante ELSE 0 END), 2) AS cantitate_transformari_out,
    -- Sold net
    ROUND(SUM(NVL(r.cant, 0) - NVL(r.cante, 0)), 2) AS sold_net_cantitate,
    ROUND(SUM((NVL(r.cant, 0) - NVL(r.cante, 0)) * NVL(r.pret, 0)), 2) AS sold_net_valoare
FROM vrul r
WHERE r.cont IN ('301', '341', '345')
  AND r.dataact >= ADD_MONTHS(TRUNC(SYSDATE), -:months)
GROUP BY TO_CHAR(r.dataact, 'YYYY-MM'),
         CASE WHEN r.cont = '301' THEN 'Materii prime'
              WHEN r.cont = '341' THEN 'Semifabricate'
              WHEN r.cont = '345' THEN 'Produse finite'
              ELSE 'Altele' END
ORDER BY luna, tip
"""

Modificari Cheie

  1. Eliminat tip_miscare din SELECT si GROUP BY
  2. Agregare conditionala cu CASE WHEN ... THEN ... ELSE 0 END in SUM()
  3. Coloane separate pentru fiecare tip de miscare
  4. Adaugat coloane valoare pe langa cantitati

Update Legends in main.py (in jurul liniei 224)

Adauga in dictionarul legends:

'analiza_prajitorie': {
    'CANTITATE_INTRARI': 'Cantitate intrata (cant > 0, cante = 0)',
    'VALOARE_INTRARI': 'Valoare intrari = cantitate x pret',
    'CANTITATE_IESIRI': 'Cantitate iesita (cant = 0, cante > 0)',
    'VALOARE_IESIRI': 'Valoare iesiri = cantitate x pret',
    'CANTITATE_TRANSFORMARI_IN': 'Cantitate intrata in transformari',
    'CANTITATE_TRANSFORMARI_OUT': 'Cantitate iesita din transformari',
    'SOLD_NET_CANTITATE': 'Sold net = Total intrari - Total iesiri',
    'SOLD_NET_VALOARE': 'Valoare neta a soldului'
}

ISSUE 2: Query-uri Financiare "No Data" - DIAGNOSTIC NECESAR

Query-uri Afectate

Query View Folosit Linie in queries.py Filtru Curent
DSO_DPO vbalanta_parteneri 796-844 an = EXTRACT(YEAR FROM SYSDATE) AND luna = EXTRACT(MONTH FROM SYSDATE)
SOLDURI_CLIENTI vbalanta_parteneri 636-654 Acelasi + cont LIKE '4111%'
SOLDURI_FURNIZORI vbalanta_parteneri 659-677 Acelasi + cont LIKE '401%'
AGING_CREANTE vireg_parteneri 682-714 cont LIKE '4111%' OR '461%'
FACTURI_RESTANTE vireg_parteneri 719-734 Acelasi + datascad < SYSDATE
POZITIA_CASH vbal 849-872 cont LIKE '512%' OR '531%'
CICLU_CONVERSIE_CASH Multiple 877-940 Combina toate de mai sus

User-ul confirma ca DATELE EXISTA - trebuie diagnosticat problema

Cauze Posibile

  1. Numele view-urilor difera in baza de date
  2. Numele coloanelor difera (an, luna, solddeb, soldcred)
  3. Prefixele codurilor de cont nu se potrivesc (4111%, 401%, 512%)
  4. Pragurile HAVING sunt prea restrictive (> 1, > 100)

FIX IMEDIAT: Relaxeaza Pragurile HAVING

SOLDURI_CLIENTI (linia 652):

-- DE LA:
HAVING ABS(SUM(b.solddeb - b.soldcred)) > 1
-- LA:
HAVING ABS(SUM(b.solddeb - b.soldcred)) > 0.01

SOLDURI_FURNIZORI (linia 675):

-- DE LA:
HAVING ABS(SUM(b.soldcred - b.solddeb)) > 1
-- LA:
HAVING ABS(SUM(b.soldcred - b.solddeb)) > 0.01

AGING_CREANTE (linia 712):

-- DE LA:
HAVING SUM(sold_ramas) > 100
-- LA:
HAVING SUM(sold_ramas) > 0.01

AGING_DATORII (linia 770):

-- DE LA:
HAVING SUM(sold_ramas) > 100
-- LA:
HAVING SUM(sold_ramas) > 0.01

POZITIA_CASH (linia 870):

-- DE LA:
HAVING ABS(SUM(b.solddeb - b.soldcred)) > 0.01
-- Deja OK, dar verifica daca vbal exista

Daca Tot Nu Functioneaza - Verifica View-urile

Ruleaza in Oracle:

-- Verifica daca view-urile exista
SELECT view_name FROM user_views
WHERE view_name IN ('VBALANTA_PARTENERI', 'VIREG_PARTENERI', 'VBAL', 'VRUL');

-- Verifica daca exista date pentru luna curenta
SELECT an, luna, COUNT(*)
FROM vbalanta_parteneri
WHERE an = EXTRACT(YEAR FROM SYSDATE)
GROUP BY an, luna
ORDER BY luna DESC;

-- Verifica prefixele de cont existente
SELECT DISTINCT SUBSTR(cont, 1, 4) AS prefix_cont
FROM vbalanta_parteneri
WHERE an = EXTRACT(YEAR FROM SYSDATE);

ISSUE 3: Recomandari in Sumar Executiv

Stare Curenta

  • Sheet sumar_executiv (linia 166) - contine doar KPIs
  • Sheet recomandari (linia 168) - sheet separat cu toate recomandarile

Solutia: Metoda noua in report_generator.py

Adauga metoda noua in clasa ExcelReportGenerator (dupa linia 167 in report_generator.py)

def add_sheet_with_recommendations(self, name: str, df: pd.DataFrame,
                                    recommendations_df: pd.DataFrame,
                                    title: str = None, description: str = None,
                                    legend: dict = None, top_n_recommendations: int = 5):
    """Adauga sheet formatat cu KPIs si top recomandari dedesubt"""
    sheet_name = name[:31]
    ws = self.wb.create_sheet(title=sheet_name)

    start_row = 1

    # Adauga titlu
    if title:
        ws.cell(row=start_row, column=1, value=title)
        ws.cell(row=start_row, column=1).font = Font(bold=True, size=14)
        start_row += 1

    # Adauga descriere
    if description:
        ws.cell(row=start_row, column=1, value=description)
        ws.cell(row=start_row, column=1).font = Font(italic=True, size=10, color='666666')
        start_row += 1

    # Adauga timestamp
    ws.cell(row=start_row, column=1, value=f"Generat: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
    ws.cell(row=start_row, column=1).font = Font(size=9, color='999999')
    start_row += 2

    # === SECTIUNEA 1: KPIs ===
    if df is not None and not df.empty:
        # Header
        for col_idx, col_name in enumerate(df.columns, 1):
            cell = ws.cell(row=start_row, column=col_idx, value=col_name)
            cell.font = self.header_font
            cell.fill = self.header_fill
            cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
            cell.border = self.border

        # Date
        for row_idx, row in enumerate(df.itertuples(index=False), start_row + 1):
            for col_idx, value in enumerate(row, 1):
                cell = ws.cell(row=row_idx, column=col_idx, value=value)
                cell.border = self.border
                if isinstance(value, (int, float)):
                    cell.number_format = '#,##0.00' if isinstance(value, float) else '#,##0'
                    cell.alignment = Alignment(horizontal='right')

        start_row = start_row + len(df) + 3

    # === SECTIUNEA 2: TOP RECOMANDARI ===
    if recommendations_df is not None and not recommendations_df.empty:
        ws.cell(row=start_row, column=1, value="Top Recomandari Prioritare")
        ws.cell(row=start_row, column=1).font = Font(bold=True, size=12, color='366092')
        start_row += 1

        # Sorteaza dupa prioritate (ALERTA primul, apoi ATENTIE, apoi OK)
        df_sorted = recommendations_df.copy()
        status_order = {'ALERTA': 0, 'ATENTIE': 1, 'OK': 2}
        df_sorted['_order'] = df_sorted['STATUS'].map(status_order).fillna(3)
        df_sorted = df_sorted.sort_values('_order').head(top_n_recommendations)
        df_sorted = df_sorted.drop(columns=['_order'])

        # Coloane de afisat
        display_cols = ['STATUS', 'CATEGORIE', 'INDICATOR', 'VALOARE', 'RECOMANDARE']
        display_cols = [c for c in display_cols if c in df_sorted.columns]

        # Header cu background mov
        for col_idx, col_name in enumerate(display_cols, 1):
            cell = ws.cell(row=start_row, column=col_idx, value=col_name)
            cell.font = self.header_font
            cell.fill = PatternFill(start_color='8E44AD', end_color='8E44AD', fill_type='solid')
            cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
            cell.border = self.border

        # Randuri cu colorare dupa status
        for row_idx, (_, row) in enumerate(df_sorted.iterrows(), start_row + 1):
            status = row.get('STATUS', 'OK')
            for col_idx, col_name in enumerate(display_cols, 1):
                value = row.get(col_name, '')
                cell = ws.cell(row=row_idx, column=col_idx, value=value)
                cell.border = self.border
                cell.alignment = Alignment(wrap_text=True)

                # Colorare condiționata
                if status == 'ALERTA':
                    cell.fill = PatternFill(start_color='FADBD8', end_color='FADBD8', fill_type='solid')
                elif status == 'ATENTIE':
                    cell.fill = PatternFill(start_color='FCF3CF', end_color='FCF3CF', fill_type='solid')
                else:
                    cell.fill = PatternFill(start_color='D5F5E3', end_color='D5F5E3', fill_type='solid')

    # Auto-adjust latime coloane
    for col_idx in range(1, 8):
        ws.column_dimensions[get_column_letter(col_idx)].width = 22

    ws.freeze_panes = ws.cell(row=5, column=1)

Modifica main.py - Loop-ul de Creare Sheet-uri (in jurul liniei 435)

for query_name in sheet_order:
    if query_name in results:
        # Tratare speciala pentru 'sumar_executiv' - adauga recomandari sub KPIs
        if query_name == 'sumar_executiv':
            query_info = QUERIES.get(query_name, {})
            excel_gen.add_sheet_with_recommendations(
                name='Sumar Executiv',
                df=results['sumar_executiv'],
                recommendations_df=results.get('recomandari'),
                title=query_info.get('title', 'Sumar Executiv'),
                description=query_info.get('description', ''),
                legend=legends.get('sumar_executiv'),
                top_n_recommendations=5
            )
        # Pastreaza sheet-ul complet de recomandari
        elif query_name == 'recomandari':
            excel_gen.add_sheet(
                name='RECOMANDARI',
                df=results['recomandari'],
                title='Recomandari Automate (Lista Completa)',
                description='Toate insight-urile si actiunile sugerate bazate pe analiza datelor',
                legend=legends.get('recomandari')
            )
        elif query_name in QUERIES:
            # ... logica existenta neschimbata

ISSUE 4: Reordonare Sheet-uri

Fisier: main.py liniile 165-221

Noul sheet_order (inlocuieste complet liniile 165-221)

    sheet_order = [
        # SUMAR EXECUTIV
        'sumar_executiv',
        'sumar_executiv_yoy',
        'recomandari',

        # INDICATORI AGREGATI (MUTATI SUS - imagine de ansamblu)
        'indicatori_agregati_venituri',
        'indicatori_agregati_venituri_yoy',
        'portofoliu_clienti',
        'concentrare_risc',
        'concentrare_risc_yoy',
        'sezonalitate_lunara',

        # INDICATORI GENERALI & LICHIDITATE
        'indicatori_generali',
        'indicatori_lichiditate',
        'clasificare_datorii',
        'grad_acoperire_datorii',
        'proiectie_lichiditate',

        # ALERTE
        'vanzari_sub_cost',
        'clienti_marja_mica',

        # CICLU CASH
        'ciclu_conversie_cash',

        # ANALIZA CLIENTI
        'marja_per_client',
        'clienti_ranking_profit',
        'frecventa_clienti',
        'concentrare_clienti',
        'trending_clienti',
        'marja_client_categorie',

        # PRODUSE
        'top_produse',
        'marja_per_categorie',
        'marja_per_gestiune',
        'articole_negestionabile',
        'productie_vs_revanzare',

        # PRETURI
        'dispersie_preturi',
        'clienti_sub_medie',
        'evolutie_discount',

        # FINANCIAR
        'dso_dpo',
        'dso_dpo_yoy',
        'solduri_clienti',
        'aging_creante',
        'facturi_restante',
        'solduri_furnizori',
        'aging_datorii',
        'facturi_restante_furnizori',
        'pozitia_cash',

        # ISTORIC
        'vanzari_lunare',

        # STOC
        'stoc_curent',
        'stoc_lent',
        'rotatie_stocuri',

        # PRODUCTIE
        'analiza_prajitorie',
    ]

Ordinea de Implementare

Pasul 1: queries.py

  1. Inlocuieste ANALIZA_PRAJITORIE (liniile 450-478) cu versiunea cu agregare conditionala
  2. Relaxeaza pragurile HAVING in:
    • SOLDURI_CLIENTI (linia 652): > 1 -> > 0.01
    • SOLDURI_FURNIZORI (linia 675): > 1 -> > 0.01
    • AGING_CREANTE (linia 712): > 100 -> > 0.01
    • AGING_DATORII (linia 770): > 100 -> > 0.01

Pasul 2: report_generator.py

  1. Adauga metoda add_sheet_with_recommendations() dupa linia 167
  2. Asigura-te ca importurile includ PatternFill, get_column_letter din openpyxl

Pasul 3: main.py

  1. Inlocuieste array-ul sheet_order (liniile 165-221)
  2. Modifica loop-ul de creare sheet-uri pentru sumar_executiv (in jurul liniei 435)
  3. Adauga legend pentru analiza_prajitorie in dictionarul legends

Pasul 4: Testare

  1. Ruleaza cu python main.py --months 1 pentru test rapid
  2. Verifica sheet-ul analiza_prajitorie - format columnar
  3. Verifica query-urile financiare - trebuie sa returneze date
  4. Verifica Sumar Executiv - sectiune recomandari dedesubt
  5. Verifica ordinea sheet-urilor - agregatele dupa sumar

Fisiere Critice

Fisier Ce se modifica Linii
queries.py ANALIZA_PRAJITORIE SQL 450-478
queries.py HAVING thresholds 652, 675, 712, 770
report_generator.py Metoda noua dupa 167
main.py sheet_order array 165-221
main.py Loop creare sheet-uri ~435
main.py legends dict ~224

Note pentru Sesiunea Viitoare

  1. Prioritate ALERTA: Query-urile financiare "no data" - user-ul a confirmat ca datele EXISTA. Daca relaxarea HAVING nu rezolva, trebuie verificate numele view-urilor si coloanelor in Oracle.

  2. Import necesar in report_generator.py:

from openpyxl.utils import get_column_letter
from openpyxl.styles import PatternFill
  1. Testare: Dupa implementare, ruleaza raportul si verifica fiecare din cele 4 fix-uri.