Files
roa2web-service-auto/reports-app/backend/app/routers/trial_balance.py
Marius Mutu 0b00b66ed5 feat: Add Trial Balance (Balanță de Verificare) feature
Comprehensive implementation of Trial Balance page with filtering,
pagination, and sorting capabilities.

Backend Changes:
- Added Pydantic models for Trial Balance (trial_balance.py)
  - TrialBalanceItem: Individual balance record
  - TrialBalanceFilters: Filter parameters
  - TrialBalancePagination: Pagination metadata
  - TrialBalanceResponse: Complete API response
- Created FastAPI router (/api/trial-balance) with:
  - Filtering by account number (cont) and description (denumire)
  - Pagination support (configurable page size)
  - Sorting on all columns (ascendent/descendent)
  - Company-based access control via JWT
  - Query against Oracle VBAL table
- Registered router in main.py

Frontend Changes:
- Created Pinia store (trialBalanceStore.js) with:
  - State management for trial balance data
  - Filters (luna, an, cont, denumire)
  - Pagination controls
  - Sorting functionality
  - Error handling and loading states
- Built TrialBalanceView.vue component featuring:
  - PrimeVue DataTable with responsive design
  - Period display (month/year)
  - Dual input filters (account number + description)
  - Debounced search (500ms)
  - Clear filters functionality
  - Formatted currency display (Romanian locale)
  - Balance columns (Debit/Credit) for:
    - Sold Precedent (Previous Balance)
    - Rulaj Lunar (Monthly Movement)
    - Sold Final (Final Balance)
  - Loading spinner and empty state
  - Mobile-friendly responsive layout
- Added route: /trial-balance with auth guard
- Added menu item in HamburgerMenu (Navigation section)
  - Icon: pi-calculator
  - Label: "Balanță de Verificare"

Technical Details:
- Follows established CSS architecture (no :deep(), uses design tokens)
- Consistent with InvoicesView patterns
- Implements proper error handling
- Uses Oracle NVL for null value handling
- ROW_NUMBER pagination for Oracle compatibility

Testing: Manual testing required (Phase 5)

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

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

203 lines
7.6 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)
valid_sort_columns = ['CONT', 'DCONT', 'SD_PREC', 'SC_PREC', 'RD_LUNA',
'RC_LUNA', 'SD_FINAL', 'SC_FINAL']
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
base_query = f"""
SELECT
CONT,
DCONT,
NVL(SD_PREC, 0) as SD_PREC,
NVL(SC_PREC, 0) as SC_PREC,
NVL(RD_LUNA, 0) as RD_LUNA,
NVL(RC_LUNA, 0) as RC_LUNA,
NVL(SD_FINAL, 0) as SD_FINAL,
NVL(SC_FINAL, 0) as SC_FINAL
FROM {schema}.VBAL
WHERE COD_FIRMA = :cod_firma
AND AN = :an
AND LUNA = :luna
"""
params = {
'cod_firma': company,
'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(DCONT) 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
items = []
for row in rows:
item = TrialBalanceItem(
cont=row[0] or '',
dcont=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)}"
)