Files
roa2web-service-auto/backend/modules/reports/services/dashboard_service.py
Claude Agent b137e80b71 feat: multi-Oracle server support with runtime switching
Complete implementation of multi-server Oracle database support:

Backend:
- Multi-pool Oracle with lazy loading per server
- Email-to-server cache for automatic server discovery
- JWT tokens include server_id claim
- /auth/check-identity and /auth/check-email endpoints
- /auth/my-servers endpoint for listing user's accessible servers
- Server switch with password re-authentication

Frontend:
- New ServerSelector component for header dropdown
- Multi-step login flow (identity → server → password)
- Server switching from header with password modal
- Mobile drawer menu with server selection
- Dark mode support for all new components
- URL bookmark support with ?server= query param

Scripts:
- Unified start.sh replacing start-prod.sh/start-test.sh
- Unified ssh-tunnel.sh with multi-server support
- Updated status.sh for new architecture

Tests:
- E2E tests for multi-server and single-server login flows
- Backend unit tests for all new endpoints
- Oracle multi-pool integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:39:06 +00:00

2008 lines
100 KiB
Python

# import sys # Removed - no longer needed
import os
from shared.database.oracle_pool import oracle_pool
from ..models.dashboard import DashboardSummary, TreasuryAccount, TrendData
from ..cache.decorators import cached
from decimal import Decimal
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
from fastapi import Request
import logging
logger = logging.getLogger(__name__)
class DashboardService:
"""Service pentru dashboard - date agregate"""
@staticmethod
def _build_period_cte(schema: str, luna: Optional[int] = None, an: Optional[int] = None) -> tuple[str, dict]:
"""
Construiește CTE pentru luna curentă.
Dacă luna și an sunt specificate, le folosește.
Altfel, folosește MAX(anul*12+luna) din calendar.
Returns:
tuple: (cte_sql, params_dict)
"""
if luna is not None and an is not None:
# Folosește parametrii specificați
cte_sql = f"""
WITH luna_curenta AS (
SELECT :param_an as anul, :param_luna as luna FROM DUAL
)"""
params = {'param_an': an, 'param_luna': luna}
else:
# Folosește MAX din calendar
cte_sql = f"""
WITH luna_curenta AS (
SELECT anul, luna FROM {schema}.calendar
WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)
)"""
params = {}
return cte_sql, params
@staticmethod
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""
Obține schema pentru company_id (CACHED PERMANENT)
CRITICAL: Acest query este cel mai frecvent - executat la FIECARE request API.
Cache permanent reduce queries cu 99.99%.
"""
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
schema_query = """
SELECT schema
FROM CONTAFIN_ORACLE.v_nom_firme
WHERE id_firma = :company_id
"""
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema not found for company {company_id}")
return schema_result[0]
@staticmethod
@cached(cache_type='dashboard_summary', key_params=['company', 'username', 'luna', 'an', 'server_id'])
async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> DashboardSummary:
"""
Obține toate datele pentru dashboard într-un singur apel (CACHED 30 min)
Execută 2 query-uri separate: facturi și trezorerie
Args:
company: ID-ul firmei
username: Numele utilizatorului
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
request: Request object pentru cache metadata
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
company_id = int(company)
schema = await DashboardService._get_schema(company_id, server_id)
# Construiește CTE pentru perioada curentă
period_cte, period_params = DashboardService._build_period_cte(schema, luna, an)
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Query 1: Statistici facturi cu breakdown pe perioade - FIXED ORA-00937
facturi_query = f"""
{period_cte},
perioada_stats AS (
SELECT
an, luna,
-- CLIENȚI
SUM(CASE
WHEN cont IN ('4111','461')
THEN precdeb + debit ELSE 0
END) as clienti_facturat_luna,
SUM(CASE
WHEN cont IN ('4111','461')
THEN preccred + credit ELSE 0
END) as clienti_incasat_luna,
-- FURNIZORI
SUM(CASE
WHEN cont IN ('401','404','462')
THEN preccred + credit ELSE 0
END) as furnizori_facturat_luna,
SUM(CASE
WHEN cont IN ('401','404','462')
THEN precdeb + debit ELSE 0
END) as furnizori_achitat_luna
FROM {schema}.vireg_parteneri
WHERE cont IN ('4111', '461', '401', '404', '462')
AND an >= (SELECT anul-1 FROM luna_curenta)
GROUP BY an, luna
),
facturi_stats AS (
SELECT
-- CLIENȚI - Totaluri
SUM(CASE
WHEN cont IN ('4111','461')
THEN precdeb + debit ELSE 0
END) as clienti_total_facturat,
SUM(CASE
WHEN cont IN ('4111','461')
THEN preccred + credit ELSE 0
END) as clienti_total_incasat,
SUM(CASE
WHEN cont = '419'
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as clienti_avansuri,
-- CLIENȚI - Sold Net Total
SUM(CASE
WHEN cont IN ('4111','461')
THEN (precdeb + debit) - (preccred + credit)
WHEN cont = '419'
THEN -((preccred + credit) - (precdeb + debit))
ELSE 0
END) as clienti_sold_total,
-- CLIENȚI - Sold În Termen (datascad >= azi sau NULL)
SUM(CASE
WHEN cont IN ('4111','461')
AND (datascad IS NULL OR datascad >= TRUNC(SYSDATE))
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_sold_in_termen,
-- CLIENȚI - Sold Restant (datascad < azi)
SUM(CASE
WHEN cont IN ('4111','461')
AND datascad < TRUNC(SYSDATE)
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_sold_restant,
-- CLIENȚI - Restanțe pe perioade
SUM(CASE
WHEN cont IN ('4111','461')
AND TRUNC(SYSDATE) - datascad BETWEEN 1 AND 7
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_restant_7,
SUM(CASE
WHEN cont IN ('4111','461')
AND TRUNC(SYSDATE) - datascad BETWEEN 8 AND 14
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_restant_14,
SUM(CASE
WHEN cont IN ('4111','461')
AND TRUNC(SYSDATE) - datascad BETWEEN 15 AND 30
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_restant_30,
SUM(CASE
WHEN cont IN ('4111','461')
AND TRUNC(SYSDATE) - datascad BETWEEN 31 AND 60
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_restant_60,
SUM(CASE
WHEN cont IN ('4111','461')
AND TRUNC(SYSDATE) - datascad BETWEEN 61 AND 90
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_restant_90,
SUM(CASE
WHEN cont IN ('4111','461')
AND TRUNC(SYSDATE) - datascad > 90
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_restant_90plus,
-- CLIENȚI - Scadențe pe perioade (datascad în viitor)
SUM(CASE
WHEN cont IN ('4111','461')
AND datascad - TRUNC(SYSDATE) BETWEEN 1 AND 7
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_scadent_7,
SUM(CASE
WHEN cont IN ('4111','461')
AND datascad - TRUNC(SYSDATE) BETWEEN 8 AND 14
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_scadent_14,
SUM(CASE
WHEN cont IN ('4111','461')
AND datascad - TRUNC(SYSDATE) BETWEEN 15 AND 30
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_scadent_30,
SUM(CASE
WHEN cont IN ('4111','461')
AND datascad - TRUNC(SYSDATE) BETWEEN 31 AND 60
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_scadent_60,
SUM(CASE
WHEN cont IN ('4111','461')
AND datascad - TRUNC(SYSDATE) BETWEEN 61 AND 90
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_scadent_90,
SUM(CASE
WHEN cont IN ('4111','461')
AND datascad - TRUNC(SYSDATE) > 90
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as clienti_scadent_90plus,
-- FURNIZORI - Totaluri
SUM(CASE
WHEN cont IN ('401','404','462')
THEN preccred + credit ELSE 0
END) as furnizori_total_facturat,
SUM(CASE
WHEN cont IN ('401','404','462')
THEN precdeb + debit ELSE 0
END) as furnizori_total_achitat,
SUM(CASE
WHEN cont LIKE '409%'
THEN (precdeb + debit) - (preccred + credit) ELSE 0
END) as furnizori_avansuri,
-- FURNIZORI - Sold Net Total
SUM(CASE
WHEN cont IN ('401','404','462')
THEN (preccred + credit) - (precdeb + debit)
WHEN cont LIKE '409%'
THEN -((precdeb + debit) - (preccred + credit))
ELSE 0
END) as furnizori_sold_total,
-- FURNIZORI - Sold În Termen
SUM(CASE
WHEN cont IN ('401','404','462')
AND (datascad IS NULL OR datascad >= TRUNC(SYSDATE))
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_sold_in_termen,
-- FURNIZORI - Sold Restant
SUM(CASE
WHEN cont IN ('401','404','462')
AND datascad < TRUNC(SYSDATE)
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_sold_restant,
-- FURNIZORI - Restanțe pe perioade
SUM(CASE
WHEN cont IN ('401','404','462')
AND TRUNC(SYSDATE) - datascad BETWEEN 1 AND 7
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_restant_7,
SUM(CASE
WHEN cont IN ('401','404','462')
AND TRUNC(SYSDATE) - datascad BETWEEN 8 AND 14
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_restant_14,
SUM(CASE
WHEN cont IN ('401','404','462')
AND TRUNC(SYSDATE) - datascad BETWEEN 15 AND 30
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_restant_30,
SUM(CASE
WHEN cont IN ('401','404','462')
AND TRUNC(SYSDATE) - datascad BETWEEN 31 AND 60
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_restant_60,
SUM(CASE
WHEN cont IN ('401','404','462')
AND TRUNC(SYSDATE) - datascad BETWEEN 61 AND 90
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_restant_90,
SUM(CASE
WHEN cont IN ('401','404','462')
AND TRUNC(SYSDATE) - datascad > 90
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_restant_90plus,
-- FURNIZORI - Scadențe pe perioade
SUM(CASE
WHEN cont IN ('401','404','462')
AND datascad - TRUNC(SYSDATE) BETWEEN 1 AND 7
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_scadent_7,
SUM(CASE
WHEN cont IN ('401','404','462')
AND datascad - TRUNC(SYSDATE) BETWEEN 8 AND 14
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_scadent_14,
SUM(CASE
WHEN cont IN ('401','404','462')
AND datascad - TRUNC(SYSDATE) BETWEEN 15 AND 30
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_scadent_30,
SUM(CASE
WHEN cont IN ('401','404','462')
AND datascad - TRUNC(SYSDATE) BETWEEN 31 AND 60
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_scadent_60,
SUM(CASE
WHEN cont IN ('401','404','462')
AND datascad - TRUNC(SYSDATE) BETWEEN 61 AND 90
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_scadent_90,
SUM(CASE
WHEN cont IN ('401','404','462')
AND datascad - TRUNC(SYSDATE) > 90
THEN (preccred + credit) - (precdeb + debit) ELSE 0
END) as furnizori_scadent_90plus
FROM {schema}.vireg_parteneri
WHERE an = (SELECT anul FROM luna_curenta)
AND luna = (SELECT luna FROM luna_curenta)
AND cont IN ('4111', '461', '419', '401', '404', '462', '409','4091','4092','4093','4094')
)
SELECT
fs.*,
-- BREAKDOWN pe perioade - Luna anterioară
(SELECT NVL(clienti_facturat_luna, 0) FROM perioada_stats p
WHERE p.an*12+p.luna = (SELECT anul*12+luna-1 FROM luna_curenta)) as clienti_facturat_luna_anterioara,
(SELECT NVL(furnizori_facturat_luna, 0) FROM perioada_stats p
WHERE p.an*12+p.luna = (SELECT anul*12+luna-1 FROM luna_curenta)) as furnizori_facturat_luna_anterioara,
-- BREAKDOWN pe perioade - Anul curent vs anterior
(SELECT NVL(SUM(clienti_facturat_luna), 0) FROM perioada_stats p
WHERE p.an = (SELECT anul FROM luna_curenta)) as clienti_facturat_an_curent,
(SELECT NVL(SUM(clienti_facturat_luna), 0) FROM perioada_stats p
WHERE p.an = (SELECT anul-1 FROM luna_curenta)) as clienti_facturat_an_anterior,
(SELECT NVL(SUM(furnizori_facturat_luna), 0) FROM perioada_stats p
WHERE p.an = (SELECT anul FROM luna_curenta)) as furnizori_facturat_an_curent,
(SELECT NVL(SUM(furnizori_facturat_luna), 0) FROM perioada_stats p
WHERE p.an = (SELECT anul-1 FROM luna_curenta)) as furnizori_facturat_an_anterior
FROM facturi_stats fs
"""
cursor.execute(facturi_query, period_params)
facturi_row = cursor.fetchone()
# Query 2: Trezorerie (folosește același period_cte)
treasury_query = f"""
{period_cte}
SELECT
cont,
nume as nume_banca,
CASE
WHEN cont = '5121' THEN 'Bancă LEI'
WHEN cont = '5124' THEN 'Bancă VALUTA'
WHEN cont = '5311' THEN 'Casă LEI'
WHEN cont = '5314' THEN 'Casă VALUTA'
END as nume_cont,
SUM(CASE
WHEN cont IN ('5121','5311') THEN solddeb - soldcred
WHEN cont IN ('5124','5314') THEN soldvaldeb - soldvalcred
END) as sold,
CASE
WHEN cont IN ('5121','5311') THEN 'RON'
WHEN cont IN ('5124','5314') THEN NVL(nume_val, 'EUR')
END as valuta
FROM {schema}.vbalanta_parteneri
WHERE an = (SELECT anul FROM luna_curenta)
AND luna = (SELECT luna FROM luna_curenta)
AND cont IN ('5121', '5124', '5311', '5314')
AND ((cont IN ('5121','5311') AND soldcred - solddeb != 0)
OR (cont IN ('5124','5314') AND soldvalcred - soldvaldeb != 0))
GROUP BY cont, nume, nume_val
ORDER BY cont, nume
"""
cursor.execute(treasury_query, period_params)
treasury_rows = cursor.fetchall()
# Query 3: Solduri TVA din tabelul vbal (folosește același period_cte)
tva_query = f"""
{period_cte}
SELECT
cont,
precdeb,
preccred,
ruldeb,
rulcred
FROM {schema}.vbal
WHERE an = (SELECT anul FROM luna_curenta)
AND luna = (SELECT luna FROM luna_curenta)
AND cont IN ('4423', '4424', '4426', '4427')
ORDER BY cont
"""
cursor.execute(tva_query, period_params)
tva_rows = cursor.fetchall()
# Procesare solduri TVA
tva_data = {
'4423': {'precdeb': Decimal('0'), 'preccred': Decimal('0'), 'ruldeb': Decimal('0'), 'rulcred': Decimal('0')},
'4424': {'precdeb': Decimal('0'), 'preccred': Decimal('0'), 'ruldeb': Decimal('0'), 'rulcred': Decimal('0')},
'4426': {'precdeb': Decimal('0'), 'preccred': Decimal('0'), 'ruldeb': Decimal('0'), 'rulcred': Decimal('0')},
'4427': {'precdeb': Decimal('0'), 'preccred': Decimal('0'), 'ruldeb': Decimal('0'), 'rulcred': Decimal('0')}
}
for row in tva_rows:
cont = row[0]
if cont in tva_data:
tva_data[cont]['precdeb'] = Decimal(str(row[1] or 0))
tva_data[cont]['preccred'] = Decimal(str(row[2] or 0))
tva_data[cont]['ruldeb'] = Decimal(str(row[3] or 0))
tva_data[cont]['rulcred'] = Decimal(str(row[4] or 0))
# Calcul TVA Luna Precedentă - FIE de plată (4423) FIE de recuperat (4424)
sold_4423 = tva_data['4423']['preccred'] - tva_data['4423']['precdeb']
sold_4424 = tva_data['4424']['precdeb'] - tva_data['4424']['preccred']
if sold_4423 > 0:
tva_plata_precedent = sold_4423
tva_recuperat_precedent = Decimal('0')
elif sold_4424 > 0:
tva_recuperat_precedent = sold_4424
tva_plata_precedent = Decimal('0')
else:
tva_plata_precedent = Decimal('0')
tva_recuperat_precedent = Decimal('0')
# Calcul TVA Luna Curentă - FIE de plată FIE de recuperat
diferenta_curent = tva_data['4427']['rulcred'] - tva_data['4426']['ruldeb']
if diferenta_curent > 0:
tva_plata_curent = diferenta_curent
tva_recuperat_curent = Decimal('0')
else:
tva_recuperat_curent = -diferenta_curent
tva_plata_curent = Decimal('0')
# Procesare trezorerie
treasury_accounts = []
treasury_totals = {}
for row in treasury_rows:
account = TreasuryAccount(
cont=row[0],
nume_banca=row[1],
nume_cont=row[2],
sold=Decimal(str(row[3] or 0)),
valuta=row[4]
)
treasury_accounts.append(account)
# Calculează totaluri pe valută
if account.valuta not in treasury_totals:
treasury_totals[account.valuta] = Decimal('0')
treasury_totals[account.valuta] += account.sold
# Returnează rezultatul complet cu toate câmpurile calculate
return DashboardSummary(
# CLIENȚI - Totaluri principale
clienti_total_facturat=Decimal(str(facturi_row[0] or 0)),
clienti_total_incasat=Decimal(str(facturi_row[1] or 0)),
clienti_avansuri=Decimal(str(facturi_row[2] or 0)),
clienti_sold_total=Decimal(str(facturi_row[3] or 0)),
clienti_sold_in_termen=Decimal(str(facturi_row[4] or 0)),
clienti_sold_restant=Decimal(str(facturi_row[5] or 0)),
# CLIENȚI - Restanțe pe perioade
clienti_restant_7=Decimal(str(facturi_row[6] or 0)),
clienti_restant_14=Decimal(str(facturi_row[7] or 0)),
clienti_restant_30=Decimal(str(facturi_row[8] or 0)),
clienti_restant_60=Decimal(str(facturi_row[9] or 0)),
clienti_restant_90=Decimal(str(facturi_row[10] or 0)),
clienti_restant_90plus=Decimal(str(facturi_row[11] or 0)),
# CLIENȚI - Scadențe pe perioade
clienti_scadent_7=Decimal(str(facturi_row[12] or 0)),
clienti_scadent_14=Decimal(str(facturi_row[13] or 0)),
clienti_scadent_30=Decimal(str(facturi_row[14] or 0)),
clienti_scadent_60=Decimal(str(facturi_row[15] or 0)),
clienti_scadent_90=Decimal(str(facturi_row[16] or 0)),
clienti_scadent_90plus=Decimal(str(facturi_row[17] or 0)),
# FURNIZORI - Totaluri principale
furnizori_total_facturat=Decimal(str(facturi_row[18] or 0)),
furnizori_total_achitat=Decimal(str(facturi_row[19] or 0)),
furnizori_avansuri=Decimal(str(facturi_row[20] or 0)),
furnizori_sold_total=Decimal(str(facturi_row[21] or 0)),
furnizori_sold_in_termen=Decimal(str(facturi_row[22] or 0)),
furnizori_sold_restant=Decimal(str(facturi_row[23] or 0)),
# FURNIZORI - Restanțe pe perioade
furnizori_restant_7=Decimal(str(facturi_row[24] or 0)),
furnizori_restant_14=Decimal(str(facturi_row[25] or 0)),
furnizori_restant_30=Decimal(str(facturi_row[26] or 0)),
furnizori_restant_60=Decimal(str(facturi_row[27] or 0)),
furnizori_restant_90=Decimal(str(facturi_row[28] or 0)),
furnizori_restant_90plus=Decimal(str(facturi_row[29] or 0)),
# FURNIZORI - Scadențe pe perioade
furnizori_scadent_7=Decimal(str(facturi_row[30] or 0)),
furnizori_scadent_14=Decimal(str(facturi_row[31] or 0)),
furnizori_scadent_30=Decimal(str(facturi_row[32] or 0)),
furnizori_scadent_60=Decimal(str(facturi_row[33] or 0)),
furnizori_scadent_90=Decimal(str(facturi_row[34] or 0)),
furnizori_scadent_90plus=Decimal(str(facturi_row[35] or 0)),
# TREZORERIE
treasury_accounts=treasury_accounts,
treasury_totals_by_currency=treasury_totals,
# Date suplimentare pentru trend analysis
clienti_facturat_luna_anterioara=Decimal(str(facturi_row[36] or 0)),
furnizori_facturat_luna_anterioara=Decimal(str(facturi_row[37] or 0)),
clienti_facturat_an_curent=Decimal(str(facturi_row[38] or 0)),
clienti_facturat_an_anterior=Decimal(str(facturi_row[39] or 0)),
furnizori_facturat_an_curent=Decimal(str(facturi_row[40] or 0)),
furnizori_facturat_an_anterior=Decimal(str(facturi_row[41] or 0)),
# Solduri TVA
tva_plata_precedent=tva_plata_precedent,
tva_recuperat_precedent=tva_recuperat_precedent,
tva_plata_curent=tva_plata_curent,
tva_recuperat_curent=tva_recuperat_curent
)
@staticmethod
@cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an', 'server_id'])
async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""Get comprehensive trend analysis data for all dashboard indicators (CACHED 30 min)
Args:
company_id: ID-ul firmei
period: Perioada pentru trends (7d, 30d, ytd, 12m)
luna: Luna contabilă (1-12), opțional - dacă nu e specificată, folosește MAX
an: Anul contabil, opțional - dacă nu e specificat, folosește MAX
request: Request object pentru cache metadata
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
try:
schema = await DashboardService._get_schema(company_id, server_id)
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Determine current period from params or database
if luna is not None and an is not None:
current_year = an
current_month = luna
else:
# Get current period from database
current_period_query = f"""
WITH luna_curenta AS (
SELECT anul, luna FROM {schema}.calendar
WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)
)
SELECT anul, luna FROM luna_curenta
"""
cursor.execute(current_period_query)
current_period = cursor.fetchone()
if not current_period:
# Fallback to current system date
current_year = 2024
current_month = 12
else:
current_year = current_period[0]
current_month = current_period[1]
# Determine period parameters
if period == 'ytd':
# Year to date - from January to current month of current year
start_year = current_year
start_month = 1
end_year = current_year
end_month = current_month
elif period == '12m':
# Last 12 months
from datetime import date
from dateutil.relativedelta import relativedelta
current_date = date(current_year, current_month, 1)
start_date = current_date - relativedelta(months=11) # 12 months including current
start_year = start_date.year
start_month = start_date.month
end_year = current_year
end_month = current_month
else:
# Default to 12 months
from datetime import date
from dateutil.relativedelta import relativedelta
current_date = date(current_year, current_month, 1)
start_date = current_date - relativedelta(months=11)
start_year = start_date.year
start_month = start_date.month
end_year = current_year
end_month = current_month
# Calculate previous period (12 months before current period)
from datetime import date
from dateutil.relativedelta import relativedelta
prev_start_date = date(start_year, start_month, 1) - relativedelta(months=12)
prev_end_date = date(end_year, end_month, 1) - relativedelta(months=12)
prev_start_year = prev_start_date.year
prev_start_month = prev_start_date.month
prev_end_year = prev_end_date.year
prev_end_month = prev_end_date.month
# Comprehensive trends query for all indicators (current + previous period)
trends_query = f"""
WITH trend_periods AS (
SELECT DISTINCT an as anul, luna
FROM {schema}.vbalanta_parteneri
WHERE (an*100 + luna >= {start_year * 100 + start_month})
AND (an*100 + luna <= {end_year * 100 + end_month})
),
prev_trend_periods AS (
SELECT DISTINCT an as anul, luna
FROM {schema}.vbalanta_parteneri
WHERE (an*100 + luna >= {prev_start_year * 100 + prev_start_month})
AND (an*100 + luna <= {prev_end_year * 100 + prev_end_month})
),
comprehensive_data AS (
SELECT
tp.anul,
tp.luna,
tp.anul||'-'||LPAD(tp.luna,2,'0') as perioada,
-- CLIENTI - facturat (cifra de afaceri - vanzari)
COALESCE(SUM(CASE
WHEN vb.cont in ('4111', '461') THEN vb.debit
ELSE 0
END), 0) as clienti_facturat,
-- CLIENTI - incasat (incasari de la clienti)
COALESCE(SUM(CASE
WHEN vb.cont IN ('4111', '461') THEN vb.credit
WHEN vb.cont in ('419') THEN vb.debit
ELSE 0
END), 0) as clienti_incasat,
-- FURNIZORI - facturat (achizitii)
COALESCE(SUM(CASE
WHEN vb.cont in ('401', '404', '462') THEN vb.credit
ELSE 0
END), 0) as furnizori_facturat,
-- FURNIZORI - achitat (plati catre furnizori)
COALESCE(SUM(CASE
WHEN vb.cont in ('401', '404', '462') THEN vb.debit
WHEN vb.cont in ('409') THEN vb.credit
ELSE 0
END), 0) as furnizori_achitat,
-- CLIENTI SOLD (balanta clienti)
COALESCE(SUM(CASE
WHEN vb.cont IN ('4111', '461') THEN vb.solddeb - vb.soldcred
WHEN vb.cont IN ('419') THEN vb.soldcred - vb.solddeb
ELSE 0
END), 0) as clienti_sold,
-- FURNIZORI SOLD (balanta furnizori)
COALESCE(SUM(CASE
WHEN vb.cont in ('401', '404', '462') THEN vb.soldcred - vb.solddeb
WHEN vb.cont IN ('409') THEN vb.solddeb - vb.soldcred
ELSE 0
END), 0) as furnizori_sold,
-- TREZORERIE SOLD
COALESCE(SUM(CASE
WHEN vb.cont IN ('5121','5311','5124','5314') THEN vb.solddeb - vb.soldcred
ELSE 0
END), 0) as trezorerie_sold
FROM trend_periods tp
LEFT JOIN {schema}.vbalanta_parteneri vb
ON vb.an = tp.anul
AND vb.luna = tp.luna
AND vb.cont in ('4111', '461', '419', '401', '404', '462', '409','5121','5311','5124','5314')
GROUP BY tp.anul, tp.luna
),
prev_comprehensive_data AS (
SELECT
tp.anul,
tp.luna,
tp.anul||'-'||LPAD(tp.luna,2,'0') as perioada,
-- CLIENTI - facturat (cifra de afaceri - vanzari)
COALESCE(SUM(CASE
WHEN vb.cont in ('4111', '461') THEN vb.debit
ELSE 0
END), 0) as clienti_facturat,
-- CLIENTI - incasat (incasari de la clienti)
COALESCE(SUM(CASE
WHEN vb.cont IN ('4111', '461') THEN vb.credit
WHEN vb.cont in ('419') THEN vb.debit
ELSE 0
END), 0) as clienti_incasat,
-- FURNIZORI - facturat (achizitii)
COALESCE(SUM(CASE
WHEN vb.cont in ('401', '404', '462') THEN vb.credit
ELSE 0
END), 0) as furnizori_facturat,
-- FURNIZORI - achitat (plati catre furnizori)
COALESCE(SUM(CASE
WHEN vb.cont in ('401', '404', '462') THEN vb.debit
WHEN vb.cont in ('409') THEN vb.credit
ELSE 0
END), 0) as furnizori_achitat,
-- CLIENTI SOLD (balanta clienti)
COALESCE(SUM(CASE
WHEN vb.cont IN ('4111', '461') THEN vb.solddeb - vb.soldcred
WHEN vb.cont IN ('419') THEN vb.soldcred - vb.solddeb
ELSE 0
END), 0) as clienti_sold,
-- FURNIZORI SOLD (balanta furnizori)
COALESCE(SUM(CASE
WHEN vb.cont in ('401', '404', '462') THEN vb.soldcred - vb.solddeb
WHEN vb.cont IN ('409') THEN vb.solddeb - vb.soldcred
ELSE 0
END), 0) as furnizori_sold,
-- TREZORERIE SOLD
COALESCE(SUM(CASE
WHEN vb.cont IN ('5121','5311','5124','5314') THEN vb.solddeb - vb.soldcred
ELSE 0
END), 0) as trezorerie_sold
FROM prev_trend_periods tp
LEFT JOIN {schema}.vbalanta_parteneri vb
ON vb.an = tp.anul
AND vb.luna = tp.luna
AND vb.cont in ('4111', '461', '419', '401', '404', '462', '409','5121','5311','5124','5314')
GROUP BY tp.anul, tp.luna
)
SELECT
'current' as data_type,
cd.anul,
cd.luna,
cd.perioada,
cd.clienti_facturat,
cd.clienti_incasat,
cd.furnizori_facturat,
cd.furnizori_achitat,
cd.clienti_sold,
cd.furnizori_sold,
cd.trezorerie_sold
FROM comprehensive_data cd
UNION ALL
SELECT
'previous' as data_type,
pcd.anul,
pcd.luna,
pcd.perioada,
pcd.clienti_facturat,
pcd.clienti_incasat,
pcd.furnizori_facturat,
pcd.furnizori_achitat,
pcd.clienti_sold,
pcd.furnizori_sold,
pcd.trezorerie_sold
FROM prev_comprehensive_data pcd
ORDER BY data_type DESC, anul ASC, luna ASC
"""
cursor.execute(trends_query)
all_results = cursor.fetchall()
# Separate current and previous results
result = [row[1:] for row in all_results if row[0] == 'current']
prev_result = [row[1:] for row in all_results if row[0] == 'previous']
if not result:
# Return empty arrays in the expected format
return {
"periods": [],
"clienti_facturat": [],
"clienti_incasat": [],
"furnizori_facturat": [],
"furnizori_achitat": [],
"clienti_sold": [],
"furnizori_sold": [],
"trezorerie_sold": [],
"rata_incasare_clienti": [],
"rata_achitare_furnizori": [],
"previous_periods": [],
"clienti_facturat_prev": [],
"clienti_incasat_prev": [],
"furnizori_facturat_prev": [],
"furnizori_achitat_prev": [],
"clienti_sold_prev": [],
"furnizori_sold_prev": [],
"trezorerie_sold_prev": [],
"metadata": {
"period": period,
"company_id": company_id,
"data_points": 0,
"grouping": "monthly"
}
}
# Process results into the expected format
periods = []
clienti_facturat = []
clienti_incasat = []
furnizori_facturat = []
furnizori_achitat = []
clienti_sold = []
furnizori_sold = []
trezorerie_sold = []
rata_incasare_clienti = []
rata_achitare_furnizori = []
# Process previous period results
previous_periods = []
clienti_facturat_prev = []
clienti_incasat_prev = []
furnizori_facturat_prev = []
furnizori_achitat_prev = []
clienti_sold_prev = []
furnizori_sold_prev = []
trezorerie_sold_prev = []
for row in result:
# After row[1:], indices are: 0=anul, 1=luna, 2=perioada, 3=clienti_facturat, etc.
periods.append(row[2]) # perioada
clienti_facturat.append(float(row[3] or 0))
clienti_incasat.append(float(row[4] or 0))
furnizori_facturat.append(float(row[5] or 0))
furnizori_achitat.append(float(row[6] or 0))
clienti_sold.append(float(row[7] or 0))
furnizori_sold.append(float(row[8] or 0))
trezorerie_sold.append(float(row[9] or 0))
# Calculate collection and payment rates
cf = float(row[3] or 0) # clienti_facturat
ci = float(row[4] or 0) # clienti_incasat
ff = float(row[5] or 0) # furnizori_facturat
fa = float(row[6] or 0) # furnizori_achitat
# Collection rate (rata incasare clienti)
rata_incasare = (ci / cf * 100) if cf > 0 else 0
rata_incasare_clienti.append(round(rata_incasare, 2))
# Payment rate (rata achitare furnizori)
rata_achitare = (fa / ff * 100) if ff > 0 else 0
rata_achitare_furnizori.append(round(rata_achitare, 2))
# Process previous period data
for row in prev_result:
previous_periods.append(row[2]) # perioada
clienti_facturat_prev.append(float(row[3] or 0))
clienti_incasat_prev.append(float(row[4] or 0))
furnizori_facturat_prev.append(float(row[5] or 0))
furnizori_achitat_prev.append(float(row[6] or 0))
clienti_sold_prev.append(float(row[7] or 0))
furnizori_sold_prev.append(float(row[8] or 0))
trezorerie_sold_prev.append(float(row[9] or 0))
# Calculate growth rates
growth_rates = {}
if len(periods) >= 2:
datasets = {
'clienti_facturat': clienti_facturat,
'clienti_incasat': clienti_incasat,
'furnizori_facturat': furnizori_facturat,
'furnizori_achitat': furnizori_achitat,
'trezorerie_sold': trezorerie_sold
}
for key, values in datasets.items():
if len(values) >= 2:
previous_value = values[0]
current_value = values[-1]
if previous_value != 0:
growth_rate = round((current_value - previous_value) / abs(previous_value) * 100, 2)
else:
growth_rate = 100.0 if current_value > 0 else -100.0 if current_value < 0 else 0.0
growth_rates[key] = growth_rate
return {
"periods": periods,
"clienti_facturat": clienti_facturat,
"clienti_incasat": clienti_incasat,
"furnizori_facturat": furnizori_facturat,
"furnizori_achitat": furnizori_achitat,
"clienti_sold": clienti_sold,
"furnizori_sold": furnizori_sold,
"trezorerie_sold": trezorerie_sold,
"rata_incasare_clienti": rata_incasare_clienti,
"rata_achitare_furnizori": rata_achitare_furnizori,
"previous_periods": previous_periods,
"clienti_facturat_prev": clienti_facturat_prev,
"clienti_incasat_prev": clienti_incasat_prev,
"furnizori_facturat_prev": furnizori_facturat_prev,
"furnizori_achitat_prev": furnizori_achitat_prev,
"clienti_sold_prev": clienti_sold_prev,
"furnizori_sold_prev": furnizori_sold_prev,
"trezorerie_sold_prev": trezorerie_sold_prev,
"metadata": {
"period": period,
"company_id": company_id,
"data_points": len(periods),
"previous_data_points": len(previous_periods),
"grouping": "monthly"
},
"growth_rates": growth_rates
}
except Exception as e:
logger.error(f"Error getting comprehensive trends: {str(e)}")
raise
@staticmethod
async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = "", server_id: Optional[str] = None):
"""
Obține date detaliate pentru tabelele din dashboard
Fixed to use existing vireg_parteneri view instead of missing tables
Args:
company: ID-ul firmei
data_type: Tipul datelor (clients, suppliers, treasury)
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
page: Pagina curentă
page_size: Mărimea paginii
search: Termen de căutare
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
logger.info(f"get_detailed_data called: company={company}, data_type={data_type}, luna={luna}, an={an}, page={page}")
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
try:
# Get schema for company
schema_query = """
SELECT schema
FROM CONTAFIN_ORACLE.v_nom_firme
WHERE id_firma = :company_id
"""
cursor.execute(schema_query, {'company_id': int(company)})
schema_result = cursor.fetchone()
if not schema_result:
logger.error(f"Schema not found for company {company}")
return {"error": "Schema not found for company", "data": [], "total": 0}
schema = schema_result[0]
logger.info(f"Found schema: {schema}")
# Construiește CTE pentru perioada curentă
period_cte, period_params = DashboardService._build_period_cte(schema, luna, an)
# Calculate offset for pagination
offset = (page - 1) * page_size
logger.info(f"Pagination params: page={page}, page_size={page_size}, offset={offset}")
# Handle treasury early return
if data_type == "treasury":
return {
"data": [],
"total": 0,
"page": page,
"page_size": page_size,
"total_pages": 0,
"message": "Date detaliate pentru trezorerie nu sunt disponibile"
}
# Build query based on data type
if data_type == "clients":
# Query cu paginare pe CLIENȚI (nu pe facturi individuale)
base_query = f"""
{period_cte},
clienti_cu_sold AS (
-- Pasul 1: Identifică TOȚI clienții cu sold != 0
SELECT DISTINCT vp.nume as client_name
FROM {schema}.vireg_parteneri vp, luna_curenta lc
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.cont IN ('4111','461')
AND vp.nume IS NOT NULL
AND ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) <> 0
AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '')
ORDER BY vp.nume ASC
),
clienti_pagina AS (
-- Pasul 2: Paginează pe CLIENȚI (25 clienți/pagină)
SELECT * FROM (
SELECT t.*, ROWNUM as rn FROM clienti_cu_sold t WHERE ROWNUM <= {offset + page_size}
) WHERE rn > {offset}
)
-- Pasul 3: Ia TOATE facturile pentru clienții din pagina curentă
SELECT
vp.nume as client,
vp.nract as numar_document,
vp.dataact as data_document,
(vp.precdeb + vp.debit) as facturat,
(vp.preccred + vp.credit) as incasat,
(vp.precdeb + vp.debit) - (vp.preccred + vp.credit) as sold,
NVL(vp.datascad, vp.dataact + 30) as data_scadenta,
CASE
WHEN NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE) THEN 'Restant'
ELSE 'In termen'
END as status
FROM {schema}.vireg_parteneri vp, luna_curenta lc, clienti_pagina cp
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.nume = cp.client_name
AND vp.cont IN ('4111','461')
AND ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) <> 0
ORDER BY vp.nume ASC, vp.dataact DESC
"""
elif data_type == "suppliers":
# Query cu paginare pe FURNIZORI (nu pe facturi individuale)
base_query = f"""
{period_cte},
furnizori_cu_sold AS (
-- Pasul 1: Identifică TOȚI furnizorii cu sold != 0
SELECT DISTINCT vp.nume as furnizor_name
FROM {schema}.vireg_parteneri vp, luna_curenta lc
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.cont IN ('401','404','462')
AND vp.nume IS NOT NULL
AND ((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) <> 0
AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '')
ORDER BY vp.nume ASC
),
furnizori_pagina AS (
-- Pasul 2: Paginează pe FURNIZORI (25 furnizori/pagină)
SELECT * FROM (
SELECT t.*, ROWNUM as rn FROM furnizori_cu_sold t WHERE ROWNUM <= {offset + page_size}
) WHERE rn > {offset}
)
-- Pasul 3: Ia TOATE facturile pentru furnizorii din pagina curentă
SELECT
vp.nume as furnizor,
vp.nract as numar_document,
vp.dataact as data_document,
(vp.preccred + vp.credit) as facturat,
(vp.precdeb + vp.debit) as achitat,
(vp.preccred + vp.credit) - (vp.precdeb + vp.debit) as sold,
NVL(vp.datascad, vp.dataact + 30) as data_scadenta,
CASE
WHEN NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE) THEN 'Restant'
ELSE 'In termen'
END as status
FROM {schema}.vireg_parteneri vp, luna_curenta lc, furnizori_pagina fp
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.nume = fp.furnizor_name
AND vp.cont IN ('401','404','462')
AND ((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) <> 0
ORDER BY vp.nume ASC, vp.dataact DESC
"""
else:
return {"error": "Invalid data type", "data": [], "total": 0}
# Get total count of CLIENȚI/FURNIZORI (not individual invoices)
if data_type == "clients":
count_query = f"""
{period_cte}
SELECT COUNT(DISTINCT vp.nume)
FROM {schema}.vireg_parteneri vp, luna_curenta lc
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.cont IN ('4111','461')
AND vp.nume IS NOT NULL
AND ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) <> 0
AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '')
"""
elif data_type == "suppliers":
count_query = f"""
{period_cte}
SELECT COUNT(DISTINCT vp.nume)
FROM {schema}.vireg_parteneri vp, luna_curenta lc
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.cont IN ('401','404','462')
AND vp.nume IS NOT NULL
AND ((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) <> 0
AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '')
"""
cursor.execute(count_query, period_params)
total = cursor.fetchone()[0]
logger.info(f"Total {data_type}: {total}")
# Execute base query directly (pagination already included in CTE)
logger.info(f"Executing query with offset={offset}, page_size={page_size}")
cursor.execute(base_query, period_params)
columns = [desc[0].lower() for desc in cursor.description]
data = []
for row in cursor.fetchall():
# Map row data to column names
data.append(dict(zip(columns, row)))
# Count unique clients/suppliers in returned data
if data_type == "clients":
unique_names = len(set(row.get('client') for row in data))
logger.info(f"Returned {len(data)} invoices from {unique_names} unique clients (expected max {page_size} clients)")
elif data_type == "suppliers":
unique_names = len(set(row.get('furnizor') for row in data))
logger.info(f"Returned {len(data)} invoices from {unique_names} unique suppliers (expected max {page_size} suppliers)")
logger.info(f"Detailed data query returned {len(data)} rows out of {total} total")
return {
"data": data,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size
}
except Exception as e:
logger.error(f"Error in get_detailed_data: {str(e)}")
return {"error": f"Database error: {str(e)}", "data": [], "total": 0}
@staticmethod
async def get_performance_data(company_id: int, period: str = "7d", server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Calculează performanța încasări/plăți pentru perioada selectată
Args:
company_id: ID-ul companiei
period: Perioada ("7d", "1m", "3m", "6m", "ytd", "12m")
server_id: ID-ul serverului Oracle (pentru multi-server)
Returns:
{
labels: List[str] - etichete pentru perioadele de timp
incasari: List[float] - valorile încasărilor pe perioadă
plati: List[float] - valorile plăților pe perioadă
indicators: {
rataIncasare: float - rata de încasare (%)
cashConversion: int - zilele de conversie cash
workingCapital: float - capitalul de lucru
trend: str - tendința ("up", "down", "stable")
}
}
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Get schema
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
schema = schema_result[0]
# Pentru acum returnăm date mock cu structura corectă
# TODO: Implementați query-uri Oracle pentru perioada selectată
# Mock data based on period
if period == "7d":
labels = ["Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"]
incasari = [45000, 52000, 38000, 61000, 49000, 33000, 28000]
plati = [31000, 44000, 29000, 48000, 37000, 25000, 19000]
elif period == "1m":
labels = ["Săpt 1", "Săpt 2", "Săpt 3", "Săpt 4"]
incasari = [185000, 201000, 168000, 195000]
plati = [142000, 156000, 133000, 151000]
elif period == "3m":
labels = ["Luna 1", "Luna 2", "Luna 3"]
incasari = [749000, 698000, 823000]
plati = [582000, 519000, 634000]
elif period == "6m":
labels = ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun"]
incasari = [749000, 698000, 823000, 756000, 681000, 792000]
plati = [582000, 519000, 634000, 588000, 523000, 612000]
elif period == "ytd":
labels = ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun", "Iul", "Aug", "Sep", "Oct", "Nov", "Dec"]
incasari = [749000, 698000, 823000, 756000, 681000, 792000, 834000, 712000, 768000, 695000, 743000, 821000]
plati = [582000, 519000, 634000, 588000, 523000, 612000, 647000, 553000, 596000, 539000, 577000, 638000]
else: # 12m
labels = ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun", "Iul", "Aug", "Sep", "Oct", "Nov", "Dec"]
incasari = [749000, 698000, 823000, 756000, 681000, 792000, 834000, 712000, 768000, 695000, 743000, 821000]
plati = [582000, 519000, 634000, 588000, 523000, 612000, 647000, 553000, 596000, 539000, 577000, 638000]
# Calculate indicators
total_incasari = sum(incasari)
total_plati = sum(plati)
rata_incasare = round((total_incasari / (total_incasari * 1.15)) * 100, 1) if total_incasari > 0 else 0 # Assuming 15% more was invoiced
cash_conversion = 45 # Days - mock value
working_capital = total_incasari - total_plati
# Determine trend
if len(incasari) >= 2:
trend = "up" if incasari[-1] > incasari[-2] else "down" if incasari[-1] < incasari[-2] else "stable"
else:
trend = "stable"
return {
"labels": labels,
"incasari": incasari,
"plati": plati,
"indicators": {
"rataIncasare": rata_incasare,
"cashConversion": cash_conversion,
"workingCapital": working_capital,
"trend": trend
}
}
except Exception as e:
logger.error(f"Error getting performance data: {str(e)}")
raise
@staticmethod
async def get_cashflow_forecast(company_id: int, period: str = "7d", server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Calculează previziunea cash flow bazată pe scadențe
Args:
company_id: ID-ul companiei
period: Perioada ("7d", "1m", "3m", "6m")
server_id: ID-ul serverului Oracle (pentru multi-server)
Returns:
{
periods: List[str] - perioadele de timp
inflows: List[float] - intrările de cash estimate
outflows: List[float] - ieșirile de cash estimate
netFlow: List[float] - fluxul net pe perioadă
cumulative: List[float] - fluxul cumulativ
criticalDays: List[str] - zilele cu deficit critic
netTotal: float - totalul net al perioadei
}
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Get schema
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
schema = schema_result[0]
# Pentru acum returnăm date mock cu structura corectă
# TODO: Implementați query-uri pentru scadențe viitoare
# Mock data based on period
if period == "7d":
periods = ["Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"]
inflows = [25000, 18000, 32000, 45000, 28000, 15000, 12000]
outflows = [31000, 22000, 19000, 38000, 25000, 18000, 8000]
elif period == "1m":
periods = ["Săpt 1", "Săpt 2", "Săpt 3", "Săpt 4"]
inflows = [95000, 112000, 88000, 105000]
outflows = [108000, 92000, 75000, 89000]
elif period == "3m":
periods = ["Luna 1", "Luna 2", "Luna 3"]
inflows = [400000, 365000, 420000]
outflows = [364000, 298000, 385000]
else: # 6m
periods = ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun"]
inflows = [400000, 365000, 420000, 385000, 342000, 396000]
outflows = [364000, 298000, 385000, 356000, 289000, 367000]
# Calculate net flow and cumulative
net_flow = [inf - out for inf, out in zip(inflows, outflows)]
cumulative = []
running_total = 150000 # Starting cash position (mock)
for net in net_flow:
running_total += net
cumulative.append(running_total)
# Identify critical days (negative cumulative)
critical_days = []
for i, (period_name, cum) in enumerate(zip(periods, cumulative)):
if cum < 0:
critical_days.append(period_name)
net_total = sum(net_flow)
return {
"periods": periods,
"inflows": inflows,
"outflows": outflows,
"netFlow": net_flow,
"cumulative": cumulative,
"criticalDays": critical_days,
"netTotal": net_total
}
except Exception as e:
logger.error(f"Error getting cashflow forecast: {str(e)}")
raise
@staticmethod
@cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an', 'server_id'])
async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Analizează scadențele clienți vs furnizori cu date reale din Oracle (CACHED 30 min)
Args:
company_id: ID-ul companiei
period: Perioada ("7d", "1m", "3m", "6m", "12m", "over12m")
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
server_id: ID-ul serverului Oracle (pentru multi-server)
Returns:
{
clients: List[Dict] - [{name, amount, dueDate, daysOverdue}, ...]
suppliers: List[Dict] - [{name, amount, dueDate, daysOverdue}, ...]
balance: float - balanța între clienți și furnizori
recommendations: List[str] - recomandări pentru îmbunătățire
}
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Get schema
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
schema = schema_result[0]
# Construiește CTE pentru perioada curentă
period_cte, period_params = DashboardService._build_period_cte(schema, luna, an)
# Determină filtrele pentru perioada selectată (orizont de planificare)
# Logică: Include TOATE restanțele + scadențele viitoare din perioada selectată
if period == "7d":
days_filter = "datascad <= TRUNC(SYSDATE) + 7"
elif period == "1m":
days_filter = "datascad <= TRUNC(SYSDATE) + 30"
elif period == "3m":
days_filter = "datascad <= TRUNC(SYSDATE) + 90"
elif period == "6m":
days_filter = "datascad <= TRUNC(SYSDATE) + 180"
elif period == "12m":
days_filter = "datascad <= TRUNC(SYSDATE) + 365"
else: # "all" - toate soldurile
days_filter = "1=1"
# Query pentru clienți (facturi de încasat)
clients_query = f"""
{period_cte}
SELECT client_name,
SUM(amount) as amount,
MAX(due_date) as due_date,
MAX(days_overdue) as days_overdue
FROM (SELECT vp.nume as client_name,
((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) as amount,
NVL(vp.datascad, vp.DATAACT + 30) as due_date,
TRUNC(SYSDATE) - NVL(vp.datascad, vp.DATAACT + 30) as days_overdue
FROM {schema}.vireg_parteneri vp, luna_curenta lc
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.cont IN ('4111', '461')
AND vp.nume IS NOT NULL
AND {days_filter}
AND ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) <> 0)
GROUP BY client_name
HAVING SUM (amount) <> 0
ORDER BY days_overdue desc
"""
cursor.execute(clients_query, period_params)
clients_rows = cursor.fetchall()
clients = []
for row in clients_rows:
client_name = row[0]
amount = float(row[1] or 0)
due_date = row[2].strftime('%Y-%m-%d') if row[2] else None
days_overdue = int(row[3] or 0)
clients.append({
"name": client_name,
"amount": amount,
"dueDate": due_date,
"daysOverdue": days_overdue
})
# Sortare îmbunătățită: Restanțe primele (descrescător), apoi scadențe viitoare (crescător)
clients.sort(key=lambda x: (-1 if x["daysOverdue"] > 0 else 1, -x["daysOverdue"] if x["daysOverdue"] > 0 else x["daysOverdue"]))
# Query pentru furnizori (facturi de plătit)
suppliers_query = f"""
{period_cte}
SELECT client_name,
SUM(amount) as amount,
MIN(due_date) as due_date,
MAX(days_overdue) as days_overdue
FROM (SELECT vp.nume as client_name,
((vp.preccred + vp.credit)-(vp.precdeb + vp.debit)) as amount,
NVL(vp.datascad, vp.DATAACT + 30) as due_date,
TRUNC(SYSDATE) - NVL(vp.datascad, vp.DATAACT + 30) as days_overdue
FROM {schema}.vireg_parteneri vp, luna_curenta lc
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.cont IN ('401', '404', '462')
AND vp.nume IS NOT NULL
AND {days_filter}
AND ((vp.preccred + vp.credit)-(vp.precdeb + vp.debit)) <> 0)
GROUP BY client_name
HAVING SUM (amount) <> 0
ORDER BY days_overdue desc
"""
cursor.execute(suppliers_query, period_params)
suppliers_rows = cursor.fetchall()
suppliers = []
for row in suppliers_rows:
supplier_name = row[0]
amount = float(row[1] or 0)
due_date = row[2].strftime('%Y-%m-%d') if row[2] else None
days_overdue = int(row[3] or 0)
suppliers.append({
"name": supplier_name,
"amount": amount,
"dueDate": due_date,
"daysOverdue": days_overdue
})
# Sortare îmbunătățită: Restanțe primele (descrescător), apoi scadențe viitoare (crescător)
suppliers.sort(key=lambda x: (-1 if x["daysOverdue"] > 0 else 1, -x["daysOverdue"] if x["daysOverdue"] > 0 else x["daysOverdue"]))
# Calculează balanța
total_clients = sum(c["amount"] for c in clients)
total_suppliers = sum(s["amount"] for s in suppliers)
balance = total_clients - total_suppliers
# Generează recomandări bazate pe date reale
recommendations = []
if balance < -50000:
recommendations.extend([
"Deficit de cash flow identificat - prioritizați încasările",
"Negociați termeni de plată mai buni cu furnizorii",
"Considerați finanțare pe termen scurt"
])
elif balance > 100000:
recommendations.extend([
"Surplus de cash disponibil pentru investiții",
"Considerați plăți anticipate pentru reduceri",
"Evaluați oportunități de investire a excesului"
])
else:
recommendations.append("Balanța cash flow este echilibrată")
overdue_clients = [c for c in clients if c["daysOverdue"] > 0]
if overdue_clients:
total_overdue = sum(c["amount"] for c in overdue_clients)
recommendations.append(f"Atenție: {len(overdue_clients)} clienți în restanță (total: {total_overdue:,.0f} RON)")
overdue_suppliers = [s for s in suppliers if s["daysOverdue"] > 0]
if overdue_suppliers:
total_overdue = sum(s["amount"] for s in overdue_suppliers)
recommendations.append(f"Urgent: {len(overdue_suppliers)} furnizori în restanță (total: {total_overdue:,.0f} RON)")
# Adaugă recomandări specifice pentru clienți cu restanțe mari
critical_clients = [c for c in overdue_clients if c["daysOverdue"] > 30]
if critical_clients:
recommendations.append(f"Critică: {len(critical_clients)} clienți cu restanțe peste 30 de zile")
# Adaugă metadate pentru context complet
metadata = {
"period": period,
"total_clients": len(clients),
"total_suppliers": len(suppliers),
"overdue_clients": len(overdue_clients),
"overdue_suppliers": len(overdue_suppliers),
"critical_clients": len(critical_clients) if critical_clients else 0,
"total_overdue_amount_clients": sum(c["amount"] for c in overdue_clients) if overdue_clients else 0,
"total_overdue_amount_suppliers": sum(s["amount"] for s in overdue_suppliers) if overdue_suppliers else 0
}
return {
"clients": clients,
"suppliers": suppliers,
"balance": balance,
"recommendations": recommendations,
"metadata": metadata
}
except Exception as e:
logger.error(f"Error getting maturity analysis: {str(e)}")
raise
@staticmethod
@cached(cache_type='monthly_flows', key_params=['company', 'luna', 'an', 'server_id'])
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Obține fluxurile lunare de intrare și ieșire pentru luna curentă (CACHED 30 min)
Args:
company: ID-ul firmei
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
request: Request object pentru cache metadata
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema
company_id = company
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
schema = schema_result[0]
# Construiește CTE pentru perioada curentă (cu period column)
if luna is not None and an is not None:
period_cte = f"""
WITH luna_curenta AS (
SELECT :param_an as anul, :param_luna as luna,
:param_an || '-' || LPAD(:param_luna, 2, '0') as period
FROM DUAL
)"""
period_params = {'param_an': an, 'param_luna': luna}
else:
period_cte = f"""
WITH luna_curenta AS (
SELECT anul, luna, anul || '-' || LPAD(luna, 2, '0') as period
FROM {schema}.calendar
WHERE anul * 12 + luna = (SELECT MAX(c2.anul * 12 + c2.luna) FROM {schema}.calendar c2)
)"""
period_params = {}
# Query pentru fluxuri lunare
flows_query = f"""
{period_cte}
SELECT
SUM(CASE
WHEN vp.cont IN ('4111', '461') THEN
vp.credit
WHEN vp.cont IN ('419') THEN
vp.debit
ELSE
0
END) as monthly_inflows,
SUM(CASE
WHEN vp.cont IN ('401', '404', '462') THEN
vp.debit
WHEN vp.cont IN ('409') THEN
vp.credit
ELSE
0
END) as monthly_outflows,
MAX(lc.period) as period
FROM {schema}.vireg_parteneri vp, luna_curenta lc
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.cont IN ('4111', '461', '419', '401', '404', '462', '409')
"""
cursor.execute(flows_query, period_params)
flow_row = cursor.fetchone()
if not flow_row:
# Default period from params or current date
default_period = f"{an}-{str(luna).zfill(2)}" if luna and an else "2025-12"
return {
"inflows": 0,
"outflows": 0,
"period": default_period,
"currency": "RON"
}
return {
"inflows": float(flow_row[0] or 0),
"outflows": float(flow_row[1] or 0),
"period": flow_row[2] or "2025-08",
"currency": "RON"
}
except Exception as e:
logger.error(f"Error in get_monthly_flows: {str(e)}")
raise
@staticmethod
@cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an', 'server_id'])
async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Obține breakdown-ul trezoreriei pe casă și bancă (CACHED 30 min)
Args:
company: ID-ul firmei
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema
company_id = company
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
schema = schema_result[0]
# Construiește CTE pentru perioada curentă
period_cte, period_params = DashboardService._build_period_cte(schema, luna, an)
# Query pentru breakdown trezorerie - cu nume reale și sub-breakdown
treasury_query = f"""
{period_cte}
SELECT
vb.cont,
vb.nume as nume_real,
(vb.solddeb - vb.soldcred) as sold,
CASE
WHEN vb.cont IN ('5311', '5314', '5328') THEN 'casa'
WHEN vb.cont IN ('5121', '5124') THEN 'banca'
END as tip
FROM {schema}.vbalanta_parteneri vb, luna_curenta lc
WHERE vb.an = lc.anul
AND vb.luna = lc.luna
AND vb.cont IN ('5311', '5314', '5328', '5121', '5124')
AND (vb.solddeb - vb.soldcred) <> 0
ORDER BY tip, vb.cont
"""
cursor.execute(treasury_query, period_params)
treasury_rows = cursor.fetchall()
if not treasury_rows:
return {
"total": 0,
"breakdown": {
"casa": {"total": 0, "items": []},
"banca": {"total": 0, "items": []}
},
"currency": "RON"
}
# Procesare rezultate cu grupare pe tip
casa_items = []
banca_items = []
casa_total = 0
banca_total = 0
for row in treasury_rows:
cont = row[0]
nume_real = row[1] # Nume din vbalanta_parteneri.nume
sold = float(row[2] or 0)
tip = row[3]
item = {
"nume": nume_real or f"Cont {cont}", # Fallback la nume generic
"cont": cont,
"sold": sold
}
if tip == 'casa':
casa_items.append(item)
casa_total += sold
else: # banca
banca_items.append(item)
banca_total += sold
total = casa_total + banca_total
return {
"total": total,
"breakdown": {
"casa": {
"total": casa_total,
"items": casa_items
},
"banca": {
"total": banca_total,
"items": banca_items
}
},
"currency": "RON"
}
except Exception as e:
logger.error(f"Error in get_treasury_breakdown: {str(e)}")
raise
@staticmethod
@cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an', 'server_id'])
async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Obține breakdown-ul balanței nete pe clienți și furnizori cu detaliere pe perioade (CACHED 30 min)
Args:
company: ID-ul firmei
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema
company_id = company
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
schema = schema_result[0]
# Construiește CTE pentru perioada curentă
period_cte, period_params = DashboardService._build_period_cte(schema, luna, an)
# Query extins pentru breakdown detaliat pe perioade
balance_query = f"""
{period_cte}
SELECT
-- CLIENȚI - Sold total
SUM(CASE
WHEN vp.cont IN ('4111','461')
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit)
WHEN vp.cont = '419'
THEN -((vp.preccred + vp.credit) - (vp.precdeb + vp.debit))
ELSE 0
END) as clienti_total,
-- CLIENȚI - În termen (total)
SUM(CASE
WHEN vp.cont IN ('4111','461')
AND NVL(vp.datascad, vp.dataact + 30) >= TRUNC(SYSDATE)
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit)
WHEN vp.cont = '419'
AND NVL(vp.datascad, vp.dataact + 30) >= TRUNC(SYSDATE)
THEN -((vp.preccred + vp.credit) - (vp.precdeb + vp.debit))
ELSE 0
END) as clienti_in_termen,
-- CLIENȚI - Restanți (total)
SUM(CASE
WHEN vp.cont IN ('4111','461')
AND NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE)
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit)
WHEN vp.cont = '419'
AND NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE)
THEN -((vp.preccred + vp.credit) - (vp.precdeb + vp.debit))
ELSE 0
END) as clienti_restanti,
-- CLIENȚI - Restanți pe perioade
SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 1 AND 7
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_7,
SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 8 AND 14
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_14,
SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 15 AND 30
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_30,
SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 31 AND 60
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_60,
SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 61 AND 90
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_90,
SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) > 90
THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_90plus,
-- FURNIZORI - Sold total
SUM(CASE
WHEN vp.cont IN ('401','404','462')
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit)
WHEN vp.cont IN ('409')
THEN -((vp.precdeb + vp.debit) - (vp.preccred + vp.credit))
ELSE 0
END) as furnizori_total,
-- FURNIZORI - În termen (total)
SUM(CASE
WHEN vp.cont IN ('401','404','462') AND NVL(vp.datascad, vp.dataact + 30) >= TRUNC(SYSDATE)
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit)
WHEN vp.cont IN ('409') AND NVL(vp.datascad, vp.dataact + 30) >= TRUNC(SYSDATE)
THEN -((vp.precdeb + vp.debit) - (vp.preccred + vp.credit))
ELSE 0
END) as furnizori_in_termen,
-- FURNIZORI - Restanți (total)
SUM(CASE
WHEN vp.cont IN ('401','404','462') AND NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE)
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit)
WHEN vp.cont IN ('409') AND NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE)
THEN -((vp.precdeb + vp.debit) - (vp.preccred + vp.credit))
ELSE 0
END) as furnizori_restanti,
-- FURNIZORI - Restanți pe perioade
SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 1 AND 7
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_7,
SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 8 AND 14
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_14,
SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 15 AND 30
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_30,
SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 31 AND 60
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_60,
SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 61 AND 90
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_90,
SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) > 90
THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_90plus
FROM {schema}.vireg_parteneri vp, luna_curenta lc
WHERE vp.an = lc.anul
AND vp.luna = lc.luna
AND vp.cont IN ('4111', '461', '419', '401', '404', '462','409')
"""
cursor.execute(balance_query, period_params)
row = cursor.fetchone()
if not row:
return {
"clienti_total": 0,
"furnizori_total": 0,
"breakdown": {
"clienti": {
"total": 0,
"in_termen": {"total": 0},
"restant": {"total": 0, "perioade": {}}
},
"furnizori": {
"total": 0,
"in_termen": {"total": 0},
"restant": {"total": 0, "perioade": {}}
}
},
"currency": "RON"
}
# Procesare rezultate - INDEXARE CORECTATĂ
# Coloane: 0-8 = clienti (9 coloane), 9-17 = furnizori (9 coloane)
return {
"clienti_total": float(row[0] or 0),
"furnizori_total": float(row[9] or 0), # CORECTAT: row[9] nu row[10]
"breakdown": {
"clienti": {
"total": float(row[0] or 0),
"in_termen": {
"total": float(row[1] or 0)
},
"restant": {
"total": float(row[2] or 0),
"perioade": {
"7_zile": float(row[3] or 0),
"14_zile": float(row[4] or 0),
"30_zile": float(row[5] or 0),
"60_zile": float(row[6] or 0),
"90_zile": float(row[7] or 0),
"peste_90_zile": float(row[8] or 0)
}
}
},
"furnizori": {
"total": float(row[9] or 0), # CORECTAT: row[9] nu row[10]
"in_termen": {
"total": float(row[10] or 0) # CORECTAT: row[10] nu row[11]
},
"restant": {
"total": float(row[11] or 0), # CORECTAT: row[11] nu row[12]
"perioade": {
"7_zile": float(row[12] or 0), # CORECTAT
"14_zile": float(row[13] or 0), # CORECTAT
"30_zile": float(row[14] or 0), # CORECTAT
"60_zile": float(row[15] or 0), # CORECTAT
"90_zile": float(row[16] or 0), # CORECTAT
"peste_90_zile": float(row[17] or 0) # CORECTAT
}
}
}
},
"currency": "RON"
}
except Exception as e:
logger.error(f"Error in get_net_balance_breakdown: {str(e)}")
raise
@staticmethod
async def get_current_period(company: int, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Obține perioada curentă (an și lună) din calendarul Oracle
Args:
company: ID-ul companiei
server_id: ID-ul serverului Oracle (pentru multi-server)
Returns:
{
year: int - anul curent
month: int - luna curentă (1-12)
period: str - perioada în format YYYY-MM
}
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema
company_id = company
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
schema = schema_result[0]
# Query pentru perioada curentă din calendar Oracle
current_period_query = f"""
SELECT anul, luna, anul || '-' || LPAD(luna, 2, '0') as period
FROM {schema}.calendar
WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)
"""
cursor.execute(current_period_query)
period_row = cursor.fetchone()
if not period_row:
# Fallback la data curentă sistem
from datetime import datetime
now = datetime.now()
return {
"year": now.year,
"month": now.month,
"period": f"{now.year}-{now.month:02d}"
}
return {
"year": int(period_row[0]),
"month": int(period_row[1]),
"period": period_row[2]
}
except Exception as e:
logger.error(f"Error in get_current_period: {str(e)}")
raise