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,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loans page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from .common import page_header
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_loans():
|
|
11
|
+
"""Render loans page"""
|
|
12
|
+
page_header("Loans & Products")
|
|
13
|
+
|
|
14
|
+
tab1, tab2, tab3 = st.tabs(["My Loans", "Savings Products", "Insurance"])
|
|
15
|
+
|
|
16
|
+
with tab1:
|
|
17
|
+
st.markdown("### Active Loans")
|
|
18
|
+
|
|
19
|
+
st.markdown("""
|
|
20
|
+
<div style="background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%); border-radius: 16px; padding: 24px; margin-bottom: 20px;">
|
|
21
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
22
|
+
<div>
|
|
23
|
+
<div style="color: #00d4ff; font-size: 12px; text-transform: uppercase;">Personal Loan</div>
|
|
24
|
+
<div style="color: white; font-size: 24px; font-weight: bold; margin: 8px 0;">15,000.00 EUR</div>
|
|
25
|
+
<div style="color: #aaa; font-size: 14px;">Travaux maison</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div style="text-align: right;">
|
|
28
|
+
<div style="color: #00ff88; font-size: 18px; font-weight: bold;">5.50%</div>
|
|
29
|
+
<div style="color: #aaa; font-size: 12px;">Annual Rate</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div style="margin-top: 20px; background: rgba(255,255,255,0.1); border-radius: 8px; height: 8px;">
|
|
33
|
+
<div style="background: linear-gradient(90deg, #00d4ff, #00ff88); border-radius: 8px; height: 8px; width: 35%;"></div>
|
|
34
|
+
</div>
|
|
35
|
+
<div style="display: flex; justify-content: space-between; margin-top: 8px; color: #888; font-size: 12px;">
|
|
36
|
+
<span>Paid: 5,250.00 EUR (35%)</span>
|
|
37
|
+
<span>Remaining: 9,750.00 EUR</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div style="display: flex; gap: 20px; margin-top: 20px; color: white; font-size: 14px;">
|
|
40
|
+
<div><span style="color: #888;">Monthly:</span> 350.00 EUR</div>
|
|
41
|
+
<div><span style="color: #888;">Duration:</span> 48 months</div>
|
|
42
|
+
<div><span style="color: #888;">Next payment:</span> 05/02/2026</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
""", unsafe_allow_html=True)
|
|
46
|
+
|
|
47
|
+
with st.expander("Request New Loan"):
|
|
48
|
+
with st.form("loan_request"):
|
|
49
|
+
col1, col2 = st.columns(2)
|
|
50
|
+
with col1:
|
|
51
|
+
st.selectbox("Loan Type", ["Personal", "Auto", "Mortgage", "Student"])
|
|
52
|
+
st.number_input("Amount", min_value=1000, max_value=500000, value=10000, step=1000)
|
|
53
|
+
with col2:
|
|
54
|
+
st.slider("Duration (months)", 12, 360, 48)
|
|
55
|
+
st.text_input("Purpose")
|
|
56
|
+
|
|
57
|
+
if st.form_submit_button("Submit Request"):
|
|
58
|
+
st.info("Your loan request has been submitted for review.")
|
|
59
|
+
|
|
60
|
+
with tab2:
|
|
61
|
+
st.markdown("### Savings Products")
|
|
62
|
+
|
|
63
|
+
savings = [
|
|
64
|
+
{"name": "Livret A", "balance": "15,000.00", "rate": "3.00%", "ceiling": "22,950.00"},
|
|
65
|
+
{"name": "LDDS", "balance": "8,500.00", "rate": "3.00%", "ceiling": "12,000.00"},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
for s in savings:
|
|
69
|
+
col1, col2, col3, col4 = st.columns([2, 2, 2, 2])
|
|
70
|
+
with col1:
|
|
71
|
+
st.markdown(f"**{s['name']}**")
|
|
72
|
+
with col2:
|
|
73
|
+
st.markdown(f":green[{s['balance']} EUR]")
|
|
74
|
+
with col3:
|
|
75
|
+
st.markdown(f"Rate: {s['rate']}")
|
|
76
|
+
with col4:
|
|
77
|
+
st.caption(f"Ceiling: {s['ceiling']} EUR")
|
|
78
|
+
st.divider()
|
|
79
|
+
|
|
80
|
+
with st.expander("Open New Savings Account"):
|
|
81
|
+
with st.form("new_savings"):
|
|
82
|
+
st.selectbox("Product", ["Livret A", "LDDS", "LEP", "PEL"])
|
|
83
|
+
st.number_input("Initial Deposit", min_value=10, value=100)
|
|
84
|
+
if st.form_submit_button("Open Account"):
|
|
85
|
+
st.success("Savings account opened!")
|
|
86
|
+
|
|
87
|
+
with tab3:
|
|
88
|
+
st.markdown("### Insurance Products")
|
|
89
|
+
|
|
90
|
+
insurances = [
|
|
91
|
+
{"name": "Home Insurance", "type": "Habitation", "premium": "35.00/month", "status": "Active"},
|
|
92
|
+
{"name": "Loan Insurance", "type": "Emprunteur", "premium": "25.00/month", "status": "Active"},
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
for ins in insurances:
|
|
96
|
+
col1, col2, col3, col4 = st.columns([2, 2, 2, 2])
|
|
97
|
+
with col1:
|
|
98
|
+
st.markdown(f"**{ins['name']}**")
|
|
99
|
+
with col2:
|
|
100
|
+
st.caption(ins['type'])
|
|
101
|
+
with col3:
|
|
102
|
+
st.markdown(f":blue[{ins['premium']}]")
|
|
103
|
+
with col4:
|
|
104
|
+
st.success(ins['status'])
|
|
105
|
+
st.divider()
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Login page - Identification bancaire
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_login():
|
|
9
|
+
"""Render login page with bank-style authentication"""
|
|
10
|
+
st.markdown("""
|
|
11
|
+
<style>
|
|
12
|
+
.login-container {
|
|
13
|
+
max-width: 400px;
|
|
14
|
+
margin: 0 auto;
|
|
15
|
+
padding: 40px;
|
|
16
|
+
}
|
|
17
|
+
.step-indicator {
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
gap: 20px;
|
|
21
|
+
margin-bottom: 30px;
|
|
22
|
+
}
|
|
23
|
+
.step {
|
|
24
|
+
width: 32px;
|
|
25
|
+
height: 32px;
|
|
26
|
+
border-radius: 50%;
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
font-weight: bold;
|
|
31
|
+
}
|
|
32
|
+
.step-active {
|
|
33
|
+
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
|
|
34
|
+
color: white;
|
|
35
|
+
}
|
|
36
|
+
.step-inactive {
|
|
37
|
+
background: #333;
|
|
38
|
+
color: #666;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
41
|
+
""", unsafe_allow_html=True)
|
|
42
|
+
|
|
43
|
+
# Initialize login state
|
|
44
|
+
if "login_step" not in st.session_state:
|
|
45
|
+
st.session_state.login_step = 1
|
|
46
|
+
if "login_client_id" not in st.session_state:
|
|
47
|
+
st.session_state.login_client_id = ""
|
|
48
|
+
|
|
49
|
+
col1, col2, col3 = st.columns([1, 2, 1])
|
|
50
|
+
|
|
51
|
+
with col2:
|
|
52
|
+
st.markdown("""
|
|
53
|
+
<div style="text-align: center; margin-bottom: 40px;">
|
|
54
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#00d4ff" stroke-width="2">
|
|
55
|
+
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
|
|
56
|
+
<line x1="1" y1="10" x2="23" y2="10"></line>
|
|
57
|
+
</svg>
|
|
58
|
+
<h1 style="background: linear-gradient(90deg, #00d4ff, #7b2cbf); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-top: 16px;">NanoPy Bank</h1>
|
|
59
|
+
<p style="color: #888;">Espace client securise</p>
|
|
60
|
+
</div>
|
|
61
|
+
""", unsafe_allow_html=True)
|
|
62
|
+
|
|
63
|
+
# Step indicator
|
|
64
|
+
step1_class = "step-active" if st.session_state.login_step >= 1 else "step-inactive"
|
|
65
|
+
step2_class = "step-active" if st.session_state.login_step >= 2 else "step-inactive"
|
|
66
|
+
st.markdown(f"""
|
|
67
|
+
<div class="step-indicator">
|
|
68
|
+
<div class="step {step1_class}">1</div>
|
|
69
|
+
<div style="width: 60px; height: 2px; background: {'#00d4ff' if st.session_state.login_step >= 2 else '#333'}; align-self: center;"></div>
|
|
70
|
+
<div class="step {step2_class}">2</div>
|
|
71
|
+
</div>
|
|
72
|
+
""", unsafe_allow_html=True)
|
|
73
|
+
|
|
74
|
+
# Step 1: Client ID
|
|
75
|
+
if st.session_state.login_step == 1:
|
|
76
|
+
st.markdown("### Etape 1: Identifiant")
|
|
77
|
+
st.markdown("<p style='color: #888; font-size: 14px;'>Saisissez votre identifiant client (8 chiffres)</p>", unsafe_allow_html=True)
|
|
78
|
+
|
|
79
|
+
with st.form("login_step1"):
|
|
80
|
+
client_id = st.text_input(
|
|
81
|
+
"Identifiant client",
|
|
82
|
+
placeholder="12345678",
|
|
83
|
+
max_chars=8,
|
|
84
|
+
help="Votre identifiant a 8 chiffres figure sur votre releve de compte"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
submitted = st.form_submit_button("Continuer", use_container_width=True, type="primary")
|
|
88
|
+
|
|
89
|
+
if submitted:
|
|
90
|
+
if not client_id:
|
|
91
|
+
st.error("Veuillez saisir votre identifiant")
|
|
92
|
+
elif len(client_id) != 8 or not client_id.isdigit():
|
|
93
|
+
st.error("L'identifiant doit contenir 8 chiffres")
|
|
94
|
+
else:
|
|
95
|
+
try:
|
|
96
|
+
from nanopy_bank.core.auth import get_auth_service
|
|
97
|
+
except ImportError:
|
|
98
|
+
from ...core.auth import get_auth_service
|
|
99
|
+
|
|
100
|
+
auth = get_auth_service()
|
|
101
|
+
user = auth.get_user_by_client_id(client_id)
|
|
102
|
+
|
|
103
|
+
if user:
|
|
104
|
+
st.session_state.login_client_id = client_id
|
|
105
|
+
st.session_state.login_step = 2
|
|
106
|
+
st.rerun()
|
|
107
|
+
else:
|
|
108
|
+
st.error("Identifiant non reconnu")
|
|
109
|
+
|
|
110
|
+
# Step 2: Password
|
|
111
|
+
elif st.session_state.login_step == 2:
|
|
112
|
+
st.markdown("### Etape 2: Mot de passe")
|
|
113
|
+
st.markdown(f"<p style='color: #888; font-size: 14px;'>Identifiant: <strong>{st.session_state.login_client_id}</strong></p>", unsafe_allow_html=True)
|
|
114
|
+
|
|
115
|
+
with st.form("login_step2"):
|
|
116
|
+
password = st.text_input(
|
|
117
|
+
"Mot de passe",
|
|
118
|
+
type="password",
|
|
119
|
+
placeholder="••••••••",
|
|
120
|
+
help="Votre mot de passe personnel"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
col_a, col_b = st.columns(2)
|
|
124
|
+
with col_a:
|
|
125
|
+
if st.form_submit_button("Retour", use_container_width=True):
|
|
126
|
+
st.session_state.login_step = 1
|
|
127
|
+
st.rerun()
|
|
128
|
+
with col_b:
|
|
129
|
+
submitted = st.form_submit_button("Valider", use_container_width=True, type="primary")
|
|
130
|
+
|
|
131
|
+
if submitted:
|
|
132
|
+
if not password:
|
|
133
|
+
st.error("Veuillez saisir votre mot de passe")
|
|
134
|
+
else:
|
|
135
|
+
try:
|
|
136
|
+
from nanopy_bank.core.auth import get_auth_service
|
|
137
|
+
except ImportError:
|
|
138
|
+
from ...core.auth import get_auth_service
|
|
139
|
+
|
|
140
|
+
auth = get_auth_service()
|
|
141
|
+
session = auth.login_by_client_id(st.session_state.login_client_id, password)
|
|
142
|
+
|
|
143
|
+
if session:
|
|
144
|
+
user = auth.get_user_by_id(session.user_id)
|
|
145
|
+
st.session_state.session_id = session.session_id
|
|
146
|
+
st.session_state.user = user
|
|
147
|
+
st.session_state.logged_in = True
|
|
148
|
+
# Reset login state
|
|
149
|
+
st.session_state.login_step = 1
|
|
150
|
+
st.session_state.login_client_id = ""
|
|
151
|
+
st.success(f"Bienvenue {user.display_name}!")
|
|
152
|
+
st.rerun()
|
|
153
|
+
else:
|
|
154
|
+
st.error("Mot de passe incorrect")
|
|
155
|
+
|
|
156
|
+
# Reset link
|
|
157
|
+
st.markdown("""
|
|
158
|
+
<div style='text-align: center; margin-top: 10px;'>
|
|
159
|
+
<a href='#' style='color: #00d4ff; font-size: 12px;'>Mot de passe oublie?</a>
|
|
160
|
+
</div>
|
|
161
|
+
""", unsafe_allow_html=True)
|
|
162
|
+
|
|
163
|
+
st.divider()
|
|
164
|
+
|
|
165
|
+
st.markdown("### Comptes demo")
|
|
166
|
+
st.markdown("""
|
|
167
|
+
| Role | Identifiant | Mot de passe |
|
|
168
|
+
|------|-------------|--------------|
|
|
169
|
+
| Client | `10000001` | `demo123` |
|
|
170
|
+
| Conseiller | `20000001` | `demo123` |
|
|
171
|
+
| Directeur | `30000001` | `demo123` |
|
|
172
|
+
| Admin | `40000001` | `demo123` |
|
|
173
|
+
| Holding | `50000001` | `demo123` |
|
|
174
|
+
""")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEPA page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
from .common import page_header
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def render_sepa():
|
|
13
|
+
"""Render SEPA XML page"""
|
|
14
|
+
page_header("SEPA XML")
|
|
15
|
+
|
|
16
|
+
bank = st.session_state.bank
|
|
17
|
+
|
|
18
|
+
tab1, tab2, tab3 = st.tabs(["Export Statement", "Export Credit Transfer", "Import XML"])
|
|
19
|
+
|
|
20
|
+
with tab1:
|
|
21
|
+
st.markdown("### Export Bank Statement (camt.053)")
|
|
22
|
+
|
|
23
|
+
if bank.accounts:
|
|
24
|
+
account_options = {f"{acc.account_name} ({acc.format_iban()})": acc.iban for acc in bank.accounts.values()}
|
|
25
|
+
selected = st.selectbox("Select Account", options=list(account_options.keys()), key="stmt_account")
|
|
26
|
+
|
|
27
|
+
col1, col2 = st.columns(2)
|
|
28
|
+
with col1:
|
|
29
|
+
from_date = st.date_input("From Date", value=datetime.now() - timedelta(days=30))
|
|
30
|
+
with col2:
|
|
31
|
+
to_date = st.date_input("To Date", value=datetime.now())
|
|
32
|
+
|
|
33
|
+
if st.button("Generate Statement XML", key="gen_stmt"):
|
|
34
|
+
from nanopy_bank.sepa import SEPAGenerator
|
|
35
|
+
|
|
36
|
+
iban = account_options[selected]
|
|
37
|
+
account = bank.get_account(iban)
|
|
38
|
+
transactions = bank.get_account_transactions(iban, limit=100)
|
|
39
|
+
|
|
40
|
+
generator = SEPAGenerator(
|
|
41
|
+
initiator_name="NanoPy Bank",
|
|
42
|
+
initiator_iban=iban,
|
|
43
|
+
initiator_bic=account.bic
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
xml_content = generator.generate_statement(
|
|
47
|
+
iban=iban,
|
|
48
|
+
transactions=transactions,
|
|
49
|
+
opening_balance=account.balance - sum(tx.signed_amount for tx in transactions),
|
|
50
|
+
closing_balance=account.balance,
|
|
51
|
+
from_date=datetime.combine(from_date, datetime.min.time()),
|
|
52
|
+
to_date=datetime.combine(to_date, datetime.max.time())
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
st.download_button(
|
|
56
|
+
label="Download camt.053.xml",
|
|
57
|
+
data=xml_content,
|
|
58
|
+
file_name=f"statement_{iban}_{to_date.strftime('%Y%m%d')}.xml",
|
|
59
|
+
mime="application/xml"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
with st.expander("Preview XML"):
|
|
63
|
+
st.code(xml_content, language="xml")
|
|
64
|
+
|
|
65
|
+
with tab2:
|
|
66
|
+
st.markdown("### Export Credit Transfer (pain.001)")
|
|
67
|
+
st.info("Create a SEPA batch payment file")
|
|
68
|
+
|
|
69
|
+
with st.form("sepa_export_form"):
|
|
70
|
+
if bank.accounts:
|
|
71
|
+
account_options = {f"{acc.account_name}": acc.iban for acc in bank.accounts.values()}
|
|
72
|
+
from_account = st.selectbox("From Account", options=list(account_options.keys()))
|
|
73
|
+
|
|
74
|
+
to_name = st.text_input("Beneficiary Name")
|
|
75
|
+
to_iban = st.text_input("Beneficiary IBAN")
|
|
76
|
+
to_bic = st.text_input("Beneficiary BIC")
|
|
77
|
+
amount = st.number_input("Amount", min_value=0.01, value=100.0)
|
|
78
|
+
reference = st.text_input("Reference/Label")
|
|
79
|
+
|
|
80
|
+
if st.form_submit_button("Generate XML"):
|
|
81
|
+
from nanopy_bank.sepa import SEPAGenerator
|
|
82
|
+
|
|
83
|
+
from_iban = account_options[from_account]
|
|
84
|
+
account = bank.get_account(from_iban)
|
|
85
|
+
|
|
86
|
+
generator = SEPAGenerator(
|
|
87
|
+
initiator_name="NanoPy Bank Client",
|
|
88
|
+
initiator_iban=from_iban,
|
|
89
|
+
initiator_bic=account.bic
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
xml_content = generator.generate_credit_transfer([{
|
|
93
|
+
"amount": Decimal(str(amount)),
|
|
94
|
+
"creditor_name": to_name,
|
|
95
|
+
"creditor_iban": to_iban,
|
|
96
|
+
"creditor_bic": to_bic,
|
|
97
|
+
"remittance_info": reference
|
|
98
|
+
}])
|
|
99
|
+
|
|
100
|
+
st.download_button(
|
|
101
|
+
label="Download pain.001.xml",
|
|
102
|
+
data=xml_content,
|
|
103
|
+
file_name=f"sepa_transfer_{datetime.now().strftime('%Y%m%d%H%M%S')}.xml",
|
|
104
|
+
mime="application/xml"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
with st.expander("Preview XML"):
|
|
108
|
+
st.code(xml_content, language="xml")
|
|
109
|
+
|
|
110
|
+
with tab3:
|
|
111
|
+
st.markdown("### Import SEPA XML")
|
|
112
|
+
|
|
113
|
+
uploaded_file = st.file_uploader("Upload XML file", type=["xml"])
|
|
114
|
+
|
|
115
|
+
if uploaded_file:
|
|
116
|
+
content = uploaded_file.read().decode()
|
|
117
|
+
st.code(content[:2000] + "..." if len(content) > 2000 else content, language="xml")
|
|
118
|
+
st.info("XML parsing available. Transactions can be imported.")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Settings page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from .common import page_header
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_settings():
|
|
11
|
+
"""Render settings page"""
|
|
12
|
+
page_header("Settings")
|
|
13
|
+
|
|
14
|
+
bank = st.session_state.bank
|
|
15
|
+
|
|
16
|
+
st.markdown("### Bank Information")
|
|
17
|
+
col1, col2 = st.columns(2)
|
|
18
|
+
with col1:
|
|
19
|
+
st.text_input("Bank Name", value="NanoPy Bank", disabled=True)
|
|
20
|
+
st.text_input("BIC", value="NANPFRPP", disabled=True)
|
|
21
|
+
with col2:
|
|
22
|
+
st.text_input("Country", value="France", disabled=True)
|
|
23
|
+
st.text_input("Data Directory", value=str(bank.data_dir), disabled=True)
|
|
24
|
+
|
|
25
|
+
st.divider()
|
|
26
|
+
|
|
27
|
+
st.markdown("### Customers")
|
|
28
|
+
if bank.customers:
|
|
29
|
+
for cust_id, customer in bank.customers.items():
|
|
30
|
+
with st.expander(f"👤 {customer.full_name} ({cust_id})"):
|
|
31
|
+
st.markdown(f"**Email:** {customer.email}")
|
|
32
|
+
st.markdown(f"**Phone:** {customer.phone}")
|
|
33
|
+
st.markdown(f"**Address:** {customer.address}, {customer.postal_code} {customer.city}")
|
|
34
|
+
st.markdown(f"**Created:** {customer.created_at.strftime('%d/%m/%Y')}")
|
|
35
|
+
|
|
36
|
+
st.divider()
|
|
37
|
+
|
|
38
|
+
st.markdown("### Danger Zone")
|
|
39
|
+
if st.button("Reset All Data", type="secondary"):
|
|
40
|
+
if st.checkbox("I confirm I want to delete all data"):
|
|
41
|
+
import shutil
|
|
42
|
+
shutil.rmtree(bank.data_dir, ignore_errors=True)
|
|
43
|
+
bank.data_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
bank.customers.clear()
|
|
45
|
+
bank.accounts.clear()
|
|
46
|
+
bank.transactions.clear()
|
|
47
|
+
st.success("All data has been reset")
|
|
48
|
+
st.rerun()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transfers page
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from .common import format_currency, page_header
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render_transfers():
|
|
12
|
+
"""Render transfers page"""
|
|
13
|
+
page_header("Transfers")
|
|
14
|
+
|
|
15
|
+
bank = st.session_state.bank
|
|
16
|
+
|
|
17
|
+
if not bank.accounts:
|
|
18
|
+
st.warning("No accounts available. Create one first.")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
tab1, tab2 = st.tabs(["New Transfer", "Transfer History"])
|
|
22
|
+
|
|
23
|
+
with tab1:
|
|
24
|
+
st.markdown("### New Transfer")
|
|
25
|
+
|
|
26
|
+
with st.form("transfer_form"):
|
|
27
|
+
col1, col2 = st.columns(2)
|
|
28
|
+
|
|
29
|
+
with col1:
|
|
30
|
+
st.markdown("**From Account**")
|
|
31
|
+
from_options = {f"{acc.account_name} ({format_currency(acc.balance)})": acc.iban for acc in bank.accounts.values()}
|
|
32
|
+
from_account = st.selectbox("Source Account", options=list(from_options.keys()))
|
|
33
|
+
|
|
34
|
+
st.markdown("**Amount**")
|
|
35
|
+
amount = st.number_input("Amount (EUR)", min_value=0.01, value=100.0, step=10.0)
|
|
36
|
+
|
|
37
|
+
with col2:
|
|
38
|
+
st.markdown("**To Account**")
|
|
39
|
+
transfer_type = st.radio("Transfer Type", ["Internal", "External (SEPA)"])
|
|
40
|
+
|
|
41
|
+
if transfer_type == "Internal":
|
|
42
|
+
to_options = {f"{acc.account_name}": acc.iban for acc in bank.accounts.values()}
|
|
43
|
+
to_account = st.selectbox("Destination Account", options=list(to_options.keys()))
|
|
44
|
+
to_iban = to_options[to_account]
|
|
45
|
+
to_name = ""
|
|
46
|
+
to_bic = ""
|
|
47
|
+
else:
|
|
48
|
+
to_iban = st.text_input("Beneficiary IBAN", placeholder="FR76 1234 5678 9012 3456 7890 123")
|
|
49
|
+
to_bic = st.text_input("BIC (optional)", placeholder="BNPAFRPP")
|
|
50
|
+
to_name = st.text_input("Beneficiary Name")
|
|
51
|
+
|
|
52
|
+
label = st.text_input("Label/Reference", placeholder="Rent payment")
|
|
53
|
+
|
|
54
|
+
submitted = st.form_submit_button("Send Transfer", type="primary")
|
|
55
|
+
|
|
56
|
+
if submitted:
|
|
57
|
+
try:
|
|
58
|
+
from_iban = from_options[from_account]
|
|
59
|
+
|
|
60
|
+
if transfer_type == "Internal":
|
|
61
|
+
debit_tx, credit_tx = bank.transfer(from_iban, to_iban, Decimal(str(amount)), label)
|
|
62
|
+
st.success(f"Transfer completed! Reference: {debit_tx.reference}")
|
|
63
|
+
else:
|
|
64
|
+
tx = bank.sepa_credit_transfer(from_iban, to_iban, to_bic, to_name, Decimal(str(amount)), label)
|
|
65
|
+
st.success(f"SEPA transfer initiated! Reference: {tx.reference}")
|
|
66
|
+
|
|
67
|
+
st.rerun()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
st.error(f"Error: {e}")
|
|
70
|
+
|
|
71
|
+
with tab2:
|
|
72
|
+
st.markdown("### Recent Transfers")
|
|
73
|
+
|
|
74
|
+
from nanopy_bank.core import TransactionType
|
|
75
|
+
|
|
76
|
+
all_transfers = [
|
|
77
|
+
tx for tx in bank.transactions.values()
|
|
78
|
+
if tx.transaction_type in [TransactionType.TRANSFER, TransactionType.SEPA_CREDIT]
|
|
79
|
+
]
|
|
80
|
+
all_transfers.sort(key=lambda x: x.created_at, reverse=True)
|
|
81
|
+
|
|
82
|
+
if all_transfers:
|
|
83
|
+
for tx in all_transfers[:20]:
|
|
84
|
+
col1, col2, col3 = st.columns([3, 2, 2])
|
|
85
|
+
with col1:
|
|
86
|
+
st.markdown(f"**{tx.label}**")
|
|
87
|
+
st.caption(f"{tx.from_iban or 'N/A'} → {tx.to_iban or tx.counterparty_iban or 'N/A'}")
|
|
88
|
+
with col2:
|
|
89
|
+
st.caption(tx.created_at.strftime("%d/%m/%Y %H:%M"))
|
|
90
|
+
with col3:
|
|
91
|
+
st.markdown(f":red[-{format_currency(tx.amount)}]")
|
|
92
|
+
st.divider()
|
|
93
|
+
else:
|
|
94
|
+
st.info("No transfers yet")
|
nanopy_bank/ui/pages.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI Pages - Modular page components
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# This module contains page components imported by the main app
|
|
6
|
+
# Each function renders a specific section of the UI
|
|
7
|
+
|
|
8
|
+
# Page functions are defined in app.py for now
|
|
9
|
+
# This module serves as a placeholder for future modularization
|
|
10
|
+
|
|
11
|
+
dashboard = None
|
|
12
|
+
accounts = None
|
|
13
|
+
transfers = None
|
|
14
|
+
cards = None
|
|
15
|
+
sepa = None
|
|
16
|
+
settings = None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nanopy-bank
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: Online Banking System with Streamlit UI and SEPA XML support
|
|
5
|
+
Home-page: https://github.com/Web3-League/nanopy-bank
|
|
6
|
+
Author: NanoPy Team
|
|
7
|
+
Author-email: dev@nanopy.chain
|
|
8
|
+
Keywords: banking sepa xml iso20022 streamlit fintech
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: streamlit>=1.28.0
|
|
19
|
+
Requires-Dist: streamlit-shadcn-ui>=0.1.0
|
|
20
|
+
Requires-Dist: streamlit-extras>=0.3.0
|
|
21
|
+
Requires-Dist: streamlit-option-menu>=0.3.0
|
|
22
|
+
Requires-Dist: lxml>=4.9.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
25
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
26
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
27
|
+
Requires-Dist: schwifty>=2023.0.0
|
|
28
|
+
Requires-Dist: reportlab>=4.0.0
|
|
29
|
+
Requires-Dist: qrcode>=7.4.0
|
|
30
|
+
Requires-Dist: pillow>=10.0.0
|
|
31
|
+
Requires-Dist: click>=8.0.0
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: requires-dist
|
|
40
|
+
Dynamic: requires-python
|
|
41
|
+
Dynamic: summary
|
|
42
|
+
|
|
43
|
+
# NanoPy Bank
|
|
44
|
+
|
|
45
|
+
Online Banking System built with Python, Streamlit and shadcn-ui.
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- Account Management (IBAN, BIC)
|
|
50
|
+
- Transaction History
|
|
51
|
+
- SEPA/ISO20022 XML Import/Export
|
|
52
|
+
- Real-time Balance Updates
|
|
53
|
+
- Multi-currency Support
|
|
54
|
+
- PDF Statements
|
|
55
|
+
- API for integrations
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install nanopy-bank
|
|
61
|
+
nanopy-bank serve
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Start the banking UI
|
|
68
|
+
nanopy-bank serve --port 8501
|
|
69
|
+
|
|
70
|
+
# Generate SEPA XML
|
|
71
|
+
nanopy-bank export-sepa --account FR7612345678901234567890123
|
|
72
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
nanopy_bank/__init__.py,sha256=5HoUAbjRpOgOw_W0T5QGdCi9LPUfeen-7HRiDmlRStA,396
|
|
2
|
+
nanopy_bank/app.py,sha256=Vqna-bHSHEXQEWLxC5zeu4jHta9YfQ7pCBpsfnrJ60g,10363
|
|
3
|
+
nanopy_bank/cli.py,sha256=PDJY3q6s1KESZ3osvUwfpA_X4UctDHO3eqP5Ro1sQ0w,4845
|
|
4
|
+
nanopy_bank/api/__init__.py,sha256=2f6_BP4BTTRly8TxWsotQ1i7KX7xglmO2ahHyD8I_Ew,146
|
|
5
|
+
nanopy_bank/api/server.py,sha256=BnkQ4BelJrRNYEa2eYhxNf1-UQC1OYXrSK-JuSvu4mo,10049
|
|
6
|
+
nanopy_bank/core/__init__.py,sha256=1IppYfqmKRwlOhEM2t93d0UCigK1qpzP4NOOrMDAGq4,1761
|
|
7
|
+
nanopy_bank/core/audit.py,sha256=QRLDlv7ij7BlM4z88J3La_GmFi7qc_mKggHAiEW9WM4,12853
|
|
8
|
+
nanopy_bank/core/auth.py,sha256=2k77FeITfGZXvuASQcrKEcHyZzfG9jq74BNSgm2wJA8,9570
|
|
9
|
+
nanopy_bank/core/bank.py,sha256=4GjRW3t3EWMO_-fY-DXuDhET2wJyu3dKmQnQ6p-4lSY,15223
|
|
10
|
+
nanopy_bank/core/beneficiary.py,sha256=ZqVGLyrwPstichsZNwnN9y6gFSvZuQXrX_GpOvdiaUY,8610
|
|
11
|
+
nanopy_bank/core/branch.py,sha256=7Tje3FbUzut-ThqUPmdMbmvRdo7V5P9G4fVvs_-p-HM,11138
|
|
12
|
+
nanopy_bank/core/fees.py,sha256=6iCDQ5dIcmuijCE1h2_ySRKdqgVWFvRb9Gk_2pXVpX0,8927
|
|
13
|
+
nanopy_bank/core/holding.py,sha256=1u2yijNBN5fvrbYOcSE72PtsU7i4U_xyplTKcjTRSns,14240
|
|
14
|
+
nanopy_bank/core/models.py,sha256=AjdxWRrjhkd2LhtbTs-T3h2aPZgYZ59GJ3GsULhRoqs,10346
|
|
15
|
+
nanopy_bank/core/products.py,sha256=Z8yAiVQpgNE8ww3NeG_8fTcgvjTguJHHvVlWzW1EDi0,11496
|
|
16
|
+
nanopy_bank/data/__init__.py,sha256=6IT2do_H-zAupW1wAE-ALw8sIXhY6_ZnVKz8Lumsk-8,572
|
|
17
|
+
nanopy_bank/data/demo.py,sha256=I6dEbVsI2X6RSQBPm8ZnuvMsAAnxAsAlfMeLGRMWskY,39413
|
|
18
|
+
nanopy_bank/documents/__init__.py,sha256=mF4jvOKVX2-OXs2U8mfQ8gTZnkjPo31HQ2phn45O4D4,204
|
|
19
|
+
nanopy_bank/documents/statement.py,sha256=X8dw4Zt-DErfcrhyNJZOo-y2baoSBAyVVSKKAK10ayM,11013
|
|
20
|
+
nanopy_bank/sepa/__init__.py,sha256=fQeaKo3C4ARKRlGKUDKQQZP7ZN8ajZKzDeCExHe01JM,145
|
|
21
|
+
nanopy_bank/sepa/sepa.py,sha256=HszXec-yKQnp0fkKkdj6vqskMhScoss_8ezML0Ju1lw,18940
|
|
22
|
+
nanopy_bank/storage/__init__.py,sha256=8Rf0jygfM8ZEGZa5CkpkVOM9MCEMtYDq2dnQjPyDe50,196
|
|
23
|
+
nanopy_bank/storage/json_storage.py,sha256=ec7m09BzftqoNiEeUh8zSh22ri78twIDP-5vs6V65u8,3984
|
|
24
|
+
nanopy_bank/storage/sqlite_storage.py,sha256=r5Z3cHyv2IRaf0-RGuFMoinTi3vSe37T2Mk8hKzcaUw,13622
|
|
25
|
+
nanopy_bank/ui/__init__.py,sha256=vcpfgNk8_b6a--ii7GhQJqoNHNPqzWefIEDChALa2G0,236
|
|
26
|
+
nanopy_bank/ui/pages.py,sha256=gt60uZ9B5XZT6nK2tPyDjA1DghhWXvx_LSXaaPzF5Kg,381
|
|
27
|
+
nanopy_bank/ui/pages/__init__.py,sha256=Cq6Q6H_a0HODNhP_cbq60msCwc9WG9Zn8xE3Sp68xYM,853
|
|
28
|
+
nanopy_bank/ui/pages/accounts.py,sha256=ueexihflR1r1Tkv19jqsJF_MsHgq3_7Amnu1vlXOTLs,3538
|
|
29
|
+
nanopy_bank/ui/pages/advisor.py,sha256=0J3XQgsUkvnRBoemOGbs7W4kjJeraOCEkJ50mhMfLFQ,5725
|
|
30
|
+
nanopy_bank/ui/pages/audit.py,sha256=NpID0zVZ6w_lLyR77ZEDYuOPxtBu_ZLONNPju3tmcWQ,2885
|
|
31
|
+
nanopy_bank/ui/pages/beneficiaries.py,sha256=QT9qk35w-Kh6vS-IGT0d3MiMGkQDIdUTAJ7JHUv5asE,4958
|
|
32
|
+
nanopy_bank/ui/pages/branches.py,sha256=o2fF_2ejhT2VnQjz79TcGgyPTZCGeivVvPP6FW8fgdE,2256
|
|
33
|
+
nanopy_bank/ui/pages/cards.py,sha256=y73cFQgS33MTmbFL7S3i45Yy-YlxFLiH5rXm9YR9Qxg,1132
|
|
34
|
+
nanopy_bank/ui/pages/common.py,sha256=BuGDBERfKn-RGLn_p0QPl53mBG3qHvLSQZPvcLSLwT0,514
|
|
35
|
+
nanopy_bank/ui/pages/dashboard.py,sha256=91cZuWc6DMrUkgIXdQuFoF81zBSrUG_jrsHrmvmg_Jw,3931
|
|
36
|
+
nanopy_bank/ui/pages/fees.py,sha256=zkRGZ0Xxuf02BJgjhgMehr2tSUlzwqLulePHc9TBEoY,2334
|
|
37
|
+
nanopy_bank/ui/pages/holding.py,sha256=PJoV_m_ZSXuUXdAFjdlGTE4NwRCe2QiA9XFAOEu7NIE,43970
|
|
38
|
+
nanopy_bank/ui/pages/loans.py,sha256=wkIix4gJVlGzmsY-1APZHmAJMCX--NWW2yULHgDGjvI,4635
|
|
39
|
+
nanopy_bank/ui/pages/login.py,sha256=Irg3omWM1LpzMjlxjFi7dqFWlMUj3Huw9y0sZW5Ru5o,7195
|
|
40
|
+
nanopy_bank/ui/pages/sepa.py,sha256=sKZPkFNFhcp15f8yieDN3K9mcDYCvApv6MUh1U5R7rM,4648
|
|
41
|
+
nanopy_bank/ui/pages/settings.py,sha256=pgmS3g5v6a7_ehfoo36lOUjCLibkjrAbU5qZKPDuwuQ,1640
|
|
42
|
+
nanopy_bank/ui/pages/transfers.py,sha256=NQ_LYhbpLA3JFA5HQdneFgwDA5GVjBlHMdGlGwOai94,3735
|
|
43
|
+
nanopy_bank-1.0.8.dist-info/METADATA,sha256=zyobSWisptp6XNlRzt2l_ipHJ8quzmsZwjk1oMRblgc,1952
|
|
44
|
+
nanopy_bank-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
45
|
+
nanopy_bank-1.0.8.dist-info/entry_points.txt,sha256=nXwEPBtjV-N5yI0PBtv7MnI9dWUceqxfHexhVSgpxGU,53
|
|
46
|
+
nanopy_bank-1.0.8.dist-info/top_level.txt,sha256=Rm_HWM6virTPC-zKy8xu0Ml0QqYeCLYnl-pEgFfV6p0,12
|
|
47
|
+
nanopy_bank-1.0.8.dist-info/RECORD,,
|