fix: Add bank/cash name sorting to treasury register
- Add bancasa as third sorting criterion (date, number, bank name) - Sort null-date rows (previous balances) alphabetically by bank name - Sort bank names alphabetically in PDF export - Improve window function for cumulative balance calculation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,25 +3,32 @@ from decimal import Decimal
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
|
class AccountingPeriod(BaseModel):
|
||||||
|
"""Model pentru perioada contabilă"""
|
||||||
|
an: Optional[int] = None
|
||||||
|
luna: Optional[int] = None
|
||||||
|
|
||||||
class BankCashRegister(BaseModel):
|
class BankCashRegister(BaseModel):
|
||||||
"""Model pentru Registrul de Casă și Bancă"""
|
"""Model pentru Registrul de Casă și Bancă"""
|
||||||
nume: str
|
nume: str
|
||||||
nract: int
|
nract: Optional[int] = None
|
||||||
dataact: datetime
|
dataact: Optional[datetime] = None
|
||||||
nume_cont_bancar: str # din vbalanta_parteneri.nume
|
nume_cont_bancar: str # din vbalanta_parteneri.nume
|
||||||
incasari: Decimal
|
incasari: Decimal
|
||||||
plati: Decimal
|
plati: Decimal
|
||||||
sold: Decimal
|
sold: Decimal
|
||||||
valuta: str
|
valuta: Optional[str] = None
|
||||||
tip_registru: str # "BANCA LEI", "CASA VALUTA" etc
|
tip_registru: str # "BANCA LEI", "CASA VALUTA" etc
|
||||||
explicatia: str
|
explicatia: str
|
||||||
|
|
||||||
class RegisterFilter(BaseModel):
|
class RegisterFilter(BaseModel):
|
||||||
"""Filtre pentru registrul de casă și bancă"""
|
"""Filtre pentru registrul de casă și bancă"""
|
||||||
company: str
|
company: str
|
||||||
|
register_type: Optional[str] = None # BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA sau None pentru toate
|
||||||
date_from: Optional[datetime] = None
|
date_from: Optional[datetime] = None
|
||||||
date_to: Optional[datetime] = None
|
date_to: Optional[datetime] = None
|
||||||
partner_name: Optional[str] = None
|
partner_name: Optional[str] = None
|
||||||
|
bank_account: Optional[str] = None # Filter for specific bank/cash account (bancasa)
|
||||||
page: int = 1
|
page: int = 1
|
||||||
page_size: int = 50
|
page_size: int = 50
|
||||||
|
|
||||||
@@ -35,3 +42,4 @@ class RegisterListResponse(BaseModel):
|
|||||||
page: int
|
page: int
|
||||||
page_size: int
|
page_size: int
|
||||||
has_more: bool
|
has_more: bool
|
||||||
|
accounting_period: Optional[AccountingPeriod] = None
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -15,9 +15,11 @@ router = APIRouter()
|
|||||||
@router.get("/bank-cash-register", response_model=RegisterListResponse)
|
@router.get("/bank-cash-register", response_model=RegisterListResponse)
|
||||||
async def get_bank_cash_register(
|
async def get_bank_cash_register(
|
||||||
company: str = Query(description="Codul firmei"),
|
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"),
|
||||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
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)"),
|
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
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: int = Query(1, ge=1, description="Pagina"),
|
||||||
page_size: int = Query(50, ge=1, le=10000000, description="Mărimea paginii"),
|
page_size: int = Query(50, ge=1, le=10000000, description="Mărimea paginii"),
|
||||||
current_user: CurrentUser = Depends(get_current_user)
|
current_user: CurrentUser = Depends(get_current_user)
|
||||||
@@ -26,6 +28,7 @@ async def get_bank_cash_register(
|
|||||||
Obține registrul de casă și bancă
|
Obține registrul de casă și bancă
|
||||||
|
|
||||||
- Necesită autentificare JWT
|
- Necesită autentificare JWT
|
||||||
|
- Suportă filtrare pe tip registru: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA
|
||||||
- Suportă filtrare și paginare
|
- Suportă filtrare și paginare
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -33,6 +36,14 @@ async def get_bank_cash_register(
|
|||||||
if company not in current_user.companies:
|
if company not in current_user.companies:
|
||||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||||
|
|
||||||
|
# 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
|
# Convertește datele
|
||||||
date_from_obj = None
|
date_from_obj = None
|
||||||
date_to_obj = None
|
date_to_obj = None
|
||||||
@@ -51,9 +62,11 @@ async def get_bank_cash_register(
|
|||||||
|
|
||||||
filter_params = RegisterFilter(
|
filter_params = RegisterFilter(
|
||||||
company=company,
|
company=company,
|
||||||
|
register_type=register_type,
|
||||||
date_from=date_from_obj,
|
date_from=date_from_obj,
|
||||||
date_to=date_to_obj,
|
date_to=date_to_obj,
|
||||||
partner_name=partner_name,
|
partner_name=partner_name,
|
||||||
|
bank_account=bank_account,
|
||||||
page=page,
|
page=page,
|
||||||
page_size=page_size
|
page_size=page_size
|
||||||
)
|
)
|
||||||
@@ -65,3 +78,37 @@ async def get_bank_cash_register(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea registrului: {str(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(
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
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)}")
|
||||||
@@ -2,10 +2,12 @@ import sys
|
|||||||
import os
|
import os
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||||
|
|
||||||
|
import oracledb
|
||||||
from database.oracle_pool import oracle_pool
|
from database.oracle_pool import oracle_pool
|
||||||
from ..models.treasury import BankCashRegister, RegisterFilter, RegisterListResponse
|
from ..models.treasury import BankCashRegister, RegisterFilter, RegisterListResponse, AccountingPeriod
|
||||||
from ..cache.decorators import cached
|
from ..cache.decorators import cached
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Optional, List, Tuple, Any
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -32,11 +34,85 @@ class TreasuryService:
|
|||||||
|
|
||||||
return schema_result[0]
|
return schema_result[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_view_query(schema: str, register_type: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Construiește query-ul pentru view-ul vbancasa corespunzător.
|
||||||
|
Dacă register_type este None, returnează UNION ALL pentru toate tipurile.
|
||||||
|
NU se filtrează pe incasari/plati > 0 - se afișează TOATE înregistrările!
|
||||||
|
"""
|
||||||
|
view_configs = {
|
||||||
|
'BANCA_LEI': {
|
||||||
|
'view': f'{schema}.vbancasa_5121_cum',
|
||||||
|
'incasari_col': 'incasari',
|
||||||
|
'plati_col': 'plati',
|
||||||
|
'valuta': "'RON'",
|
||||||
|
'tip': "'BANCA LEI'"
|
||||||
|
},
|
||||||
|
'BANCA_VALUTA': {
|
||||||
|
'view': f'{schema}.vbancasa_5124_cum',
|
||||||
|
'incasari_col': 'incasval',
|
||||||
|
'plati_col': 'platival',
|
||||||
|
'valuta': "COALESCE(numeval, 'EUR')",
|
||||||
|
'tip': "'BANCA VALUTA'"
|
||||||
|
},
|
||||||
|
'CASA_LEI': {
|
||||||
|
'view': f'{schema}.vbancasa_5311_cum',
|
||||||
|
'incasari_col': 'incasari',
|
||||||
|
'plati_col': 'plati',
|
||||||
|
'valuta': "'RON'",
|
||||||
|
'tip': "'CASA LEI'"
|
||||||
|
},
|
||||||
|
'CASA_VALUTA': {
|
||||||
|
'view': f'{schema}.vbancasa_5314_cum',
|
||||||
|
'incasari_col': 'incasval',
|
||||||
|
'plati_col': 'platival',
|
||||||
|
'valuta': "COALESCE(numeval, 'EUR')",
|
||||||
|
'tip': "'CASA VALUTA'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_select(config):
|
||||||
|
# NU se filtrează - se afișează TOATE înregistrările
|
||||||
|
# SOLD CUMULAT: Running balance per bancasa using window function
|
||||||
|
# NULL-date rows (opening balance) come first due to NULLS FIRST
|
||||||
|
return f"""
|
||||||
|
SELECT
|
||||||
|
nume, nract, dataact, bancasa,
|
||||||
|
{config['incasari_col']} as incasari,
|
||||||
|
{config['plati_col']} as plati,
|
||||||
|
SUM({config['incasari_col']} - {config['plati_col']}) OVER (
|
||||||
|
PARTITION BY bancasa
|
||||||
|
ORDER BY dataact ASC NULLS FIRST, nract ASC NULLS FIRST
|
||||||
|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||||
|
) as sold,
|
||||||
|
{config['valuta']} as valuta,
|
||||||
|
{config['tip']} as tip_registru,
|
||||||
|
explicatia
|
||||||
|
FROM {config['view']}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if register_type and register_type in view_configs:
|
||||||
|
return build_select(view_configs[register_type])
|
||||||
|
else:
|
||||||
|
# UNION ALL pentru toate tipurile
|
||||||
|
queries = [build_select(cfg) for cfg in view_configs.values()]
|
||||||
|
return " UNION ALL ".join(queries)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@cached(cache_type='treasury', key_params=['filter_params', 'username'])
|
@cached(cache_type='treasury', key_params=['filter_params', 'username'])
|
||||||
async def get_bank_cash_register(filter_params: RegisterFilter, username: str) -> RegisterListResponse:
|
async def get_bank_cash_register(filter_params: RegisterFilter, username: str) -> RegisterListResponse:
|
||||||
"""
|
"""
|
||||||
Obține registrul de casă și bancă din vbancasa views (CACHED 10 min)
|
Obține registrul de casă și bancă din vbancasa views (CACHED 10 min)
|
||||||
|
|
||||||
|
IMPORTANT: PACK_SESIUNE.SETAN și SETLUNA trebuie executate în ACEEAȘI
|
||||||
|
tranzacție cu SELECT-ul din vbancasa* views!
|
||||||
|
|
||||||
|
Folosim un bloc PL/SQL anonim care:
|
||||||
|
1. Obține anul și luna curentă din calendar
|
||||||
|
2. Apelează PACK_SESIUNE.SETAN și SETLUNA
|
||||||
|
3. Execută SELECT-ul din vbancasa*
|
||||||
|
Toate în aceeași tranzacție!
|
||||||
"""
|
"""
|
||||||
company_id = int(filter_params.company)
|
company_id = int(filter_params.company)
|
||||||
schema = await TreasuryService._get_schema(company_id)
|
schema = await TreasuryService._get_schema(company_id)
|
||||||
@@ -44,101 +120,116 @@ class TreasuryService:
|
|||||||
async with oracle_pool.get_connection() as connection:
|
async with oracle_pool.get_connection() as connection:
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
|
|
||||||
# Query pentru registrele de bancă și casă
|
# Construiește query-ul pentru tipul de registru selectat
|
||||||
union_queries = []
|
base_select = TreasuryService._get_view_query(schema, filter_params.register_type)
|
||||||
|
|
||||||
# BANCA LEI (5121)
|
# Construiește WHERE conditions
|
||||||
union_queries.append(f"""
|
|
||||||
SELECT
|
|
||||||
vb.nume, vb.nract, vb.dataact, vb.bancasa,
|
|
||||||
vb.incasari, vb.plati,
|
|
||||||
vb.incasari - vb.plati as sold,
|
|
||||||
'RON' as valuta,
|
|
||||||
'BANCA LEI' as tip_registru,
|
|
||||||
vb.explicatia
|
|
||||||
FROM {schema}.vbancasa_5121_cum vb
|
|
||||||
WHERE (vb.incasari > 0 OR vb.plati > 0)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# BANCA VALUTA (5124)
|
|
||||||
union_queries.append(f"""
|
|
||||||
SELECT
|
|
||||||
vb.nume, vb.nract, vb.dataact, vb.bancasa,
|
|
||||||
vb.incasval, vb.platival,
|
|
||||||
vb.incasval - vb.platival as sold,
|
|
||||||
COALESCE(vb.numeval, 'EUR') as valuta,
|
|
||||||
'BANCA VALUTA' as tip_registru,
|
|
||||||
vb.explicatia
|
|
||||||
FROM {schema}.vbancasa_5124_cum vb
|
|
||||||
WHERE (vb.incasval > 0 OR vb.platival > 0)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# CASA LEI (5311)
|
|
||||||
union_queries.append(f"""
|
|
||||||
SELECT
|
|
||||||
vb.nume, vb.nract, vb.dataact, vb.bancasa,
|
|
||||||
vb.incasari, vb.plati,
|
|
||||||
vb.incasari - vb.plati as sold,
|
|
||||||
'RON' as valuta,
|
|
||||||
'CASA LEI' as tip_registru,
|
|
||||||
vb.explicatia
|
|
||||||
FROM {schema}.vbancasa_5311_cum vb
|
|
||||||
WHERE (vb.incasari > 0 OR vb.plati > 0)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# CASA VALUTA (5314)
|
|
||||||
union_queries.append(f"""
|
|
||||||
SELECT
|
|
||||||
vb.nume, vb.nract, vb.dataact, vb.bancasa,
|
|
||||||
vb.incasval, vb.platival,
|
|
||||||
vb.incasval - vb.platival as sold,
|
|
||||||
COALESCE(vb.numeval, 'EUR') as valuta,
|
|
||||||
'CASA VALUTA' as tip_registru,
|
|
||||||
vb.explicatia
|
|
||||||
FROM {schema}.vbancasa_5314_cum vb
|
|
||||||
WHERE (vb.incasval > 0 OR vb.platival > 0)
|
|
||||||
""")
|
|
||||||
|
|
||||||
base_query = " UNION ALL ".join(union_queries)
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
where_conditions = []
|
where_conditions = []
|
||||||
|
|
||||||
if filter_params.date_from:
|
# Date filter preserves NULL-date rows (opening balance)
|
||||||
where_conditions.append("dataact >= :date_from")
|
# for correct cumulative sum calculation
|
||||||
params['date_from'] = filter_params.date_from
|
if filter_params.date_from and filter_params.date_to:
|
||||||
|
where_conditions.append(f"(dataact IS NULL OR (dataact >= TO_DATE('{filter_params.date_from.strftime('%Y-%m-%d')}', 'YYYY-MM-DD') AND dataact <= TO_DATE('{filter_params.date_to.strftime('%Y-%m-%d')}', 'YYYY-MM-DD')))")
|
||||||
if filter_params.date_to:
|
elif filter_params.date_from:
|
||||||
where_conditions.append("dataact <= :date_to")
|
where_conditions.append(f"(dataact IS NULL OR dataact >= TO_DATE('{filter_params.date_from.strftime('%Y-%m-%d')}', 'YYYY-MM-DD'))")
|
||||||
params['date_to'] = filter_params.date_to
|
elif filter_params.date_to:
|
||||||
|
where_conditions.append(f"(dataact IS NULL OR dataact <= TO_DATE('{filter_params.date_to.strftime('%Y-%m-%d')}', 'YYYY-MM-DD'))")
|
||||||
|
|
||||||
if filter_params.partner_name:
|
if filter_params.partner_name:
|
||||||
where_conditions.append("UPPER(nume) LIKE UPPER(:partner_name)")
|
# Escape single quotes pentru SQL
|
||||||
params['partner_name'] = f"%{filter_params.partner_name}%"
|
partner_escaped = filter_params.partner_name.replace("'", "''")
|
||||||
|
where_conditions.append(f"UPPER(nume) LIKE UPPER('%{partner_escaped}%')")
|
||||||
|
|
||||||
|
if filter_params.bank_account:
|
||||||
|
# Escape single quotes pentru SQL
|
||||||
|
bank_escaped = filter_params.bank_account.replace("'", "''")
|
||||||
|
where_conditions.append(f"bancasa = '{bank_escaped}'")
|
||||||
|
|
||||||
|
where_clause = ""
|
||||||
if where_conditions:
|
if where_conditions:
|
||||||
base_query = f"SELECT * FROM ({base_query}) WHERE {' AND '.join(where_conditions)}"
|
where_clause = " WHERE " + " AND ".join(where_conditions)
|
||||||
|
|
||||||
# Count pentru paginare
|
|
||||||
count_query = f"SELECT COUNT(*) FROM ({base_query})"
|
|
||||||
cursor.execute(count_query, params)
|
|
||||||
total_count = cursor.fetchone()[0]
|
|
||||||
|
|
||||||
# Query cu paginare
|
|
||||||
base_query += " ORDER BY dataact DESC, nract"
|
|
||||||
|
|
||||||
|
# Paginare Oracle
|
||||||
offset = (filter_params.page - 1) * filter_params.page_size
|
offset = (filter_params.page - 1) * filter_params.page_size
|
||||||
limit = offset + filter_params.page_size
|
limit_val = 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)
|
# Bloc PL/SQL anonim care face totul într-o singură tranzacție:
|
||||||
rows = cursor.fetchall()
|
# 1. Obține anul și luna din calendar
|
||||||
|
# 2. Setează PACK_SESIUNE.SETAN și SETLUNA
|
||||||
|
# 3. Returnează datele prin REF CURSOR
|
||||||
|
# IMPORTANT: Folosim ROW_NUMBER() pentru paginare corectă cu ORDER BY NULLS FIRST
|
||||||
|
plsql_block = f"""
|
||||||
|
DECLARE
|
||||||
|
v_an NUMBER;
|
||||||
|
v_luna NUMBER;
|
||||||
|
v_cursor SYS_REFCURSOR;
|
||||||
|
BEGIN
|
||||||
|
-- Obține anul și luna curentă din calendar
|
||||||
|
SELECT anul, luna INTO v_an, v_luna
|
||||||
|
FROM {schema}.calendar
|
||||||
|
WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar);
|
||||||
|
|
||||||
|
-- Setează contextul de sesiune (OBLIGATORIU înainte de SELECT din vbancasa*)
|
||||||
|
{schema}.PACK_SESIUNE.SETAN(v_an);
|
||||||
|
{schema}.PACK_SESIUNE.SETLUNA(v_luna);
|
||||||
|
|
||||||
|
-- Return accounting period
|
||||||
|
:out_an := v_an;
|
||||||
|
:out_luna := v_luna;
|
||||||
|
|
||||||
|
-- Returnează datele prin cursor cu ROW_NUMBER pentru paginare corectă
|
||||||
|
-- Pentru rânduri cu dataact=NULL (solduri precedente), sortare după bancasa
|
||||||
|
-- Pentru rânduri cu date, sortare după data, număr, bancasa
|
||||||
|
OPEN :result_cursor FOR
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT t.*, ROW_NUMBER() OVER (
|
||||||
|
ORDER BY dataact ASC NULLS FIRST,
|
||||||
|
CASE WHEN dataact IS NULL THEN bancasa END ASC,
|
||||||
|
nract ASC NULLS FIRST,
|
||||||
|
bancasa ASC
|
||||||
|
) as rn
|
||||||
|
FROM ({base_select}) t{where_clause}
|
||||||
|
) WHERE rn > {offset} AND rn <= {offset + limit_val};
|
||||||
|
END;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Creează cursor pentru rezultate (oracledb.CURSOR pentru REF CURSOR)
|
||||||
|
result_cursor = cursor.var(oracledb.CURSOR)
|
||||||
|
out_an = cursor.var(int)
|
||||||
|
out_luna = cursor.var(int)
|
||||||
|
|
||||||
|
# Execută blocul PL/SQL cu REF CURSOR
|
||||||
|
cursor.execute(plsql_block, {'result_cursor': result_cursor, 'out_an': out_an, 'out_luna': out_luna})
|
||||||
|
|
||||||
|
# Get accounting period values
|
||||||
|
accounting_year = out_an.getvalue()
|
||||||
|
accounting_month = out_luna.getvalue()
|
||||||
|
|
||||||
|
# Obține rezultatele din cursor
|
||||||
|
ref_cursor = result_cursor.getvalue()
|
||||||
|
rows = ref_cursor.fetchall()
|
||||||
|
ref_cursor.close()
|
||||||
|
|
||||||
|
# Pentru count total, executăm alt bloc PL/SQL
|
||||||
|
count_plsql = f"""
|
||||||
|
DECLARE
|
||||||
|
v_an NUMBER;
|
||||||
|
v_luna NUMBER;
|
||||||
|
BEGIN
|
||||||
|
SELECT anul, luna INTO v_an, v_luna
|
||||||
|
FROM {schema}.calendar
|
||||||
|
WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar);
|
||||||
|
|
||||||
|
{schema}.PACK_SESIUNE.SETAN(v_an);
|
||||||
|
{schema}.PACK_SESIUNE.SETLUNA(v_luna);
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO :total_count FROM ({base_select}) sub{where_clause};
|
||||||
|
END;
|
||||||
|
"""
|
||||||
|
|
||||||
|
total_count_var = cursor.var(int)
|
||||||
|
cursor.execute(count_plsql, {'total_count': total_count_var})
|
||||||
|
total_count = total_count_var.getvalue()
|
||||||
|
|
||||||
# Procesare rezultate
|
# Procesare rezultate
|
||||||
registers = []
|
registers = []
|
||||||
@@ -146,23 +237,26 @@ class TreasuryService:
|
|||||||
total_plati = Decimal('0.00')
|
total_plati = Decimal('0.00')
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
# Skip ROWNUM
|
# Coloane: nume, nract, dataact, bancasa, incasari, plati, sold, valuta, tip_registru, explicatia, rn
|
||||||
|
# row[0-9] = date, row[10] = rn (ROW_NUMBER la final)
|
||||||
register_data = BankCashRegister(
|
register_data = BankCashRegister(
|
||||||
nume=row[1] or '',
|
nume=row[0] or '',
|
||||||
nract=row[2] or 0,
|
nract=row[1],
|
||||||
dataact=row[3],
|
dataact=row[2],
|
||||||
nume_cont_bancar=row[4] or '',
|
nume_cont_bancar=row[3] or '',
|
||||||
incasari=Decimal(str(row[5] or 0)),
|
incasari=Decimal(str(row[4] or 0)),
|
||||||
plati=Decimal(str(row[6] or 0)),
|
plati=Decimal(str(row[5] or 0)),
|
||||||
sold=Decimal(str(row[7] or 0)),
|
sold=Decimal(str(row[6] or 0)),
|
||||||
valuta=row[8],
|
valuta=row[7],
|
||||||
tip_registru=row[9],
|
tip_registru=row[8],
|
||||||
explicatia=row[10] or ''
|
explicatia=row[9] or ''
|
||||||
)
|
)
|
||||||
registers.append(register_data)
|
registers.append(register_data)
|
||||||
total_incasari += register_data.incasari
|
total_incasari += register_data.incasari
|
||||||
total_plati += register_data.plati
|
total_plati += register_data.plati
|
||||||
|
|
||||||
|
logger.info(f"Treasury query for company {company_id}, type={filter_params.register_type}: {len(registers)} records, total={total_count}")
|
||||||
|
|
||||||
return RegisterListResponse(
|
return RegisterListResponse(
|
||||||
registers=registers,
|
registers=registers,
|
||||||
total_count=total_count,
|
total_count=total_count,
|
||||||
@@ -171,5 +265,66 @@ class TreasuryService:
|
|||||||
total_plati=total_plati,
|
total_plati=total_plati,
|
||||||
page=filter_params.page,
|
page=filter_params.page,
|
||||||
page_size=filter_params.page_size,
|
page_size=filter_params.page_size,
|
||||||
has_more=len(registers) == filter_params.page_size
|
has_more=len(registers) == filter_params.page_size,
|
||||||
|
accounting_period=AccountingPeriod(an=accounting_year, luna=accounting_month)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@cached(cache_type='treasury', key_params=['company_id', 'register_type'])
|
||||||
|
async def get_bank_cash_accounts(company_id: int, register_type: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Obține lista distinctă de conturi bancă/casă (bancasa) pentru dropdown.
|
||||||
|
Cached pentru performanță.
|
||||||
|
IMPORTANT: Trebuie să setăm contextul PACK_SESIUNE înainte de a accesa vbancasa views!
|
||||||
|
"""
|
||||||
|
schema = await TreasuryService._get_schema(company_id)
|
||||||
|
|
||||||
|
# Map register_type to view
|
||||||
|
view_map = {
|
||||||
|
'BANCA_LEI': f'{schema}.vbancasa_5121_cum',
|
||||||
|
'BANCA_VALUTA': f'{schema}.vbancasa_5124_cum',
|
||||||
|
'CASA_LEI': f'{schema}.vbancasa_5311_cum',
|
||||||
|
'CASA_VALUTA': f'{schema}.vbancasa_5314_cum'
|
||||||
|
}
|
||||||
|
|
||||||
|
if register_type not in view_map:
|
||||||
|
return []
|
||||||
|
|
||||||
|
view_name = view_map[register_type]
|
||||||
|
|
||||||
|
async with oracle_pool.get_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# PL/SQL block to set session context and get accounts
|
||||||
|
plsql_block = f"""
|
||||||
|
DECLARE
|
||||||
|
v_an NUMBER;
|
||||||
|
v_luna NUMBER;
|
||||||
|
BEGIN
|
||||||
|
-- Get current year and month from calendar
|
||||||
|
SELECT anul, luna INTO v_an, v_luna
|
||||||
|
FROM {schema}.calendar
|
||||||
|
WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar);
|
||||||
|
|
||||||
|
-- Set session context (REQUIRED before accessing vbancasa* views)
|
||||||
|
{schema}.PACK_SESIUNE.SETAN(v_an);
|
||||||
|
{schema}.PACK_SESIUNE.SETLUNA(v_luna);
|
||||||
|
|
||||||
|
-- Return accounts via cursor
|
||||||
|
OPEN :result_cursor FOR
|
||||||
|
SELECT DISTINCT bancasa
|
||||||
|
FROM {view_name}
|
||||||
|
WHERE bancasa IS NOT NULL
|
||||||
|
ORDER BY bancasa;
|
||||||
|
END;
|
||||||
|
"""
|
||||||
|
|
||||||
|
result_cursor = cursor.var(oracledb.CURSOR)
|
||||||
|
cursor.execute(plsql_block, {'result_cursor': result_cursor})
|
||||||
|
|
||||||
|
ref_cursor = result_cursor.getvalue()
|
||||||
|
rows = ref_cursor.fetchall()
|
||||||
|
ref_cursor.close()
|
||||||
|
|
||||||
|
accounts = [row[0] for row in rows if row[0]]
|
||||||
|
logger.info(f"Found {len(accounts)} bank/cash accounts for company {company_id}, type={register_type}")
|
||||||
|
return accounts
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const useTreasuryStore = defineStore("treasury", () => {
|
|||||||
total_incasari: 0,
|
total_incasari: 0,
|
||||||
total_plati: 0,
|
total_plati: 0,
|
||||||
});
|
});
|
||||||
|
const accountingPeriod = ref({ an: null, luna: null });
|
||||||
|
|
||||||
const loadBankCashRegister = async (companyId, filters = {}) => {
|
const loadBankCashRegister = async (companyId, filters = {}) => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
@@ -39,6 +40,11 @@ export const useTreasuryStore = defineStore("treasury", () => {
|
|||||||
total_plati: response.data.total_plati,
|
total_plati: response.data.total_plati,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store accounting period if available
|
||||||
|
if (response.data.accounting_period) {
|
||||||
|
accountingPeriod.value = response.data.accounting_period;
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.detail || "Failed to load register";
|
error.value = err.response?.data?.detail || "Failed to load register";
|
||||||
@@ -57,6 +63,7 @@ export const useTreasuryStore = defineStore("treasury", () => {
|
|||||||
registers.value = [];
|
registers.value = [];
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
accountingPeriod.value = { an: null, luna: null };
|
||||||
pagination.value = {
|
pagination.value = {
|
||||||
page: 0,
|
page: 0,
|
||||||
rows: 50,
|
rows: 50,
|
||||||
@@ -70,6 +77,7 @@ export const useTreasuryStore = defineStore("treasury", () => {
|
|||||||
error,
|
error,
|
||||||
pagination,
|
pagination,
|
||||||
totals,
|
totals,
|
||||||
|
accountingPeriod,
|
||||||
loadBankCashRegister,
|
loadBankCashRegister,
|
||||||
setPagination,
|
setPagination,
|
||||||
reset,
|
reset,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const formatNumberForPDF = (value) => {
|
|||||||
* @param {Array} data - Array of objects to export
|
* @param {Array} data - Array of objects to export
|
||||||
* @param {Array} columns - Column definitions [{field: 'key', header: 'Display Name', type: 'text|number|currency', width: 30}]
|
* @param {Array} columns - Column definitions [{field: 'key', header: 'Display Name', type: 'text|number|currency', width: 30}]
|
||||||
* @param {String} filename - Name of the file (without extension)
|
* @param {String} filename - Name of the file (without extension)
|
||||||
* @param {Object} header - Header configuration {companyName: '', title: '', period: ''}
|
* @param {Object} header - Header configuration {companyName: '', title: '', period: '', subtitle2: '', initialBalances: [], totalInitialBalance: 0}
|
||||||
*/
|
*/
|
||||||
export const exportToPDF = (data, columns, filename, header) => {
|
export const exportToPDF = (data, columns, filename, header) => {
|
||||||
try {
|
try {
|
||||||
@@ -78,6 +78,9 @@ export const exportToPDF = (data, columns, filename, header) => {
|
|||||||
const marginRight = 8;
|
const marginRight = 8;
|
||||||
const contentWidth = pageWidth - marginLeft - marginRight;
|
const contentWidth = pageWidth - marginLeft - marginRight;
|
||||||
|
|
||||||
|
// Check if there are initial balances to display
|
||||||
|
const hasInitialBalances = header.initialBalances && header.initialBalances.length > 0;
|
||||||
|
|
||||||
// Function to add header (called for each page)
|
// Function to add header (called for each page)
|
||||||
const addHeader = () => {
|
const addHeader = () => {
|
||||||
// Line 1: Company name (left aligned, bold, larger font)
|
// Line 1: Company name (left aligned, bold, larger font)
|
||||||
@@ -100,12 +103,31 @@ export const exportToPDF = (data, columns, filename, header) => {
|
|||||||
const periodWidth = doc.getTextWidth(periodText);
|
const periodWidth = doc.getTextWidth(periodText);
|
||||||
const periodX = marginLeft + (contentWidth - periodWidth) / 2;
|
const periodX = marginLeft + (contentWidth - periodWidth) / 2;
|
||||||
doc.text(periodText, periodX, 32);
|
doc.text(periodText, periodX, 32);
|
||||||
|
|
||||||
|
// Line 4: Subtitle2 - filters (left aligned, below period) - optional
|
||||||
|
let currentY = 32;
|
||||||
|
if (header.subtitle2) {
|
||||||
|
currentY = 39;
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setFont(undefined, "normal");
|
||||||
|
doc.text(header.subtitle2, marginLeft, currentY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial Balances section - rendered just before table, closer to it
|
||||||
|
// This is handled in didDrawPage for first page only
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prepare table data
|
// Prepare table data and track total rows
|
||||||
const tableColumns = columns.map((col) => col.header);
|
const tableColumns = columns.map((col) => col.header);
|
||||||
const tableRows = data.map((row) =>
|
const totalRowIndices = new Set(); // Track which rows are totals
|
||||||
columns.map((col) => {
|
|
||||||
|
const tableRows = data.map((row, rowIndex) => {
|
||||||
|
// Track total rows for special styling
|
||||||
|
if (row._isTotal) {
|
||||||
|
totalRowIndices.add(rowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns.map((col) => {
|
||||||
const value = row[col.field];
|
const value = row[col.field];
|
||||||
if (col.type === "currency") {
|
if (col.type === "currency") {
|
||||||
return formatCurrency(value);
|
return formatCurrency(value);
|
||||||
@@ -113,8 +135,8 @@ export const exportToPDF = (data, columns, filename, header) => {
|
|||||||
return formatNumberForPDF(value);
|
return formatNumberForPDF(value);
|
||||||
}
|
}
|
||||||
return value || "-";
|
return value || "-";
|
||||||
}),
|
});
|
||||||
);
|
});
|
||||||
|
|
||||||
// Function to add footer (called for each page)
|
// Function to add footer (called for each page)
|
||||||
const addFooter = (pageNum, totalPages) => {
|
const addFooter = (pageNum, totalPages) => {
|
||||||
@@ -183,7 +205,64 @@ export const exportToPDF = (data, columns, filename, header) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableStartY = 36;
|
// Start table lower based on header content
|
||||||
|
let tableStartY = 36;
|
||||||
|
if (header.subtitle2) tableStartY = 43;
|
||||||
|
if (hasInitialBalances) {
|
||||||
|
// Initial balances rendered close to table (just 3mm above table header)
|
||||||
|
const balancesCount = header.initialBalances.length;
|
||||||
|
const hasTotal = balancesCount > 1;
|
||||||
|
const balancesHeight = (balancesCount * 5) + (hasTotal ? 7 : 0);
|
||||||
|
// Base position after header content
|
||||||
|
const baseY = header.subtitle2 ? 43 : 36;
|
||||||
|
tableStartY = baseY + balancesHeight + 5; // balances + small gap before table
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to draw initial balances (called only on first page)
|
||||||
|
const drawInitialBalances = (tableY) => {
|
||||||
|
if (!hasInitialBalances) return;
|
||||||
|
|
||||||
|
const valueRightEdge = pageWidth - marginRight;
|
||||||
|
const balancesCount = header.initialBalances.length;
|
||||||
|
const hasTotal = balancesCount > 1;
|
||||||
|
const balancesHeight = (balancesCount * 5) + (hasTotal ? 7 : 0);
|
||||||
|
|
||||||
|
// Start position: just above table header (3mm gap)
|
||||||
|
let y = tableY - 3 - (hasTotal ? 7 : 0) - (balancesCount * 5);
|
||||||
|
|
||||||
|
doc.setFont(undefined, "normal");
|
||||||
|
doc.setFontSize(9);
|
||||||
|
|
||||||
|
// Draw each balance line: "AccountName sold precedent: VALUE"
|
||||||
|
header.initialBalances.forEach((item) => {
|
||||||
|
const value = formatNumberForPDF(item.sold);
|
||||||
|
const valueWidth = doc.getTextWidth(value);
|
||||||
|
const label = `${item.accountName} sold precedent:`;
|
||||||
|
|
||||||
|
doc.text(label, valueRightEdge - valueWidth - doc.getTextWidth(" sold precedent:") - doc.getTextWidth(item.accountName) - 2, y);
|
||||||
|
doc.text(value, valueRightEdge - valueWidth, y);
|
||||||
|
y += 5;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Total only if multiple accounts
|
||||||
|
if (hasTotal) {
|
||||||
|
// Separator line
|
||||||
|
doc.setDrawColor(150, 150, 150);
|
||||||
|
doc.line(valueRightEdge - 40, y - 2, valueRightEdge, y - 2);
|
||||||
|
|
||||||
|
// Total line
|
||||||
|
doc.setFont(undefined, "bold");
|
||||||
|
const totalValue = formatNumberForPDF(header.totalInitialBalance || 0);
|
||||||
|
const totalValueWidth = doc.getTextWidth(totalValue);
|
||||||
|
const totalLabel = "TOTAL sold precedent:";
|
||||||
|
const totalLabelWidth = doc.getTextWidth(totalLabel);
|
||||||
|
|
||||||
|
doc.text(totalLabel, valueRightEdge - totalValueWidth - 3 - totalLabelWidth, y + 2);
|
||||||
|
doc.text(totalValue, valueRightEdge - totalValueWidth, y + 2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let isFirstPage = true;
|
||||||
|
|
||||||
// Add table using autoTable (call as function, not method)
|
// Add table using autoTable (call as function, not method)
|
||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
@@ -218,16 +297,28 @@ export const exportToPDF = (data, columns, filename, header) => {
|
|||||||
},
|
},
|
||||||
tableWidth: pageWidth - marginLeft - marginRight, // Use full page width
|
tableWidth: pageWidth - marginLeft - marginRight, // Use full page width
|
||||||
theme: "grid",
|
theme: "grid",
|
||||||
didDrawPage: function (data) {
|
didDrawPage: function () {
|
||||||
// Add header to each page
|
// Add header to each page
|
||||||
addHeader();
|
addHeader();
|
||||||
|
// Draw initial balances only on first page
|
||||||
|
if (isFirstPage && hasInitialBalances) {
|
||||||
|
drawInitialBalances(tableStartY);
|
||||||
|
isFirstPage = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
didParseCell: function (data) {
|
didParseCell: function (data) {
|
||||||
// Force alignment based on column type (body cells only)
|
// Force alignment based on column type (body cells only)
|
||||||
if (data.section === "body") {
|
if (data.section === "body") {
|
||||||
|
const rowIndex = data.row.index;
|
||||||
const colIndex = data.column.index;
|
const colIndex = data.column.index;
|
||||||
const column = columns[colIndex];
|
const column = columns[colIndex];
|
||||||
|
|
||||||
|
// Style total rows differently (bold, light gray background)
|
||||||
|
if (totalRowIndices.has(rowIndex)) {
|
||||||
|
data.cell.styles.fontStyle = "bold";
|
||||||
|
data.cell.styles.fillColor = [230, 230, 230]; // Light gray
|
||||||
|
}
|
||||||
|
|
||||||
if (column) {
|
if (column) {
|
||||||
if (column.type === "number" || column.type === "currency") {
|
if (column.type === "number" || column.type === "currency") {
|
||||||
data.cell.styles.halign = "right";
|
data.cell.styles.halign = "right";
|
||||||
@@ -351,6 +442,375 @@ export const exportSoldNetBreakdown = (summaryData) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export Bank Cash Register to PDF with grouped format
|
||||||
|
* Matches the Romanian standard format with:
|
||||||
|
* - Bank name + Sold precedent on same line
|
||||||
|
* - Daily totals (Total zi)
|
||||||
|
* - Cumulative totals (Total cumulat)
|
||||||
|
*
|
||||||
|
* @param {Array} data - Array of register entries
|
||||||
|
* @param {Object} header - Header configuration
|
||||||
|
* @param {String} filename - Output filename
|
||||||
|
*/
|
||||||
|
export const exportBankCashRegisterPDF = (data, header, filename) => {
|
||||||
|
try {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
console.error("No data to export");
|
||||||
|
return { success: false, error: "No data available" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = new jsPDF("landscape", "mm", "a4");
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||||||
|
const pageHeight = doc.internal.pageSize.getHeight();
|
||||||
|
const marginLeft = 8;
|
||||||
|
const marginRight = 8;
|
||||||
|
const contentWidth = pageWidth - marginLeft - marginRight;
|
||||||
|
|
||||||
|
// Remove diacritics helper
|
||||||
|
const removeDiacritics = (text) => {
|
||||||
|
if (!text) return "";
|
||||||
|
return text
|
||||||
|
.replace(/[ăâ]/gi, (m) => (m === m.toLowerCase() ? "a" : "A"))
|
||||||
|
.replace(/[î]/gi, (m) => (m === m.toLowerCase() ? "i" : "I"))
|
||||||
|
.replace(/[ș]/gi, (m) => (m === m.toLowerCase() ? "s" : "S"))
|
||||||
|
.replace(/[ț]/gi, (m) => (m === m.toLowerCase() ? "t" : "T"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Group data by bank account (bancasa)
|
||||||
|
const groupedByBank = {};
|
||||||
|
const initialBalances = {};
|
||||||
|
|
||||||
|
data.forEach((row) => {
|
||||||
|
const bankName = row.nume_cont_bancar || "Necunoscut";
|
||||||
|
if (!groupedByBank[bankName]) {
|
||||||
|
groupedByBank[bankName] = [];
|
||||||
|
initialBalances[bankName] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!row.dataact) {
|
||||||
|
// Initial balance row (null date) - sold precedent
|
||||||
|
initialBalances[bankName] = parseFloat(row.sold) || 0;
|
||||||
|
} else {
|
||||||
|
// Transaction row with date
|
||||||
|
groupedByBank[bankName].push(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Table columns definition
|
||||||
|
const tableColumns = [
|
||||||
|
"Data act",
|
||||||
|
"Nr.act",
|
||||||
|
"Explicatia",
|
||||||
|
"Incasari",
|
||||||
|
"Plati",
|
||||||
|
"Sold",
|
||||||
|
];
|
||||||
|
|
||||||
|
const columnWidths = {
|
||||||
|
0: contentWidth * 0.10, // Data act
|
||||||
|
1: contentWidth * 0.08, // Nr.act
|
||||||
|
2: contentWidth * 0.42, // Explicatia
|
||||||
|
3: contentWidth * 0.13, // Incasari
|
||||||
|
4: contentWidth * 0.13, // Plati
|
||||||
|
5: contentWidth * 0.14, // Sold
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnStyles = {};
|
||||||
|
Object.keys(columnWidths).forEach((idx) => {
|
||||||
|
columnStyles[idx] = { cellWidth: columnWidths[idx] };
|
||||||
|
if (idx >= 3) {
|
||||||
|
columnStyles[idx].halign = "right";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentY = 15;
|
||||||
|
let pageNum = 1;
|
||||||
|
|
||||||
|
// Function to add page header
|
||||||
|
const addPageHeader = () => {
|
||||||
|
// Company name (left)
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.setFont(undefined, "bold");
|
||||||
|
doc.text(removeDiacritics(header.companyName || ""), marginLeft, 12);
|
||||||
|
|
||||||
|
// Luna: MM / YYYY (right)
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setFont(undefined, "normal");
|
||||||
|
const lunaText = `Luna: ${header.luna || ""} / ${header.an || ""}`;
|
||||||
|
const lunaWidth = doc.getTextWidth(lunaText);
|
||||||
|
doc.text(lunaText, pageWidth - marginRight - lunaWidth, 12);
|
||||||
|
|
||||||
|
// Title centered
|
||||||
|
doc.setFontSize(13);
|
||||||
|
doc.setFont(undefined, "bold");
|
||||||
|
const titleWidth = doc.getTextWidth(header.title || "");
|
||||||
|
doc.text(header.title || "", marginLeft + (contentWidth - titleWidth) / 2, 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to check if we need a new page (for tables spanning multiple pages within a bank)
|
||||||
|
const checkNewPage = (neededHeight = 20) => {
|
||||||
|
if (currentY + neededHeight > pageHeight - 15) {
|
||||||
|
doc.addPage();
|
||||||
|
pageNum++;
|
||||||
|
addPageHeader();
|
||||||
|
currentY = 28;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process each bank account - each on a new page (sorted alphabetically)
|
||||||
|
const bankNames = Object.keys(groupedByBank).sort((a, b) => a.localeCompare(b, 'ro'));
|
||||||
|
|
||||||
|
bankNames.forEach((bankName, bankIndex) => {
|
||||||
|
const bankRows = groupedByBank[bankName];
|
||||||
|
const soldPrecedent = initialBalances[bankName] || 0;
|
||||||
|
|
||||||
|
// Start each bank/casa on a new page (except first one which is already on page 1)
|
||||||
|
if (bankIndex > 0) {
|
||||||
|
doc.addPage();
|
||||||
|
pageNum++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add full page header (company, title, luna/an)
|
||||||
|
addPageHeader();
|
||||||
|
currentY = 28;
|
||||||
|
|
||||||
|
// Bank/Casa header: "Banca: NAME" (left) + "Sold precedent: VALUE" (right)
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setFont(undefined, "bold");
|
||||||
|
const bankLabel = header.isBanca ? "Banca:" : "Casa:";
|
||||||
|
const bankHeaderText = `${bankLabel} ${removeDiacritics(bankName)}`;
|
||||||
|
doc.text(bankHeaderText, marginLeft, currentY);
|
||||||
|
|
||||||
|
const soldPrecedentText = `Sold precedent: ${formatNumberForPDF(soldPrecedent)}`;
|
||||||
|
const soldPrecedentWidth = doc.getTextWidth(soldPrecedentText);
|
||||||
|
doc.text(soldPrecedentText, pageWidth - marginRight - soldPrecedentWidth, currentY);
|
||||||
|
|
||||||
|
currentY += 6;
|
||||||
|
|
||||||
|
// Handle case when there are no transactions (only initial balance)
|
||||||
|
if (bankRows.length === 0) {
|
||||||
|
// Draw empty table with header only
|
||||||
|
autoTable(doc, {
|
||||||
|
head: [tableColumns],
|
||||||
|
body: [],
|
||||||
|
startY: currentY,
|
||||||
|
styles: {
|
||||||
|
fontSize: 8,
|
||||||
|
cellPadding: 1.5,
|
||||||
|
lineColor: [200, 200, 200],
|
||||||
|
lineWidth: 0.1,
|
||||||
|
},
|
||||||
|
headStyles: {
|
||||||
|
fillColor: [41, 128, 185],
|
||||||
|
textColor: 255,
|
||||||
|
fontStyle: "bold",
|
||||||
|
halign: "center",
|
||||||
|
fontSize: 8,
|
||||||
|
},
|
||||||
|
columnStyles: columnStyles,
|
||||||
|
margin: { left: marginLeft, right: marginRight },
|
||||||
|
tableWidth: contentWidth,
|
||||||
|
theme: "grid",
|
||||||
|
});
|
||||||
|
|
||||||
|
currentY = doc.lastAutoTable.finalY;
|
||||||
|
|
||||||
|
// Show total with sold precedent (no transactions)
|
||||||
|
const totalRows = [
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Total:",
|
||||||
|
formatNumberForPDF(0),
|
||||||
|
formatNumberForPDF(0),
|
||||||
|
formatNumberForPDF(soldPrecedent),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalsStartY = currentY;
|
||||||
|
|
||||||
|
autoTable(doc, {
|
||||||
|
body: totalRows,
|
||||||
|
startY: currentY,
|
||||||
|
styles: {
|
||||||
|
fontSize: 8,
|
||||||
|
cellPadding: 1.5,
|
||||||
|
fontStyle: "bold",
|
||||||
|
lineWidth: 0,
|
||||||
|
},
|
||||||
|
columnStyles: columnStyles,
|
||||||
|
margin: { left: marginLeft, right: marginRight },
|
||||||
|
tableWidth: contentWidth,
|
||||||
|
theme: "plain",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw outer border for totals box
|
||||||
|
const totalsEndY = doc.lastAutoTable.finalY;
|
||||||
|
doc.setDrawColor(200, 200, 200);
|
||||||
|
doc.setLineWidth(0.1);
|
||||||
|
doc.rect(marginLeft, totalsStartY, contentWidth, totalsEndY - totalsStartY);
|
||||||
|
|
||||||
|
currentY = doc.lastAutoTable.finalY + 3;
|
||||||
|
} else {
|
||||||
|
// Group bank rows by date
|
||||||
|
const groupedByDate = {};
|
||||||
|
bankRows.forEach((row) => {
|
||||||
|
const dateKey = row.dataact;
|
||||||
|
if (!groupedByDate[dateKey]) {
|
||||||
|
groupedByDate[dateKey] = [];
|
||||||
|
}
|
||||||
|
groupedByDate[dateKey].push(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cumulative totals for the bank
|
||||||
|
let cumulativeIncasari = 0;
|
||||||
|
let cumulativePlati = 0;
|
||||||
|
let lastSold = soldPrecedent;
|
||||||
|
|
||||||
|
const dates = Object.keys(groupedByDate).sort();
|
||||||
|
|
||||||
|
dates.forEach((dateKey, dateIndex) => {
|
||||||
|
const dateRows = groupedByDate[dateKey];
|
||||||
|
const dateFormatted = dateKey
|
||||||
|
? new Date(dateKey).toLocaleDateString("ro-RO")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
checkNewPage(30);
|
||||||
|
|
||||||
|
// Prepare rows for this date
|
||||||
|
const tableRows = [];
|
||||||
|
let dailyIncasari = 0;
|
||||||
|
let dailyPlati = 0;
|
||||||
|
|
||||||
|
dateRows.forEach((row) => {
|
||||||
|
const incasari = parseFloat(row.incasari) || 0;
|
||||||
|
const plati = parseFloat(row.plati) || 0;
|
||||||
|
|
||||||
|
dailyIncasari += incasari;
|
||||||
|
dailyPlati += plati;
|
||||||
|
lastSold = parseFloat(row.sold) || lastSold;
|
||||||
|
|
||||||
|
tableRows.push([
|
||||||
|
dateFormatted,
|
||||||
|
row.nract || "",
|
||||||
|
removeDiacritics(row.explicatia || row.nume || ""),
|
||||||
|
formatNumberForPDF(incasari),
|
||||||
|
formatNumberForPDF(plati),
|
||||||
|
formatNumberForPDF(row.sold),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
cumulativeIncasari += dailyIncasari;
|
||||||
|
cumulativePlati += dailyPlati;
|
||||||
|
|
||||||
|
// Draw table for this date group
|
||||||
|
autoTable(doc, {
|
||||||
|
head: dateIndex === 0 ? [tableColumns] : [],
|
||||||
|
body: tableRows,
|
||||||
|
startY: currentY,
|
||||||
|
styles: {
|
||||||
|
fontSize: 8,
|
||||||
|
cellPadding: 1.5,
|
||||||
|
lineColor: [200, 200, 200],
|
||||||
|
lineWidth: 0.1,
|
||||||
|
overflow: "linebreak",
|
||||||
|
},
|
||||||
|
headStyles: {
|
||||||
|
fillColor: [41, 128, 185],
|
||||||
|
textColor: 255,
|
||||||
|
fontStyle: "bold",
|
||||||
|
halign: "center",
|
||||||
|
fontSize: 8,
|
||||||
|
},
|
||||||
|
columnStyles: columnStyles,
|
||||||
|
margin: { left: marginLeft, right: marginRight },
|
||||||
|
tableWidth: contentWidth,
|
||||||
|
theme: "grid",
|
||||||
|
showHead: dateIndex === 0 ? "firstPage" : "never",
|
||||||
|
});
|
||||||
|
|
||||||
|
currentY = doc.lastAutoTable.finalY;
|
||||||
|
|
||||||
|
// Daily total + Cumulative total rows in same box
|
||||||
|
checkNewPage(16);
|
||||||
|
|
||||||
|
const totalRows = [
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`Total zi: ${dateFormatted}`,
|
||||||
|
formatNumberForPDF(dailyIncasari),
|
||||||
|
formatNumberForPDF(dailyPlati),
|
||||||
|
"Sold",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Total cumulat:",
|
||||||
|
formatNumberForPDF(cumulativeIncasari),
|
||||||
|
formatNumberForPDF(cumulativePlati),
|
||||||
|
formatNumberForPDF(lastSold),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalsStartY = currentY;
|
||||||
|
|
||||||
|
autoTable(doc, {
|
||||||
|
body: totalRows,
|
||||||
|
startY: currentY,
|
||||||
|
styles: {
|
||||||
|
fontSize: 8,
|
||||||
|
cellPadding: 1.5,
|
||||||
|
fontStyle: "bold",
|
||||||
|
lineWidth: 0,
|
||||||
|
},
|
||||||
|
columnStyles: columnStyles,
|
||||||
|
margin: { left: marginLeft, right: marginRight },
|
||||||
|
tableWidth: contentWidth,
|
||||||
|
theme: "plain",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw outer border for totals box (no internal lines)
|
||||||
|
const totalsEndY = doc.lastAutoTable.finalY;
|
||||||
|
doc.setDrawColor(200, 200, 200);
|
||||||
|
doc.setLineWidth(0.1);
|
||||||
|
doc.rect(marginLeft, totalsStartY, contentWidth, totalsEndY - totalsStartY);
|
||||||
|
|
||||||
|
currentY = doc.lastAutoTable.finalY + 3;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add footer to all pages (Generat: DATE on left, Pagina X din Y on right)
|
||||||
|
const totalPages = doc.internal.getNumberOfPages();
|
||||||
|
const generatedText = `Generat: ${new Date().toLocaleString("ro-RO")}`;
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
doc.setPage(i);
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.setFont(undefined, "normal");
|
||||||
|
const footerY = pageHeight - 8;
|
||||||
|
|
||||||
|
// Left: Generated date
|
||||||
|
doc.text(generatedText, marginLeft, footerY);
|
||||||
|
|
||||||
|
// Right: Page number
|
||||||
|
const pageText = `Pagina ${i} din ${totalPages}`;
|
||||||
|
const pageTextWidth = doc.getTextWidth(pageText);
|
||||||
|
doc.text(pageText, pageWidth - marginRight - pageTextWidth, footerY);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.save(`${filename}_${new Date().toISOString().split("T")[0]}.pdf`);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Bank Cash Register PDF export error:", error);
|
||||||
|
return { success: false, error: error.message || "PDF generation failed" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export Trend Data
|
* Export Trend Data
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -797,7 +797,15 @@ const searchData = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watchers - removed unused watchers
|
// Watch for company changes - reload dashboard when company changes
|
||||||
|
watch(
|
||||||
|
() => companyStore.selectedCompany,
|
||||||
|
async (newCompany) => {
|
||||||
|
if (newCompany) {
|
||||||
|
await loadDashboardData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user