rumwurschteln
This commit is contained in:
parent
99a7997818
commit
8b4760b92c
4 changed files with 69 additions and 13 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue