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>
203 lines
7.6 KiB
Python
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)}"
|
|
)
|