fix telegram
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"""Reports module router factory."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
def create_reports_router() -> APIRouter:
|
||||
"""
|
||||
Create and configure Reports module router.
|
||||
|
||||
Includes all report-related endpoints:
|
||||
- /invoices - Invoice management
|
||||
- /dashboard - Dashboard and metrics
|
||||
- /treasury - Treasury operations
|
||||
- /trial-balance - Trial balance reports
|
||||
- /cache - Cache management
|
||||
|
||||
Returns:
|
||||
APIRouter: Configured router for reports module
|
||||
"""
|
||||
router = APIRouter()
|
||||
|
||||
# Import routers here to avoid circular imports
|
||||
from .invoices import router as invoices_router
|
||||
from .dashboard import router as dashboard_router
|
||||
from .treasury import router as treasury_router
|
||||
from .trial_balance import router as trial_balance_router
|
||||
from .cache import router as cache_router
|
||||
|
||||
# Include all sub-routers (no prefix - already prefixed in main.py with /api/reports)
|
||||
router.include_router(invoices_router, prefix="/invoices", tags=["reports-invoices"])
|
||||
router.include_router(dashboard_router, prefix="/dashboard", tags=["reports-dashboard"])
|
||||
router.include_router(treasury_router, prefix="/treasury", tags=["reports-treasury"])
|
||||
router.include_router(trial_balance_router, prefix="/trial-balance", tags=["reports-trial-balance"])
|
||||
router.include_router(cache_router, prefix="/cache", tags=["reports-cache"])
|
||||
|
||||
return router
|
||||
@@ -0,0 +1,398 @@
|
||||
"""
|
||||
API Router pentru managementul cache-ului
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict, Any
|
||||
# import sys # Removed - no longer needed
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
from shared.auth.dependencies import get_current_user
|
||||
from shared.auth.models import CurrentUser
|
||||
from ..cache import get_cache, get_event_monitor, toggle_event_monitor
|
||||
|
||||
router = APIRouter(tags=["cache"])
|
||||
|
||||
|
||||
# Pydantic Models
|
||||
|
||||
class CacheStatsResponse(BaseModel):
|
||||
"""Răspuns statistici cache"""
|
||||
enabled: bool
|
||||
global_enabled: bool
|
||||
user_enabled: bool
|
||||
cache_type: str
|
||||
hit_rate: float
|
||||
total_hits: int
|
||||
total_misses: int
|
||||
queries_saved: Dict[str, int]
|
||||
response_times: Dict[str, Dict[str, Any]]
|
||||
cache_size: Dict[str, int]
|
||||
auto_invalidate: bool
|
||||
last_cleanup: Optional[str] = None
|
||||
|
||||
|
||||
class InvalidateCacheRequest(BaseModel):
|
||||
"""Request pentru invalidare cache"""
|
||||
company_id: Optional[int] = None
|
||||
cache_type: Optional[str] = None
|
||||
|
||||
|
||||
class ToggleUserCacheRequest(BaseModel):
|
||||
"""Request pentru toggle cache per-user"""
|
||||
enabled: bool
|
||||
|
||||
|
||||
class ToggleGlobalCacheRequest(BaseModel):
|
||||
"""Request pentru toggle cache global"""
|
||||
enabled: bool
|
||||
|
||||
|
||||
class ToggleAutoInvalidateRequest(BaseModel):
|
||||
"""Request pentru toggle auto-invalidation"""
|
||||
enabled: bool
|
||||
|
||||
|
||||
# Helper Functions
|
||||
|
||||
async def _calculate_cache_stats() -> Dict[str, Any]:
|
||||
"""Calculate comprehensive cache statistics"""
|
||||
cache = get_cache()
|
||||
if not cache:
|
||||
raise HTTPException(status_code=503, detail="Cache not initialized")
|
||||
|
||||
# Get basic cache stats
|
||||
stats = await cache.get_stats()
|
||||
|
||||
# Calculate hit rate
|
||||
memory_stats = stats.get('memory', {})
|
||||
total_hits = memory_stats.get('hits', 0)
|
||||
total_misses = memory_stats.get('misses', 0)
|
||||
total_requests = total_hits + total_misses
|
||||
hit_rate = (total_hits / total_requests * 100) if total_requests > 0 else 0
|
||||
|
||||
# Calculate queries saved (from performance_log)
|
||||
queries_saved = await _calculate_queries_saved(cache)
|
||||
|
||||
# Calculate response times per cache type
|
||||
response_times = await _calculate_response_times(cache)
|
||||
|
||||
# Get cache sizes
|
||||
cache_size = {
|
||||
'memory': memory_stats.get('size', 0),
|
||||
'sqlite': stats.get('sqlite', {}).get('active_entries', 0)
|
||||
}
|
||||
|
||||
# Get event monitor status
|
||||
monitor = get_event_monitor()
|
||||
auto_invalidate = monitor.running if monitor else False
|
||||
|
||||
return {
|
||||
'enabled': cache.config.enabled,
|
||||
'global_enabled': cache.config.enabled,
|
||||
'cache_type': cache.config.cache_type,
|
||||
'hit_rate': round(hit_rate, 1),
|
||||
'total_hits': total_hits,
|
||||
'total_misses': total_misses,
|
||||
'queries_saved': queries_saved,
|
||||
'response_times': response_times,
|
||||
'cache_size': cache_size,
|
||||
'auto_invalidate': auto_invalidate,
|
||||
'last_cleanup': None # TODO: track last cleanup time
|
||||
}
|
||||
|
||||
|
||||
async def _calculate_queries_saved(cache) -> Dict[str, int]:
|
||||
"""Calculate queries saved by time period"""
|
||||
import aiosqlite
|
||||
|
||||
try:
|
||||
async with aiosqlite.connect(cache.sqlite.db_path) as db:
|
||||
now = time.time()
|
||||
today_start = now - 86400 # 24 hours
|
||||
week_start = now - 604800 # 7 days
|
||||
|
||||
# Today
|
||||
async with db.execute("""
|
||||
SELECT COUNT(*) FROM performance_log
|
||||
WHERE cache_hit = 1 AND timestamp >= ?
|
||||
""", (today_start,)) as cursor:
|
||||
today = (await cursor.fetchone())[0]
|
||||
|
||||
# This week
|
||||
async with db.execute("""
|
||||
SELECT COUNT(*) FROM performance_log
|
||||
WHERE cache_hit = 1 AND timestamp >= ?
|
||||
""", (week_start,)) as cursor:
|
||||
week = (await cursor.fetchone())[0]
|
||||
|
||||
# All time
|
||||
async with db.execute("""
|
||||
SELECT COUNT(*) FROM performance_log
|
||||
WHERE cache_hit = 1
|
||||
""") as cursor:
|
||||
total = (await cursor.fetchone())[0]
|
||||
|
||||
return {
|
||||
'today': today,
|
||||
'week': week,
|
||||
'total': total
|
||||
}
|
||||
except Exception as e:
|
||||
return {'today': 0, 'week': 0, 'total': 0}
|
||||
|
||||
|
||||
async def _calculate_response_times(cache) -> Dict[str, Dict[str, Any]]:
|
||||
"""Calculate average response times per cache type"""
|
||||
import aiosqlite
|
||||
|
||||
try:
|
||||
async with aiosqlite.connect(cache.sqlite.db_path) as db:
|
||||
# Get average times per cache type
|
||||
async with db.execute("""
|
||||
SELECT
|
||||
cache_type,
|
||||
AVG(CASE WHEN cache_hit = 1 THEN response_time_ms ELSE NULL END) as avg_cached,
|
||||
AVG(CASE WHEN cache_hit = 0 THEN response_time_ms ELSE NULL END) as avg_oracle
|
||||
FROM performance_log
|
||||
WHERE timestamp >= ?
|
||||
GROUP BY cache_type
|
||||
""", (time.time() - 86400,)) as cursor: # Last 24 hours
|
||||
results = await cursor.fetchall()
|
||||
|
||||
response_times = {}
|
||||
for row in results:
|
||||
cache_type, avg_cached, avg_oracle = row
|
||||
if avg_cached and avg_oracle:
|
||||
improvement = int((avg_oracle - avg_cached) / avg_oracle * 100)
|
||||
response_times[cache_type] = {
|
||||
'cached': int(avg_cached),
|
||||
'oracle': int(avg_oracle),
|
||||
'improvement': improvement
|
||||
}
|
||||
|
||||
return response_times
|
||||
except Exception as e:
|
||||
return {}
|
||||
|
||||
|
||||
# API Endpoints
|
||||
|
||||
@router.get("/stats", response_model=CacheStatsResponse)
|
||||
async def get_cache_stats(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține statistici complete cache
|
||||
|
||||
Returns:
|
||||
- Hit rate, queries saved, response times
|
||||
- Cache sizes (memory + SQLite)
|
||||
- Auto-invalidation status
|
||||
- Per-user cache setting
|
||||
"""
|
||||
try:
|
||||
cache = get_cache()
|
||||
if not cache:
|
||||
raise HTTPException(status_code=503, detail="Cache not initialized")
|
||||
|
||||
# Get base stats
|
||||
stats = await _calculate_cache_stats()
|
||||
|
||||
# Add user-specific setting
|
||||
user_enabled = await cache.is_enabled_for_user(current_user.username)
|
||||
stats['user_enabled'] = user_enabled
|
||||
|
||||
return CacheStatsResponse(**stats)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error retrieving cache stats: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/invalidate")
|
||||
async def invalidate_cache(
|
||||
request: InvalidateCacheRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Invalidează cache
|
||||
|
||||
Args:
|
||||
company_id: Opțional - invalidează doar pentru această companie
|
||||
cache_type: Opțional - invalidează doar acest tip de cache
|
||||
|
||||
Returns:
|
||||
Message de confirmare
|
||||
"""
|
||||
try:
|
||||
cache = get_cache()
|
||||
if not cache:
|
||||
raise HTTPException(status_code=503, detail="Cache not initialized")
|
||||
|
||||
await cache.invalidate(
|
||||
company_id=request.company_id,
|
||||
cache_type=request.cache_type
|
||||
)
|
||||
|
||||
if request.company_id and request.cache_type:
|
||||
message = f"Cache invalidated for company {request.company_id}, type {request.cache_type}"
|
||||
elif request.company_id:
|
||||
message = f"Cache invalidated for company {request.company_id}"
|
||||
elif request.cache_type:
|
||||
message = f"Cache invalidated for type {request.cache_type}"
|
||||
else:
|
||||
message = "All cache invalidated"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": message,
|
||||
"invalidated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error invalidating cache: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/toggle-user")
|
||||
async def toggle_user_cache(
|
||||
request: ToggleUserCacheRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Toggle cache per-user
|
||||
|
||||
Permite utilizatorului să activeze/dezactiveze cache-ul pentru el
|
||||
Folosit pentru A/B testing și comparații de performanță
|
||||
|
||||
Args:
|
||||
enabled: True pentru activare, False pentru dezactivare
|
||||
|
||||
Returns:
|
||||
Noul status
|
||||
"""
|
||||
try:
|
||||
cache = get_cache()
|
||||
if not cache:
|
||||
raise HTTPException(status_code=503, detail="Cache not initialized")
|
||||
|
||||
await cache.set_user_cache_enabled(current_user.username, request.enabled)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"username": current_user.username,
|
||||
"cache_enabled": request.enabled,
|
||||
"message": f"Cache {'enabled' if request.enabled else 'disabled'} for user {current_user.username}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error toggling user cache: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/toggle-global")
|
||||
async def toggle_global_cache(
|
||||
request: ToggleGlobalCacheRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Toggle cache global (ADMIN only)
|
||||
|
||||
Activează/dezactivează cache-ul la nivel global pentru toți utilizatorii
|
||||
|
||||
Args:
|
||||
enabled: True pentru activare, False pentru dezactivare
|
||||
|
||||
Returns:
|
||||
Noul status global
|
||||
"""
|
||||
try:
|
||||
# TODO: Add admin permission check
|
||||
# For now, allow any authenticated user
|
||||
|
||||
cache = get_cache()
|
||||
if not cache:
|
||||
raise HTTPException(status_code=503, detail="Cache not initialized")
|
||||
|
||||
# Update config (NOTE: This is runtime only, .env needs manual update)
|
||||
cache.config.enabled = request.enabled
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"global_enabled": request.enabled,
|
||||
"message": f"Cache {'enabled' if request.enabled else 'disabled'} globally",
|
||||
"note": "This change is runtime only. Update .env CACHE_ENABLED for persistence."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error toggling global cache: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/toggle-auto-invalidate")
|
||||
async def toggle_auto_invalidation(
|
||||
request: ToggleAutoInvalidateRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Toggle auto-invalidation monitoring
|
||||
|
||||
Activează/dezactivează monitorizarea automată a {schema}.act
|
||||
pentru invalidarea cache-ului când se detectează modificări
|
||||
|
||||
Args:
|
||||
enabled: True pentru activare, False pentru dezactivare
|
||||
|
||||
Returns:
|
||||
Noul status auto-invalidation
|
||||
"""
|
||||
try:
|
||||
# TODO: Add admin permission check
|
||||
# For now, allow any authenticated user
|
||||
|
||||
await toggle_event_monitor(request.enabled)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"auto_invalidate_enabled": request.enabled,
|
||||
"message": f"Auto-invalidation {'enabled' if request.enabled else 'disabled'}",
|
||||
"note": "Monitors max(id_act) in {schema}.act tables for changes"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error toggling auto-invalidation: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def cache_health():
|
||||
"""
|
||||
Health check pentru sistemul de cache
|
||||
|
||||
Returns:
|
||||
Status cache, mărime, și uptime
|
||||
"""
|
||||
try:
|
||||
cache = get_cache()
|
||||
if not cache:
|
||||
return {
|
||||
"status": "not_initialized",
|
||||
"enabled": False
|
||||
}
|
||||
|
||||
stats = await cache.get_stats()
|
||||
monitor = get_event_monitor()
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"enabled": cache.config.enabled,
|
||||
"cache_type": cache.config.cache_type,
|
||||
"memory_size": stats.get('memory', {}).get('size', 0),
|
||||
"sqlite_size": stats.get('sqlite', {}).get('active_entries', 0),
|
||||
"auto_invalidate_running": monitor.running if monitor else False
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from typing import Optional
|
||||
import os
|
||||
|
||||
from shared.auth.dependencies import get_current_user
|
||||
from shared.auth.models import CurrentUser
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from ..models.dashboard import DashboardSummary, TrendsResponse, TrendData
|
||||
from ..models.financial_indicators import FinancialIndicatorsResponse
|
||||
from ..services.dashboard_service import DashboardService
|
||||
from ..services.financial_indicators_service import FinancialIndicatorsService
|
||||
from ..cache.decorators import cached
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_dashboard_summary(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține toate datele pentru dashboard într-un singur apel
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează statistici clienți/furnizori și trezorerie
|
||||
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
|
||||
- Suportă filtrare pe luna/an contabil (dacă nu sunt specificate, folosește ultima perioadă)
|
||||
"""
|
||||
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}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_complete_summary(company, current_user.username, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert Pydantic model to dict for JSON serialization
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot)
|
||||
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
|
||||
if include_metadata:
|
||||
cache_hit = getattr(request.state, 'cache_hit', False)
|
||||
response_time = getattr(request.state, 'response_time_ms', 0)
|
||||
cache_source = getattr(request.state, 'cache_source', None)
|
||||
result_dict['cache_hit'] = cache_hit
|
||||
result_dict['response_time_ms'] = response_time
|
||||
# Always include cache_source, even if None
|
||||
result_dict['cache_source'] = cache_source
|
||||
|
||||
return result_dict
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea datelor dashboard: {str(e)}")
|
||||
|
||||
@router.get("/trends", response_model=TrendsResponse)
|
||||
async def get_dashboard_trends(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
period: str = Query(default="30d", description="Perioada pentru trends: 7d, 30d, ytd, 12m"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
compare_previous: bool = Query(default=True, description="Compară cu perioada anterioară"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține trenduri pentru indicatorii principali (clienți/furnizori)
|
||||
|
||||
- period: "7d" (7 zile), "30d" (30 zile), "ytd" (year to date), "12m" (12 luni)
|
||||
- luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă)
|
||||
- compare_previous: dacă să compare cu perioada anterioară
|
||||
- Necesită autentificare JWT
|
||||
- Returnează date pentru grafice de trenduri
|
||||
"""
|
||||
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}")
|
||||
|
||||
# Validează perioada
|
||||
valid_periods = ["7d", "30d", "ytd", "12m"]
|
||||
if period not in valid_periods:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Perioadă nevalidă: {period}. Valori permise: {', '.join(valid_periods)}"
|
||||
)
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
# Obține datele de trenduri
|
||||
result = await DashboardService.get_trends(int(company), period, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot)
|
||||
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
|
||||
if include_metadata:
|
||||
cache_hit = getattr(request.state, 'cache_hit', False)
|
||||
response_time = getattr(request.state, 'response_time_ms', 0)
|
||||
cache_source = getattr(request.state, 'cache_source', None)
|
||||
result_dict['cache_hit'] = cache_hit
|
||||
result_dict['response_time_ms'] = response_time
|
||||
# Always include cache_source, even if None
|
||||
result_dict['cache_source'] = cache_source
|
||||
|
||||
# Return as TrendsResponse
|
||||
return TrendsResponse(**result_dict)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Value error in trends endpoint: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea trendurilor: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea trendurilor: {str(e)}")
|
||||
|
||||
@router.get("/detailed-data")
|
||||
async def get_detailed_data(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
data_type: str = Query(description="Tipul de date: clients, suppliers, treasury"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=25, ge=1, le=100),
|
||||
search: str = Query(default="", description="Termen de căutare"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține date detaliate pentru tabelele din dashboard
|
||||
"""
|
||||
logger.info(f"[ROUTER] detailed-data called: company={company}, data_type={data_type}")
|
||||
try:
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
logger.info(f"[ROUTER] Calling DashboardService.get_detailed_data")
|
||||
result = await DashboardService.get_detailed_data(
|
||||
company=company,
|
||||
data_type=data_type,
|
||||
luna=luna,
|
||||
an=an,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search,
|
||||
server_id=server_id
|
||||
)
|
||||
|
||||
logger.info(f"[ROUTER] Service returned: {len(result.get('data', []))} rows")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea datelor detaliate: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/performance")
|
||||
async def get_performance(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m|ytd|12m)$", description="Perioada pentru analiză"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează date performanță pentru perioada selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează grafice încasări vs plăți pentru perioada selectată
|
||||
- Calculează indicatori: rata încasării, cash conversion, working capital
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_performance_data(company, period, server_id=server_id)
|
||||
|
||||
# Convert to Chart.js compatible format
|
||||
return {
|
||||
"labels": result.get("labels", []),
|
||||
"datasets": [{
|
||||
"data": result.get("data", []),
|
||||
"label": result.get("label", "Performance"),
|
||||
"borderColor": result.get("borderColor", "#3B82F6"),
|
||||
"backgroundColor": result.get("backgroundColor", "rgba(59, 130, 246, 0.1)"),
|
||||
"tension": 0.4
|
||||
}]
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea datelor de performanță: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea datelor de performanță: {str(e)}")
|
||||
|
||||
@router.get("/cashflow")
|
||||
async def get_cashflow(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m)$", description="Perioada pentru previziune"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează previziune cash flow pentru perioada selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Analizează scadențele viitoare pentru calculul cash flow-ului
|
||||
- Identifică zilele critice cu deficit de cash
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_cashflow_forecast(company, period, server_id=server_id)
|
||||
|
||||
# Convert to Chart.js compatible format
|
||||
return {
|
||||
"labels": result.get("labels", []),
|
||||
"datasets": [{
|
||||
"data": result.get("data", []),
|
||||
"label": result.get("label", "Cash Flow"),
|
||||
"borderColor": result.get("borderColor", "#10B981"),
|
||||
"backgroundColor": result.get("backgroundColor", "rgba(16, 185, 129, 0.1)"),
|
||||
"tension": 0.4
|
||||
}]
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea previziunii cash flow: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea previziunii cash flow: {str(e)}")
|
||||
|
||||
@router.get("/maturity")
|
||||
async def get_maturity_analysis(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m|12m|all)$", description="Orizont de planificare pentru analiza scadențelor"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează analiza scadențelor pentru orizontul de planificare selectat
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Logică: Include TOATE restanțele + scadențele viitoare din perioada selectată
|
||||
- luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă)
|
||||
- Perioade disponibile:
|
||||
* 7d: Toate restanțele + scadențe următoarelor 7 zile
|
||||
* 1m: Toate restanțele + scadențe următoarelor 30 zile
|
||||
* 3m: Toate restanțele + scadențe următoarelor 90 zile
|
||||
* 6m: Toate restanțele + scadențe următoarelor 180 zile
|
||||
* 12m: Toate restanțele + scadențe următoarelor 365 zile
|
||||
* all: Toate soldurile (fără filtru)
|
||||
- Compară scadențele clienți vs furnizori
|
||||
- Calculează balanța și oferă recomandări
|
||||
- Returnează metadate cu statistici complete
|
||||
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_maturity_analysis(company, period, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot)
|
||||
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
|
||||
if include_metadata:
|
||||
cache_hit = getattr(request.state, 'cache_hit', False)
|
||||
response_time = getattr(request.state, 'response_time_ms', 0)
|
||||
cache_source = getattr(request.state, 'cache_source', None)
|
||||
result_dict['cache_hit'] = cache_hit
|
||||
result_dict['response_time_ms'] = response_time
|
||||
# Always include cache_source, even if None
|
||||
result_dict['cache_source'] = cache_source
|
||||
|
||||
return result_dict
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea analizei scadențelor: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea analizei scadențelor: {str(e)}")
|
||||
|
||||
@router.get("/monthly-flows")
|
||||
async def get_monthly_flows(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează fluxurile lunare pentru firma selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează date pentru analiza fluxurilor lunare
|
||||
- luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă)
|
||||
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
# Apelăm serviciul cu request pentru cache metadata
|
||||
result = await DashboardService.get_monthly_flows(company, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot / Dashboard)
|
||||
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
|
||||
if include_metadata:
|
||||
cache_hit = getattr(request.state, 'cache_hit', False)
|
||||
response_time = getattr(request.state, 'response_time_ms', 0)
|
||||
cache_source = getattr(request.state, 'cache_source', None)
|
||||
result_dict['cache_hit'] = cache_hit
|
||||
result_dict['response_time_ms'] = response_time
|
||||
result_dict['cache_source'] = cache_source
|
||||
|
||||
return result_dict
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea fluxurilor lunare: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea fluxurilor lunare: {str(e)}")
|
||||
|
||||
@router.get("/treasury-breakdown")
|
||||
async def get_treasury_breakdown(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează defalcarea trezoreriei pentru firma selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează distribuția soldurilor pe conturi și tipuri
|
||||
- luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă)
|
||||
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_treasury_breakdown(company, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot)
|
||||
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
|
||||
if include_metadata:
|
||||
cache_hit = getattr(request.state, 'cache_hit', False)
|
||||
response_time = getattr(request.state, 'response_time_ms', 0)
|
||||
cache_source = getattr(request.state, 'cache_source', None)
|
||||
result_dict['cache_hit'] = cache_hit
|
||||
result_dict['response_time_ms'] = response_time
|
||||
# Always include cache_source, even if None
|
||||
result_dict['cache_source'] = cache_source
|
||||
|
||||
return result_dict
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea defalcării trezoreriei: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea defalcării trezoreriei: {str(e)}")
|
||||
|
||||
@router.get("/net-balance-breakdown")
|
||||
async def get_net_balance_breakdown(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează defalcarea balanței nete pentru firma selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează analiza detaliată a balanței nete
|
||||
- luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă)
|
||||
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_net_balance_breakdown(company, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot)
|
||||
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
|
||||
if include_metadata:
|
||||
cache_hit = getattr(request.state, 'cache_hit', False)
|
||||
response_time = getattr(request.state, 'response_time_ms', 0)
|
||||
cache_source = getattr(request.state, 'cache_source', None)
|
||||
result_dict['cache_hit'] = cache_hit
|
||||
result_dict['response_time_ms'] = response_time
|
||||
# Always include cache_source, even if None
|
||||
result_dict['cache_source'] = cache_source
|
||||
|
||||
return result_dict
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea defalcării balanței nete: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea defalcării balanței nete: {str(e)}")
|
||||
|
||||
@router.get("/current-period")
|
||||
async def get_current_period(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează perioada curentă (an și lună) din calendarul Oracle
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează anul, luna și perioada curentă în format YYYY-MM
|
||||
- Folosit pentru afișarea lunii curente în dashboard
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_current_period(company, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea perioadei curente: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea perioadei curente: {str(e)}")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/financial-indicators",
|
||||
tags=["dashboard"]
|
||||
)
|
||||
async def get_financial_indicators(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei (required)"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
include_sparklines: bool = Query(True, description="Include date istorice pentru sparklines (12 luni)"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează toți indicatorii financiari calculați pentru firma selectată.
|
||||
|
||||
Acest endpoint agregă datele din:
|
||||
- Lichiditate: Current Ratio, Quick Ratio, Cash Ratio
|
||||
- Eficiență: DSO, DPO, Cash Conversion Cycle, rate încasare/plată
|
||||
- Risc: creanțe/datorii restante, raport datorii/trezorerie
|
||||
- Cash Flow: flux net lunar, YTD, YoY, acoperire
|
||||
- Dinamică: creștere vânzări/achiziții YoY, marjă implicită
|
||||
- Altman Z-Score: scor și componente X1-X4
|
||||
|
||||
Parametri:
|
||||
- company (required): ID-ul firmei pentru care se calculează indicatorii
|
||||
- luna (optional): Luna contabilă (1-12). Dacă nu este specificată,
|
||||
se folosește ultima perioadă disponibilă.
|
||||
- an (optional): Anul contabil (2000-2100). Dacă nu este specificat,
|
||||
se folosește anul curent.
|
||||
- include_sparklines (optional, default=true): Dacă să includă date istorice
|
||||
pentru vizualizarea trendului pe ultimele 12 luni (sparkline_data și sparkline_labels
|
||||
în fiecare indicator)
|
||||
|
||||
Cache:
|
||||
- TTL: 30 minute pentru indicatori curenți (cache_type='financial_indicators')
|
||||
- TTL: 1 oră pentru date istorice sparkline (cache_type='financial_indicators_historical')
|
||||
- Se invalidează automat la schimbarea datelor din balanță
|
||||
|
||||
Necesită autentificare JWT și acces la firma specificată.
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Nu aveți acces la firma {company}"
|
||||
)
|
||||
|
||||
# Dacă luna/an nu sunt specificate, obținem perioada curentă
|
||||
# Folosim variabile tipizate explicit pentru a evita erori de tip
|
||||
resolved_luna: int
|
||||
resolved_an: int
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
if luna is None or an is None:
|
||||
try:
|
||||
current_period = await DashboardService.get_current_period(company, server_id=server_id)
|
||||
resolved_luna = luna if luna is not None else current_period.get('luna', 12)
|
||||
resolved_an = an if an is not None else current_period.get('an', 2024)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get current period: {e}, using defaults")
|
||||
from datetime import datetime
|
||||
resolved_luna = luna if luna is not None else datetime.now().month
|
||||
resolved_an = an if an is not None else datetime.now().year
|
||||
else:
|
||||
resolved_luna = luna
|
||||
resolved_an = an
|
||||
|
||||
# Dacă include_sparklines este True, folosim metoda care include datele istorice
|
||||
if include_sparklines:
|
||||
response = await FinancialIndicatorsService.get_indicators_with_sparklines(
|
||||
company, resolved_luna, resolved_an, months=12, request=request, server_id=server_id
|
||||
)
|
||||
|
||||
# FIX: Cache poate returna dict în loc de obiect Pydantic
|
||||
# Extragem valorile pentru logging în mod compatibil cu ambele tipuri
|
||||
if isinstance(response, dict):
|
||||
zscore_val = response.get('altman_zscore', {}).get('zscore', {}).get('value')
|
||||
zscore_status = response.get('altman_zscore', {}).get('zscore', {}).get('status')
|
||||
else:
|
||||
zscore_val = response.altman_zscore.zscore.value
|
||||
zscore_status = response.altman_zscore.zscore.status
|
||||
|
||||
logger.info(
|
||||
f"Financial indicators with sparklines for company {company}, "
|
||||
f"luna={resolved_luna}, an={resolved_an}: "
|
||||
f"Z-Score={zscore_val} ({zscore_status}), "
|
||||
f"cache_hit={getattr(request.state, 'cache_hit', False)}, "
|
||||
f"response_time={getattr(request.state, 'response_time_ms', 0):.1f}ms"
|
||||
)
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot / Dashboard)
|
||||
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
|
||||
if include_metadata:
|
||||
result_dict = response.dict() if hasattr(response, 'dict') else response
|
||||
result_dict['cache_hit'] = getattr(request.state, 'cache_hit', False)
|
||||
result_dict['response_time_ms'] = getattr(request.state, 'response_time_ms', 0)
|
||||
result_dict['cache_source'] = getattr(request.state, 'cache_source', None)
|
||||
return result_dict
|
||||
return response
|
||||
|
||||
# Dacă include_sparklines este False, calculăm doar indicatorii curenți
|
||||
import asyncio
|
||||
|
||||
# Apelăm serviciul pentru fiecare categorie de indicatori
|
||||
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
|
||||
# Executăm toate calculele în paralel pentru performanță
|
||||
(
|
||||
lichiditate,
|
||||
eficienta,
|
||||
risc,
|
||||
cash_flow,
|
||||
dinamica,
|
||||
altman_zscore,
|
||||
profitabilitate,
|
||||
solvabilitate
|
||||
) = await asyncio.gather(
|
||||
lichiditate_task,
|
||||
eficienta_task,
|
||||
risc_task,
|
||||
cash_flow_task,
|
||||
dinamica_task,
|
||||
altman_task,
|
||||
profitabilitate_task,
|
||||
solvabilitate_task
|
||||
)
|
||||
|
||||
# Construim răspunsul
|
||||
response = FinancialIndicatorsResponse(
|
||||
lichiditate=lichiditate,
|
||||
eficienta=eficienta,
|
||||
risc=risc,
|
||||
cash_flow=cash_flow,
|
||||
dinamica=dinamica,
|
||||
altman_zscore=altman_zscore,
|
||||
profitabilitate=profitabilitate,
|
||||
solvabilitate=solvabilitate
|
||||
)
|
||||
|
||||
# FIX: Cache poate returna dict în loc de obiect Pydantic
|
||||
if isinstance(altman_zscore, dict):
|
||||
zscore_val = altman_zscore.get('zscore', {}).get('value')
|
||||
zscore_status = altman_zscore.get('zscore', {}).get('status')
|
||||
else:
|
||||
zscore_val = altman_zscore.zscore.value
|
||||
zscore_status = altman_zscore.zscore.status
|
||||
|
||||
logger.info(
|
||||
f"Financial indicators for company {company}, luna={resolved_luna}, an={resolved_an}: "
|
||||
f"Z-Score={zscore_val} ({zscore_status})"
|
||||
)
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot / Dashboard)
|
||||
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
|
||||
if include_metadata:
|
||||
result_dict = response.dict() if hasattr(response, 'dict') else response
|
||||
result_dict['cache_hit'] = getattr(request.state, 'cache_hit', False)
|
||||
result_dict['response_time_ms'] = getattr(request.state, 'response_time_ms', 0)
|
||||
result_dict['cache_source'] = getattr(request.state, 'cache_source', None)
|
||||
return result_dict
|
||||
return response
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea indicatorilor financiari: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la obținerea indicatorilor financiari: {str(e)}"
|
||||
)
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
API Router pentru facturi
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from typing import List, Optional
|
||||
from datetime import date
|
||||
# import sys # Removed - no longer needed
|
||||
import os
|
||||
|
||||
from shared.auth.dependencies import get_current_user, require_company_access
|
||||
from shared.auth.models import CurrentUser
|
||||
from ..models.invoice import InvoiceFilter, InvoiceListResponse, InvoiceSummary
|
||||
from ..services.invoice_service import InvoiceService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=InvoiceListResponse)
|
||||
async def get_invoices(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
cont: Optional[str] = Query(None, description="Filtru după cont contabil"),
|
||||
only_unpaid: bool = Query(True, description="Doar facturile neachitate"),
|
||||
min_amount: Optional[float] = Query(None, description="Suma minimă"),
|
||||
max_amount: Optional[float] = Query(None, description="Suma maximă"),
|
||||
page: int = Query(1, ge=1, description="Pagina"),
|
||||
page_size: int = Query(50, ge=1, le=10000000, description="Mărimea paginii"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține lista de facturi pentru o firmă
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Utilizatorul trebuie să aibă acces la firma specificată
|
||||
- Suportă filtrare după luna/an contabil și paginare
|
||||
"""
|
||||
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}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
filter_params = InvoiceFilter(
|
||||
company=company,
|
||||
partner_type=partner_type,
|
||||
luna=luna,
|
||||
an=an,
|
||||
partner_name=partner_name,
|
||||
cont=cont,
|
||||
only_unpaid=only_unpaid,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
result = await InvoiceService.get_invoices(filter_params, current_user.username, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea facturilor: {str(e)}")
|
||||
|
||||
@router.get("/summary", response_model=InvoiceSummary)
|
||||
async def get_invoices_summary(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Obține rezumatul facturilor pentru dashboard"""
|
||||
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}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
result = await InvoiceService.get_invoice_summary(company, partner_type, current_user.username, server_id=server_id)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea rezumatului facturilor: {str(e)}")
|
||||
|
||||
@router.get("/{invoice_number}")
|
||||
async def get_invoice_details(
|
||||
request: Request,
|
||||
invoice_number: str,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Obține detaliile unei facturi specifice"""
|
||||
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}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
result = await InvoiceService.get_invoice_details(company, invoice_number, current_user.username, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea detaliilor facturii: {str(e)}")
|
||||
|
||||
@router.get("/export/{format}")
|
||||
async def export_invoices(
|
||||
request: Request,
|
||||
format: str,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
only_unpaid: bool = Query(True, description="Doar facturile neachitate"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Export facturi în format specificat (excel, pdf, csv)
|
||||
Această funcție va fi implementată în viitor
|
||||
"""
|
||||
# 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}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None) # For future use
|
||||
|
||||
# Verifică formatul
|
||||
if format not in ["excel", "pdf", "csv"]:
|
||||
raise HTTPException(status_code=400, detail="Format invalid. Formatele suportate sunt: excel, pdf, csv")
|
||||
|
||||
# Pentru moment, returnează o eroare că funcția nu este implementată
|
||||
raise HTTPException(status_code=501, detail=f"Export în format {format} nu este încă implementat")
|
||||
@@ -0,0 +1,123 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
# import sys # Removed - no longer needed
|
||||
import os
|
||||
|
||||
from shared.auth.dependencies import get_current_user
|
||||
from shared.auth.models import CurrentUser
|
||||
from ..models.treasury import RegisterFilter, RegisterListResponse
|
||||
from ..services.treasury_service import TreasuryService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/bank-cash-register", response_model=RegisterListResponse)
|
||||
async def get_bank_cash_register(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
register_type: Optional[str] = Query(None, description="Tipul registrului: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA sau None pentru toate"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
|
||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
bank_account: Optional[str] = Query(None, description="Filtru cont bancă/casă (bancasa)"),
|
||||
page: int = Query(1, ge=1, description="Pagina"),
|
||||
page_size: int = Query(50, ge=1, le=10000000, description="Mărimea paginii"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține registrul de casă și bancă
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Suportă filtrare pe tip registru: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA
|
||||
- Suportă filtrare și paginare
|
||||
"""
|
||||
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}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
# Validează register_type dacă e specificat
|
||||
valid_types = ['BANCA_LEI', 'BANCA_VALUTA', 'CASA_LEI', 'CASA_VALUTA']
|
||||
if register_type and register_type not in valid_types:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Tip registru invalid. Valori acceptate: {', '.join(valid_types)}"
|
||||
)
|
||||
|
||||
# Convertește datele
|
||||
date_from_obj = None
|
||||
date_to_obj = None
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
date_from_obj = date.fromisoformat(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Format dată început invalid")
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_obj = date.fromisoformat(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Format dată sfârșit invalid")
|
||||
|
||||
filter_params = RegisterFilter(
|
||||
company=company,
|
||||
register_type=register_type,
|
||||
luna=luna,
|
||||
an=an,
|
||||
date_from=date_from_obj,
|
||||
date_to=date_to_obj,
|
||||
partner_name=partner_name,
|
||||
bank_account=bank_account,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
result = await TreasuryService.get_bank_cash_register(filter_params, current_user.username, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea registrului: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/bank-cash-accounts", response_model=List[str])
|
||||
async def get_bank_cash_accounts(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
register_type: str = Query(description="Tipul registrului: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține lista distinctă de conturi bancă/casă pentru dropdown
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează lista de valori bancasa pentru tipul de registru selectat
|
||||
"""
|
||||
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}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
# Validează register_type
|
||||
valid_types = ['BANCA_LEI', 'BANCA_VALUTA', 'CASA_LEI', 'CASA_VALUTA']
|
||||
if register_type not in valid_types:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Tip registru invalid. Valori acceptate: {', '.join(valid_types)}"
|
||||
)
|
||||
|
||||
result = await TreasuryService.get_bank_cash_accounts(int(company), register_type, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea conturilor: {str(e)}")
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
API Router for Trial Balance (Balanță de Verificare)
|
||||
Refactored to use service layer with caching
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
# import sys # Removed - no longer needed
|
||||
import os
|
||||
|
||||
from shared.auth.dependencies import get_current_user
|
||||
from shared.auth.models import CurrentUser
|
||||
from ..models.trial_balance import TrialBalanceResponse
|
||||
from ..services.trial_balance_service import TrialBalanceService
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=TrialBalanceResponse)
|
||||
async def get_trial_balance(
|
||||
request: Request,
|
||||
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=1000000, 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
|
||||
- **CACHED 10 min** - folosește sistem cache two-tier (L1 Memory + L2 SQLite)
|
||||
"""
|
||||
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}"
|
||||
)
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
# 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
|
||||
|
||||
# Convert company to int
|
||||
company_id = int(company)
|
||||
|
||||
# Call service (with caching) - all business logic moved to service
|
||||
data = await TrialBalanceService.get_trial_balance(
|
||||
company_id=company_id,
|
||||
luna=luna,
|
||||
an=an,
|
||||
cont_filter=cont_filter,
|
||||
denumire_filter=denumire_filter,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
username=current_user.username,
|
||||
server_id=server_id
|
||||
)
|
||||
|
||||
return TrialBalanceResponse(
|
||||
success=True,
|
||||
data=data
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# Schema not found or validation error
|
||||
logger.error(f"Validation error in trial balance: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
# Log unexpected errors
|
||||
logger.error(f"Error fetching trial balance: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la obținerea balanței de verificare: {str(e)}"
|
||||
)
|
||||
Reference in New Issue
Block a user