Cache System (Backend): - Implemented two-tier hybrid cache: L1 (in-memory) + L2 (SQLite) - L1 cache: Fast dictionary-based with 5-minute TTL for hot data - L2 cache: Persistent SQLite with 1-hour TTL for warm data - Cache decorator with automatic tier management and fallback - Cache key generation with per-user isolation - Event monitoring system for cache statistics - Cache benchmarking utilities for performance testing - Added cache management endpoints: /api/cache/stats, /api/cache/clear, /api/cache/benchmark - Cache configuration via environment variables (CACHE_ENABLED, CACHE_L1_TTL, etc.) Backend Services: - Updated dashboard_service to use @cached decorator with request context - Added cache support to invoice_service and treasury_service - Integrated cache manager into main.py with lifespan events - Added Request parameter to service methods for cache metadata Frontend Enhancements: - New CacheStatsView.vue for real-time cache monitoring dashboard - Cache store (cacheStore.js) for state management - Updated router to include /cache-stats route - Navigation updates in DashboardHeader and HamburgerMenu - Cache stats accessible from main navigation Telegram Bot Improvements: - Enhanced formatters with YTD comparison data - Improved menu navigation and button layout - Better error handling and user feedback - Bot startup improvements with graceful shutdown Auth & Middleware: - Enhanced middleware with cache metadata injection - Improved request state handling for cache source tracking Development: - Updated start-dev.sh with better error handling - Added TELEGRAM_EMAIL_AUTH_PLAN.md documentation - Updated requirements.txt with aiosqlite for async SQLite Performance: - L1 cache provides <1ms response for hot data - L2 cache provides ~5ms response for warm data - Database queries only for cold data or cache misses - Cache hit rates tracked and displayed in real-time 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
282 lines
12 KiB
Python
282 lines
12 KiB
Python
"""
|
|
Service pentru logica facturi - Portează query-urile din aplicația Flask
|
|
"""
|
|
import sys
|
|
import os
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
|
|
|
from database.oracle_pool import oracle_pool
|
|
from typing import List, Tuple
|
|
from ..models.invoice import Invoice, InvoiceFilter, InvoiceListResponse, InvoiceSummary
|
|
from ..cache.decorators import cached
|
|
from decimal import Decimal
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class InvoiceService:
|
|
"""Service pentru gestionarea facturilor"""
|
|
|
|
@staticmethod
|
|
@cached(cache_type='schema', key_params=['company_id'])
|
|
async def _get_schema(company_id: int) -> str:
|
|
"""Obține schema pentru company_id (CACHED PERMANENT)"""
|
|
async with oracle_pool.get_connection() as connection:
|
|
with connection.cursor() as cursor:
|
|
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 ValueError(f"Schema not found for company {company_id}")
|
|
|
|
return schema_result[0]
|
|
|
|
@staticmethod
|
|
@cached(cache_type='invoices', key_params=['filter_params', 'username'])
|
|
async def get_invoices(filter_params: InvoiceFilter, username: str) -> InvoiceListResponse:
|
|
"""
|
|
Obține lista de facturi - Query simplu pentru afișare în tabel (CACHED 10 min)
|
|
"""
|
|
company_id = int(filter_params.company)
|
|
schema = await InvoiceService._get_schema(company_id)
|
|
|
|
async with oracle_pool.get_connection() as connection:
|
|
with connection.cursor() as cursor:
|
|
|
|
# Determină conturile în funcție de partner_type
|
|
if filter_params.partner_type == "CLIENTI":
|
|
conturi = "'4111', '461'"
|
|
elif filter_params.partner_type == "FURNIZORI":
|
|
conturi = "'401', '404', '462'"
|
|
else:
|
|
conturi = "'4111'" # default
|
|
|
|
# Query cu calculele corecte pentru solduri
|
|
base_query = f"""
|
|
SELECT
|
|
vp.NUME,
|
|
vp.NRACT,
|
|
vp.DATAACT,
|
|
vp.DATASCAD,
|
|
vp.CONTRACT,
|
|
vp.COD_FISCAL,
|
|
vp.REG_COMERT,
|
|
CASE
|
|
WHEN vp.CONT IN ('4111','461') THEN vp.PRECDEB + vp.DEBIT -- Total facturat clienți
|
|
WHEN vp.CONT IN ('401','404','462') THEN vp.PRECCRED + vp.CREDIT -- Total facturat furnizori
|
|
END as total_facturat,
|
|
CASE
|
|
WHEN vp.CONT IN ('4111','461') THEN vp.PRECCRED + vp.CREDIT -- Încasat clienți
|
|
WHEN vp.CONT IN ('401','404','462') THEN vp.PRECDEB + vp.DEBIT -- Achitat furnizori
|
|
END as achitat,
|
|
CASE
|
|
WHEN vp.CONT IN ('4111','461') THEN
|
|
(vp.PRECDEB + vp.DEBIT) - (vp.PRECCRED + vp.CREDIT) -- Sold clienți
|
|
WHEN vp.CONT IN ('401','404','462') THEN
|
|
(vp.PRECCRED + vp.CREDIT) - (vp.PRECDEB + vp.DEBIT) -- Sold furnizori
|
|
END as sold,
|
|
vp.CONT,
|
|
CASE
|
|
WHEN vp.DATASCAD < SYSDATE THEN 'restant'
|
|
ELSE 'in_termen'
|
|
END as status
|
|
FROM {schema}.vireg_parteneri vp
|
|
WHERE vp.an = (SELECT anul FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar))
|
|
AND vp.luna = (SELECT luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar))
|
|
AND (
|
|
(:partner_type = 'CLIENTI' AND vp.cont IN ('4111', '461'))
|
|
OR
|
|
(:partner_type = 'FURNIZORI' AND vp.cont IN ('401', '404', '462'))
|
|
)
|
|
"""
|
|
|
|
params = {'partner_type': filter_params.partner_type}
|
|
|
|
# Adaugă filtre dinamice
|
|
if filter_params.date_from:
|
|
base_query += " AND vp.dataact >= :date_from"
|
|
params['date_from'] = filter_params.date_from
|
|
|
|
if filter_params.date_to:
|
|
base_query += " AND vp.dataact <= :date_to"
|
|
params['date_to'] = filter_params.date_to
|
|
|
|
if filter_params.partner_name:
|
|
base_query += " AND UPPER(vp.nume) LIKE UPPER(:partner_name)"
|
|
params['partner_name'] = f"%{filter_params.partner_name}%"
|
|
|
|
if filter_params.min_amount:
|
|
base_query += " AND total_facturat >= :min_amount"
|
|
params['min_amount'] = filter_params.min_amount
|
|
|
|
if filter_params.max_amount:
|
|
base_query += " AND total_facturat <= :max_amount"
|
|
params['max_amount'] = filter_params.max_amount
|
|
|
|
if filter_params.only_unpaid:
|
|
# Nu putem folosi aliasul "sold" în WHERE în Oracle, trebuie să repetăm calculul
|
|
base_query += """ AND (
|
|
CASE
|
|
WHEN vp.CONT IN ('4111','461') THEN
|
|
(vp.PRECDEB + vp.DEBIT) - (vp.PRECCRED + vp.CREDIT)
|
|
WHEN vp.CONT IN ('401','404','462') THEN
|
|
(vp.PRECCRED + vp.CREDIT) - (vp.PRECDEB + vp.DEBIT)
|
|
END
|
|
) > 0"""
|
|
|
|
# Count total pentru paginare
|
|
count_query = f"SELECT COUNT(*) FROM ({base_query})"
|
|
cursor.execute(count_query, params)
|
|
total_count = cursor.fetchone()[0]
|
|
|
|
# Adaugă ORDER BY și paginare
|
|
base_query += " ORDER BY vp.DATAACT DESC, vp.NUME, vp.NRACT"
|
|
|
|
# Paginare Oracle
|
|
offset = (filter_params.page - 1) * filter_params.page_size
|
|
limit = offset + filter_params.page_size
|
|
paginated_query = f"""
|
|
SELECT * FROM (
|
|
SELECT ROWNUM as rn, t.* FROM ({base_query}) t WHERE ROWNUM <= :limit
|
|
) WHERE rn > :offset
|
|
"""
|
|
params['offset'] = offset
|
|
params['limit'] = limit
|
|
|
|
cursor.execute(paginated_query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
# Procesează rezultatele cu structura nouă
|
|
invoices = []
|
|
total_amount = Decimal('0.00')
|
|
|
|
for row in rows:
|
|
# Skip ROWNUM, extrage valorile din query-ul nou
|
|
nume = row[1]
|
|
nract = row[2]
|
|
dataact = row[3]
|
|
datascad = row[4]
|
|
contract = row[5]
|
|
cod_fiscal = row[6]
|
|
reg_comert = row[7]
|
|
total_facturat = Decimal(str(row[8] or 0))
|
|
achitat = Decimal(str(row[9] or 0))
|
|
sold = Decimal(str(row[10] or 0))
|
|
cont = row[11]
|
|
status = row[12]
|
|
|
|
invoice_data = {
|
|
'nume': nume or '',
|
|
'nract': nract or 0,
|
|
'dataact': dataact,
|
|
'datascad': datascad,
|
|
'contract': contract,
|
|
'cod_fiscal': cod_fiscal,
|
|
'reg_comert': reg_comert,
|
|
'totctva': total_facturat,
|
|
'achitat': achitat,
|
|
'soldfinal': sold
|
|
}
|
|
|
|
invoice = Invoice(**invoice_data)
|
|
invoices.append(invoice)
|
|
total_amount += total_facturat
|
|
|
|
return InvoiceListResponse(
|
|
invoices=invoices,
|
|
total_count=total_count,
|
|
filtered_count=len(invoices),
|
|
total_amount=total_amount,
|
|
page=filter_params.page,
|
|
page_size=filter_params.page_size,
|
|
has_more=len(invoices) == filter_params.page_size
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_invoice_details(company: str, invoice_number: str, username: str) -> Invoice:
|
|
"""
|
|
Obține detaliile unei facturi specifice
|
|
"""
|
|
async with oracle_pool.get_connection() as connection:
|
|
with connection.cursor() as cursor:
|
|
# Obține schema din v_nom_firme bazat pe id_firma
|
|
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 ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
|
|
|
|
schema = schema_result[0]
|
|
|
|
# Query simplu pentru detalii factură
|
|
detail_query = f"""
|
|
SELECT
|
|
NUME,
|
|
NRACT,
|
|
DATAACT,
|
|
DATASCAD,
|
|
CONTRACT,
|
|
COD_FISCAL,
|
|
REG_COMERT,
|
|
PRECDEB,
|
|
PRECCRED,
|
|
DEBIT,
|
|
CREDIT,
|
|
CONT
|
|
FROM {schema}.vireg_parteneri
|
|
WHERE nract = :invoice_number
|
|
AND an = (select anul from {schema}.calendar where anul*12+luna = (select max(anul*12+luna) as anmax from {schema}.calendar))
|
|
AND luna = (select luna from {schema}.calendar where anul*12+luna = (select max(anul*12+luna) as anmax from {schema}.calendar))
|
|
"""
|
|
|
|
cursor.execute(detail_query, {'invoice_number': invoice_number})
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
raise ValueError(f"Factura {invoice_number} nu a fost găsită")
|
|
|
|
# Extrage valorile
|
|
nume = row[0]
|
|
nract = row[1]
|
|
dataact = row[2]
|
|
datascad = row[3]
|
|
contract = row[4]
|
|
cod_fiscal = row[5]
|
|
reg_comert = row[6]
|
|
precdeb = Decimal(str(row[7] or 0))
|
|
preccred = Decimal(str(row[8] or 0))
|
|
debit = Decimal(str(row[9] or 0))
|
|
credit = Decimal(str(row[10] or 0))
|
|
cont = row[11]
|
|
|
|
# Calculează valorile în funcție de tipul contului
|
|
if cont in ('4111', '461'): # CLIENTI
|
|
totctva = precdeb + debit
|
|
achitat = preccred + credit
|
|
soldfinal = precdeb - preccred + debit - credit
|
|
else: # FURNIZORI
|
|
totctva = preccred + credit
|
|
achitat = precdeb + debit
|
|
soldfinal = preccred - precdeb + credit - debit
|
|
|
|
invoice_data = {
|
|
'nume': nume or '',
|
|
'nract': nract or 0,
|
|
'dataact': dataact,
|
|
'datascad': datascad,
|
|
'contract': contract,
|
|
'cod_fiscal': cod_fiscal,
|
|
'reg_comert': reg_comert,
|
|
'totctva': totctva,
|
|
'achitat': achitat,
|
|
'soldfinal': soldfinal
|
|
}
|
|
|
|
return Invoice(**invoice_data) |