# import sys # Removed - no longer needed import os import oracledb from shared.database.oracle_pool import oracle_pool from ..models.treasury import BankCashRegister, RegisterFilter, RegisterListResponse, AccountingPeriod from ..cache.decorators import cached from decimal import Decimal from typing import Optional, List, Tuple, Any import logging logger = logging.getLogger(__name__) class TreasuryService: """Service pentru trezorerie - registru casă și bancă""" @staticmethod @cached(cache_type='schema', key_params=['company_id']) async def _get_schema(company_id: int) -> str: """Obține schema pentru company_id (CACHED PERMANENT)""" async with oracle_pool.get_connection() 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 def _get_view_query(schema: str, register_type: Optional[str] = None) -> str: """ Construiește query-ul pentru view-ul vbancasa corespunzător. Dacă register_type este None, returnează UNION ALL pentru toate tipurile. NU se filtrează pe incasari/plati > 0 - se afișează TOATE înregistrările! """ view_configs = { 'BANCA_LEI': { 'view': f'{schema}.vbancasa_5121_cum', 'incasari_col': 'incasari', 'plati_col': 'plati', 'valuta': "'RON'", 'tip': "'BANCA LEI'" }, 'BANCA_VALUTA': { 'view': f'{schema}.vbancasa_5124_cum', 'incasari_col': 'incasval', 'plati_col': 'platival', 'valuta': "COALESCE(numeval, 'EUR')", 'tip': "'BANCA VALUTA'" }, 'CASA_LEI': { 'view': f'{schema}.vbancasa_5311_cum', 'incasari_col': 'incasari', 'plati_col': 'plati', 'valuta': "'RON'", 'tip': "'CASA LEI'" }, 'CASA_VALUTA': { 'view': f'{schema}.vbancasa_5314_cum', 'incasari_col': 'incasval', 'plati_col': 'platival', 'valuta': "COALESCE(numeval, 'EUR')", 'tip': "'CASA VALUTA'" } } def build_select(config): # NU se filtrează - se afișează TOATE înregistrările # SOLD CUMULAT: Running balance per bancasa using window function # NULL-date rows (opening balance) come first due to NULLS FIRST return f""" SELECT nume, nract, dataact, bancasa, {config['incasari_col']} as incasari, {config['plati_col']} as plati, SUM({config['incasari_col']} - {config['plati_col']}) OVER ( PARTITION BY bancasa ORDER BY dataact ASC NULLS FIRST, nract ASC NULLS FIRST ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) as sold, {config['valuta']} as valuta, {config['tip']} as tip_registru, explicatia FROM {config['view']} """ if register_type and register_type in view_configs: return build_select(view_configs[register_type]) else: # UNION ALL pentru toate tipurile queries = [build_select(cfg) for cfg in view_configs.values()] return " UNION ALL ".join(queries) @staticmethod @cached(cache_type='treasury', key_params=['filter_params', 'username']) async def get_bank_cash_register(filter_params: RegisterFilter, username: str) -> RegisterListResponse: """ Obține registrul de casă și bancă din vbancasa views (CACHED 10 min) IMPORTANT: PACK_SESIUNE.SETAN și SETLUNA trebuie executate în ACEEAȘI tranzacție cu SELECT-ul din vbancasa* views! Folosim un bloc PL/SQL anonim care: 1. Obține anul și luna curentă din calendar 2. Apelează PACK_SESIUNE.SETAN și SETLUNA 3. Execută SELECT-ul din vbancasa* Toate în aceeași tranzacție! """ company_id = int(filter_params.company) schema = await TreasuryService._get_schema(company_id) async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Construiește query-ul pentru tipul de registru selectat base_select = TreasuryService._get_view_query(schema, filter_params.register_type) # Construiește WHERE conditions where_conditions = [] # Date filter preserves NULL-date rows (opening balance) # for correct cumulative sum calculation if filter_params.date_from and filter_params.date_to: where_conditions.append(f"(dataact IS NULL OR (dataact >= TO_DATE('{filter_params.date_from.strftime('%Y-%m-%d')}', 'YYYY-MM-DD') AND dataact <= TO_DATE('{filter_params.date_to.strftime('%Y-%m-%d')}', 'YYYY-MM-DD')))") elif filter_params.date_from: where_conditions.append(f"(dataact IS NULL OR dataact >= TO_DATE('{filter_params.date_from.strftime('%Y-%m-%d')}', 'YYYY-MM-DD'))") elif filter_params.date_to: where_conditions.append(f"(dataact IS NULL OR dataact <= TO_DATE('{filter_params.date_to.strftime('%Y-%m-%d')}', 'YYYY-MM-DD'))") if filter_params.partner_name: # Escape single quotes pentru SQL partner_escaped = filter_params.partner_name.replace("'", "''") where_conditions.append(f"UPPER(nume) LIKE UPPER('%{partner_escaped}%')") if filter_params.bank_account: # Escape single quotes pentru SQL bank_escaped = filter_params.bank_account.replace("'", "''") where_conditions.append(f"bancasa = '{bank_escaped}'") where_clause = "" if where_conditions: where_clause = " WHERE " + " AND ".join(where_conditions) # Paginare Oracle offset = (filter_params.page - 1) * filter_params.page_size limit_val = filter_params.page_size # Determine period to use: from params or MAX from calendar if filter_params.luna and filter_params.an: use_param_period = True period_select = f""" v_an := :param_an; v_luna := :param_luna; """ else: use_param_period = False period_select = f""" SELECT anul, luna INTO v_an, v_luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar); """ # Bloc PL/SQL anonim care face totul într-o singură tranzacție: # 1. Obține anul și luna din params sau calendar # 2. Setează PACK_SESIUNE.SETAN și SETLUNA # 3. Returnează datele prin REF CURSOR # IMPORTANT: Folosim ROW_NUMBER() pentru paginare corectă cu ORDER BY NULLS FIRST plsql_block = f""" DECLARE v_an NUMBER; v_luna NUMBER; v_cursor SYS_REFCURSOR; BEGIN -- Obține anul și luna din parametri sau calendar {period_select} -- Setează contextul de sesiune (OBLIGATORIU înainte de SELECT din vbancasa*) {schema}.PACK_SESIUNE.SETAN(v_an); {schema}.PACK_SESIUNE.SETLUNA(v_luna); -- Return accounting period :out_an := v_an; :out_luna := v_luna; -- Returnează datele prin cursor cu ROW_NUMBER pentru paginare corectă -- Pentru rânduri cu dataact=NULL (solduri precedente), sortare după bancasa -- Pentru rânduri cu date, sortare după data, număr, bancasa OPEN :result_cursor FOR SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER ( ORDER BY dataact ASC NULLS FIRST, CASE WHEN dataact IS NULL THEN bancasa END ASC, nract ASC NULLS FIRST, bancasa ASC ) as rn FROM ({base_select}) t{where_clause} ) WHERE rn > {offset} AND rn <= {offset + limit_val}; END; """ # Creează cursor pentru rezultate (oracledb.CURSOR pentru REF CURSOR) result_cursor = cursor.var(oracledb.CURSOR) out_an = cursor.var(int) out_luna = cursor.var(int) # Build params dict exec_params = {'result_cursor': result_cursor, 'out_an': out_an, 'out_luna': out_luna} if use_param_period: exec_params['param_an'] = filter_params.an exec_params['param_luna'] = filter_params.luna # Execută blocul PL/SQL cu REF CURSOR cursor.execute(plsql_block, exec_params) # Get accounting period values accounting_year = out_an.getvalue() accounting_month = out_luna.getvalue() # Obține rezultatele din cursor ref_cursor = result_cursor.getvalue() rows = ref_cursor.fetchall() ref_cursor.close() # Pentru count total, executăm alt bloc PL/SQL count_plsql = f""" DECLARE v_an NUMBER; v_luna NUMBER; BEGIN -- Obține anul și luna din parametri sau calendar {period_select} {schema}.PACK_SESIUNE.SETAN(v_an); {schema}.PACK_SESIUNE.SETLUNA(v_luna); SELECT COUNT(*) INTO :total_count FROM ({base_select}) sub{where_clause}; END; """ total_count_var = cursor.var(int) count_params = {'total_count': total_count_var} if use_param_period: count_params['param_an'] = filter_params.an count_params['param_luna'] = filter_params.luna cursor.execute(count_plsql, count_params) total_count = total_count_var.getvalue() # Query pentru TOTALURI din TOATE înregistrările filtrate (nu doar pagina curentă) # sold_precedent = suma sold pentru rânduri cu dataact IS NULL # total_incasari = suma incasari pentru rânduri cu dataact IS NOT NULL # total_plati = suma plati pentru rânduri cu dataact IS NOT NULL # Notă: where_clause poate fi gol sau poate conține "WHERE ..." # Dacă e gol, adăugăm WHERE; dacă nu, adăugăm AND dataact_null_cond = " AND dataact IS NULL" if where_clause else " WHERE dataact IS NULL" dataact_not_null_cond = " AND dataact IS NOT NULL" if where_clause else " WHERE dataact IS NOT NULL" totals_plsql = f""" DECLARE v_an NUMBER; v_luna NUMBER; BEGIN -- Obține anul și luna din parametri sau calendar {period_select} {schema}.PACK_SESIUNE.SETAN(v_an); {schema}.PACK_SESIUNE.SETLUNA(v_luna); -- Sold precedent: suma sold pentru rânduri fără dată (opening balance) SELECT NVL(SUM(sold), 0) INTO :sold_precedent_all FROM ({base_select}) sub{where_clause}{dataact_null_cond}; -- Total încasări: suma incasari pentru rânduri cu dată (transactions) SELECT NVL(SUM(incasari), 0) INTO :total_incasari_all FROM ({base_select}) sub{where_clause}{dataact_not_null_cond}; -- Total plăți: suma plati pentru rânduri cu dată (transactions) SELECT NVL(SUM(plati), 0) INTO :total_plati_all FROM ({base_select}) sub{where_clause}{dataact_not_null_cond}; END; """ sold_precedent_all_var = cursor.var(oracledb.NUMBER) total_incasari_all_var = cursor.var(oracledb.NUMBER) total_plati_all_var = cursor.var(oracledb.NUMBER) totals_params = { 'sold_precedent_all': sold_precedent_all_var, 'total_incasari_all': total_incasari_all_var, 'total_plati_all': total_plati_all_var } if use_param_period: totals_params['param_an'] = filter_params.an totals_params['param_luna'] = filter_params.luna cursor.execute(totals_plsql, totals_params) sold_precedent_all = Decimal(str(sold_precedent_all_var.getvalue() or 0)) total_incasari_all = Decimal(str(total_incasari_all_var.getvalue() or 0)) total_plati_all = Decimal(str(total_plati_all_var.getvalue() or 0)) sold_final_all = sold_precedent_all + total_incasari_all - total_plati_all # Procesare rezultate registers = [] total_incasari = Decimal('0.00') total_plati = Decimal('0.00') for row in rows: # Coloane: nume, nract, dataact, bancasa, incasari, plati, sold, valuta, tip_registru, explicatia, rn # row[0-9] = date, row[10] = rn (ROW_NUMBER la final) register_data = BankCashRegister( nume=row[0] or '', nract=row[1], dataact=row[2], nume_cont_bancar=row[3] or '', incasari=Decimal(str(row[4] or 0)), plati=Decimal(str(row[5] or 0)), sold=Decimal(str(row[6] or 0)), valuta=row[7], tip_registru=row[8], explicatia=row[9] or '' ) registers.append(register_data) total_incasari += register_data.incasari total_plati += register_data.plati logger.info(f"Treasury query for company {company_id}, type={filter_params.register_type}: {len(registers)} records, total={total_count}") return RegisterListResponse( registers=registers, total_count=total_count, filtered_count=len(registers), total_incasari=total_incasari, total_plati=total_plati, page=filter_params.page, page_size=filter_params.page_size, has_more=len(registers) == filter_params.page_size, accounting_period=AccountingPeriod(an=accounting_year, luna=accounting_month), # Totaluri din TOATE înregistrările filtrate sold_precedent_all=sold_precedent_all, total_incasari_all=total_incasari_all, total_plati_all=total_plati_all, sold_final_all=sold_final_all ) @staticmethod @cached(cache_type='treasury', key_params=['company_id', 'register_type']) async def get_bank_cash_accounts(company_id: int, register_type: str) -> List[str]: """ Obține lista distinctă de conturi bancă/casă (bancasa) pentru dropdown. Cached pentru performanță. IMPORTANT: Trebuie să setăm contextul PACK_SESIUNE înainte de a accesa vbancasa views! """ schema = await TreasuryService._get_schema(company_id) # Map register_type to view view_map = { 'BANCA_LEI': f'{schema}.vbancasa_5121_cum', 'BANCA_VALUTA': f'{schema}.vbancasa_5124_cum', 'CASA_LEI': f'{schema}.vbancasa_5311_cum', 'CASA_VALUTA': f'{schema}.vbancasa_5314_cum' } if register_type not in view_map: return [] view_name = view_map[register_type] async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # PL/SQL block to set session context and get accounts plsql_block = f""" DECLARE v_an NUMBER; v_luna NUMBER; BEGIN -- Get current year and month from calendar SELECT anul, luna INTO v_an, v_luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar); -- Set session context (REQUIRED before accessing vbancasa* views) {schema}.PACK_SESIUNE.SETAN(v_an); {schema}.PACK_SESIUNE.SETLUNA(v_luna); -- Return accounts via cursor OPEN :result_cursor FOR SELECT DISTINCT bancasa FROM {view_name} WHERE bancasa IS NOT NULL ORDER BY bancasa; END; """ result_cursor = cursor.var(oracledb.CURSOR) cursor.execute(plsql_block, {'result_cursor': result_cursor}) ref_cursor = result_cursor.getvalue() rows = ref_cursor.fetchall() ref_cursor.close() accounts = [row[0] for row in rows if row[0]] logger.info(f"Found {len(accounts)} bank/cash accounts for company {company_id}, type={register_type}") return accounts