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.
Files changed (47) hide show
  1. nanopy_bank/__init__.py +20 -0
  2. nanopy_bank/api/__init__.py +10 -0
  3. nanopy_bank/api/server.py +242 -0
  4. nanopy_bank/app.py +282 -0
  5. nanopy_bank/cli.py +152 -0
  6. nanopy_bank/core/__init__.py +85 -0
  7. nanopy_bank/core/audit.py +404 -0
  8. nanopy_bank/core/auth.py +306 -0
  9. nanopy_bank/core/bank.py +407 -0
  10. nanopy_bank/core/beneficiary.py +258 -0
  11. nanopy_bank/core/branch.py +319 -0
  12. nanopy_bank/core/fees.py +243 -0
  13. nanopy_bank/core/holding.py +416 -0
  14. nanopy_bank/core/models.py +308 -0
  15. nanopy_bank/core/products.py +300 -0
  16. nanopy_bank/data/__init__.py +31 -0
  17. nanopy_bank/data/demo.py +846 -0
  18. nanopy_bank/documents/__init__.py +11 -0
  19. nanopy_bank/documents/statement.py +304 -0
  20. nanopy_bank/sepa/__init__.py +10 -0
  21. nanopy_bank/sepa/sepa.py +452 -0
  22. nanopy_bank/storage/__init__.py +11 -0
  23. nanopy_bank/storage/json_storage.py +127 -0
  24. nanopy_bank/storage/sqlite_storage.py +326 -0
  25. nanopy_bank/ui/__init__.py +14 -0
  26. nanopy_bank/ui/pages/__init__.py +33 -0
  27. nanopy_bank/ui/pages/accounts.py +85 -0
  28. nanopy_bank/ui/pages/advisor.py +140 -0
  29. nanopy_bank/ui/pages/audit.py +73 -0
  30. nanopy_bank/ui/pages/beneficiaries.py +115 -0
  31. nanopy_bank/ui/pages/branches.py +64 -0
  32. nanopy_bank/ui/pages/cards.py +36 -0
  33. nanopy_bank/ui/pages/common.py +18 -0
  34. nanopy_bank/ui/pages/dashboard.py +100 -0
  35. nanopy_bank/ui/pages/fees.py +60 -0
  36. nanopy_bank/ui/pages/holding.py +943 -0
  37. nanopy_bank/ui/pages/loans.py +105 -0
  38. nanopy_bank/ui/pages/login.py +174 -0
  39. nanopy_bank/ui/pages/sepa.py +118 -0
  40. nanopy_bank/ui/pages/settings.py +48 -0
  41. nanopy_bank/ui/pages/transfers.py +94 -0
  42. nanopy_bank/ui/pages.py +16 -0
  43. nanopy_bank-1.0.8.dist-info/METADATA +72 -0
  44. nanopy_bank-1.0.8.dist-info/RECORD +47 -0
  45. nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
  46. nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
  47. nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,11 @@
1
+ """
2
+ Documents module - PDF generation, RIB, statements
3
+ """
4
+
5
+ from .statement import StatementGenerator
6
+ from .rib import RIBGenerator
7
+
8
+ __all__ = [
9
+ "StatementGenerator",
10
+ "RIBGenerator",
11
+ ]
@@ -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
+ )
@@ -0,0 +1,10 @@
1
+ """
2
+ SEPA XML Module - ISO 20022
3
+ """
4
+
5
+ from .sepa import SEPAGenerator, SEPAParser
6
+
7
+ __all__ = [
8
+ "SEPAGenerator",
9
+ "SEPAParser",
10
+ ]