From 8b4760b92cdf006d6a5a92f5323c117f8cd892c3 Mon Sep 17 00:00:00 2001 From: Fritz Grimpen Date: Fri, 4 Oct 2024 11:57:49 +0000 Subject: [PATCH] rumwurschteln --- mail_templates/beitrag.eml | 19 +++++---- triad/triad/client.py | 3 ++ tryton-scripts/beitragsrechnungen.py | 2 +- tryton-scripts/mailings.py | 58 ++++++++++++++++++++++++++-- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/mail_templates/beitrag.eml b/mail_templates/beitrag.eml index f853b8a..00e14ba 100644 --- a/mail_templates/beitrag.eml +++ b/mail_templates/beitrag.eml @@ -1,19 +1,20 @@ Hallo {{ invoice.party.name }}, -für deine ordentliche Mitgliedschaft im Chaos Computer Club Bremen e.V. ist ein Mitgliedsbeitrag fällig. Für das Beitragsjahr {{ year.year }} beträgt der monatliche Beitrag {{ monthly_fee }} €. Dieser Beitrag ist jeweils zu Beginn des Monats fällig. +für deine ordentliche Mitgliedschaft im Chaos Computer Club Bremen e.V. ist ein Mitgliedsbeitrag fällig. Für das Beitragsjahr {{ year.year }} beträgt der monatliche Beitrag {{ monthly_fee|format_currency }}. Dieser Beitrag ist jeweils zu Beginn des Monats fällig. Wir berechnen dir daher die folgenden Mitgliedsbeiträge: {% for line in invoice.lines %} -{{ line.description.ljust(50) }} {{ "{: >.2f}".format(line.unit_price) }} € +{{ line.description.ljust(39) }} {{ (line.quantity|format_quantity).rjust(5) }} × {{ (line.unit_price|format_currency).rjust(9) }} = {{ (line.amount|format_currency).rjust(12) }} {%- endfor %} +{{ "".ljust(72, '-') }} +{{ "Summe".ljust(59) }} {{ (invoice.total_amount|format_currency).rjust(12) }} {% if sepa_mandate -%} -Wir werden deine Mitgliedsbeiträge per SEPA-Lastschrift von deinem Konto mit der IBAN {{ sepa_mandate.account_number.number }} zu den folgenden Terminen einziehen: - -{% for payment in invoice.lines_to_pay if not payment.reconiciliation and payment.payable_receivable_balance %} -{{ payment.maturity_date }} {{ "{: >.2f}".format(payment.amount) }} € +Wir werden deine Mitgliedsbeiträge per SEPA-Lastschrift von deinem Konto mit der IBAN {{ sepa_mandate.account_number.number|strip_iban }} zu den folgenden Terminen einziehen: +{% for maturity_date, amount in payments %} +{{ maturity_date }} {{ (amount|format_currency).rjust(25) }} {%- endfor %} -{% else -%} +{%- else -%} Bitte überweise deine Mitgliedsbeiträge fristgerecht auf unser Konto: Kontoinhaber: Chaos Computer Club Bremen e.V. @@ -22,9 +23,11 @@ Bank: GLS Gemeinschaftsbank eG BIC: GENODEM1GLS Verwendungszweck: Beitrag {{ invoice.party.name }} -Du kannst den Beitrag für das gesamte Jahr {{ year.year }} im Voraus überweisen. +Du kannst den Beitrag für das gesamte Jahr {{ year.year }} in Höhe von {{ invoice.total_amount|format_currency }} im Voraus überweisen. {%- endif %} +Solltest du eine Zuwendungsbestätigung (§ 10b EStG) für deine gezahlten Beiträge benötigen, wende dich bitte per E-Mail an vorstand@ccchb.de. + Viele Grüße, der Vorstand diff --git a/triad/triad/client.py b/triad/triad/client.py index 264b01e..8b26fe6 100644 --- a/triad/triad/client.py +++ b/triad/triad/client.py @@ -18,6 +18,9 @@ class BaseClient(object): # model_cls.__module__ = self.__class__.__module__ return model_cls + def get_wizard(self, name): + return proteus.Wizard(name, config=self.config) + @classmethod def from_session(cls, session): config = proteus.config.XmlrpcConfig(session.url, headers=session.auth_headers()) diff --git a/tryton-scripts/beitragsrechnungen.py b/tryton-scripts/beitragsrechnungen.py index a506353..d4c9b92 100644 --- a/tryton-scripts/beitragsrechnungen.py +++ b/tryton-scripts/beitragsrechnungen.py @@ -138,6 +138,6 @@ with triad.client.Session.start(args.url, args.username, args.password) as sessi print(f'\tInvoice: {invoice.id} {invoice.description}', file=sys.stderr) for line in invoice.lines: print(f'\t\tLine: {line.description} {line.unit_price}', file=sys.stderr) - print(f'\tYearly fee: {invoice.total_amount}', file=sys.stderr) + print(f'\tTotal amount: {invoice.total_amount}', file=sys.stderr) member['invoice_id'] = invoice.id csvwriter.writerow(member) diff --git a/tryton-scripts/mailings.py b/tryton-scripts/mailings.py index 573cfbb..2037523 100644 --- a/tryton-scripts/mailings.py +++ b/tryton-scripts/mailings.py @@ -5,23 +5,65 @@ import csv import datetime import decimal import email.message +import functools import getpass +import operator import pathlib import subprocess import tempfile +import babel.numbers +import dateutil.relativedelta import jinja2 import triad.client +def strip_iban(iban): + iban = [c for c in iban if not c.isspace()] + return "".join(iban[0:3] + ["X" for c in iban[3:-3]] + iban[-3:]) + +def format_currency(amount, currency='EUR', locale='de_DE'): + return babel.numbers.format_currency(amount, currency, locale=locale) + +def format_quantity(amount, locale='de_DE'): + return babel.numbers.format_decimal(amount, locale=locale) + class Env(object): jinja2_env: jinja2.Environment args: argparse.Namespace session: triad.client.Session +def deduce_payments(client, invoice): + total_amount = invoice.total_amount + remainder = total_amount + for line in invoice.payment_term.lines: + if line.type == 'fixed': + amount = line.amount + elif line.type == 'percent': + amount = remainder * line.ratio + elif line.type =='percent_on_total': + amount = total_amount * line.ratio + elif line.type == 'remainder': + amount = remainder + else: + continue + relativedeltas = ( + dateutil.relativedelta.relativedelta( + day=relativedelta.day, + month=int(relativedelta.month.index) if relativedelta.month else None, + days=relativedelta.days, + weeks=relativedelta.weeks, + months=relativedelta.months, + weekday=int(relativedelta.weekday.index) if relativedelta.weekday else None + ) for relativedelta in line.relativedeltas + ) + date = functools.reduce(operator.add, relativedeltas, invoice.payment_term_date) + remainder -= amount + yield (date, amount) + def fee_invoice(env: Env): csvreader = csv.DictReader(env.args.csvfile, fieldnames=env.args.csv_fields) - csvwriter = csv.DictWriter(env.args.output, fieldnames=csvreader.fieldnames + ["message_id"]) + csvwriter = csv.DictWriter(env.args.output, fieldnames=csvreader.fieldnames + (["message_id"] if 'message_id' not in csvreader.fieldnames else [])) csvwriter.writeheader() invoicing_client = triad.client.InvoicingClient.from_session(env.session) template = env.jinja2_env.get_template("beitrag.eml") @@ -36,23 +78,28 @@ def fee_invoice(env: Env): sepa_mandate = None else: sepa_mandate = sepa_mandate.sepa_mandate + if invoice.state in {"draft", "validated"} and sepa_mandate is not None: + payments = list(deduce_payments(invoicing_client, invoice)) + else: + payments = [(move.maturity_date, move.amount) for move in invoice.lines_to_pay if not move.reconciliation and move.payable_receivable_balance != 0] template_args = { 'monthly_fee': decimal.Decimal(member.get('monthly_fee') or '0.0'), "yearly_fee": decimal.Decimal(member.get('yearly_fee') or '0.0'), 'invoice': invoice, + "payments": payments, 'sepa_mandate': sepa_mandate, - 'payments': [], + 'payments': payments, 'sender': env.args.email_sender, "year": year, "fee_months": [datetime.date(year.year, month, 1) for month in range(1, 13)] } + member.setdefault('message_id', email.utils.make_msgid(domain='verein.ccchb.de')) msg = email.message.EmailMessage() msg["From"] = env.args.email_from msg["To"] = email_address msg["Date"] = env.args.email_date or datetime.datetime.now() msg["Subject"] = f"Beitragsrechnung {year.year} ({invoice.number})" - msg["Message-Id"] = email.utils.make_msgid(domain="verein.ccchb.de") - member['message_id'] = msg["Message-Id"] + msg["Message-Id"] = member['message_id'] msg.set_content(template.render(**template_args)) with (env.args.output_dir / member['message_id']).open("wb") as mailfile: mailfile.write(bytes(msg)) @@ -161,6 +208,9 @@ def main(): env.jinja2_env = jinja2.Environment( loader=jinja2.FileSystemLoader(args.template_dir) ) + env.jinja2_env.filters["strip_iban"] = strip_iban + env.jinja2_env.filters["format_currency"] = format_currency + env.jinja2_env.filters["format_quantity"] = format_quantity env.args = args if not args.uri: