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>
This commit is contained in:
448
PLAN_FIXES_2025_11_28.md
Normal file
448
PLAN_FIXES_2025_11_28.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# 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)
|
||||
|
||||
```sql
|
||||
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`:
|
||||
```python
|
||||
'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):
|
||||
```sql
|
||||
-- DE LA:
|
||||
HAVING ABS(SUM(b.solddeb - b.soldcred)) > 1
|
||||
-- LA:
|
||||
HAVING ABS(SUM(b.solddeb - b.soldcred)) > 0.01
|
||||
```
|
||||
|
||||
**SOLDURI_FURNIZORI** (linia 675):
|
||||
```sql
|
||||
-- DE LA:
|
||||
HAVING ABS(SUM(b.soldcred - b.solddeb)) > 1
|
||||
-- LA:
|
||||
HAVING ABS(SUM(b.soldcred - b.solddeb)) > 0.01
|
||||
```
|
||||
|
||||
**AGING_CREANTE** (linia 712):
|
||||
```sql
|
||||
-- DE LA:
|
||||
HAVING SUM(sold_ramas) > 100
|
||||
-- LA:
|
||||
HAVING SUM(sold_ramas) > 0.01
|
||||
```
|
||||
|
||||
**AGING_DATORII** (linia 770):
|
||||
```sql
|
||||
-- DE LA:
|
||||
HAVING SUM(sold_ramas) > 100
|
||||
-- LA:
|
||||
HAVING SUM(sold_ramas) > 0.01
|
||||
```
|
||||
|
||||
**POZITIA_CASH** (linia 870):
|
||||
```sql
|
||||
-- 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:
|
||||
```sql
|
||||
-- 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)
|
||||
|
||||
```python
|
||||
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)
|
||||
|
||||
```python
|
||||
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)
|
||||
|
||||
```python
|
||||
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:
|
||||
```python
|
||||
from openpyxl.utils import get_column_letter
|
||||
from openpyxl.styles import PatternFill
|
||||
```
|
||||
|
||||
3. **Testare**: Dupa implementare, ruleaza raportul si verifica fiecare din cele 4 fix-uri.
|
||||
Reference in New Issue
Block a user