Commit 8fa9659
feat: Multiple Bank Accounts (IBAN/BIC) by contract (#511)
* feat: support multiple bank accounts with a per-contract default
Replace the single 1:1 bank account with a list owned by the user:
- User.bank_accounts (new relationship) replaces the bank_account_id column;
is_default flags the account used when a contract names none.
- Contract gains bank_account_id so a contract can invoice into a
specific account; e-invoice payee and rendered invoices resolve
contract account first, then the user's default.
- Migration backfills ownership (bankaccount.user_id) and marks the
previously linked account as default, dropping user.bank_account_id.
- Settings profile gains a multi-account editor (default radio, add/
remove). Contract form offers an Invoicing Bank Account dropdown.
- Removing an account a contract still invoices from is refused.
- Demo user gets Giro (default) + Wise USD accounts; the USD contract
invoices into the Wise account.
Closes #510
* fix: preserve partial legacy bank account updates
* demo.py aktualisieren
* demo.py aktualisieren
* chore(tests): name fixture bank accounts after the account holder
tuttle/demo.py names the demo accounts "Harry Tuttle" (the field is the
account holder / bank name); the test fixtures still said "Giro".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(users): drop the unused legacy bank account payload branch
update_profile accepted a single ``bank_account`` dict as a partial
update alongside the ``bank_accounts`` list. Nothing sends it: the
settings view sends the list and onboarding goes through users.create,
which keeps its own ``bank_account`` handling. Dropping the branch also
retires the _ACCOUNTS_UNCHANGED sentinel that only existed to tell it
apart from an absent key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(contracts): let the FK reject a stale bank account
_validate_bank_account re-queried the account before every save to
produce a friendly error. The per-user engine runs with
PRAGMA foreign_keys = ON (abstractions.py), so SQLite already rejects a
bank_account_id that no longer exists and _describe_save_error turns the
IntegrityError into the same message. Widen that message to name the
bank account, since contract now has two foreign keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: shorten the payee account lookups
Both User helpers were loops that a generator expression states in one
line, and the invoice payee lookup wrapped one attribute access in a
bare `except Exception`. Invoice.contract is lazy="subquery", so it is
loaded with the invoice and cannot raise on access; a None check is
enough.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ui): drop dead bank account form state
- bank_accountsError was declared, initialised and rendered but never
assigned: update_profile's refusal to remove an in-use account already
arrives as res.error and shows up in profileStatus.
- The .map() after the remove .filter() was a no-op — with one account it
mapped over an empty array, otherwise it re-set is_default to itself.
The backend re-picks a default when none is flagged.
- Three identical setProfile spreads collapse into patchAccount().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: drop the ad-hoc electron driver harness
ui/driver.mjs was a local Playwright REPL used to smoke-test this
branch: no package.json script, no CI job, no test references it, and it
imports playwright-core while the project depends on playwright. Keep
that kind of scratch tooling out of the tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: cover the bank account ownership backfill
f87515d1d068 rewrites data, and nothing pinned that behaviour: the
existing chain tests only prove row counts survive. Seed the old 1:1
link at the previous head and assert the linked account becomes the
owned default, an unreferenced account is kept but not promoted,
user.bank_account_id is gone, and both new FKs come back from the batch
rebuild with CASCADE / SET NULL intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* style: restore the blank line ruff wants after the constants
Removing the _ACCOUNTS_UNCHANGED sentinel in 702c626 collapsed the
two-line separator before _normalize_logo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tests): update SEPA QR tests for the multi-account bank model
user.bank_account is now a read-only property over bank_accounts, so
these QR tests still assigned to a setter that no longer exists.
Also update TestGeneratePaymentQR for generate_payment_qr's new
signature, which now takes the resolved payee account instead of the
user, so it respects the account named on the invoice's contract.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Christian Staudt <mail@clstaudt.me>
Co-authored-by: Cursor <cursoragent@cursor.com>1 parent 50ca8dd commit 8fa9659
21 files changed
Lines changed: 519 additions & 131 deletions
File tree
- templates
- invoice-anvil
- invoice-bold
- invoice-classic
- invoice-grayshades
- invoice-minimal
- invoice-modern
- invoice
- tuttle_tests
- tuttle
- app
- contracts
- users
- migrations/versions
- ui/src/components
- business
- settings
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
130 | | - | |
| 130 | + | |
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
| |||
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
157 | | - | |
158 | | - | |
| 157 | + | |
| 158 | + | |
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
104 | | - | |
| 104 | + | |
105 | 105 | | |
106 | | - | |
107 | | - | |
108 | | - | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | | - | |
156 | | - | |
| 155 | + | |
| 156 | + | |
157 | 157 | | |
158 | 158 | | |
159 | 159 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
97 | | - | |
| 96 | + | |
| 97 | + | |
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
137 | | - | |
138 | | - | |
| 137 | + | |
| 138 | + | |
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
140 | | - | |
| 140 | + | |
141 | 141 | | |
142 | | - | |
| 142 | + | |
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
132 | | - | |
| 132 | + | |
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | | - | |
156 | | - | |
| 155 | + | |
| 156 | + | |
157 | 157 | | |
158 | 158 | | |
159 | 159 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
| 105 | + | |
106 | 106 | | |
107 | | - | |
108 | | - | |
109 | | - | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| |||
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
157 | | - | |
158 | | - | |
| 157 | + | |
| 158 | + | |
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
111 | | - | |
| 111 | + | |
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| |||
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
139 | | - | |
| 139 | + | |
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
189 | 189 | | |
190 | 190 | | |
191 | 191 | | |
192 | | - | |
| 192 | + | |
193 | 193 | | |
194 | 194 | | |
195 | 195 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
294 | 294 | | |
295 | 295 | | |
296 | 296 | | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
302 | 305 | | |
303 | 306 | | |
304 | 307 | | |
| |||
308 | 311 | | |
309 | 312 | | |
310 | 313 | | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
311 | 362 | | |
312 | 363 | | |
313 | 364 | | |
| |||
369 | 420 | | |
370 | 421 | | |
371 | 422 | | |
372 | | - | |
373 | | - | |
374 | | - | |
375 | | - | |
376 | | - | |
377 | | - | |
378 | | - | |
379 | | - | |
380 | | - | |
381 | | - | |
382 | | - | |
383 | | - | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
384 | 429 | | |
385 | 430 | | |
386 | 431 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
473 | 473 | | |
474 | 474 | | |
475 | 475 | | |
| 476 | + | |
| 477 | + | |
476 | 478 | | |
477 | 479 | | |
478 | 480 | | |
| |||
847 | 849 | | |
848 | 850 | | |
849 | 851 | | |
850 | | - | |
851 | | - | |
852 | | - | |
853 | | - | |
854 | | - | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
855 | 866 | | |
856 | 867 | | |
857 | 868 | | |
| |||
0 commit comments