nanopy-bank 1.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nanopy_bank/__init__.py +20 -0
- nanopy_bank/api/__init__.py +10 -0
- nanopy_bank/api/server.py +242 -0
- nanopy_bank/app.py +282 -0
- nanopy_bank/cli.py +152 -0
- nanopy_bank/core/__init__.py +85 -0
- nanopy_bank/core/audit.py +404 -0
- nanopy_bank/core/auth.py +306 -0
- nanopy_bank/core/bank.py +407 -0
- nanopy_bank/core/beneficiary.py +258 -0
- nanopy_bank/core/branch.py +319 -0
- nanopy_bank/core/fees.py +243 -0
- nanopy_bank/core/holding.py +416 -0
- nanopy_bank/core/models.py +308 -0
- nanopy_bank/core/products.py +300 -0
- nanopy_bank/data/__init__.py +31 -0
- nanopy_bank/data/demo.py +846 -0
- nanopy_bank/documents/__init__.py +11 -0
- nanopy_bank/documents/statement.py +304 -0
- nanopy_bank/sepa/__init__.py +10 -0
- nanopy_bank/sepa/sepa.py +452 -0
- nanopy_bank/storage/__init__.py +11 -0
- nanopy_bank/storage/json_storage.py +127 -0
- nanopy_bank/storage/sqlite_storage.py +326 -0
- nanopy_bank/ui/__init__.py +14 -0
- nanopy_bank/ui/pages/__init__.py +33 -0
- nanopy_bank/ui/pages/accounts.py +85 -0
- nanopy_bank/ui/pages/advisor.py +140 -0
- nanopy_bank/ui/pages/audit.py +73 -0
- nanopy_bank/ui/pages/beneficiaries.py +115 -0
- nanopy_bank/ui/pages/branches.py +64 -0
- nanopy_bank/ui/pages/cards.py +36 -0
- nanopy_bank/ui/pages/common.py +18 -0
- nanopy_bank/ui/pages/dashboard.py +100 -0
- nanopy_bank/ui/pages/fees.py +60 -0
- nanopy_bank/ui/pages/holding.py +943 -0
- nanopy_bank/ui/pages/loans.py +105 -0
- nanopy_bank/ui/pages/login.py +174 -0
- nanopy_bank/ui/pages/sepa.py +118 -0
- nanopy_bank/ui/pages/settings.py +48 -0
- nanopy_bank/ui/pages/transfers.py +94 -0
- nanopy_bank/ui/pages.py +16 -0
- nanopy_bank-1.0.8.dist-info/METADATA +72 -0
- nanopy_bank-1.0.8.dist-info/RECORD +47 -0
- nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
- nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
- nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bank Statement PDF Generator
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, date
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from reportlab.lib.pagesizes import A4
|
|
12
|
+
from reportlab.lib.units import mm
|
|
13
|
+
from reportlab.lib import colors
|
|
14
|
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
15
|
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
|
|
16
|
+
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StatementGenerator:
|
|
20
|
+
"""
|
|
21
|
+
Generate PDF bank statements
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
bank_name: str = "NanoPy Bank",
|
|
27
|
+
bank_address: str = "1 Rue de la Banque, 75001 Paris",
|
|
28
|
+
bank_phone: str = "+33 1 23 45 67 89",
|
|
29
|
+
bank_bic: str = "NANPFRPP"
|
|
30
|
+
):
|
|
31
|
+
self.bank_name = bank_name
|
|
32
|
+
self.bank_address = bank_address
|
|
33
|
+
self.bank_phone = bank_phone
|
|
34
|
+
self.bank_bic = bank_bic
|
|
35
|
+
|
|
36
|
+
# Colors
|
|
37
|
+
self.primary_color = colors.HexColor("#1a1a2e")
|
|
38
|
+
self.accent_color = colors.HexColor("#00d4ff")
|
|
39
|
+
self.text_color = colors.HexColor("#333333")
|
|
40
|
+
self.light_gray = colors.HexColor("#f5f5f5")
|
|
41
|
+
|
|
42
|
+
def generate(
|
|
43
|
+
self,
|
|
44
|
+
account_iban: str,
|
|
45
|
+
account_name: str,
|
|
46
|
+
customer_name: str,
|
|
47
|
+
customer_address: str,
|
|
48
|
+
transactions: List[dict],
|
|
49
|
+
from_date: date,
|
|
50
|
+
to_date: date,
|
|
51
|
+
opening_balance: Decimal,
|
|
52
|
+
closing_balance: Decimal,
|
|
53
|
+
currency: str = "EUR"
|
|
54
|
+
) -> bytes:
|
|
55
|
+
"""
|
|
56
|
+
Generate a PDF bank statement
|
|
57
|
+
|
|
58
|
+
Returns: PDF content as bytes
|
|
59
|
+
"""
|
|
60
|
+
buffer = BytesIO()
|
|
61
|
+
|
|
62
|
+
# Create document
|
|
63
|
+
doc = SimpleDocTemplate(
|
|
64
|
+
buffer,
|
|
65
|
+
pagesize=A4,
|
|
66
|
+
rightMargin=20*mm,
|
|
67
|
+
leftMargin=20*mm,
|
|
68
|
+
topMargin=20*mm,
|
|
69
|
+
bottomMargin=20*mm
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Styles
|
|
73
|
+
styles = getSampleStyleSheet()
|
|
74
|
+
styles.add(ParagraphStyle(
|
|
75
|
+
name='BankTitle',
|
|
76
|
+
parent=styles['Heading1'],
|
|
77
|
+
fontSize=24,
|
|
78
|
+
textColor=self.primary_color,
|
|
79
|
+
spaceAfter=10
|
|
80
|
+
))
|
|
81
|
+
styles.add(ParagraphStyle(
|
|
82
|
+
name='SectionTitle',
|
|
83
|
+
parent=styles['Heading2'],
|
|
84
|
+
fontSize=14,
|
|
85
|
+
textColor=self.primary_color,
|
|
86
|
+
spaceBefore=15,
|
|
87
|
+
spaceAfter=10
|
|
88
|
+
))
|
|
89
|
+
styles.add(ParagraphStyle(
|
|
90
|
+
name='Normal2',
|
|
91
|
+
parent=styles['Normal'],
|
|
92
|
+
fontSize=10,
|
|
93
|
+
textColor=self.text_color
|
|
94
|
+
))
|
|
95
|
+
styles.add(ParagraphStyle(
|
|
96
|
+
name='Small',
|
|
97
|
+
parent=styles['Normal'],
|
|
98
|
+
fontSize=8,
|
|
99
|
+
textColor=colors.gray
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
# Build content
|
|
103
|
+
elements = []
|
|
104
|
+
|
|
105
|
+
# Header
|
|
106
|
+
elements.append(Paragraph(self.bank_name, styles['BankTitle']))
|
|
107
|
+
elements.append(Paragraph(self.bank_address, styles['Small']))
|
|
108
|
+
elements.append(Paragraph(f"Tel: {self.bank_phone} | BIC: {self.bank_bic}", styles['Small']))
|
|
109
|
+
elements.append(Spacer(1, 15*mm))
|
|
110
|
+
|
|
111
|
+
# Statement title
|
|
112
|
+
elements.append(Paragraph("RELEVE DE COMPTE", styles['SectionTitle']))
|
|
113
|
+
elements.append(Paragraph(
|
|
114
|
+
f"Periode du {from_date.strftime('%d/%m/%Y')} au {to_date.strftime('%d/%m/%Y')}",
|
|
115
|
+
styles['Normal2']
|
|
116
|
+
))
|
|
117
|
+
elements.append(Spacer(1, 10*mm))
|
|
118
|
+
|
|
119
|
+
# Account and customer info
|
|
120
|
+
info_data = [
|
|
121
|
+
["Titulaire:", customer_name],
|
|
122
|
+
["Adresse:", customer_address],
|
|
123
|
+
["IBAN:", self._format_iban(account_iban)],
|
|
124
|
+
["Compte:", account_name],
|
|
125
|
+
]
|
|
126
|
+
info_table = Table(info_data, colWidths=[40*mm, 120*mm])
|
|
127
|
+
info_table.setStyle(TableStyle([
|
|
128
|
+
('FONT', (0, 0), (0, -1), 'Helvetica-Bold', 10),
|
|
129
|
+
('FONT', (1, 0), (1, -1), 'Helvetica', 10),
|
|
130
|
+
('TEXTCOLOR', (0, 0), (-1, -1), self.text_color),
|
|
131
|
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
132
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
|
|
133
|
+
]))
|
|
134
|
+
elements.append(info_table)
|
|
135
|
+
elements.append(Spacer(1, 10*mm))
|
|
136
|
+
|
|
137
|
+
# Balance summary
|
|
138
|
+
elements.append(Paragraph("SYNTHESE", styles['SectionTitle']))
|
|
139
|
+
|
|
140
|
+
# Calculate totals
|
|
141
|
+
total_credit = sum(
|
|
142
|
+
Decimal(str(tx.get("amount", 0)))
|
|
143
|
+
for tx in transactions
|
|
144
|
+
if tx.get("is_credit", False)
|
|
145
|
+
)
|
|
146
|
+
total_debit = sum(
|
|
147
|
+
Decimal(str(tx.get("amount", 0)))
|
|
148
|
+
for tx in transactions
|
|
149
|
+
if not tx.get("is_credit", True)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
balance_data = [
|
|
153
|
+
["Solde initial:", f"{opening_balance:,.2f} {currency}"],
|
|
154
|
+
["Total credits:", f"+{total_credit:,.2f} {currency}"],
|
|
155
|
+
["Total debits:", f"-{total_debit:,.2f} {currency}"],
|
|
156
|
+
["Solde final:", f"{closing_balance:,.2f} {currency}"],
|
|
157
|
+
]
|
|
158
|
+
balance_table = Table(balance_data, colWidths=[60*mm, 60*mm])
|
|
159
|
+
balance_table.setStyle(TableStyle([
|
|
160
|
+
('FONT', (0, 0), (0, -1), 'Helvetica', 10),
|
|
161
|
+
('FONT', (1, 0), (1, -1), 'Helvetica-Bold', 10),
|
|
162
|
+
('TEXTCOLOR', (0, 0), (-1, -1), self.text_color),
|
|
163
|
+
('TEXTCOLOR', (1, 1), (1, 1), colors.green), # Credits in green
|
|
164
|
+
('TEXTCOLOR', (1, 2), (1, 2), colors.red), # Debits in red
|
|
165
|
+
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
|
|
166
|
+
('BACKGROUND', (0, -1), (-1, -1), self.light_gray),
|
|
167
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
|
|
168
|
+
('TOPPADDING', (0, 0), (-1, -1), 5),
|
|
169
|
+
]))
|
|
170
|
+
elements.append(balance_table)
|
|
171
|
+
elements.append(Spacer(1, 10*mm))
|
|
172
|
+
|
|
173
|
+
# Transactions
|
|
174
|
+
elements.append(Paragraph("OPERATIONS", styles['SectionTitle']))
|
|
175
|
+
|
|
176
|
+
if transactions:
|
|
177
|
+
# Table header
|
|
178
|
+
tx_data = [["Date", "Libelle", "Debit", "Credit", "Solde"]]
|
|
179
|
+
|
|
180
|
+
# Running balance
|
|
181
|
+
running_balance = opening_balance
|
|
182
|
+
|
|
183
|
+
for tx in sorted(transactions, key=lambda x: x.get("created_at", "")):
|
|
184
|
+
tx_date = tx.get("created_at", "")
|
|
185
|
+
if isinstance(tx_date, str) and tx_date:
|
|
186
|
+
try:
|
|
187
|
+
tx_date = datetime.fromisoformat(tx_date.replace("Z", "")).strftime("%d/%m/%Y")
|
|
188
|
+
except:
|
|
189
|
+
tx_date = tx_date[:10]
|
|
190
|
+
|
|
191
|
+
amount = Decimal(str(tx.get("amount", 0)))
|
|
192
|
+
is_credit = tx.get("is_credit", False)
|
|
193
|
+
|
|
194
|
+
if is_credit:
|
|
195
|
+
running_balance += amount
|
|
196
|
+
debit_str = ""
|
|
197
|
+
credit_str = f"+{amount:,.2f}"
|
|
198
|
+
else:
|
|
199
|
+
running_balance -= amount
|
|
200
|
+
debit_str = f"-{amount:,.2f}"
|
|
201
|
+
credit_str = ""
|
|
202
|
+
|
|
203
|
+
label = tx.get("label", tx.get("description", ""))[:40]
|
|
204
|
+
|
|
205
|
+
tx_data.append([
|
|
206
|
+
tx_date,
|
|
207
|
+
label,
|
|
208
|
+
debit_str,
|
|
209
|
+
credit_str,
|
|
210
|
+
f"{running_balance:,.2f}"
|
|
211
|
+
])
|
|
212
|
+
|
|
213
|
+
tx_table = Table(tx_data, colWidths=[25*mm, 70*mm, 30*mm, 30*mm, 30*mm])
|
|
214
|
+
tx_table.setStyle(TableStyle([
|
|
215
|
+
# Header
|
|
216
|
+
('BACKGROUND', (0, 0), (-1, 0), self.primary_color),
|
|
217
|
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
|
218
|
+
('FONT', (0, 0), (-1, 0), 'Helvetica-Bold', 9),
|
|
219
|
+
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
|
220
|
+
|
|
221
|
+
# Body
|
|
222
|
+
('FONT', (0, 1), (-1, -1), 'Helvetica', 8),
|
|
223
|
+
('TEXTCOLOR', (0, 1), (-1, -1), self.text_color),
|
|
224
|
+
('ALIGN', (2, 1), (4, -1), 'RIGHT'),
|
|
225
|
+
|
|
226
|
+
# Debit column in red
|
|
227
|
+
('TEXTCOLOR', (2, 1), (2, -1), colors.red),
|
|
228
|
+
# Credit column in green
|
|
229
|
+
('TEXTCOLOR', (3, 1), (3, -1), colors.green),
|
|
230
|
+
|
|
231
|
+
# Grid
|
|
232
|
+
('GRID', (0, 0), (-1, -1), 0.5, colors.lightgrey),
|
|
233
|
+
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, self.light_gray]),
|
|
234
|
+
|
|
235
|
+
# Padding
|
|
236
|
+
('TOPPADDING', (0, 0), (-1, -1), 4),
|
|
237
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
|
|
238
|
+
]))
|
|
239
|
+
elements.append(tx_table)
|
|
240
|
+
else:
|
|
241
|
+
elements.append(Paragraph("Aucune operation sur cette periode.", styles['Normal2']))
|
|
242
|
+
|
|
243
|
+
elements.append(Spacer(1, 15*mm))
|
|
244
|
+
|
|
245
|
+
# Footer
|
|
246
|
+
elements.append(Paragraph(
|
|
247
|
+
f"Document genere le {datetime.now().strftime('%d/%m/%Y a %H:%M')}",
|
|
248
|
+
styles['Small']
|
|
249
|
+
))
|
|
250
|
+
elements.append(Paragraph(
|
|
251
|
+
f"{self.bank_name} - Ce document est un releve de compte informatif.",
|
|
252
|
+
styles['Small']
|
|
253
|
+
))
|
|
254
|
+
|
|
255
|
+
# Build PDF
|
|
256
|
+
doc.build(elements)
|
|
257
|
+
|
|
258
|
+
return buffer.getvalue()
|
|
259
|
+
|
|
260
|
+
def _format_iban(self, iban: str) -> str:
|
|
261
|
+
"""Format IBAN with spaces"""
|
|
262
|
+
iban = iban.replace(" ", "")
|
|
263
|
+
return " ".join([iban[i:i+4] for i in range(0, len(iban), 4)])
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def generate_statement_pdf(
|
|
267
|
+
account,
|
|
268
|
+
customer,
|
|
269
|
+
transactions: List,
|
|
270
|
+
from_date: date,
|
|
271
|
+
to_date: date,
|
|
272
|
+
opening_balance: Decimal
|
|
273
|
+
) -> bytes:
|
|
274
|
+
"""
|
|
275
|
+
Helper function to generate statement PDF
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
account: Account object
|
|
279
|
+
customer: Customer object
|
|
280
|
+
transactions: List of Transaction objects
|
|
281
|
+
from_date: Statement start date
|
|
282
|
+
to_date: Statement end date
|
|
283
|
+
opening_balance: Balance at start of period
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
PDF content as bytes
|
|
287
|
+
"""
|
|
288
|
+
generator = StatementGenerator()
|
|
289
|
+
|
|
290
|
+
# Convert transactions to dicts
|
|
291
|
+
tx_dicts = [tx.to_dict() if hasattr(tx, 'to_dict') else tx for tx in transactions]
|
|
292
|
+
|
|
293
|
+
return generator.generate(
|
|
294
|
+
account_iban=account.iban if hasattr(account, 'iban') else account.get('iban', ''),
|
|
295
|
+
account_name=account.account_name if hasattr(account, 'account_name') else account.get('account_name', ''),
|
|
296
|
+
customer_name=customer.full_name if hasattr(customer, 'full_name') else f"{customer.get('first_name', '')} {customer.get('last_name', '')}",
|
|
297
|
+
customer_address=f"{customer.address if hasattr(customer, 'address') else customer.get('address', '')}, {customer.postal_code if hasattr(customer, 'postal_code') else customer.get('postal_code', '')} {customer.city if hasattr(customer, 'city') else customer.get('city', '')}",
|
|
298
|
+
transactions=tx_dicts,
|
|
299
|
+
from_date=from_date,
|
|
300
|
+
to_date=to_date,
|
|
301
|
+
opening_balance=opening_balance,
|
|
302
|
+
closing_balance=account.balance if hasattr(account, 'balance') else Decimal(str(account.get('balance', 0))),
|
|
303
|
+
currency=account.currency.value if hasattr(account, 'currency') else account.get('currency', 'EUR')
|
|
304
|
+
)
|