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>
2008 lines
100 KiB
Python
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
|
|
|