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>
This commit is contained in:
@@ -25,7 +25,7 @@ from auth.middleware import AuthenticationMiddleware
|
||||
# from auth.routes import create_auth_router # Fixed inline
|
||||
|
||||
# Import routere locale
|
||||
from app.routers import invoices, dashboard, treasury, companies, telegram, cache
|
||||
from app.routers import invoices, dashboard, treasury, companies, telegram, cache, trial_balance
|
||||
|
||||
# Auth endpoints pentru test
|
||||
from fastapi import APIRouter, HTTPException
|
||||
@@ -317,6 +317,7 @@ app.include_router(dashboard.router, prefix="/api/dashboard", tags=["dashboard"]
|
||||
app.include_router(treasury.router, prefix="/api/treasury", tags=["treasury"])
|
||||
app.include_router(telegram.router, prefix="/api/telegram", tags=["telegram"])
|
||||
app.include_router(cache.router, prefix="/api", tags=["cache"])
|
||||
app.include_router(trial_balance.router, prefix="/api/trial-balance", tags=["trial-balance"])
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
|
||||
84
reports-app/backend/app/models/trial_balance.py
Normal file
84
reports-app/backend/app/models/trial_balance.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Pydantic models for Trial Balance (Balanță de Verificare)
|
||||
Maps to Oracle VBAL table
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from decimal import Decimal
|
||||
|
||||
class TrialBalanceItem(BaseModel):
|
||||
"""
|
||||
Individual trial balance record from VBAL table
|
||||
"""
|
||||
cont: str = Field(description="Număr cont contabil")
|
||||
dcont: str = Field(description="Denumire cont")
|
||||
sold_precedent_debit: Decimal = Field(description="Sold precedent debit", decimal_places=2)
|
||||
sold_precedent_credit: Decimal = Field(description="Sold precedent credit", decimal_places=2)
|
||||
rulaj_lunar_debit: Decimal = Field(description="Rulaj lunar debit", decimal_places=2)
|
||||
rulaj_lunar_credit: Decimal = Field(description="Rulaj lunar credit", decimal_places=2)
|
||||
sold_final_debit: Decimal = Field(description="Sold final debit", decimal_places=2)
|
||||
sold_final_credit: Decimal = Field(description="Sold final credit", decimal_places=2)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TrialBalanceFilters(BaseModel):
|
||||
"""
|
||||
Filters applied to trial balance data
|
||||
"""
|
||||
luna: int = Field(description="Luna (1-12)")
|
||||
an: int = Field(description="An")
|
||||
cont_filter: Optional[str] = Field(default=None, description="Filtru număr cont (partial match)")
|
||||
denumire_filter: Optional[str] = Field(default=None, description="Filtru denumire cont (partial match, case-insensitive)")
|
||||
|
||||
|
||||
class TrialBalancePagination(BaseModel):
|
||||
"""
|
||||
Pagination metadata
|
||||
"""
|
||||
total_items: int = Field(description="Total number of items")
|
||||
total_pages: int = Field(description="Total number of pages")
|
||||
current_page: int = Field(description="Current page number")
|
||||
page_size: int = Field(description="Items per page")
|
||||
|
||||
|
||||
class TrialBalanceResponse(BaseModel):
|
||||
"""
|
||||
Complete response for trial balance endpoint
|
||||
"""
|
||||
success: bool = Field(default=True, description="Request success status")
|
||||
data: dict = Field(description="Trial balance data with items, pagination, and filters")
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"success": True,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"cont": "4111",
|
||||
"dcont": "Furnizori interni",
|
||||
"sold_precedent_debit": 0.00,
|
||||
"sold_precedent_credit": 15000.00,
|
||||
"rulaj_lunar_debit": 5000.00,
|
||||
"rulaj_lunar_credit": 8000.00,
|
||||
"sold_final_debit": 0.00,
|
||||
"sold_final_credit": 18000.00
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total_items": 150,
|
||||
"total_pages": 3,
|
||||
"current_page": 1,
|
||||
"page_size": 50
|
||||
},
|
||||
"filters_applied": {
|
||||
"luna": 11,
|
||||
"an": 2025,
|
||||
"cont_filter": None,
|
||||
"denumire_filter": "furnizori"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
202
reports-app/backend/app/routers/trial_balance.py
Normal file
202
reports-app/backend/app/routers/trial_balance.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
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)}"
|
||||
)
|
||||
Reference in New Issue
Block a user