forked from CGM_Public/pretix_original
247 lines
11 KiB
Python
247 lines
11 KiB
Python
#
|
|
# This file is part of pretix (Community Edition).
|
|
#
|
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
#
|
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
# this file, see <https://pretix.eu/about/en/license>.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
# <https://www.gnu.org/licenses/>.
|
|
#
|
|
import json
|
|
from collections import OrderedDict
|
|
from decimal import Decimal
|
|
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
from django.dispatch import receiver
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import gettext, gettext_lazy, pgettext_lazy
|
|
|
|
from pretix.base.i18n import language
|
|
from pretix.base.models import Invoice, OrderPayment
|
|
|
|
from ..exporter import BaseExporter
|
|
from ..signals import register_data_exporters
|
|
from ..timeframes import DateFrameField, resolve_timeframe_to_dates_inclusive
|
|
|
|
|
|
class DekodiNREIExporter(BaseExporter):
|
|
identifier = 'dekodi_nrei'
|
|
verbose_name = 'dekodi NREI (JSON)'
|
|
category = pgettext_lazy('export_category', 'Invoices')
|
|
description = gettext_lazy("Download invoices in a format that can be used by the dekodi NREI conversion software.")
|
|
|
|
# Specification: http://manuals.dekodi.de/nexuspub/schnittstellenbuch/
|
|
|
|
def _encode_invoice(self, invoice: Invoice):
|
|
p_last = invoice.order.payments.filter(state=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED]).last()
|
|
gross_total = Decimal('0.00')
|
|
net_total = Decimal('0.00')
|
|
|
|
positions = []
|
|
for l in invoice.lines.all():
|
|
positions.append({
|
|
'ADes': l.description.replace("<br />", "\n"),
|
|
'ANetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
|
|
'ANo': self.event.slug,
|
|
'AQ': -1 if invoice.is_cancellation else 1,
|
|
'AVatP': round(float(l.tax_rate), 2),
|
|
'DIDt': (l.subevent or invoice.order.event).date_from.isoformat().replace('Z', '+00:00'),
|
|
'PosGrossA': round(float(l.gross_value), 2),
|
|
'PosNetA': round(float(l.net_value), 2),
|
|
})
|
|
gross_total += l.gross_value
|
|
net_total += l.net_value
|
|
|
|
payments = []
|
|
paypal_email = None
|
|
for p in invoice.order.payments.filter(
|
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_PENDING,
|
|
OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_REFUNDED)
|
|
):
|
|
if p.provider == 'paypal':
|
|
paypal_email = p.info_data.get('payer', {}).get('payer_info', {}).get('email')
|
|
try:
|
|
ppid = p.info_data['transactions'][0]['related_resources'][0]['sale']['id']
|
|
except:
|
|
ppid = p.info_data.get('id')
|
|
payments.append({
|
|
'PTID': '1',
|
|
'PTN': 'PayPal',
|
|
'PTNo1': ppid,
|
|
'PTNo2': p.info_data.get('id'),
|
|
'PTNo7': round(float(p.amount), 2),
|
|
'PTNo8': str(self.event.currency),
|
|
'PTNo11': paypal_email or '',
|
|
'PTNo15': p.full_id or '',
|
|
})
|
|
elif p.provider == 'banktransfer':
|
|
payments.append({
|
|
'PTID': '4',
|
|
'PTN': 'Vorkasse',
|
|
'PTNo4': p.info_data.get('reference') or p.payment_provider._code(invoice.order),
|
|
'PTNo7': round(float(p.amount), 2),
|
|
'PTNo8': str(self.event.currency),
|
|
'PTNo10': p.info_data.get('payer') or '',
|
|
'PTNo14': p.info_data.get('date') or '',
|
|
'PTNo15': p.full_id or '',
|
|
})
|
|
elif p.provider == 'sepadebit':
|
|
with language(invoice.order.locale):
|
|
payments.append({
|
|
'PTID': '5',
|
|
'PTN': 'Lastschrift',
|
|
'PTNo4': gettext('Event ticket {event}-{code}').format(
|
|
event=self.event.slug.upper(),
|
|
code=invoice.order.code
|
|
),
|
|
'PTNo5': p.info_data.get('iban') or '',
|
|
'PTNo6': p.info_data.get('bic') or '',
|
|
'PTNo7': round(float(p.amount), 2),
|
|
'PTNo8': str(self.event.currency) or '',
|
|
'PTNo9': p.info_data.get('date') or '',
|
|
'PTNo10': p.info_data.get('account') or '',
|
|
'PTNo14': p.info_data.get('reference') or '',
|
|
'PTNo15': p.full_id or '',
|
|
})
|
|
elif p.provider and p.provider.startswith('stripe'):
|
|
pi = p.info_data or {}
|
|
try:
|
|
if "latest_charge" in pi and isinstance(pi.get("latest_charge"), dict):
|
|
details = pi["latest_charge"]["payment_method_details"]
|
|
card = details.get("card", {})
|
|
elif pi.get("charges") and pi["charges"]["data"]:
|
|
details = pi["charges"]["data"][0].get("payment_method_details", {})
|
|
card = details.get("card", {})
|
|
else:
|
|
details = pi["source"]
|
|
card = pi["source"]["card"]
|
|
except:
|
|
details = {}
|
|
card = {}
|
|
|
|
payments.append({
|
|
'PTID': '81',
|
|
'PTN': 'Stripe',
|
|
'PTNo1': pi.get("id") or '',
|
|
'PTNo5': card.get("last4", ""),
|
|
'PTNo7': round(float(p.amount), 2) or '',
|
|
'PTNo8': str(self.event.currency) or '',
|
|
'PTNo10': details.get('owner', {}).get('verified_name') or details.get('owner', {}).get('name') or '',
|
|
'PTNo15': p.full_id or '',
|
|
})
|
|
else:
|
|
payments.append({
|
|
'PTID': '0',
|
|
'PTN': p.provider,
|
|
'PTNo7': round(float(p.amount), 2) or '',
|
|
'PTNo8': str(self.event.currency) or '',
|
|
'PTNo15': p.full_id or '',
|
|
})
|
|
|
|
payments = [
|
|
{
|
|
k: v for k, v in p.items() if v is not None
|
|
} for p in payments
|
|
]
|
|
|
|
hdr = {
|
|
'C': str(invoice.invoice_to_country) or self.event.settings.invoice_address_from_country,
|
|
'CC': self.event.currency,
|
|
'City': invoice.invoice_to_city,
|
|
'CN': invoice.invoice_to_company,
|
|
'DIC': self.event.settings.invoice_address_from_country,
|
|
# DIC is a little bit unclean, should be the event location's country
|
|
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
|
|
'DT': '30' if invoice.is_cancellation else '10',
|
|
'EM': invoice.order.email,
|
|
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1] if invoice.invoice_to_name else '',
|
|
'FN': (
|
|
invoice.invoice_to_name.rsplit(' ', 1)[0]
|
|
if invoice.invoice_to_name and ' ' in invoice.invoice_to_name else ''
|
|
),
|
|
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
|
|
'INo': invoice.full_invoice_no,
|
|
'IsNet': invoice.reverse_charge,
|
|
'ODt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
|
|
'OID': invoice.order.code,
|
|
'SID': self.event.slug,
|
|
'SN': str(self.event),
|
|
'Str': invoice.invoice_to_street or '',
|
|
'TGrossA': round(float(gross_total), 2),
|
|
'TNetA': round(float(net_total), 2),
|
|
'TVatA': round(float(gross_total - net_total), 2),
|
|
'VatDp': False,
|
|
'Zip': invoice.invoice_to_zipcode
|
|
}
|
|
if not hdr['FamN'] and not hdr['CN']:
|
|
hdr['CN'] = "Unbekannter Kunde"
|
|
|
|
if invoice.refers:
|
|
hdr['PvrINo'] = invoice.refers.full_invoice_no
|
|
if p_last:
|
|
hdr['PmDt'] = p_last.payment_date.isoformat().replace('Z', '+00:00')
|
|
if paypal_email:
|
|
hdr['PPEm'] = paypal_email
|
|
if invoice.invoice_to_vat_id:
|
|
hdr['VatID'] = invoice.invoice_to_vat_id
|
|
|
|
return {
|
|
'IsValid': True,
|
|
'Hdr': hdr,
|
|
'InvcPstns': positions,
|
|
'PmIs': payments,
|
|
'ValidationMessage': ''
|
|
}
|
|
|
|
def render(self, form_data):
|
|
qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
|
|
|
|
if form_data.get('date_range'):
|
|
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
|
|
if d_start:
|
|
qs = qs.filter(date__gte=d_start)
|
|
if d_end:
|
|
qs = qs.filter(date__lte=d_end)
|
|
|
|
jo = {
|
|
'Format': 'NREI',
|
|
'Version': '18.10.2.0',
|
|
'SourceSystem': 'pretix',
|
|
'Data': [
|
|
self._encode_invoice(i) for i in qs
|
|
]
|
|
}
|
|
return '{}_nrei.json'.format(self.event.slug), 'application/json', json.dumps(jo, cls=DjangoJSONEncoder, indent=4)
|
|
|
|
@property
|
|
def export_form_fields(self):
|
|
return OrderedDict(
|
|
[
|
|
('date_range',
|
|
DateFrameField(
|
|
label=gettext_lazy('Date range'),
|
|
include_future_frames=False,
|
|
required=False,
|
|
help_text=gettext_lazy('Only include invoices issued in this time frame. Note that the invoice date does '
|
|
'not always correspond to the order or payment date.')
|
|
)),
|
|
]
|
|
)
|
|
|
|
|
|
@receiver(register_data_exporters, dispatch_uid="exporter_dekodi_nrei")
|
|
def register_dekodi_export(sender, **kwargs):
|
|
return DekodiNREIExporter
|