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>
206 lines
8.0 KiB
Python
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)}"
|
|
)
|