Optimize PDF export layout with compact columns and more space for partner names. Add accounting period display to invoices matching Trial Balance format. Fix date filtering to use local timezone instead of UTC. Update invoice ordering to chronological sequence (DATAACT, NRACT, NUME). **Backend changes:** - Add accounting period query from calendar table - Add currency (valuta) and cont filter support - Change invoice ordering to chronological (DATAACT ASC, NRACT ASC, NUME) - Add accounting_period field to InvoiceListResponse model **Frontend changes:** - Optimize PDF column widths (37% for partner names, compact numeric columns) - Add custom column width support in exportUtils - Fix date conversion from UTC to local timezone (prevents day shift) - Add accounting period display in PDF exports - Enhance E2E test coverage **Cleanup:** - Remove obsolete Trial Balance feature documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
304 lines
13 KiB
Python
304 lines
13 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,
|
|
NVL(vp.NUME_VAL, 'RON') as valuta,
|
|
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.cont:
|
|
base_query += " AND vp.cont = :cont"
|
|
params['cont'] = filter_params.cont
|
|
|
|
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]
|
|
|
|
# Get accounting period (luna, an) from calendar
|
|
period_query = f"""
|
|
SELECT anul, luna
|
|
FROM {schema}.calendar
|
|
WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)
|
|
"""
|
|
cursor.execute(period_query)
|
|
period_result = cursor.fetchone()
|
|
accounting_period = {
|
|
'an': period_result[0] if period_result else None,
|
|
'luna': period_result[1] if period_result else None
|
|
}
|
|
|
|
# Adaugă ORDER BY și paginare - Ordonare cronologică (DATAACT, NRACT, NUME)
|
|
base_query += " ORDER BY vp.DATAACT ASC, vp.NRACT ASC, vp.NUME"
|
|
|
|
# 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]
|
|
valuta = row[12] or 'RON'
|
|
status = row[13]
|
|
|
|
invoice_data = {
|
|
'nume': nume or '',
|
|
'nract': nract or 0,
|
|
'dataact': dataact,
|
|
'datascad': datascad,
|
|
'contract': contract,
|
|
'cod_fiscal': cod_fiscal,
|
|
'reg_comert': reg_comert,
|
|
'cont': cont,
|
|
'totctva': total_facturat,
|
|
'achitat': achitat,
|
|
'soldfinal': sold,
|
|
'valuta': valuta
|
|
}
|
|
|
|
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,
|
|
accounting_period=accounting_period
|
|
)
|
|
|
|
@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) |