Replaces "luna prec / luna curentă" columns with a clearer accounting reconciliation flow: Datorat (owed) → Achitat (paid) → Sold (remaining). Backend models gain 3 new fields per sub-account and group. Frontend shows ✓ when a debt is fully cleared. Mobile TVA card now shows both total and remaining sold. SwipeableCards gains fixedDots/fillHeight props for better layout above MobileBottomNav. Telegram formatter updated to use new fields and drops redundant RON suffix from amounts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
693 lines
25 KiB
Python
693 lines
25 KiB
Python
"""
|
|
Response formatters for bot commands.
|
|
Formats API responses into user-friendly Telegram messages.
|
|
"""
|
|
|
|
from typing import Dict, List, Any
|
|
|
|
|
|
def format_dashboard_response(data: Dict[str, Any], company_name: str = None) -> str:
|
|
"""
|
|
Format dashboard data for Telegram (content only, no header).
|
|
|
|
Note: company_name parameter kept for backwards compatibility but not used.
|
|
Use format_response_with_company() in handlers to add company header.
|
|
"""
|
|
text = ""
|
|
|
|
# Sold total trezorerie (casa + banca) - rotunjit la leu
|
|
treasury_totals = data.get('treasury_totals_by_currency', {})
|
|
sold_trezorerie = round(float(treasury_totals.get('RON', 0)))
|
|
text += f"**Sold Trezorerie:** {sold_trezorerie:,}\n\n"
|
|
|
|
# Sold Clienți - rotunjit la leu
|
|
clienti_sold = round(float(data.get('clienti_sold_total', 0)))
|
|
clienti_in_termen = round(float(data.get('clienti_sold_in_termen', 0)))
|
|
clienti_restant = round(float(data.get('clienti_sold_restant', 0)))
|
|
|
|
text += f"**Sold Clienți:** {clienti_sold:,}\n"
|
|
text += f" - În termen: {clienti_in_termen:,}\n"
|
|
text += f" - Restanță: {clienti_restant:,}\n\n"
|
|
|
|
# Sold Furnizori BRUT (pentru consistență cu detaliile) - rotunjit la leu
|
|
furnizori_in_termen = round(float(data.get('furnizori_sold_in_termen', 0)))
|
|
furnizori_restant = round(float(data.get('furnizori_sold_restant', 0)))
|
|
furnizori_sold_brut = furnizori_in_termen + furnizori_restant
|
|
furnizori_avansuri = round(float(data.get('furnizori_avansuri', 0)))
|
|
furnizori_sold_net = round(float(data.get('furnizori_sold_total', 0)))
|
|
|
|
text += f"**Sold Furnizori:** {furnizori_sold_brut:,}\n"
|
|
text += f" - În termen: {furnizori_in_termen:,}\n"
|
|
text += f" - Restanță: {furnizori_restant:,}\n"
|
|
if furnizori_avansuri != 0:
|
|
text += f" - Avansuri: {furnizori_avansuri:,}\n"
|
|
text += f" - Net (după avansuri): {furnizori_sold_net:,}"
|
|
else:
|
|
text += f" - Net: {furnizori_sold_net:,}"
|
|
|
|
# Datorii la Buget - două secțiuni: luna precedentă și luna curentă
|
|
budget_breakdown = data.get('budget_debt_breakdown', [])
|
|
if budget_breakdown:
|
|
grupe_prec = [g for g in budget_breakdown if round(float(g.get('datorat', g.get('precedent', 0)))) > 0]
|
|
grupe_crt = [g for g in budget_breakdown if round(float(g.get('curent', 0))) > 0]
|
|
|
|
if grupe_prec or grupe_crt:
|
|
text += "\n\n**Datorii la Buget:**\n"
|
|
|
|
if grupe_prec:
|
|
total_sold = sum(round(float(g.get('sold', 0))) for g in grupe_prec)
|
|
total_dat = sum(round(float(g.get('datorat', g.get('precedent', 0)))) for g in grupe_prec)
|
|
sold_total_str = f"{total_sold:,}" if total_sold > 0 else "0 \u2713"
|
|
text += f"\n _Precedent: dat: {total_dat:,}, sold: {sold_total_str}_\n"
|
|
for g in grupe_prec:
|
|
datorat = round(float(g.get('datorat', g.get('precedent', 0))))
|
|
sold = round(float(g.get('sold', 0)))
|
|
label = g.get('label', '')
|
|
sold_str = f"{sold:,}" if sold > 0 else "0 \u2713"
|
|
text += f" {label:<6} {datorat:,} · {sold_str}\n"
|
|
|
|
if grupe_crt:
|
|
items = [f"{g.get('label', '')} {round(float(g.get('curent', 0))):,}" for g in grupe_crt]
|
|
total_crt = sum(round(float(g.get('curent', 0))) for g in grupe_crt)
|
|
text += f"\n _Curent: {' \u00b7 '.join(items)} = {total_crt:,}_\n"
|
|
|
|
return text
|
|
|
|
|
|
def format_invoices_response(
|
|
invoices: List[Dict[str, Any]],
|
|
company_name: str = None,
|
|
limit: int = 10
|
|
) -> str:
|
|
"""
|
|
Format invoices list for Telegram - COMPACT TABLE FORMAT.
|
|
|
|
Args:
|
|
invoices: List of invoice dicts
|
|
company_name: Company name (kept for compatibility, not used)
|
|
limit: Maximum number of invoices to display
|
|
|
|
Returns:
|
|
Formatted Markdown string for Telegram (compact, no emojis)
|
|
"""
|
|
if not invoices:
|
|
return "Nu s-au gasit facturi cu aceste criterii."
|
|
|
|
# Header (o singură dată)
|
|
text = f"**Facturi** ({len(invoices)} total)\n\n"
|
|
text += "Nr | Client | Suma | Status\n"
|
|
text += "---|--------|------|-------\n"
|
|
|
|
# Lista facturi - compact, o linie per factură
|
|
for idx, inv in enumerate(invoices[:limit], 1):
|
|
seria = inv.get('seria', '')
|
|
numar = inv.get('numar', '')
|
|
client = inv.get('client', 'N/A')
|
|
suma = inv.get('suma_totala', 0)
|
|
status = inv.get('status', 'N/A')
|
|
|
|
# Truncate long client names for compact display
|
|
client_short = client[:20] + "..." if len(client) > 20 else client
|
|
|
|
# Status marker (no emoji)
|
|
status_marker = "PLATIT" if status == "platit" else "NEPLATIT"
|
|
|
|
text += f"{seria}{numar} | {client_short} | {suma:,.0f} | {status_marker}\n"
|
|
|
|
if len(invoices) > limit:
|
|
text += f"\n+{len(invoices) - limit} facturi"
|
|
|
|
return text
|
|
|
|
|
|
# =========================================================================
|
|
# FAZA 2: New Formatter Functions for Button Interface
|
|
# =========================================================================
|
|
|
|
|
|
def format_treasury_casa_response(data: Dict[str, Any], company_name: str = None) -> str:
|
|
"""
|
|
Format treasury CASH data for Telegram (content only, no header).
|
|
|
|
Args:
|
|
data: Dict with casa accounts and total from treasury breakdown
|
|
company_name: Company name (kept for compatibility, not used)
|
|
|
|
Returns:
|
|
Formatted Markdown string for Telegram
|
|
|
|
Example:
|
|
data = {'accounts': [...], 'total': 5000}
|
|
text = format_treasury_casa_response(data)
|
|
"""
|
|
text = ""
|
|
|
|
# Total cash balance - rotunjit la leu (0 zecimale)
|
|
total_cash = round(data.get('total', 0))
|
|
text += f"**Sold Total Cash:** {total_cash:,} RON\n\n"
|
|
|
|
# Cash accounts
|
|
casa_accounts = data.get('accounts', [])
|
|
if casa_accounts:
|
|
text += "**Conturi de Casa:**\n"
|
|
for acc in casa_accounts: # Show all accounts
|
|
name = acc.get('name', 'N/A')
|
|
balance = round(acc.get('balance', 0))
|
|
text += f" - {name}: {balance:,} RON\n"
|
|
else:
|
|
text += "Nu exista conturi de casa configurate."
|
|
|
|
return text
|
|
|
|
|
|
def format_treasury_banca_response(data: Dict[str, Any], company_name: str = None) -> str:
|
|
"""
|
|
Format treasury BANK data for Telegram (content only, no header).
|
|
|
|
Args:
|
|
data: Dict with banca accounts and total from treasury breakdown
|
|
company_name: Company name (kept for compatibility, not used)
|
|
|
|
Returns:
|
|
Formatted Markdown string for Telegram
|
|
|
|
Example:
|
|
data = {'accounts': [...], 'total': 15000}
|
|
text = format_treasury_banca_response(data)
|
|
"""
|
|
text = ""
|
|
|
|
# Total bank balance - rotunjit la leu (0 zecimale)
|
|
total_bank = round(data.get('total', 0))
|
|
text += f"**Sold Total Banca:** {total_bank:,} RON\n\n"
|
|
|
|
# Bank accounts
|
|
bank_accounts = data.get('accounts', [])
|
|
if bank_accounts:
|
|
text += "**Conturi Bancare:**\n"
|
|
for acc in bank_accounts: # Show all accounts
|
|
name = acc.get('name', 'N/A')
|
|
balance = round(acc.get('balance', 0))
|
|
text += f" - {name}: {balance:,} RON\n"
|
|
else:
|
|
text += "Nu exista conturi bancare configurate."
|
|
|
|
return text
|
|
|
|
|
|
def format_treasury_combined_response(data: Dict[str, Any], company_name: str = None) -> str:
|
|
"""
|
|
Format combined treasury data (Casa + Banca) for Telegram.
|
|
|
|
Shows grand total, Casa section with accounts, and Banca section with accounts
|
|
in a single unified message. Compact format without section titles.
|
|
|
|
Args:
|
|
data: Dict with 'casa' and 'banca' keys from get_treasury_breakdown_split()
|
|
company_name: Company name (kept for compatibility, not used)
|
|
|
|
Returns:
|
|
Formatted Markdown string with grand total and both sections
|
|
|
|
Example:
|
|
data = {'casa': {...}, 'banca': {...}}
|
|
text = format_treasury_combined_response(data)
|
|
"""
|
|
def format_amount(amount: int) -> str:
|
|
"""Format amount with period as thousands separator (Romanian style)."""
|
|
return f"{amount:,}".replace(",", ".")
|
|
|
|
text = ""
|
|
|
|
# Extract totals - rounded to whole RON
|
|
casa_total = round(data.get('casa', {}).get('total', 0))
|
|
banca_total = round(data.get('banca', {}).get('total', 0))
|
|
grand_total = casa_total + banca_total
|
|
|
|
# Grand total header
|
|
text += f"**Sold Trezorerie:** {format_amount(grand_total)} RON\n\n"
|
|
|
|
# Casa section - compact
|
|
text += f"**Casa:** {format_amount(casa_total)} RON\n"
|
|
casa_accounts = data.get('casa', {}).get('accounts', [])
|
|
if casa_accounts:
|
|
for acc in casa_accounts:
|
|
name = acc.get('name', 'N/A')
|
|
balance = round(acc.get('balance', 0))
|
|
text += f" - {name}: {format_amount(balance)} RON\n"
|
|
|
|
text += "\n"
|
|
|
|
# Banca section - compact
|
|
text += f"**Banca:** {format_amount(banca_total)} RON\n"
|
|
banca_accounts = data.get('banca', {}).get('accounts', [])
|
|
if banca_accounts:
|
|
for acc in banca_accounts:
|
|
name = acc.get('name', 'N/A')
|
|
balance = round(acc.get('balance', 0))
|
|
text += f" - {name}: {format_amount(balance)} RON\n"
|
|
|
|
return text
|
|
|
|
|
|
def format_clients_balance_response(
|
|
clients: List[Dict[str, Any]],
|
|
maturity_data: Dict[str, Any],
|
|
company_name: str = None
|
|
) -> str:
|
|
"""
|
|
Format clients balance with maturity breakdown (content only, no header).
|
|
|
|
Args:
|
|
clients: List of client dicts with id, name, balance
|
|
maturity_data: Dict with in_term, overdue, total
|
|
company_name: Company name (kept for compatibility, not used)
|
|
|
|
Returns:
|
|
Formatted Markdown string for Telegram
|
|
|
|
Example:
|
|
clients = [{'id': 1, 'name': 'Client A', 'balance': 15000}]
|
|
maturity = {'in_term': 10000, 'overdue': 5000, 'total': 15000}
|
|
text = format_clients_balance_response(clients, maturity)
|
|
"""
|
|
text = ""
|
|
|
|
# Maturity breakdown - rotunjit la leu (0 zecimale)
|
|
total = round(maturity_data.get('total', 0))
|
|
in_term = round(maturity_data.get('in_term', 0))
|
|
overdue = round(maturity_data.get('overdue', 0))
|
|
|
|
text += f"**Sold Total:** {total:,} RON\n\n"
|
|
|
|
text += "**Defalcare:**\n"
|
|
text += f" - In termen: {in_term:,} RON\n"
|
|
text += f" - Restanta: {overdue:,} RON\n\n"
|
|
|
|
# Top 10 clients
|
|
if clients:
|
|
text += f"**Top 10 Clienti** ({len(clients)} total):\n"
|
|
# Sort by balance descending
|
|
sorted_clients = sorted(
|
|
clients,
|
|
key=lambda x: x.get('balance', 0),
|
|
reverse=True
|
|
)
|
|
|
|
for idx, client in enumerate(sorted_clients[:10], 1):
|
|
name = client.get('name', 'N/A')
|
|
balance = round(client.get('balance', 0))
|
|
text += f"{idx}. {name}: {balance:,} RON\n"
|
|
|
|
if len(clients) > 10:
|
|
text += f"\nApasa butonul pentru lista completa"
|
|
else:
|
|
text += "Nu exista clienti cu solduri."
|
|
|
|
return text
|
|
|
|
|
|
def format_suppliers_balance_response(
|
|
suppliers: List[Dict[str, Any]],
|
|
maturity_data: Dict[str, Any],
|
|
company_name: str = None
|
|
) -> str:
|
|
"""
|
|
Format suppliers balance with maturity breakdown (content only, no header).
|
|
|
|
Args:
|
|
suppliers: List of supplier dicts with id, name, balance
|
|
maturity_data: Dict with in_term, overdue, total
|
|
company_name: Company name (kept for compatibility, not used)
|
|
|
|
Returns:
|
|
Formatted Markdown string for Telegram
|
|
|
|
Example:
|
|
suppliers = [{'id': 1, 'name': 'Supplier A', 'balance': 5000}]
|
|
maturity = {'in_term': 4000, 'overdue': 1000, 'total': 5000}
|
|
text = format_suppliers_balance_response(suppliers, maturity)
|
|
"""
|
|
text = ""
|
|
|
|
# Maturity breakdown - rotunjit la leu (0 zecimale)
|
|
total = round(maturity_data.get('total', 0))
|
|
in_term = round(maturity_data.get('in_term', 0))
|
|
overdue = round(maturity_data.get('overdue', 0))
|
|
|
|
text += f"**Sold Total:** {total:,} RON\n\n"
|
|
|
|
text += "**Defalcare:**\n"
|
|
text += f" - In termen: {in_term:,} RON\n"
|
|
text += f" - Restanta: {overdue:,} RON\n\n"
|
|
|
|
# Top 10 suppliers
|
|
if suppliers:
|
|
text += f"**Top 10 Furnizori** ({len(suppliers)} total):\n"
|
|
# Sort by balance descending
|
|
sorted_suppliers = sorted(
|
|
suppliers,
|
|
key=lambda x: x.get('balance', 0),
|
|
reverse=True
|
|
)
|
|
|
|
for idx, supplier in enumerate(sorted_suppliers[:10], 1):
|
|
name = supplier.get('name', 'N/A')
|
|
balance = round(supplier.get('balance', 0))
|
|
text += f"{idx}. {name}: {balance:,} RON\n"
|
|
|
|
if len(suppliers) > 10:
|
|
text += f"\nApasa butonul pentru lista completa"
|
|
else:
|
|
text += "Nu exista furnizori cu solduri."
|
|
|
|
return text
|
|
|
|
|
|
def format_cashflow_evolution_response(
|
|
performance_data: Dict[str, Any],
|
|
monthly_data: Dict[str, Any],
|
|
company_name: str = None
|
|
) -> str:
|
|
"""
|
|
Format cash flow evolution data - Table format with mini-charts.
|
|
|
|
Args:
|
|
performance_data: Dict with current_year and previous_year YTD data
|
|
monthly_data: Dict with months, incasari, plati arrays + prev year data
|
|
company_name: Company name (kept for compatibility, not used)
|
|
|
|
Returns:
|
|
Formatted Markdown string for Telegram (monospace table)
|
|
|
|
Example:
|
|
YTD 2024 vs 2023:
|
|
2024 2023 Δ Trend
|
|
Inc: 500,000 480,000 +4.2% ████░
|
|
Plt: 450,000 440,000 +2.3% ███░
|
|
Net: 50,000 40,000 +25.0% █████
|
|
"""
|
|
text = ""
|
|
|
|
# Helper functions
|
|
def calc_percent_change(current: float, previous: float) -> str:
|
|
"""Calculate percentage change: +4.2% or -3.5%"""
|
|
if previous == 0:
|
|
return "+100%" if current > 0 else "0.0%"
|
|
change = ((current - previous) / previous) * 100
|
|
sign = "+" if change >= 0 else ""
|
|
return f"{sign}{change:.1f}%"
|
|
|
|
def create_mini_chart(current: float, previous: float, width: int = 5) -> str:
|
|
"""Create mini bar chart: ████░ (proportional bars)"""
|
|
if current == 0 and previous == 0:
|
|
return "─" * width
|
|
|
|
max_val = max(current, previous)
|
|
if max_val == 0:
|
|
return "─" * width
|
|
|
|
curr_bars = int((current / max_val) * width)
|
|
prev_bars = int((previous / max_val) * width)
|
|
|
|
# Use filled and light blocks
|
|
filled = "█" * curr_bars
|
|
light = "░" * (width - curr_bars)
|
|
return filled + light
|
|
|
|
def get_trend_arrow(current: float, previous: float) -> str:
|
|
"""Get trend arrow: ↑ or ↓ or →"""
|
|
if current > previous * 1.02: # More than 2% increase
|
|
return "↑"
|
|
elif current < previous * 0.98: # More than 2% decrease
|
|
return "↓"
|
|
else:
|
|
return "→"
|
|
|
|
# Extract YTD data
|
|
current = performance_data.get('current_year', {})
|
|
previous = performance_data.get('previous_year', {})
|
|
|
|
current_year = current.get('year', '2024')
|
|
previous_year = previous.get('year', '2023')
|
|
|
|
inc_cur = round(current.get('incasari', 0))
|
|
plt_cur = round(current.get('plati', 0))
|
|
net_cur = round(current.get('net', 0))
|
|
|
|
inc_prev = round(previous.get('incasari', 0))
|
|
plt_prev = round(previous.get('plati', 0))
|
|
net_prev = round(previous.get('net', 0))
|
|
|
|
# YTD Table Header
|
|
text += f"**YTD {current_year} vs {previous_year}:**\n"
|
|
text += f"` {current_year:>10} {previous_year:>10} Δ `\n"
|
|
|
|
# YTD Rows
|
|
inc_pct = calc_percent_change(inc_cur, inc_prev)
|
|
text += f"`Inc: {inc_cur:>10,} {inc_prev:>10,} {inc_pct:>6}`\n"
|
|
|
|
plt_pct = calc_percent_change(plt_cur, plt_prev)
|
|
text += f"`Plt: {plt_cur:>10,} {plt_prev:>10,} {plt_pct:>6}`\n"
|
|
|
|
net_pct = calc_percent_change(net_cur, net_prev)
|
|
text += f"`Net: {net_cur:>10,} {net_prev:>10,} {net_pct:>6}`\n\n"
|
|
|
|
# Monthly Evolution Table - Simplified (Net only)
|
|
months = monthly_data.get('months', [])
|
|
incasari = monthly_data.get('incasari', [])
|
|
plati = monthly_data.get('plati', [])
|
|
incasari_prev = monthly_data.get('incasari_prev', [])
|
|
plati_prev = monthly_data.get('plati_prev', [])
|
|
|
|
if months and len(months) > 0:
|
|
text += "**Evolutie Net (12 luni):**\n"
|
|
text += f"` {current_year:>10} {previous_year:>10} Δ `\n"
|
|
|
|
for i, month in enumerate(months):
|
|
inc = incasari[i] if i < len(incasari) else 0
|
|
plt = plati[i] if i < len(plati) else 0
|
|
inc_p = incasari_prev[i] if i < len(incasari_prev) else 0
|
|
plt_p = plati_prev[i] if i < len(plati_prev) else 0
|
|
|
|
net = inc - plt
|
|
net_p = inc_p - plt_p
|
|
|
|
# Extract short month name (first 3 chars before apostrophe)
|
|
month_short = month.split("'")[0][:3] if "'" in month else month[:3]
|
|
|
|
# Calculate percentage change
|
|
net_pct = calc_percent_change(net, net_p)
|
|
|
|
# Format row: Luna Net'current Net'prev Δ (aligned with YTD)
|
|
text += f"`{month_short:<4} {int(net):>10,} {int(net_p):>10,} {net_pct:>6}`\n"
|
|
else:
|
|
text += "Nu exista date lunare disponibile."
|
|
|
|
return text
|
|
|
|
|
|
def format_client_detail_response(
|
|
client: Dict[str, Any],
|
|
invoices: List[Dict[str, Any]],
|
|
company_name: str = None
|
|
) -> str:
|
|
"""
|
|
Format client details with invoices - COMPACT TABLE FORMAT.
|
|
|
|
Args:
|
|
client: Dict with client info (id, name, balance)
|
|
invoices: List of invoice dicts for this client
|
|
company_name: Company name (kept for compatibility, not used)
|
|
|
|
Returns:
|
|
Formatted Markdown string for Telegram (compact, no emojis)
|
|
|
|
Example:
|
|
client = {'id': 1, 'name': 'Client A', 'balance': 15000}
|
|
invoices = [{'id': 1, 'number': 'FV001', 'amount': 5000, 'status': 'unpaid'}]
|
|
text = format_client_detail_response(client, invoices)
|
|
"""
|
|
client_name = client.get('name', 'N/A')
|
|
balance = client.get('balance', 0)
|
|
|
|
# Header with client info
|
|
text = f"**{client_name}**\n"
|
|
text += f"**Sold total: {balance:,.2f} RON**"
|
|
if invoices and len(invoices) > 1:
|
|
text += f" • {len(invoices)} facturi"
|
|
text += "\n\n"
|
|
|
|
# Invoices - compact table format (no emojis)
|
|
if invoices:
|
|
from datetime import datetime
|
|
|
|
# Sort invoices by date (most recent first)
|
|
sorted_invoices = sorted(invoices, key=lambda x: x.get('dataact') or datetime.min, reverse=True)
|
|
|
|
# Invoice list - simple format without table
|
|
text += "Facturi cu sold:\n"
|
|
text += "━━━━━━━━━━━━━━━━━━━━\n"
|
|
|
|
# Invoice rows - one line each, simple format
|
|
for inv in sorted_invoices[:10]:
|
|
# Backend returns: nract, totctva, soldfinal, datascad, dataact, achitat
|
|
number = str(inv.get('nract', 'N/A'))
|
|
dataact = inv.get('dataact')
|
|
|
|
# Parse date - handle various formats to ensure dd.mm.yyyy
|
|
if dataact:
|
|
if isinstance(dataact, str):
|
|
try:
|
|
# Try ISO format first: "2024-10-25" or "2024-10-25 00:00:00"
|
|
if '-' in dataact and len(dataact) >= 10:
|
|
parsed_date = datetime.strptime(dataact[:10], '%Y-%m-%d')
|
|
date_str = parsed_date.strftime('%d.%m.%Y')
|
|
# Already in dd.mm.yyyy format
|
|
elif '.' in dataact:
|
|
date_str = dataact.split()[0][:10] # Take just date part
|
|
else:
|
|
date_str = dataact[:10] if len(dataact) >= 10 else dataact
|
|
except:
|
|
date_str = dataact[:10] if len(dataact) >= 10 else dataact
|
|
else:
|
|
# Datetime object - format as dd.mm.yyyy
|
|
date_str = dataact.strftime('%d.%m.%Y')
|
|
else:
|
|
date_str = 'N/A'
|
|
|
|
sold = float(inv.get('soldfinal', 0) or 0)
|
|
|
|
# Simple format: Nr • Data • Sold
|
|
text += f"Nr {number} • {date_str} • {sold:,.2f} RON\n"
|
|
|
|
if len(invoices) > 10:
|
|
text += f"\n\n+{len(invoices) - 10} facturi"
|
|
else:
|
|
text += "Nu exista facturi neachitate"
|
|
|
|
return text
|
|
|
|
|
|
def format_supplier_detail_response(
|
|
supplier: Dict[str, Any],
|
|
invoices: List[Dict[str, Any]],
|
|
company_name: str = None
|
|
) -> str:
|
|
"""
|
|
Format supplier details with invoices - COMPACT TABLE FORMAT.
|
|
|
|
Args:
|
|
supplier: Dict with supplier info (id, name, balance)
|
|
invoices: List of invoice dicts for this supplier
|
|
company_name: Company name (kept for compatibility, not used)
|
|
|
|
Returns:
|
|
Formatted Markdown string for Telegram (compact, no emojis)
|
|
|
|
Example:
|
|
supplier = {'id': 1, 'name': 'Supplier A', 'balance': 5000}
|
|
invoices = [{'id': 1, 'number': 'FC001', 'amount': 2000, 'status': 'unpaid'}]
|
|
text = format_supplier_detail_response(supplier, invoices)
|
|
"""
|
|
supplier_name = supplier.get('name', 'N/A')
|
|
balance = supplier.get('balance', 0)
|
|
|
|
# Header with supplier info
|
|
text = f"**{supplier_name}**\n"
|
|
text += f"**Sold total: {balance:,.2f} RON**"
|
|
if invoices and len(invoices) > 1:
|
|
text += f" • {len(invoices)} facturi"
|
|
text += "\n\n"
|
|
|
|
# Invoices - compact table format (no emojis)
|
|
if invoices:
|
|
from datetime import datetime
|
|
|
|
# Sort invoices by date (most recent first)
|
|
sorted_invoices = sorted(invoices, key=lambda x: x.get('dataact') or datetime.min, reverse=True)
|
|
|
|
# Invoice list - simple format without table
|
|
text += "Facturi cu sold:\n"
|
|
text += "━━━━━━━━━━━━━━━━━━━━\n"
|
|
|
|
# Invoice rows - one line each, simple format
|
|
for inv in sorted_invoices[:10]:
|
|
# Backend returns: nract, totctva, soldfinal, datascad, dataact, achitat
|
|
number = str(inv.get('nract', 'N/A'))
|
|
dataact = inv.get('dataact')
|
|
|
|
# Parse date - handle various formats to ensure dd.mm.yyyy
|
|
if dataact:
|
|
if isinstance(dataact, str):
|
|
try:
|
|
# Try ISO format first: "2024-10-25" or "2024-10-25 00:00:00"
|
|
if '-' in dataact and len(dataact) >= 10:
|
|
parsed_date = datetime.strptime(dataact[:10], '%Y-%m-%d')
|
|
date_str = parsed_date.strftime('%d.%m.%Y')
|
|
# Already in dd.mm.yyyy format
|
|
elif '.' in dataact:
|
|
date_str = dataact.split()[0][:10] # Take just date part
|
|
else:
|
|
date_str = dataact[:10] if len(dataact) >= 10 else dataact
|
|
except:
|
|
date_str = dataact[:10] if len(dataact) >= 10 else dataact
|
|
else:
|
|
# Datetime object - format as dd.mm.yyyy
|
|
date_str = dataact.strftime('%d.%m.%Y')
|
|
else:
|
|
date_str = 'N/A'
|
|
|
|
sold = float(inv.get('soldfinal', 0) or 0)
|
|
|
|
# Simple format: Nr • Data • Sold
|
|
text += f"Nr {number} • {date_str} • {sold:,.2f} RON\n"
|
|
|
|
if len(invoices) > 10:
|
|
text += f"\n\n+{len(invoices) - 10} facturi"
|
|
else:
|
|
text += "Nu exista facturi neachitate"
|
|
|
|
return text
|
|
|
|
|
|
# =========================================================================
|
|
# FAZA 6: Performance Footer for Cache Monitoring
|
|
# =========================================================================
|
|
|
|
|
|
def add_performance_footer(message: str, cache_hit: bool, time_ms: float, cache_source: str = None) -> str:
|
|
"""
|
|
Add compact performance footer to bot responses.
|
|
|
|
Shows data source (cached L1/L2 or database) and response time.
|
|
Format: "cached L1 | 15ms", "cached L2 | 25ms" or "db | 285ms"
|
|
|
|
Args:
|
|
message: Existing message text
|
|
cache_hit: True if data came from cache
|
|
time_ms: Response time in milliseconds
|
|
cache_source: Cache source ("L1" for memory, "L2" for SQLite) if cache_hit is True
|
|
|
|
Returns:
|
|
Message with performance footer appended
|
|
|
|
Example:
|
|
>>> add_performance_footer("Dashboard data...", True, 52.3, "L1")
|
|
"Dashboard data...\n\ncached L1 | 52ms"
|
|
>>> add_performance_footer("Dashboard data...", True, 25.8, "L2")
|
|
"Dashboard data...\n\ncached L2 | 26ms"
|
|
>>> add_performance_footer("Dashboard data...", False, 285.7)
|
|
"Dashboard data...\n\ndb | 286ms"
|
|
"""
|
|
if cache_hit and cache_source:
|
|
source = f"cached {cache_source}"
|
|
elif cache_hit:
|
|
source = "cached" # Fallback if source not provided
|
|
else:
|
|
source = "db"
|
|
|
|
footer = f"\n\n`{source} | {time_ms:.0f}ms`"
|
|
return message + footer
|
|
|