Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions templates/invoice-anvil/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
<tr>
<td class="payment-info">
<div>
{% if user.bank_account %}IBAN: <strong>{{ user.bank_account.IBAN }}</strong><br>{% endif %}
{% if bank_account %}IBAN: <strong>{{ bank_account.IBAN }}</strong><br>{% endif %}
</div>
</td>
<td class="bold">{{ invoice.effective_due_date }}</td>
Expand All @@ -154,8 +154,8 @@

<div class="footer">
<div class="footer-info">
{% if user.bank_account %}<span>IBAN: {{ user.bank_account.IBAN }} </span> |
<span>BIC: {{ user.bank_account.BIC }}</span> |{% endif %}
{% if bank_account %}<span>IBAN: {{ bank_account.IBAN }} </span> |
<span>BIC: {{ bank_account.BIC }}</span> |{% endif %}
{% if seller_tax_id %}<span>{{ seller_tax_id_label }}: {{ seller_tax_id }}</span>{% endif %}
</div>
</div>
Expand Down
12 changes: 6 additions & 6 deletions templates/invoice-bold/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@
<div class="bottom-section">
<div class="payment">
<span class="label">{{ l.payment }}</span>
{% if user.bank_account %}
{% if bank_account %}
<table class="payment-table">
{% if user.bank_account.name %}<tr><td class="payment-key">{{ l.account_holder }}</td><td class="payment-val">{{ user.bank_account.name }}</td></tr>{% endif %}
<tr><td class="payment-key">IBAN</td><td class="payment-val">{{ user.bank_account.IBAN }}</td></tr>
{% if user.bank_account.BIC %}<tr><td class="payment-key">BIC</td><td class="payment-val">{{ user.bank_account.BIC }}</td></tr>{% endif %}
{% if bank_account.name %}<tr><td class="payment-key">{{ l.account_holder }}</td><td class="payment-val">{{ bank_account.name }}</td></tr>{% endif %}
<tr><td class="payment-key">IBAN</td><td class="payment-val">{{ bank_account.IBAN }}</td></tr>
{% if bank_account.BIC %}<tr><td class="payment-key">BIC</td><td class="payment-val">{{ bank_account.BIC }}</td></tr>{% endif %}
</table>
{% endif %}
</div>
Expand Down Expand Up @@ -149,8 +149,8 @@

<div class="footer">
<span>{{ user.name }}</span>
{% if user.bank_account %}<span>IBAN: {{ user.bank_account.IBAN }}</span>
{% if user.bank_account.BIC %}<span>BIC: {{ user.bank_account.BIC }}</span>{% endif %}{% endif %}
{% if bank_account %}<span>IBAN: {{ bank_account.IBAN }}</span>
{% if bank_account.BIC %}<span>BIC: {{ bank_account.BIC }}</span>{% endif %}{% endif %}
{% if seller_tax_id %}<span>{{ seller_tax_id_label }}: {{ seller_tax_id }}</span>{% endif %}
</div>

Expand Down
8 changes: 4 additions & 4 deletions templates/invoice-classic/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@
<div class="payment">
<span class="party-label">Payment Details</span>
<div class="payment-value">
{% if user.bank_account %}IBAN: {{ user.bank_account.IBAN }}
{% if user.bank_account.BIC %}<br>BIC: {{ user.bank_account.BIC }}{% endif %}{% endif %}
{% if bank_account %}IBAN: {{ bank_account.IBAN }}
{% if bank_account.BIC %}<br>BIC: {{ bank_account.BIC }}{% endif %}{% endif %}
</div>
</div>
<div class="totals">
Expand Down Expand Up @@ -134,8 +134,8 @@

<div class="footer">
<span>{{ user.name }}</span>
{% if user.bank_account %}<span>IBAN: {{ user.bank_account.IBAN }}</span>
{% if user.bank_account.BIC %}<span>BIC: {{ user.bank_account.BIC }}</span>{% endif %}{% endif %}
{% if bank_account %}<span>IBAN: {{ bank_account.IBAN }}</span>
{% if bank_account.BIC %}<span>BIC: {{ bank_account.BIC }}</span>{% endif %}{% endif %}
{% if seller_tax_id %}<span>{{ seller_tax_id_label }}: {{ seller_tax_id }}</span>{% endif %}
</div>

Expand Down
4 changes: 2 additions & 2 deletions templates/invoice-grayshades/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ <h1 class="invoice-title">{{ l.invoice_no }} {{ invoice.number }}</h1>
<span class="footer-name">{{ user.name }}</span>
</div>
<div class="footer-center">
{% if user.bank_account %}<span>IBAN: {{ user.bank_account.IBAN }}</span>{% endif %}
{% if bank_account %}<span>IBAN: {{ bank_account.IBAN }}</span>{% endif %}
{% if user.phone_number %}<span>Tel. {{ user.phone_number }}</span>{% endif %}
{% if user.bank_account and user.bank_account.BIC %}<br><span>BIC: {{ user.bank_account.BIC }}</span>{% endif %}
{% if bank_account and bank_account.BIC %}<br><span>BIC: {{ bank_account.BIC }}</span>{% endif %}
{% if user.website %}<span>{{ user.website }}</span>{% endif %}
</div>
<div class="footer-right">
Expand Down
6 changes: 3 additions & 3 deletions templates/invoice-minimal/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
<div class="payment">
<span class="field-label">{{ l.payment }}</span>
<div class="payment-value">
{% if user.bank_account %}<div>IBAN: {{ user.bank_account.IBAN }}{% if user.bank_account.BIC %}&emsp;BIC: {{ user.bank_account.BIC }}{% endif %}</div>{% endif %}
{% if bank_account %}<div>IBAN: {{ bank_account.IBAN }}{% if bank_account.BIC %}&emsp;BIC: {{ bank_account.BIC }}{% endif %}</div>{% endif %}
</div>
</div>

Expand All @@ -149,8 +149,8 @@

<div class="footer">
<span>{{ user.name }}</span>
{% if user.bank_account %}<span>IBAN: {{ user.bank_account.IBAN }}</span>
{% if user.bank_account.BIC %}<span>BIC: {{ user.bank_account.BIC }}</span>{% endif %}{% endif %}
{% if bank_account %}<span>IBAN: {{ bank_account.IBAN }}</span>
{% if bank_account.BIC %}<span>BIC: {{ bank_account.BIC }}</span>{% endif %}{% endif %}
{% if seller_tax_id %}<span>{{ seller_tax_id_label }}: {{ seller_tax_id }}</span>{% endif %}
</div>

Expand Down
12 changes: 6 additions & 6 deletions templates/invoice-modern/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@
<div class="bottom-section">
<div class="payment">
<span class="label">{{ l.payment }}</span>
{% if user.bank_account %}
{% if bank_account %}
<table class="payment-table">
{% if user.bank_account.name %}<tr><td class="payment-key">{{ l.account_holder if l.account_holder else "Account" }}</td><td class="payment-val">{{ user.bank_account.name }}</td></tr>{% endif %}
<tr><td class="payment-key">IBAN</td><td class="payment-val">{{ user.bank_account.IBAN }}</td></tr>
{% if user.bank_account.BIC %}<tr><td class="payment-key">BIC</td><td class="payment-val">{{ user.bank_account.BIC }}</td></tr>{% endif %}
{% if bank_account.name %}<tr><td class="payment-key">{{ l.account_holder if l.account_holder else "Account" }}</td><td class="payment-val">{{ bank_account.name }}</td></tr>{% endif %}
<tr><td class="payment-key">IBAN</td><td class="payment-val">{{ bank_account.IBAN }}</td></tr>
{% if bank_account.BIC %}<tr><td class="payment-key">BIC</td><td class="payment-val">{{ bank_account.BIC }}</td></tr>{% endif %}
</table>
{% endif %}
</div>
Expand Down Expand Up @@ -151,8 +151,8 @@

<div class="footer">
<span>{{ user.name }}</span>
{% if user.bank_account %}<span>IBAN: {{ user.bank_account.IBAN }}</span>
{% if user.bank_account.BIC %}<span>BIC: {{ user.bank_account.BIC }}</span>{% endif %}{% endif %}
{% if bank_account %}<span>IBAN: {{ bank_account.IBAN }}</span>
{% if bank_account.BIC %}<span>BIC: {{ bank_account.BIC }}</span>{% endif %}{% endif %}
{% if seller_tax_id %}<span>{{ seller_tax_id_label }}: {{ seller_tax_id }}</span>{% endif %}
</div>

Expand Down
4 changes: 2 additions & 2 deletions templates/invoice/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ <h1>{% if is_reminder %}{{ reminder_title }}{% else %}Invoice No. {{ invoice.num
<tbody>
<tr>
<td>{{ invoice.effective_due_date }}</td>
<td>{% if user.bank_account %}{{ user.bank_account.IBAN }}{% endif %}</td>
<td>{% if bank_account %}{{ bank_account.IBAN }}{% endif %}</td>
<td>{{ invoice.total | as_currency }}</td>
</tr>
</tbody>
Expand Down Expand Up @@ -136,7 +136,7 @@ <h1>{% if is_reminder %}{{ reminder_title }}{% else %}Invoice No. {{ invoice.num
</td>
<td>
{% if seller_tax_id %}{{ seller_tax_id_label }}: {{ seller_tax_id }}<br>{% endif %}
{% if user.bank_account %}IBAN: {{ user.bank_account.IBAN }}{% endif %}
{% if bank_account %}IBAN: {{ bank_account.IBAN }}{% endif %}
</td>
</tr>
</tbody>
Expand Down
2 changes: 1 addition & 1 deletion tuttle/app/contracts/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def _describe_save_error(exc) -> str:
if "NOT NULL" in detail:
return "A required field is missing."
if "FOREIGN KEY" in detail or "foreign key" in detail:
return "The selected client is invalid."
return "The selected client or bank account is invalid."
return "Failed to save the contract."

toggle_complete_status = CrudIntent.toggle_completed
79 changes: 62 additions & 17 deletions tuttle/app/users/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,14 @@ def create(self, params: Dict[str, Any], **_kw) -> IntentResult:
)
bank = params.get("bank_account")
if bank and any(bank.get(k) for k in ("IBAN", "BIC", "name")):
user.bank_account = BankAccount(
name=bank.get("name", ""),
IBAN=bank.get("IBAN", ""),
BIC=bank.get("BIC", ""),
)
user.bank_accounts = [
BankAccount(
name=bank.get("name", ""),
IBAN=bank.get("IBAN", ""),
BIC=bank.get("BIC", ""),
is_default=True,
)
]
s.add(user)
s.commit()
engine.dispose()
Expand All @@ -308,6 +311,54 @@ def create(self, params: Dict[str, Any], **_kw) -> IntentResult:

# -- profile update -------------------------------------------------------

def _resolve_bank_accounts(self, profile, raw):
"""Resolve an incoming ``bank_accounts`` list into a full replacement.

Returns ``(error_msg, accounts)`` where a falsy ``error_msg`` means
success.

Existing accounts keep their identity by id, so contracts referencing
one keep pointing at it. Exactly one account ends up default (the
flagged one, else the first). Removing an account a contract still
invoices from is refused.
"""
rows = [r for r in raw if isinstance(r, dict) and any((r.get(k) or "").strip() for k in ("name", "IBAN", "BIC"))]

existing = {a.id: a for a in profile.bank_accounts if a.id is not None}
flagged = [r for r in rows if r.get("is_default")]
default_index = rows.index(flagged[0]) if flagged else (0 if rows else -1)

accounts = []
for i, row in enumerate(rows):
fields = {k: (row.get(k) or "").strip() for k in ("name", "IBAN", "BIC")}
account = existing.get(row.get("id"))
if account is None:
account = BankAccount(**fields)
else:
for key, value in fields.items():
setattr(account, key, value)
account.is_default = i == default_index
accounts.append(account)

removed_ids = {a.id for a in existing.values()} - {a.id for a in accounts}
if removed_ids:
from ...model import Contract

engine = sql_create_engine(f"sqlite:///{get_active_db()}")
try:
with SqlSession(engine) as s:
referenced = s.exec(select(Contract).where(Contract.bank_account_id.in_(removed_ids))).all()
finally:
engine.dispose()
if referenced:
titles = ", ".join(c.title for c in referenced)
return (
"Cannot remove a bank account that is still used for invoicing "
f"by: {titles}. Clear the account on the contract(s) first.",
None,
)
return None, accounts

def update_profile(self, profile_data: Dict[str, Any]) -> IntentResult:
"""Update the active user's profile from a dict."""
ds = UserDataSource()
Expand Down Expand Up @@ -369,18 +420,12 @@ def update_profile(self, profile_data: Dict[str, Any]) -> IntentResult:
else:
profile.address = Address(**{k: v for k, v in addr.items() if k != "id" and not k.startswith("_")})

bank = profile_data.get("bank_account")
if bank is not None:
if profile.bank_account:
for k in ("name", "IBAN", "BIC"):
if k in bank:
setattr(profile.bank_account, k, bank[k])
else:
profile.bank_account = BankAccount(
name=bank.get("name", ""),
IBAN=bank.get("IBAN", ""),
BIC=bank.get("BIC", ""),
)
raw_accounts = profile_data.get("bank_accounts")
if isinstance(raw_accounts, list):
error, accounts = self._resolve_bank_accounts(profile, raw_accounts)
if error:
return IntentResult(was_intent_successful=False, error_msg=error)
profile.bank_accounts = accounts

with ds.create_session() as s:
s.add(profile)
Expand Down
21 changes: 16 additions & 5 deletions tuttle/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,8 @@ def create_usd_security_data(user: User) -> tuple[Project, Invoice, ClientContac
volume=10,
term_of_payment=14,
billing_cycle=Cycle.monthly,
# Harry keeps a dedicated USD account for dollar invoices.
bank_account=next(a for a in user.bank_accounts if not a.is_default),
)

project = Project(
Expand Down Expand Up @@ -847,11 +849,20 @@ def create_demo_user() -> User:
postal_code="555555",
country="Brazil",
),
bank_account=BankAccount(
name="Giro",
IBAN="BZ99830994950003161565",
BIC="BANKINFO101",
),
bank_accounts=[
BankAccount(
name="Harry Tuttle",
IBAN="BZ99830994950003161565",
BIC="BANKINFO101",
is_default=True,
),
BankAccount(
name="Harry Tuttle",
IBAN="US88589600002547441617",
BIC="TRWIBEB1",
is_default=False,
),
],
)
return user

Expand Down
11 changes: 7 additions & 4 deletions tuttle/einvoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,15 @@ def build_zugferd_document(
doc.trade.settlement.currency_code = currency

# -- Payment means (not in MINIMUM) ----------------------------------------
if not is_minimum and user.bank_account and user.bank_account.IBAN:
# The bank account named on the contract takes precedence; otherwise the
# user's default account (which ``User.bank_account`` resolves to).
payee = contract.bank_account or user.bank_account
if not is_minimum and payee and payee.IBAN:
pm = PaymentMeans()
pm.type_code = "58" # SEPA credit transfer
pm.payee_account.iban = user.bank_account.IBAN
if user.bank_account.BIC and profile not in ("BASIC",):
pm.payee_institution.bic = user.bank_account.BIC
pm.payee_account.iban = payee.IBAN
if payee.BIC and profile not in ("BASIC",):
pm.payee_institution.bic = payee.BIC
doc.trade.settlement.payment_means.add(pm)

# -- Line items & tax (not in MINIMUM) -------------------------------------
Expand Down
Loading
Loading