Raport stocuri marfa 371 pe vechimi (dosar credit banca)
Sectiune dedicata pentru banca cu sumar pe grupe x bucket vechime (0-6 luni, 6-12 luni, 1-2 ani, 2-3 ani, >3 ani), detaliu articole si reconciliere contra sold contabil 371 din balanta (vbal). - queries.py: STOCURI_371_SUMAR (ROLLUP grupa/subgrupa), STOCURI_371_DETALIU, STOCURI_371_SOLD_CONTABIL (sold sintetic din vbal, an/luna snapshot). Filtru stoc (cants+cant-cante) <> 0 pentru acoperire cu soldul contabil. - main.py: CLI --aging-dates pentru evolutie multi-data a stocului >3 ani, pagina PDF dedicata cu nota metodologica + reconciliere (marker la diff >1%). - recommendations.py: alerta CONCENTRARE cand stoc 371 >3 ani depaseste prag. - config.py: threshold aged_stock_371_pct (default 15%). - run.bat: header documentar cu argumentele disponibile + exemple. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,7 @@ RECOMMENDATION_THRESHOLDS = {
|
|||||||
# Stock indicators
|
# Stock indicators
|
||||||
'stoc_zile_max': 90, # days - slow stock threshold
|
'stoc_zile_max': 90, # days - slow stock threshold
|
||||||
'rotatie_minima': 4, # minimum annual rotation
|
'rotatie_minima': 4, # minimum annual rotation
|
||||||
|
'aged_stock_371_pct': 0.15, # prag CONCENTRARE pentru stoc 371 >3 ani
|
||||||
|
|
||||||
# Receivables aging
|
# Receivables aging
|
||||||
'restante_90_procent': 10, # % - max receivables > 90 days
|
'restante_90_procent': 10, # % - max receivables > 90 days
|
||||||
|
|||||||
202
main.py
202
main.py
@@ -280,6 +280,25 @@ class PerformanceLogger:
|
|||||||
print(f"\n📝 Log saved to: {log_file}")
|
print(f"\n📝 Log saved to: {log_file}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_aging_dates(raw: str) -> list:
|
||||||
|
"""Parse --aging-dates CLI value into sorted list of data_referinta datetimes.
|
||||||
|
|
||||||
|
Accepts 'YYYY-MM,YYYY-MM,...' (end-of-month), returns list of first-day-next-month datetimes.
|
||||||
|
Raises SystemExit on any invalid token so user sees a clear message, not a crash.
|
||||||
|
"""
|
||||||
|
dates = []
|
||||||
|
for token in raw.split(','):
|
||||||
|
tok = token.strip()
|
||||||
|
if not tok:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
dates.append(compute_data_referinta(tok))
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"❌ --aging-dates: token invalid '{tok}' (astept YYYY-MM). {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
return sorted(set(dates))
|
||||||
|
|
||||||
|
|
||||||
def compute_data_referinta(end_month_str: str = None) -> datetime:
|
def compute_data_referinta(end_month_str: str = None) -> datetime:
|
||||||
"""
|
"""
|
||||||
Compute reference date (first day of month AFTER the last reporting month).
|
Compute reference date (first day of month AFTER the last reporting month).
|
||||||
@@ -389,6 +408,11 @@ def generate_reports(args):
|
|||||||
results = {}
|
results = {}
|
||||||
perf = PerformanceLogger() # Initialize performance logger
|
perf = PerformanceLogger() # Initialize performance logger
|
||||||
|
|
||||||
|
# Parse multi-date aging request (validation before DB connection)
|
||||||
|
aging_dates_list = []
|
||||||
|
if getattr(args, 'aging_dates', None):
|
||||||
|
aging_dates_list = parse_aging_dates(args.aging_dates)
|
||||||
|
|
||||||
with OracleConnection() as conn:
|
with OracleConnection() as conn:
|
||||||
print("\n📥 Extragere date din Oracle:\n")
|
print("\n📥 Extragere date din Oracle:\n")
|
||||||
|
|
||||||
@@ -398,6 +422,37 @@ def generate_reports(args):
|
|||||||
results[query_name] = df
|
results[query_name] = df
|
||||||
perf.stop(rows=len(df) if df is not None and not df.empty else 0)
|
perf.stop(rows=len(df) if df is not None and not df.empty else 0)
|
||||||
|
|
||||||
|
# --- Evolutie multi-data stoc 371 >3 ani (optional) ---
|
||||||
|
if aging_dates_list:
|
||||||
|
print(f"\n📥 Extragere evolutie stoc 371 pe {len(aging_dates_list)} date:")
|
||||||
|
evolutie_rows = []
|
||||||
|
sumar_info = QUERIES['stocuri_371_sumar']
|
||||||
|
original_param = sumar_info['params'].get('data_referinta')
|
||||||
|
for dref in aging_dates_list:
|
||||||
|
perf.start(f"QUERY: stocuri_371_sumar@{dref.strftime('%Y-%m-%d')}")
|
||||||
|
sumar_info['params']['data_referinta'] = dref
|
||||||
|
df_e = execute_query(conn, f"stocuri_371_sumar@{dref.strftime('%Y-%m')}", sumar_info)
|
||||||
|
perf.stop(rows=len(df_e) if df_e is not None else 0)
|
||||||
|
if df_e is None or df_e.empty:
|
||||||
|
continue
|
||||||
|
grand = df_e[df_e['GROUPING_LEVEL'] == 3]
|
||||||
|
if grand.empty:
|
||||||
|
continue
|
||||||
|
row = grand.iloc[0]
|
||||||
|
val_total = float(row.get('VALOARE_TOTAL', 0) or 0)
|
||||||
|
val_3 = float(row.get('VAL_PESTE_3_ANI', 0) or 0)
|
||||||
|
evolutie_rows.append({
|
||||||
|
'DATA_REFERINTA': (dref - timedelta(days=1)).strftime('%Y-%m-%d'),
|
||||||
|
'VALOARE_TOTAL': round(val_total, 2),
|
||||||
|
'VALOARE_PESTE_3_ANI': round(val_3, 2),
|
||||||
|
'PROCENT_PESTE_3_ANI': round(val_3 / val_total * 100, 2) if val_total > 0 else 0.0
|
||||||
|
})
|
||||||
|
# restore original data_referinta for downstream consumers
|
||||||
|
sumar_info['params']['data_referinta'] = original_param
|
||||||
|
results['stocuri_371_evolutie'] = pd.DataFrame(evolutie_rows)
|
||||||
|
else:
|
||||||
|
results['stocuri_371_evolutie'] = pd.DataFrame()
|
||||||
|
|
||||||
# Generate Excel Report
|
# Generate Excel Report
|
||||||
print("\n📝 Generare raport Excel...")
|
print("\n📝 Generare raport Excel...")
|
||||||
excel_gen = ExcelReportGenerator(excel_path)
|
excel_gen = ExcelReportGenerator(excel_path)
|
||||||
@@ -537,6 +592,9 @@ def generate_reports(args):
|
|||||||
# STOC
|
# STOC
|
||||||
'stoc_curent',
|
'stoc_curent',
|
||||||
'stoc_lent',
|
'stoc_lent',
|
||||||
|
'stocuri_371_sumar',
|
||||||
|
'stocuri_371_detaliu',
|
||||||
|
'stocuri_371_evolutie',
|
||||||
'rotatie_stocuri',
|
'rotatie_stocuri',
|
||||||
|
|
||||||
# PRODUCTIE
|
# PRODUCTIE
|
||||||
@@ -576,6 +634,28 @@ def generate_reports(args):
|
|||||||
'VALOARE': 'Cantitate × preț achiziție',
|
'VALOARE': 'Cantitate × preț achiziție',
|
||||||
'ZILE_FARA_MISCARE': 'Zile de la ultima ieșire (dataout) sau intrare'
|
'ZILE_FARA_MISCARE': 'Zile de la ultima ieșire (dataout) sau intrare'
|
||||||
},
|
},
|
||||||
|
'stocuri_371_sumar': {
|
||||||
|
'GROUPING_LEVEL': '0 = detaliu subgrupa, 1 = subtotal per grupa, 3 = grand total (ROLLUP)',
|
||||||
|
'VALOARE_TOTAL': 'Total valoare stoc cont 371 la data de referinta (cost achizitie fara TVA)',
|
||||||
|
'VAL_0_6_LUNI': 'Valoare articole cu vechime 0-180 zile',
|
||||||
|
'VAL_6_12_LUNI': 'Valoare articole cu vechime 181-365 zile',
|
||||||
|
'VAL_1_2_ANI': 'Valoare articole cu vechime 366-730 zile',
|
||||||
|
'VAL_2_3_ANI': 'Valoare articole cu vechime 731-1095 zile',
|
||||||
|
'VAL_PESTE_3_ANI': 'Valoare articole cu vechime peste 1095 zile (risc haircut bancar)'
|
||||||
|
},
|
||||||
|
'stocuri_371_detaliu': {
|
||||||
|
'CANTITATE': 'Stoc final = cants + cant - cante',
|
||||||
|
'VALOARE': 'Cantitate × pret achizitie (cost, fara TVA)',
|
||||||
|
'ZILE_VECHIME': '(data_referinta - 1) - NVL(dataout, datain)',
|
||||||
|
'ANI_VECHIME': 'zile_vechime / 365',
|
||||||
|
'BUCKET_VECHIME': 'Grupare: 0-6 luni / 6-12 luni / 1-2 ani / 2-3 ani / >3 ani'
|
||||||
|
},
|
||||||
|
'stocuri_371_evolutie': {
|
||||||
|
'DATA_REFERINTA': 'End-of-month pentru care s-a calculat snapshot-ul',
|
||||||
|
'VALOARE_TOTAL': 'Total stoc 371 la acea data',
|
||||||
|
'VALOARE_PESTE_3_ANI': 'Portiunea cu vechime >3 ani (trend an-la-an pentru banca)',
|
||||||
|
'PROCENT_PESTE_3_ANI': '% din total care depaseste 3 ani'
|
||||||
|
},
|
||||||
'rotatie_stocuri': {
|
'rotatie_stocuri': {
|
||||||
'VALOARE_STOC': 'Stoc curent (cants+cant-cante) × preț achiziție',
|
'VALOARE_STOC': 'Stoc curent (cants+cant-cante) × preț achiziție',
|
||||||
'VANZARI_12_LUNI': 'Doar vânzări (nu transferuri/consumuri) din ultimele 12 luni',
|
'VANZARI_12_LUNI': 'Doar vânzări (nu transferuri/consumuri) din ultimele 12 luni',
|
||||||
@@ -916,24 +996,37 @@ def generate_reports(args):
|
|||||||
'grad_acoperire_datorii', 'proiectie_lichiditate'
|
'grad_acoperire_datorii', 'proiectie_lichiditate'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Metadata pentru sheet-uri sintetice (nu sunt in QUERIES dar sunt randate)
|
||||||
|
synthetic_sheets = {
|
||||||
|
'stocuri_371_evolutie': {
|
||||||
|
'title': 'Stocuri Marfa 371 - Evolutie >3 ani',
|
||||||
|
'description': 'Valoare totala 371 si portiune >3 ani la mai multe end-of-month (trend)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for query_name in sheet_order:
|
for query_name in sheet_order:
|
||||||
# Skip consolidated sheets and their source sheets
|
# Skip consolidated sheets and their source sheets
|
||||||
if query_name in consolidated_sheets:
|
if query_name in consolidated_sheets:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if query_name in results and query_name in QUERIES:
|
if query_name in results and (query_name in QUERIES or query_name in synthetic_sheets):
|
||||||
query_info = QUERIES[query_name]
|
df_sheet = results[query_name]
|
||||||
# Create short sheet name from query name
|
if df_sheet is None or df_sheet.empty:
|
||||||
|
continue
|
||||||
|
if query_name in QUERIES:
|
||||||
|
meta = QUERIES[query_name]
|
||||||
|
else:
|
||||||
|
meta = synthetic_sheets[query_name]
|
||||||
sheet_name = query_name.replace('_', ' ').title()[:31]
|
sheet_name = query_name.replace('_', ' ').title()[:31]
|
||||||
perf.start(f"EXCEL: {query_name} detail sheet")
|
perf.start(f"EXCEL: {query_name} detail sheet")
|
||||||
excel_gen.add_sheet(
|
excel_gen.add_sheet(
|
||||||
name=sheet_name,
|
name=sheet_name,
|
||||||
df=results[query_name],
|
df=df_sheet,
|
||||||
title=query_info.get('title', query_name),
|
title=meta.get('title', query_name),
|
||||||
description=query_info.get('description', ''),
|
description=meta.get('description', ''),
|
||||||
legend=legends.get(query_name)
|
legend=legends.get(query_name)
|
||||||
)
|
)
|
||||||
df_rows = len(results[query_name]) if results[query_name] is not None else 0
|
df_rows = len(df_sheet)
|
||||||
perf.stop(rows=df_rows)
|
perf.stop(rows=df_rows)
|
||||||
|
|
||||||
perf.start("EXCEL: Save workbook")
|
perf.start("EXCEL: Save workbook")
|
||||||
@@ -1087,6 +1180,94 @@ def generate_reports(args):
|
|||||||
max_rows=20
|
max_rows=20
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Pagina dedicata BANCA: Stocuri Marfa 371 pe Vechimi + reconciliere
|
||||||
|
# =========================================================================
|
||||||
|
df_sumar_371 = results.get('stocuri_371_sumar')
|
||||||
|
if df_sumar_371 is not None and not df_sumar_371.empty:
|
||||||
|
perf.start("PDF: Stocuri 371 bank page")
|
||||||
|
pdf_gen.add_page_break()
|
||||||
|
|
||||||
|
nota_metodologica = (
|
||||||
|
"METODOLOGIE: Vechimea este calculata ca numar de zile intre data de referinta "
|
||||||
|
f"({end_date.strftime('%d.%m.%Y')}) si ultima miscare a articolului in stoc "
|
||||||
|
"(iesire, sau intrare daca nu a existat nicio iesire). Valoarea este costul de "
|
||||||
|
"achizitie fara TVA (cant × pret din vstoc). Se includ toate pozitiile cu stoc "
|
||||||
|
"nenul (<> 0), inclusiv cele negative (corectii / erori de inventar), ca sa se "
|
||||||
|
"reconcilieze corect cu soldul contabil 371 din balanta (vbal, cont sintetic)."
|
||||||
|
)
|
||||||
|
pdf_gen.add_explanation(nota_metodologica)
|
||||||
|
|
||||||
|
pdf_gen.add_table_section(
|
||||||
|
"Raport Stocuri Marfa 371 - Sumar Vechimi (pentru banca)",
|
||||||
|
df_sumar_371,
|
||||||
|
columns=['GRUPA', 'SUBGRUPA', 'VALOARE_TOTAL', 'VAL_0_6_LUNI',
|
||||||
|
'VAL_6_12_LUNI', 'VAL_1_2_ANI', 'VAL_2_3_ANI', 'VAL_PESTE_3_ANI'],
|
||||||
|
max_rows=30
|
||||||
|
)
|
||||||
|
|
||||||
|
df_evolutie_371 = results.get('stocuri_371_evolutie')
|
||||||
|
if df_evolutie_371 is not None and not df_evolutie_371.empty:
|
||||||
|
pdf_gen.add_table_section(
|
||||||
|
"Evolutie stoc 371 >3 ani (multi-data)",
|
||||||
|
df_evolutie_371,
|
||||||
|
columns=['DATA_REFERINTA', 'VALOARE_TOTAL', 'VALOARE_PESTE_3_ANI', 'PROCENT_PESTE_3_ANI'],
|
||||||
|
max_rows=20
|
||||||
|
)
|
||||||
|
|
||||||
|
df_detaliu_371 = results.get('stocuri_371_detaliu')
|
||||||
|
if df_detaliu_371 is not None and not df_detaliu_371.empty:
|
||||||
|
pdf_gen.add_table_section(
|
||||||
|
"Top 25 articole dupa vechime (detaliu 371)",
|
||||||
|
df_detaliu_371,
|
||||||
|
columns=['COD_ARTICOL', 'DENUMIRE', 'CANTITATE', 'VALOARE',
|
||||||
|
'ZILE_VECHIME', 'BUCKET_VECHIME'],
|
||||||
|
max_rows=25
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reconciliere contabila
|
||||||
|
df_sold = results.get('stocuri_371_sold_contabil')
|
||||||
|
grand_total_row = df_sumar_371[df_sumar_371['GROUPING_LEVEL'] == 3]
|
||||||
|
total_raport = float(grand_total_row['VALOARE_TOTAL'].iloc[0]) if not grand_total_row.empty else 0.0
|
||||||
|
|
||||||
|
sold_contabil = None
|
||||||
|
if df_sold is not None and not df_sold.empty:
|
||||||
|
val = df_sold['SOLD_371'].iloc[0]
|
||||||
|
if val is not None:
|
||||||
|
sold_contabil = float(val)
|
||||||
|
|
||||||
|
if sold_contabil is None:
|
||||||
|
recon_rows = [
|
||||||
|
{'INDICATOR': 'Total valoare stoc 371 (raport)', 'VALOARE_RON': f'{total_raport:,.2f}'},
|
||||||
|
{'INDICATOR': 'Sold contabil 371 (vbal)', 'VALOARE_RON': 'INDISPONIBIL (verifica balanta vbal pe luna ref)'},
|
||||||
|
{'INDICATOR': 'Diferenta', 'VALOARE_RON': 'N/A'}
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
diferenta = total_raport - sold_contabil
|
||||||
|
pct = (diferenta / sold_contabil * 100) if sold_contabil else 0.0
|
||||||
|
marker = ' ⚠' if abs(pct) > 1.0 else ''
|
||||||
|
recon_rows = [
|
||||||
|
{'INDICATOR': 'Total valoare stoc 371 (raport)', 'VALOARE_RON': f'{total_raport:,.2f}'},
|
||||||
|
{'INDICATOR': 'Sold contabil 371 (vbal, la data ref)', 'VALOARE_RON': f'{sold_contabil:,.2f}'},
|
||||||
|
{'INDICATOR': f'Diferenta{marker}', 'VALOARE_RON': f'{diferenta:,.2f} ({pct:+.2f}%)'}
|
||||||
|
]
|
||||||
|
|
||||||
|
df_recon = pd.DataFrame(recon_rows)
|
||||||
|
pdf_gen.add_table_section(
|
||||||
|
"RECONCILIERE CONTABILA",
|
||||||
|
df_recon,
|
||||||
|
columns=['INDICATOR', 'VALOARE_RON'],
|
||||||
|
max_rows=5
|
||||||
|
)
|
||||||
|
|
||||||
|
if sold_contabil is not None and abs((total_raport - sold_contabil) / sold_contabil * 100) > 1.0:
|
||||||
|
pdf_gen.add_explanation(
|
||||||
|
"Diferenta >1% intre raport si soldul contabil. Cauze uzuale: evaluare "
|
||||||
|
"pret mediu vs FIFO, inregistrari contabile ulterioare snapshot-ului vstoc, "
|
||||||
|
"sau cont 371 analitic (371.1, 371.2) netotalizat in vbal."
|
||||||
|
)
|
||||||
|
perf.stop()
|
||||||
|
|
||||||
perf.start("PDF: Save document")
|
perf.start("PDF: Save document")
|
||||||
pdf_gen.save()
|
pdf_gen.save()
|
||||||
perf.stop()
|
perf.stop()
|
||||||
@@ -1132,6 +1313,13 @@ Exemple:
|
|||||||
help='Luna finala de raportare YYYY-MM (default: luna completa anterioara). Ex: 2026-01'
|
help='Luna finala de raportare YYYY-MM (default: luna completa anterioara). Ex: 2026-01'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--aging-dates',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help='Lista YYYY-MM separata prin virgula pentru sectiunea evolutie stoc 371 >3 ani. Ex: 2022-12,2023-12,2024-12,2025-12'
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--output-dir', '-o',
|
'--output-dir', '-o',
|
||||||
type=Path,
|
type=Path,
|
||||||
|
|||||||
108
queries.py
108
queries.py
@@ -440,6 +440,96 @@ ORDER BY zile_fara_miscare DESC NULLS FIRST
|
|||||||
FETCH FIRST 100 ROWS ONLY
|
FETCH FIRST 100 ROWS ONLY
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 12b. STOCURI MARFĂ 371 — SUMAR PE VECHIMI (pentru dosar credit bancă)
|
||||||
|
# =============================================================================
|
||||||
|
STOCURI_371_SUMAR = """
|
||||||
|
-- NOTĂ: :data_referinta = prima zi a lunii URMĂTOARE end-month (convenție proiect).
|
||||||
|
-- (:data_referinta - 1) = ultima zi a lunii de raport — folosit pentru:
|
||||||
|
-- (a) filtru snapshot vstoc: an=YEAR(data_ref-1), luna=MONTH(data_ref-1)
|
||||||
|
-- (b) calcul vechime: (data_ref - 1) - NVL(dataout, datain)
|
||||||
|
WITH s AS (
|
||||||
|
SELECT
|
||||||
|
s.id_grupa, s.grupa, s.id_subgrupa, s.subgrupa,
|
||||||
|
(s.cants + s.cant - s.cante) * s.pret AS val,
|
||||||
|
ROUND((:data_referinta - 1) - NVL(s.dataout, s.datain)) AS zile
|
||||||
|
FROM vstoc s
|
||||||
|
WHERE s.an = EXTRACT(YEAR FROM (:data_referinta - 1))
|
||||||
|
AND s.luna = EXTRACT(MONTH FROM (:data_referinta - 1))
|
||||||
|
AND s.cont = '371'
|
||||||
|
AND (s.cants + s.cant - s.cante) <> 0
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NVL(grupa, 'NECLASIFICAT') AS grupa,
|
||||||
|
NVL(subgrupa, '-') AS subgrupa,
|
||||||
|
GROUPING_ID(grupa, subgrupa) AS grouping_level,
|
||||||
|
ROUND(SUM(val), 2) AS valoare_total,
|
||||||
|
ROUND(SUM(CASE WHEN zile <= 180 THEN val ELSE 0 END), 2) AS val_0_6_luni,
|
||||||
|
ROUND(SUM(CASE WHEN zile > 180 AND zile <= 365 THEN val ELSE 0 END), 2) AS val_6_12_luni,
|
||||||
|
ROUND(SUM(CASE WHEN zile > 365 AND zile <= 730 THEN val ELSE 0 END), 2) AS val_1_2_ani,
|
||||||
|
ROUND(SUM(CASE WHEN zile > 730 AND zile <= 1095 THEN val ELSE 0 END), 2) AS val_2_3_ani,
|
||||||
|
ROUND(SUM(CASE WHEN zile > 1095 THEN val ELSE 0 END), 2) AS val_peste_3_ani
|
||||||
|
FROM s
|
||||||
|
GROUP BY ROLLUP(grupa, subgrupa)
|
||||||
|
ORDER BY GROUPING_ID(grupa, subgrupa), grupa, subgrupa
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 12c. STOCURI MARFĂ 371 — DETALIU ARTICOLE
|
||||||
|
# =============================================================================
|
||||||
|
STOCURI_371_DETALIU = """
|
||||||
|
-- Vezi NOTA de la STOCURI_371_SUMAR.
|
||||||
|
SELECT
|
||||||
|
s.codmat AS cod_articol,
|
||||||
|
s.denumire,
|
||||||
|
s.nume_gestiune,
|
||||||
|
NVL(s.grupa, 'NECLASIFICAT') AS grupa,
|
||||||
|
NVL(s.subgrupa, '-') AS subgrupa,
|
||||||
|
s.um,
|
||||||
|
(s.cants + s.cant - s.cante) AS cantitate,
|
||||||
|
ROUND(s.pret, 4) AS pret_unitar,
|
||||||
|
ROUND((s.cants + s.cant - s.cante) * s.pret, 2) AS valoare,
|
||||||
|
s.datain AS data_intrare,
|
||||||
|
s.dataout AS data_ultima_iesire,
|
||||||
|
ROUND((:data_referinta - 1) - NVL(s.dataout, s.datain)) AS zile_vechime,
|
||||||
|
ROUND(((:data_referinta - 1) - NVL(s.dataout, s.datain)) / 365, 2) AS ani_vechime,
|
||||||
|
CASE
|
||||||
|
WHEN ROUND((:data_referinta - 1) - NVL(s.dataout, s.datain)) <= 180 THEN '0-6 luni'
|
||||||
|
WHEN ROUND((:data_referinta - 1) - NVL(s.dataout, s.datain)) <= 365 THEN '6-12 luni'
|
||||||
|
WHEN ROUND((:data_referinta - 1) - NVL(s.dataout, s.datain)) <= 730 THEN '1-2 ani'
|
||||||
|
WHEN ROUND((:data_referinta - 1) - NVL(s.dataout, s.datain)) <= 1095 THEN '2-3 ani'
|
||||||
|
ELSE '>3 ani'
|
||||||
|
END AS bucket_vechime,
|
||||||
|
s.lot,
|
||||||
|
s.adata_expirare AS data_expirare,
|
||||||
|
s.furnizor
|
||||||
|
FROM vstoc s
|
||||||
|
WHERE s.an = EXTRACT(YEAR FROM (:data_referinta - 1))
|
||||||
|
AND s.luna = EXTRACT(MONTH FROM (:data_referinta - 1))
|
||||||
|
AND s.cont = '371'
|
||||||
|
AND (s.cants + s.cant - s.cante) <> 0
|
||||||
|
ORDER BY zile_vechime DESC, valoare DESC
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 12d. SOLD CONTABIL 371 (reconciliere cu raport)
|
||||||
|
# =============================================================================
|
||||||
|
STOCURI_371_SOLD_CONTABIL = """
|
||||||
|
-- Sold contabil 371 la finalul lunii de raportare, din balanta de verificare (vbal).
|
||||||
|
-- Filtrul (an, luna) = ultima luna raportata, identic cu vstoc:
|
||||||
|
-- an = EXTRACT(YEAR FROM (:data_referinta - 1))
|
||||||
|
-- luna= EXTRACT(MONTH FROM (:data_referinta - 1))
|
||||||
|
-- 371 este cont de activ => sold debitor. Folosim (solddeb - soldcred) ca sa fie
|
||||||
|
-- robust daca exista inregistrari cu sold credit (corectii). Se aduna pe toate
|
||||||
|
-- sucursalele (id_sucursala), pentru ca raportul vstoc acopera toate gestiunile.
|
||||||
|
SELECT
|
||||||
|
ROUND(SUM(NVL(b.solddeb, 0) - NVL(b.soldcred, 0)), 2) AS sold_371
|
||||||
|
FROM vbal b
|
||||||
|
WHERE b.cont = '371'
|
||||||
|
AND b.an = EXTRACT(YEAR FROM (:data_referinta - 1))
|
||||||
|
AND b.luna = EXTRACT(MONTH FROM (:data_referinta - 1))
|
||||||
|
"""
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 13. ROTAȚIE STOCURI
|
# 13. ROTAȚIE STOCURI
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -2496,6 +2586,24 @@ QUERIES = {
|
|||||||
'title': '⚠️ Stoc Lent (>90 zile)',
|
'title': '⚠️ Stoc Lent (>90 zile)',
|
||||||
'description': 'Produse fără mișcare de peste 90 de zile'
|
'description': 'Produse fără mișcare de peste 90 de zile'
|
||||||
},
|
},
|
||||||
|
'stocuri_371_sumar': {
|
||||||
|
'sql': STOCURI_371_SUMAR,
|
||||||
|
'params': {},
|
||||||
|
'title': 'Stocuri Marfă 371 — Sumar Vechimi (pentru bancă)',
|
||||||
|
'description': 'Valoarea stocului cont 371 pe grupe și buckets de vechime la data de referință'
|
||||||
|
},
|
||||||
|
'stocuri_371_detaliu': {
|
||||||
|
'sql': STOCURI_371_DETALIU,
|
||||||
|
'params': {},
|
||||||
|
'title': 'Stocuri Marfă 371 — Detaliu Articole',
|
||||||
|
'description': 'Lista completă articole cont 371 cu vechime și valoare'
|
||||||
|
},
|
||||||
|
'stocuri_371_sold_contabil': {
|
||||||
|
'sql': STOCURI_371_SOLD_CONTABIL,
|
||||||
|
'params': {},
|
||||||
|
'title': 'Stocuri Marfă 371 — Sold Contabil (reconciliere)',
|
||||||
|
'description': 'Sold 371 din rulaje la data ref pentru cross-check'
|
||||||
|
},
|
||||||
'rotatie_stocuri': {
|
'rotatie_stocuri': {
|
||||||
'sql': ROTATIE_STOCURI,
|
'sql': ROTATIE_STOCURI,
|
||||||
'params': {},
|
'params': {},
|
||||||
|
|||||||
@@ -297,7 +297,26 @@ class RecommendationsEngine:
|
|||||||
vezi_detalii='Sheet: Rotatie Stocuri'
|
vezi_detalii='Sheet: Rotatie Stocuri'
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Check cash conversion cycle
|
# 3. Check aged stock 371 (bank collateral haircut risk)
|
||||||
|
stocuri_371 = results.get('stocuri_371_detaliu')
|
||||||
|
if stocuri_371 is not None and not stocuri_371.empty and 'VALOARE' in stocuri_371.columns:
|
||||||
|
valoare_total = stocuri_371['VALOARE'].sum()
|
||||||
|
if valoare_total > 0 and 'BUCKET_VECHIME' in stocuri_371.columns:
|
||||||
|
peste_3 = stocuri_371[stocuri_371['BUCKET_VECHIME'] == '>3 ani']['VALOARE'].sum()
|
||||||
|
procent = peste_3 / valoare_total
|
||||||
|
prag = self.thresholds.get('aged_stock_371_pct', 0.15)
|
||||||
|
if procent > prag:
|
||||||
|
self._add_recommendation(
|
||||||
|
categorie='Stoc 371',
|
||||||
|
indicator='Marfa invechita cont 371',
|
||||||
|
valoare=f'{procent*100:.1f}%',
|
||||||
|
status='CONCENTRARE',
|
||||||
|
explicatie=f'{procent*100:.1f}% din valoarea stocului 371 are >3 ani ({peste_3:,.0f} RON). Banca aplica haircut la evaluare colateral.',
|
||||||
|
recomandare='Lichidare / provizionare recomandata inainte de depunere dosar credit.',
|
||||||
|
vezi_detalii='Sheet: stocuri_371_detaliu'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Check cash conversion cycle
|
||||||
ciclu = results.get('ciclu_conversie_cash')
|
ciclu = results.get('ciclu_conversie_cash')
|
||||||
if ciclu is not None and not ciclu.empty:
|
if ciclu is not None and not ciclu.empty:
|
||||||
dio_row = ciclu[ciclu['INDICATOR'].str.contains('stoc|DIO', case=False, na=False)]
|
dio_row = ciclu[ciclu['INDICATOR'].str.contains('stoc|DIO', case=False, na=False)]
|
||||||
|
|||||||
19
run.bat
19
run.bat
@@ -1,5 +1,22 @@
|
|||||||
@echo off
|
@echo off
|
||||||
REM Run Data Intelligence Report Generator (Windows)
|
REM =============================================================================
|
||||||
|
REM Data Intelligence Report Generator - ROMFAST
|
||||||
|
REM =============================================================================
|
||||||
|
REM
|
||||||
|
REM Argumente disponibile:
|
||||||
|
REM --months N Numar luni pentru analiza (default: 12)
|
||||||
|
REM --end-month YYYY-MM Luna finala de raportare (default: luna precedenta)
|
||||||
|
REM Ex: --end-month 2025-12
|
||||||
|
REM --aging-dates LIST Lista YYYY-MM pentru evolutie stoc 371 >3 ani
|
||||||
|
REM Ex: --aging-dates 2022-12,2023-12,2024-12,2025-12
|
||||||
|
REM --output-dir PATH Director output (default: ./output)
|
||||||
|
REM
|
||||||
|
REM Exemple:
|
||||||
|
REM run.bat (raport standard)
|
||||||
|
REM run.bat --end-month 2025-12 (raport dosar banca)
|
||||||
|
REM run.bat --end-month 2025-12 --aging-dates 2022-12,2023-12,2024-12,2025-12
|
||||||
|
REM (raport dosar banca + trend)
|
||||||
|
REM =============================================================================
|
||||||
|
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
call .venv\Scripts\activate.bat
|
call .venv\Scripts\activate.bat
|
||||||
|
|||||||
Reference in New Issue
Block a user