rumwurschteln

This commit is contained in:
Fritz Grimpen 2024-10-04 11:57:49 +00:00
parent 99a7997818
commit 8b4760b92c
4 changed files with 69 additions and 13 deletions

View file

@ -1,19 +1,20 @@
Hallo {{ invoice.party.name }}, 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: Wir berechnen dir daher die folgenden Mitgliedsbeiträge:
{% for line in invoice.lines %} {% 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 %} {%- endfor %}
{{ "".ljust(72, '-') }}
{{ "Summe".ljust(59) }} {{ (invoice.total_amount|format_currency).rjust(12) }}
{% if sepa_mandate -%} {% 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: 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 %}
{% for payment in invoice.lines_to_pay if not payment.reconiciliation and payment.payable_receivable_balance %} {{ maturity_date }} {{ (amount|format_currency).rjust(25) }}
{{ payment.maturity_date }} {{ "{: >.2f}".format(payment.amount) }} €
{%- endfor %} {%- endfor %}
{% else -%} {%- else -%}
Bitte überweise deine Mitgliedsbeiträge fristgerecht auf unser Konto: Bitte überweise deine Mitgliedsbeiträge fristgerecht auf unser Konto:
Kontoinhaber: Chaos Computer Club Bremen e.V. Kontoinhaber: Chaos Computer Club Bremen e.V.
@ -22,9 +23,11 @@ Bank: GLS Gemeinschaftsbank eG
BIC: GENODEM1GLS BIC: GENODEM1GLS
Verwendungszweck: Beitrag {{ invoice.party.name }} 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 %} {%- 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, Viele Grüße,
der Vorstand der Vorstand

View file

@ -18,6 +18,9 @@ class BaseClient(object):
# model_cls.__module__ = self.__class__.__module__ # model_cls.__module__ = self.__class__.__module__
return model_cls return model_cls
def get_wizard(self, name):
return proteus.Wizard(name, config=self.config)
@classmethod @classmethod
def from_session(cls, session): def from_session(cls, session):
config = proteus.config.XmlrpcConfig(session.url, headers=session.auth_headers()) config = proteus.config.XmlrpcConfig(session.url, headers=session.auth_headers())

View file

@ -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) print(f'\tInvoice: {invoice.id} {invoice.description}', file=sys.stderr)
for line in invoice.lines: for line in invoice.lines:
print(f'\t\tLine: {line.description} {line.unit_price}', file=sys.stderr) 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 member['invoice_id'] = invoice.id
csvwriter.writerow(member) csvwriter.writerow(member)

View file

@ -5,23 +5,65 @@ import csv
import datetime import datetime
import decimal import decimal
import email.message import email.message
import functools
import getpass import getpass
import operator
import pathlib import pathlib
import subprocess import subprocess
import tempfile import tempfile
import babel.numbers
import dateutil.relativedelta
import jinja2 import jinja2
import triad.client 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): class Env(object):
jinja2_env: jinja2.Environment jinja2_env: jinja2.Environment
args: argparse.Namespace args: argparse.Namespace
session: triad.client.Session 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): def fee_invoice(env: Env):
csvreader = csv.DictReader(env.args.csvfile, fieldnames=env.args.csv_fields) 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() csvwriter.writeheader()
invoicing_client = triad.client.InvoicingClient.from_session(env.session) invoicing_client = triad.client.InvoicingClient.from_session(env.session)
template = env.jinja2_env.get_template("beitrag.eml") template = env.jinja2_env.get_template("beitrag.eml")
@ -36,23 +78,28 @@ def fee_invoice(env: Env):
sepa_mandate = None sepa_mandate = None
else: else:
sepa_mandate = sepa_mandate.sepa_mandate 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 = { template_args = {
'monthly_fee': decimal.Decimal(member.get('monthly_fee') or '0.0'), 'monthly_fee': decimal.Decimal(member.get('monthly_fee') or '0.0'),
"yearly_fee": decimal.Decimal(member.get('yearly_fee') or '0.0'), "yearly_fee": decimal.Decimal(member.get('yearly_fee') or '0.0'),
'invoice': invoice, 'invoice': invoice,
"payments": payments,
'sepa_mandate': sepa_mandate, 'sepa_mandate': sepa_mandate,
'payments': [], 'payments': payments,
'sender': env.args.email_sender, 'sender': env.args.email_sender,
"year": year, "year": year,
"fee_months": [datetime.date(year.year, month, 1) for month in range(1, 13)] "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 = email.message.EmailMessage()
msg["From"] = env.args.email_from msg["From"] = env.args.email_from
msg["To"] = email_address msg["To"] = email_address
msg["Date"] = env.args.email_date or datetime.datetime.now() msg["Date"] = env.args.email_date or datetime.datetime.now()
msg["Subject"] = f"Beitragsrechnung {year.year} ({invoice.number})" msg["Subject"] = f"Beitragsrechnung {year.year} ({invoice.number})"
msg["Message-Id"] = email.utils.make_msgid(domain="verein.ccchb.de") msg["Message-Id"] = member['message_id']
member['message_id'] = msg["Message-Id"]
msg.set_content(template.render(**template_args)) msg.set_content(template.render(**template_args))
with (env.args.output_dir / member['message_id']).open("wb") as mailfile: with (env.args.output_dir / member['message_id']).open("wb") as mailfile:
mailfile.write(bytes(msg)) mailfile.write(bytes(msg))
@ -161,6 +208,9 @@ def main():
env.jinja2_env = jinja2.Environment( env.jinja2_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(args.template_dir) 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 env.args = args
if not args.uri: if not args.uri: