Files
roa2web-service-auto/reports-app/backend/app/routers/trial_balance.py
Marius Mutu 6c373c609e fix: Update Trial Balance to use real VBAL VIEW structure
After database verification, VBAL is a VIEW (not a table) that exists
in each company schema with a different structure than initially assumed.

Backend Changes:
- Updated models (trial_balance.py):
  - Changed column names to match real VBAL VIEW
  - CONT (account number)
  - DENUMIRE (account description, not DCONT)
  - PRECDEB/PRECCRED (previous balance, not SD_PREC/SC_PREC)
  - RULDEB/RULCRED (monthly movement, not RD_LUNA/RC_LUNA)
  - SOLDDEB/SOLDCRED (final balance, not SD_FINAL/SC_FINAL)
  - Made DENUMIRE optional (can be NULL in VIEW)

- Updated router (trial_balance.py):
  - Removed COD_FIRMA filter (not in VIEW)
  - Query now uses: {schema}.VBAL WHERE AN = :an AND LUNA = :luna
  - Fixed column names in SELECT (DENUMIRE instead of DCONT)
  - Updated sort columns validation
  - Fixed result processing to match new column order

Frontend Changes:
- Updated TrialBalanceView.vue:
  - Changed field from 'dcont' to 'denumire' in DataTable column

Database Verification:
- VBAL VIEW confirmed in ROMFAST schema (24,217 records)
- Current data available up to November 2025
- Structure verified with 22 columns
- VIEW exists in all company schemas

Testing Notes:
- Backend endpoint ready for testing
- Frontend field names now match API response
- Ready for manual testing with real company data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 00:57:14 +02:00

206 lines
8.0 KiB
Python

"""
API Router for Trial Balance (Balanță de Verificare)
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from typing import Optional
from datetime import date
from decimal import Decimal
import sys
import os
import math
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
from auth.dependencies import get_current_user
from auth.models import CurrentUser
from database.oracle_pool import oracle_pool
from ..models.trial_balance import (
TrialBalanceItem,
TrialBalanceFilters,
TrialBalancePagination,
TrialBalanceResponse
)
router = APIRouter()
@router.get("/", response_model=TrialBalanceResponse)
async def get_trial_balance(
company: str = Query(description="Codul firmei (ID)"),
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna (1-12), default: luna curentă"),
an: Optional[int] = Query(None, ge=2000, le=2100, description="An, default: anul curent"),
cont_filter: Optional[str] = Query(None, description="Filtru număr cont (ex: '512', '4111')"),
denumire_filter: Optional[str] = Query(None, description="Filtru denumire cont (partial match, case-insensitive)"),
sort_by: str = Query("CONT", description="Coloană pentru sortare"),
sort_order: str = Query("asc", description="Ordinea sortării (asc | desc)"),
page: int = Query(1, ge=1, description="Pagina"),
page_size: int = Query(50, ge=1, le=500, description="Mărimea paginii"),
current_user: CurrentUser = Depends(get_current_user)
):
"""
Obține balanța de verificare sintetică pentru o firmă
- Necesită autentificare JWT
- Utilizatorul trebuie să aibă acces la firma specificată
- Suportă filtrare după cont și denumire
- Suportă paginare și sortare
"""
try:
# Verifică dacă utilizatorul are acces la firma specificată
if company not in current_user.companies:
raise HTTPException(
status_code=403,
detail=f"Nu aveți acces la firma {company}"
)
# Setează valorile implicite pentru lună și an (luna și anul curent)
current_date = date.today()
if luna is None:
luna = current_date.month
if an is None:
an = current_date.year
# Validează sort_order
if sort_order.lower() not in ['asc', 'desc']:
sort_order = 'asc'
# Validează sort_by (previne SQL injection)
# Real column names from VBAL VIEW
valid_sort_columns = ['CONT', 'DENUMIRE', 'PRECDEB', 'PRECCRED',
'RULDEB', 'RULCRED', 'SOLDDEB', 'SOLDCRED']
if sort_by.upper() not in valid_sort_columns:
sort_by = 'CONT'
# Obține datele din Oracle
async with oracle_pool.get_connection() as connection:
with connection.cursor() as cursor:
# Obține schema din v_nom_firme
company_id = int(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 HTTPException(
status_code=404,
detail=f"Schema nu a fost găsită pentru firma {company}"
)
schema = schema_result[0]
# Construiește query-ul de bază pentru VBAL VIEW
# VBAL este un VIEW în fiecare schemă de companie
# Structura reală: CONT, DENUMIRE, AN, LUNA, PRECDEB, PRECCRED, RULDEB, RULCRED, SOLDDEB, SOLDCRED
base_query = f"""
SELECT
CONT,
NVL(DENUMIRE, '') as DENUMIRE,
NVL(PRECDEB, 0) as PRECDEB,
NVL(PRECCRED, 0) as PRECCRED,
NVL(RULDEB, 0) as RULDEB,
NVL(RULCRED, 0) as RULCRED,
NVL(SOLDDEB, 0) as SOLDDEB,
NVL(SOLDCRED, 0) as SOLDCRED
FROM {schema}.VBAL
WHERE AN = :an
AND LUNA = :luna
"""
params = {
'an': an,
'luna': luna
}
# Adaugă filtre dinamice
if cont_filter:
base_query += " AND CONT LIKE :cont_filter"
params['cont_filter'] = f"{cont_filter}%"
if denumire_filter:
base_query += " AND UPPER(DENUMIRE) LIKE UPPER(:denumire_filter)"
params['denumire_filter'] = f"%{denumire_filter}%"
# Count total pentru paginare
count_query = f"SELECT COUNT(*) FROM ({base_query})"
cursor.execute(count_query, params)
total_count = cursor.fetchone()[0]
# Adaugă sortare
base_query += f" ORDER BY {sort_by.upper()} {sort_order.upper()}"
# Paginare Oracle (ROW_NUMBER în loc de ROWNUM pentru a funcționa cu ORDER BY)
offset = (page - 1) * page_size
limit = offset + page_size
paginated_query = f"""
SELECT * FROM (
SELECT a.*, ROWNUM rnum FROM (
{base_query}
) a WHERE ROWNUM <= :limit
) WHERE rnum > :offset
"""
params['offset'] = offset
params['limit'] = limit
cursor.execute(paginated_query, params)
rows = cursor.fetchall()
# Procesează rezultatele
# Index columns: CONT(0), DENUMIRE(1), PRECDEB(2), PRECCRED(3),
# RULDEB(4), RULCRED(5), SOLDDEB(6), SOLDCRED(7), rnum(8)
items = []
for row in rows:
item = TrialBalanceItem(
cont=row[0] or '',
denumire=row[1] or '',
sold_precedent_debit=Decimal(str(row[2] or 0)),
sold_precedent_credit=Decimal(str(row[3] or 0)),
rulaj_lunar_debit=Decimal(str(row[4] or 0)),
rulaj_lunar_credit=Decimal(str(row[5] or 0)),
sold_final_debit=Decimal(str(row[6] or 0)),
sold_final_credit=Decimal(str(row[7] or 0))
)
items.append(item.dict())
# Calculează paginarea
total_pages = math.ceil(total_count / page_size) if page_size > 0 else 0
# Construiește răspunsul
response_data = {
"items": items,
"pagination": {
"total_items": total_count,
"total_pages": total_pages,
"current_page": page,
"page_size": page_size
},
"filters_applied": {
"luna": luna,
"an": an,
"cont_filter": cont_filter,
"denumire_filter": denumire_filter
}
}
return TrialBalanceResponse(
success=True,
data=response_data
)
except HTTPException:
# Re-raise HTTP exceptions
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
# Log the error for debugging
import logging
logging.error(f"Error fetching trial balance: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Eroare la obținerea balanței de verificare: {str(e)}"
)