- 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>
16 KiB
16 KiB
Plan: Corectii Report Generator - 28.11.2025
Probleme de Rezolvat
- Analiza Prajitorie - intrarile si iesirile apar pe randuri diferite in loc de coloane
- Query-uri Financiare "No Data" - DSO/DPO, Solduri clienti/furnizori, Aging, Pozitia Cash, Ciclu Conversie Cash nu afiseaza date (user confirma ca datele EXISTA)
- Recomandari in Sumar Executiv - trebuie incluse sub KPIs in sheet-ul Sumar Executiv
- 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
- Eliminat
tip_miscaredin SELECT si GROUP BY - Agregare conditionala cu
CASE WHEN ... THEN ... ELSE 0 ENDin SUM() - Coloane separate pentru fiecare tip de miscare
- 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
- Numele view-urilor difera in baza de date
- Numele coloanelor difera (
an,luna,solddeb,soldcred) - Prefixele codurilor de cont nu se potrivesc (4111%, 401%, 512%)
- 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
- Inlocuieste ANALIZA_PRAJITORIE (liniile 450-478) cu versiunea cu agregare conditionala
- 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
- SOLDURI_CLIENTI (linia 652):
Pasul 2: report_generator.py
- Adauga metoda
add_sheet_with_recommendations()dupa linia 167 - Asigura-te ca importurile includ
PatternFill,get_column_letterdin openpyxl
Pasul 3: main.py
- Inlocuieste array-ul
sheet_order(liniile 165-221) - Modifica loop-ul de creare sheet-uri pentru
sumar_executiv(in jurul liniei 435) - Adauga legend pentru
analiza_prajitoriein dictionarullegends
Pasul 4: Testare
- Ruleaza cu
python main.py --months 1pentru test rapid - Verifica sheet-ul
analiza_prajitorie- format columnar - Verifica query-urile financiare - trebuie sa returneze date
- Verifica
Sumar Executiv- sectiune recomandari dedesubt - 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
-
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.
-
Import necesar in report_generator.py:
from openpyxl.utils import get_column_letter
from openpyxl.styles import PatternFill
- Testare: Dupa implementare, ruleaza raportul si verifica fiecare din cele 4 fix-uri.