Adauga comanda /tranzactii pentru vizualizare tranzactii interactive
- Sistem interactiv de selectie conturi cu state management - Filtrare pe perioade: [nr cont] [nr zile] (ex: 2 30 = ultimele 30 zile) - Format compact: tranzactii grupate pe date, fara sold - Extragere inteligenta comercianti din platile POS (TID pattern) - Escape automat caractere speciale Markdown pentru Telegram - Timeout 5 minute pentru sesiuni de selectie - Suport comenzi: doar numar = ultimele 10, numar + zile = filtrare Corectari: - Fix import timedelta pentru filtrare pe date - Fix conflict nume variabila period vs csv_period - Fix Markdown parsing errors (underscore, dot, etc) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import json
|
||||
import csv
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import glob
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
@@ -51,6 +51,9 @@ class TelegramTriggerBot:
|
||||
self.last_update_id = 0
|
||||
self.poll_timeout = POLL_TIMEOUT
|
||||
|
||||
# State management pentru selecție interactivă conturi
|
||||
self.pending_account_selection = {} # {user_id: {'accounts': [...], 'timestamp': ...}}
|
||||
|
||||
if not self.bot_token:
|
||||
raise ValueError("TELEGRAM_BOT_TOKEN nu este setat în .env!")
|
||||
|
||||
@@ -70,6 +73,7 @@ class TelegramTriggerBot:
|
||||
{"command": "scrape_zip", "description": "Rulează scraper + trimite ZIP"},
|
||||
{"command": "scrape_solduri", "description": "Extrage doar soldurile (fără CSV)"},
|
||||
{"command": "solduri", "description": "Afișează ultimul fișier solduri"},
|
||||
{"command": "tranzactii", "description": "Afișează tranzacții recente din cont"},
|
||||
{"command": "zip", "description": "Trimite ultimele fișiere ca ZIP"},
|
||||
{"command": "status", "description": "Status sistem"},
|
||||
{"command": "help", "description": "Ajutor comenzi"}
|
||||
@@ -318,6 +322,321 @@ class TelegramTriggerBot:
|
||||
logging.error(f"Eroare show_cached_balances: {e}", exc_info=True)
|
||||
self.send_message(chat_id, f"*EROARE*\n\n```\n{str(e)}\n```", reply_to_message_id)
|
||||
|
||||
def show_transactions_menu(self, chat_id, user_id, reply_to_message_id=None):
|
||||
"""Afișează meniu cu conturi disponibile pentru selecție tranzacții"""
|
||||
try:
|
||||
data_dir = Path('data')
|
||||
|
||||
if not data_dir.exists():
|
||||
self.send_message(chat_id, "*EROARE*\n\nDirectorul 'data' nu există!", reply_to_message_id)
|
||||
return
|
||||
|
||||
# Găsește toate fișierele de tranzacții
|
||||
transaction_files = sorted(
|
||||
data_dir.glob('tranzactii_*.csv'),
|
||||
key=lambda x: x.stat().st_mtime,
|
||||
reverse=True
|
||||
)
|
||||
|
||||
if not transaction_files:
|
||||
self.send_message(
|
||||
chat_id,
|
||||
"*EROARE*\n\nNu s-au găsit fișiere cu tranzacții!\n\nRulează mai întâi /scrape pentru a descărca tranzacții.",
|
||||
reply_to_message_id
|
||||
)
|
||||
return
|
||||
|
||||
# Extrage nume conturi unice din numele fișierelor
|
||||
# Format: tranzactii_Nume_Cont_YYYY-MM-DD_HH-MM-SS.csv
|
||||
accounts = {}
|
||||
for file_path in transaction_files:
|
||||
filename = file_path.stem # fără extensie
|
||||
# Elimină prefixul "tranzactii_" și sufixul timestamp
|
||||
# rsplit('_', 3) split-uiește: ['Nume', 'Cont', 'YYYY-MM-DD', 'HH-MM-SS']
|
||||
parts = filename.replace('tranzactii_', '').rsplit('_', 3)
|
||||
if len(parts) >= 4:
|
||||
# parts[0] = nume de bază, parts[1] = număr cont
|
||||
account_name = f"{parts[0]}_{parts[1]}" # "Nume_Cont"
|
||||
# Convertește Nume_Cont → Nume Cont
|
||||
display_name = account_name.replace('_', ' ')
|
||||
|
||||
# Păstrează doar cel mai recent fișier pentru fiecare cont
|
||||
if display_name not in accounts:
|
||||
accounts[display_name] = file_path
|
||||
|
||||
if not accounts:
|
||||
self.send_message(
|
||||
chat_id,
|
||||
"*EROARE*\n\nNu s-au putut procesa fișierele de tranzacții!",
|
||||
reply_to_message_id
|
||||
)
|
||||
return
|
||||
|
||||
# Sortează conturile alfabetic
|
||||
sorted_accounts = sorted(accounts.items())
|
||||
|
||||
# Construiește mesaj
|
||||
message = "*TRANZACTII BANCARE*\n\n"
|
||||
message += "Conturi disponibile:\n\n"
|
||||
|
||||
for idx, (account_name, file_path) in enumerate(sorted_accounts, 1):
|
||||
message += f"{idx}. {account_name}\n"
|
||||
|
||||
message += f"\n*Scrie numarul contului (1-{len(sorted_accounts)}):*\n"
|
||||
message += " • Doar numar (ex: 2) = ultimele 10\n"
|
||||
message += " • Numar + zile (ex: 2 7, 2 30)"
|
||||
|
||||
# Salvează starea pentru user
|
||||
self.pending_account_selection[user_id] = {
|
||||
'accounts': sorted_accounts,
|
||||
'timestamp': datetime.now().timestamp()
|
||||
}
|
||||
|
||||
self.send_message(chat_id, message, reply_to_message_id)
|
||||
logging.info(f"Afișat meniu tranzacții pentru user {user_id}: {len(sorted_accounts)} conturi")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Eroare show_transactions_menu: {e}", exc_info=True)
|
||||
self.send_message(chat_id, f"*EROARE*\n\n```\n{str(e)}\n```", reply_to_message_id)
|
||||
|
||||
def show_account_transactions(self, chat_id, user_id, account_index, reply_to_message_id=None, period="10"):
|
||||
"""Afișează tranzacții pentru contul selectat (perioada: 10/luna/sapt)"""
|
||||
try:
|
||||
# Verifică dacă userul are o selecție pending
|
||||
if user_id not in self.pending_account_selection:
|
||||
self.send_message(
|
||||
chat_id,
|
||||
"*EROARE*\n\nNu există selecție activă. Folosește /tranzactii pentru a începe.",
|
||||
reply_to_message_id
|
||||
)
|
||||
return
|
||||
|
||||
pending_data = self.pending_account_selection[user_id]
|
||||
accounts = pending_data['accounts']
|
||||
|
||||
# Verifică timeout (5 minute)
|
||||
if datetime.now().timestamp() - pending_data['timestamp'] > 300:
|
||||
del self.pending_account_selection[user_id]
|
||||
self.send_message(
|
||||
chat_id,
|
||||
"*TIMEOUT*\n\nSelecția a expirat. Folosește /tranzactii pentru a începe din nou.",
|
||||
reply_to_message_id
|
||||
)
|
||||
return
|
||||
|
||||
# Verifică index valid
|
||||
if account_index < 1 or account_index > len(accounts):
|
||||
self.send_message(
|
||||
chat_id,
|
||||
f"*EROARE*\n\nNumăr invalid! Scrie un număr între 1 și {len(accounts)}.",
|
||||
reply_to_message_id
|
||||
)
|
||||
return
|
||||
|
||||
# Obține contul selectat
|
||||
account_name, csv_path = accounts[account_index - 1]
|
||||
|
||||
# Verifică dacă fișierul există
|
||||
if not csv_path.exists():
|
||||
del self.pending_account_selection[user_id]
|
||||
self.send_message(
|
||||
chat_id,
|
||||
f"*EROARE*\n\nFișierul pentru contul {account_name} nu mai există!",
|
||||
reply_to_message_id
|
||||
)
|
||||
return
|
||||
|
||||
# Citește CSV-ul
|
||||
# Format BT: Primele 17 linii = metadata, linia 18 = header, linia 19+ = date
|
||||
transactions = []
|
||||
account_iban = ""
|
||||
csv_period = "" # Perioada din CSV (nu parametrul funcției!)
|
||||
|
||||
with open(csv_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Extrage metadate
|
||||
if len(lines) > 8:
|
||||
# Linia 9: Perioada:,29.10.2025-12.11.2025
|
||||
period_line = lines[8].strip()
|
||||
if 'Perioada:' in period_line:
|
||||
csv_period = period_line.split(',')[1] if ',' in period_line else ""
|
||||
|
||||
# Linia 8: Numar cont:,RO32BTRLRONCRT0637236701 RON
|
||||
iban_line = lines[7].strip()
|
||||
if 'Numar cont:' in iban_line:
|
||||
account_iban = iban_line.split(',')[1] if ',' in iban_line else ""
|
||||
|
||||
# Citește tranzacțiile (de la header - linia 18, index 17)
|
||||
if len(lines) > 17:
|
||||
csv_content = ''.join(lines[17:]) # Include header + date
|
||||
reader = csv.DictReader(csv_content.splitlines())
|
||||
|
||||
for row in reader:
|
||||
if row.get('Data tranzactie'): # Skip linii goale
|
||||
transactions.append(row)
|
||||
|
||||
# Șterge selecția pending
|
||||
del self.pending_account_selection[user_id]
|
||||
|
||||
if not transactions:
|
||||
self.send_message(
|
||||
chat_id,
|
||||
f"*{account_name}*\n\nNu există tranzacții în acest fișier.",
|
||||
reply_to_message_id
|
||||
)
|
||||
return
|
||||
|
||||
# Filtrează tranzacțiile în funcție de perioada selectată (nr zile)
|
||||
from collections import defaultdict
|
||||
|
||||
recent_transactions = []
|
||||
period_description = ""
|
||||
|
||||
# Convertește period în număr de zile
|
||||
try:
|
||||
num_days = int(period)
|
||||
except (ValueError, TypeError):
|
||||
num_days = 10 # Default
|
||||
|
||||
if num_days <= 0:
|
||||
num_days = 10 # Sigură
|
||||
|
||||
# Filtrează pe bază de zile
|
||||
if num_days == 10:
|
||||
# Optimizare: ultimele 10 tranzacții direct
|
||||
recent_transactions = transactions[:10]
|
||||
period_description = "Ultimele 10 tranzactii"
|
||||
else:
|
||||
# Filtrare pe bază de dată
|
||||
now = datetime.now()
|
||||
cutoff_date = now - timedelta(days=num_days)
|
||||
|
||||
for tx in transactions:
|
||||
tx_date_str = tx.get('Data tranzactie', '')
|
||||
if tx_date_str:
|
||||
try:
|
||||
tx_date = datetime.strptime(tx_date_str, '%Y-%m-%d')
|
||||
if tx_date >= cutoff_date:
|
||||
recent_transactions.append(tx)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
period_description = f"Ultimele {num_days} zile"
|
||||
|
||||
# Grupează tranzacțiile pe date
|
||||
transactions_by_date = defaultdict(list)
|
||||
|
||||
for tx in recent_transactions:
|
||||
date = tx.get('Data tranzactie', '')
|
||||
if date:
|
||||
transactions_by_date[date].append(tx)
|
||||
|
||||
# Construiește mesaj
|
||||
message = f"*TRANZACTII - {account_name}*\n\n"
|
||||
|
||||
if period_description:
|
||||
message += f"Perioada: {period_description}\n"
|
||||
|
||||
message += f"Total: {len(recent_transactions)} tranzactii\n"
|
||||
message += "=" * 30 + "\n\n"
|
||||
|
||||
# Sortează datele descrescător (cele mai recente primul)
|
||||
sorted_dates = sorted(transactions_by_date.keys(), reverse=True)
|
||||
|
||||
for date in sorted_dates:
|
||||
# Header pentru dată
|
||||
message += f"*{date}*\n"
|
||||
|
||||
# Tranzacțiile din acea zi
|
||||
for tx in transactions_by_date[date]:
|
||||
description = tx.get('Descriere', '')
|
||||
debit = tx.get('Debit', '').strip().replace('"', '').replace(',', '')
|
||||
credit = tx.get('Credit', '').strip().replace('"', '').replace(',', '')
|
||||
|
||||
# Extrage nume mai inteligent din descriere
|
||||
display_name = description
|
||||
desc_parts = description.split(';')
|
||||
|
||||
if len(desc_parts) > 2:
|
||||
# Caz special: Plăți POS - extrage comerciantul din Parts[1]
|
||||
if 'POS' in desc_parts[0] and len(desc_parts) > 1:
|
||||
# Căutăm pattern: "TID:XXXXXXX <COMERCIANT> <REST>"
|
||||
import re
|
||||
tid_match = re.search(r'TID:\S+\s+(.+?)\s{2,}', desc_parts[1])
|
||||
if tid_match:
|
||||
candidate = tid_match.group(1).strip()
|
||||
else:
|
||||
# Fallback: încearcă să extragă după TID până la două spații
|
||||
if 'TID:' in desc_parts[1]:
|
||||
after_tid = desc_parts[1].split('TID:')[1]
|
||||
# Skip ID-ul TID și ia textul până la două spații consecutive
|
||||
parts_after = after_tid.split(None, 1) # Split la primul spațiu
|
||||
if len(parts_after) > 1:
|
||||
# Ia textul până la " " sau până la sfârșitul
|
||||
merchant_text = parts_after[1]
|
||||
double_space_idx = merchant_text.find(' ')
|
||||
if double_space_idx > 0:
|
||||
candidate = merchant_text[:double_space_idx].strip()
|
||||
else:
|
||||
candidate = merchant_text.strip()
|
||||
else:
|
||||
candidate = desc_parts[2].strip()
|
||||
else:
|
||||
candidate = desc_parts[2].strip()
|
||||
else:
|
||||
# Încearcă part[2] (de obicei numele)
|
||||
candidate = desc_parts[2].strip()
|
||||
|
||||
# Dacă part[2] este doar număr/scurt/REF, încearcă part[3]
|
||||
if (candidate.isdigit() or
|
||||
len(candidate) < 3 or
|
||||
candidate.startswith('REF:')):
|
||||
if len(desc_parts) > 3:
|
||||
candidate = desc_parts[3].strip()
|
||||
|
||||
# Dacă tot e invalid, folosește tipul tranzacției (part[0])
|
||||
if len(candidate) < 3 or candidate.startswith('REF:'):
|
||||
candidate = desc_parts[0].strip()
|
||||
|
||||
display_name = candidate
|
||||
|
||||
# Truncate dacă prea lung
|
||||
if len(display_name) > 35:
|
||||
display_name = display_name[:32] + "..."
|
||||
|
||||
# Escape caractere speciale Markdown pentru Telegram
|
||||
# Caracterele care trebuie escapate: _ * [ ] ( ) ~ ` > # + - = | { } . !
|
||||
markdown_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
|
||||
for char in markdown_chars:
|
||||
display_name = display_name.replace(char, '\\' + char)
|
||||
|
||||
# Determină suma (debit poate avea deja minus în CSV)
|
||||
if credit:
|
||||
amount_str = f"+{credit}"
|
||||
elif debit:
|
||||
# Elimină minus-ul dacă există deja
|
||||
debit_clean = debit.lstrip('-')
|
||||
amount_str = f"-{debit_clean}"
|
||||
else:
|
||||
amount_str = "0.00"
|
||||
|
||||
# Format compact: " • Nume: +suma RON"
|
||||
message += f" {display_name}: {amount_str} RON\n"
|
||||
|
||||
message += "\n"
|
||||
|
||||
self.send_message(chat_id, message, reply_to_message_id)
|
||||
logging.info(f"Afișat {len(recent_transactions)} tranzacții pentru {account_name}")
|
||||
|
||||
except Exception as e:
|
||||
# Curăță selecția în caz de eroare
|
||||
if user_id in self.pending_account_selection:
|
||||
del self.pending_account_selection[user_id]
|
||||
|
||||
logging.error(f"Eroare show_account_transactions: {e}", exc_info=True)
|
||||
self.send_message(chat_id, f"*EROARE*\n\n```\n{str(e)}\n```", reply_to_message_id)
|
||||
|
||||
def send_zip_files(self, chat_id, reply_to_message_id=None):
|
||||
"""Trimite ultimele fișiere ca arhivă ZIP"""
|
||||
try:
|
||||
@@ -471,6 +790,25 @@ class TelegramTriggerBot:
|
||||
self.send_message(chat_id, "*ACCES INTERZIS*\n\nNu ai permisiunea sa folosesti acest bot.", message_id)
|
||||
return
|
||||
|
||||
# Procesează răspuns numeric pentru selecție cont tranzacții
|
||||
if user_id in self.pending_account_selection:
|
||||
# Parse număr cont + opțional nr zile
|
||||
# Formate acceptate: "2" (default 10), "2 7" (7 zile), "2 30" (30 zile)
|
||||
parts = text.strip().split()
|
||||
|
||||
|
||||
if len(parts) >= 1 and parts[0].isdigit():
|
||||
account_index = int(parts[0])
|
||||
|
||||
# Determină numărul de zile
|
||||
period = "10" # default: ultimele 10 tranzacții
|
||||
if len(parts) >= 2 and parts[1].isdigit():
|
||||
period = parts[1] # număr de zile
|
||||
|
||||
logging.info(f"Răspuns selecție cont: {account_index}, perioada: {period} zile de la user {user_id}")
|
||||
self.show_account_transactions(chat_id, user_id, account_index, message_id, period)
|
||||
return
|
||||
|
||||
# Procesează comenzi
|
||||
if text == '/start':
|
||||
welcome_msg = "*BTGO Scraper Trigger Bot*\n\n"
|
||||
@@ -482,6 +820,7 @@ class TelegramTriggerBot:
|
||||
"`/scrape_zip` - Ruleaza scraper + trimite ZIP\n"
|
||||
"`/scrape_solduri` - Extrage doar soldurile (rapid)\n"
|
||||
"`/solduri` - Afiseaza ultimul fisier solduri\n"
|
||||
"`/tranzactii` - Afiseaza tranzactii recente\n"
|
||||
"`/zip` - Trimite ultimele fisiere ca ZIP\n"
|
||||
"`/status` - Status sistem\n"
|
||||
"`/help` - Ajutor"
|
||||
@@ -504,6 +843,10 @@ class TelegramTriggerBot:
|
||||
logging.info(f"Comandă /solduri primită în {context}")
|
||||
self.show_cached_balances(chat_id, message_id)
|
||||
|
||||
elif text == '/tranzactii':
|
||||
logging.info(f"Comandă /tranzactii primită în {context}")
|
||||
self.show_transactions_menu(chat_id, user_id, message_id)
|
||||
|
||||
elif text == '/zip':
|
||||
logging.info(f"Comandă /zip primită în {context}")
|
||||
self.send_zip_files(chat_id, message_id)
|
||||
@@ -539,6 +882,7 @@ class TelegramTriggerBot:
|
||||
"`/scrape_zip` - Ruleaza scraper + trimite arhiva ZIP\n"
|
||||
"`/scrape_solduri` - Extrage doar soldurile (fara CSV tranzactii)\n"
|
||||
"`/solduri` - Afiseaza ultimul fisier solduri (instant)\n"
|
||||
"`/tranzactii` - Afiseaza tranzactii recente din cont (interactiv)\n"
|
||||
"`/zip` - Trimite ultimele fisiere ca arhiva ZIP (fara scraping)\n"
|
||||
"`/status` - Informatii sistem\n"
|
||||
"`/help` - Acest mesaj\n\n"
|
||||
|
||||
Reference in New Issue
Block a user