diff --git a/reports-app/backend/app/main.py b/reports-app/backend/app/main.py index 51f39c8..c9f261f 100644 --- a/reports-app/backend/app/main.py +++ b/reports-app/backend/app/main.py @@ -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(): diff --git a/reports-app/backend/app/models/trial_balance.py b/reports-app/backend/app/models/trial_balance.py new file mode 100644 index 0000000..a68e671 --- /dev/null +++ b/reports-app/backend/app/models/trial_balance.py @@ -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" + } + } + } + } diff --git a/reports-app/backend/app/routers/trial_balance.py b/reports-app/backend/app/routers/trial_balance.py new file mode 100644 index 0000000..4f735c8 --- /dev/null +++ b/reports-app/backend/app/routers/trial_balance.py @@ -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)}" + ) diff --git a/reports-app/frontend/src/components/layout/HamburgerMenu.vue b/reports-app/frontend/src/components/layout/HamburgerMenu.vue index d7cf7fb..ad7c3de 100644 --- a/reports-app/frontend/src/components/layout/HamburgerMenu.vue +++ b/reports-app/frontend/src/components/layout/HamburgerMenu.vue @@ -46,6 +46,17 @@ Bank & Cash +
+ {{ currentPeriodText }} - {{ companyStore.selectedCompany?.firma || "Selectați companie" }} +
++ Selectați o companie pentru a vizualiza balanța de verificare: +
+Nu au fost găsite date pentru perioada selectată
+Se încarcă balanța de verificare...
+