text: import inspect
  import json
  import logging
  import urllib.error
  from datetime import date, timedelta
  from decimal import ROUND_HALF_UP, Decimal
  
  import vat_moss.exchange_rates
  from django.conf import settings
  from django.core.files.base import ContentFile
  from django.core.serializers.json import DjangoJSONEncoder
  from django.db import transaction
  from django.db.models import Count
  from django.dispatch import receiver
  from django.utils import timezone
  from django.utils.formats import date_format
  from django.utils.timezone import now
  from django.utils.translation import gettext as _, pgettext
  from django_countries.fields import Country
  from django_scopes import scope, scopes_disabled
  from i18nfield.strings import LazyI18nString
  
  from pretix.base.i18n import language
  from pretix.base.models import (
      Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
  )
  from pretix.base.models.tax import EU_CURRENCIES
  from pretix.base.services.tasks import TransactionAwareTask
  from pretix.base.settings import GlobalSettingsObject
  from pretix.base.signals import invoice_line_text, periodic_task
  from pretix.celery_app import app
  from pretix.helpers.database import rolledback_transaction
  from pretix.helpers.models import modelcopy
  
  logger = logging.getLogger(__name__)
  
  
  @transaction.atomic
  def build_invoice(invoice: Invoice) -> Invoice:
      invoice.locale = invoice.event.settings.get('invoice_language', invoice.event.settings.locale)
      if invoice.locale == '__user__':
          invoice.locale = invoice.order.locale or invoice.event.settings.locale
  
      lp = invoice.order.payments.last()
  
      with language(invoice.locale):
          invoice.invoice_from = invoice.event.settings.get('invoice_address_from')
          invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
          invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
          invoice.invoice_from_city = invoice.event.settings.get('invoice_address_from_city')
          invoice.invoice_from_country = invoice.event.settings.get('invoice_address_from_country')
          invoice.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id')
          invoice.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id')
  
          introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
          additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
          footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
          if lp and lp.payment_provider:
              if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
                  payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
              else:
                  payment = str(lp.payment_provider.render_invoice_text(invoice.order))
          else:
              payment = ""
          if invoice.event.settings.invoice_include_expire_date and invoice.order.status == Order.STATUS_PENDING:
              if payment:
                  payment += "<br />"
              payment += pgettext("invoice", "Please complete your payment before {expire_date}.").format(
                  expire_date=date_format(invoice.order.expires, "SHORT_DATE_FORMAT")
              )
  
          invoice.introductory_text = str(introductory).replace('\n', '<br />')
          invoice.additional_text = str(additional).replace('\n', '<br />')
          invoice.footer_text = str(footer)
          invoice.payment_provider_text = str(payment).replace('\n', '<br />')
  
          try:
              ia = invoice.order.invoice_address
              addr_template = pgettext("invoice", """{i.company}
  {i.name}
  {i.street}
  {i.zipcode} {i.city} {state}
  {country}""")
              invoice.invoice_to = "\n".join(
                  a.strip() for a in addr_template.format(
                      i=ia,
                      country=ia.country.name if ia.country else ia.country_old,
                      state=ia.state_for_address
                  ).split("\n") if a.strip()
              )
              invoice.internal_reference = ia.internal_reference
              invoice.custom_field = ia.custom_field
              invoice.invoice_to_company = ia.company
              invoice.invoice_to_name = ia.name
              invoice.invoice_to_street = ia.street
              invoice.invoice_to_zipcode = ia.zipcode
              invoice.invoice_to_city = ia.city
              invoice.invoice_to_country = ia.country
              invoice.invoice_to_state = ia.state
              invoice.invoice_to_beneficiary = ia.beneficiary
  
              if ia.vat_id:
                  invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % ia.vat_id
                  invoice.invoice_to_vat_id = ia.vat_id
  
              cc = str(ia.country)
  
              if cc in EU_CURRENCIES and EU_CURRENCIES[cc] != invoice.event.currency:
                  invoice.foreign_currency_display = EU_CURRENCIES[cc]
  
                  if settings.FETCH_ECB_RATES:
                      gs = GlobalSettingsObject()
                      rates_date = gs.settings.get('ecb_rates_date', as_type=date)
                      rates_dict = gs.settings.get('ecb_rates_dict', as_type=dict)
                      convert = (
                          rates_date and rates_dict and
                          rates_date > (now() - timedelta(days=7)).date() and
                          invoice.event.currency in rates_dict and
                          invoice.foreign_currency_display in rates_dict
                      )
                      if convert:
                          invoice.foreign_currency_rate = (
                              Decimal(rates_dict[invoice.foreign_currency_display])
                              / Decimal(rates_dict[invoice.event.currency])
                          ).quantize(Decimal('0.0001'), ROUND_HALF_UP)
                          invoice.foreign_currency_rate_date = rates_date
  
          except InvoiceAddress.DoesNotExist:
              ia = None
              invoice.invoice_to = ""
  
          invoice.file = None
          invoice.save()
          invoice.lines.all().delete()
  
          positions = list(
              invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
                  addon_c=Count('addons')
              ).prefetch_related('answers', 'answers__question').order_by('positionid', 'id')
          )
  
          reverse_charge = False
  
          positions.sort(key=lambda p: p.sort_key)
          for i, p in enumerate(positions):
              if not invoice.event.settings.invoice_include_free and p.price == Decimal('0.00') and not p.addon_c:
                  continue
  
              desc = str(p.item.name)
              if p.variation:
                  desc += " - " + str(p.variation.value)
              if p.addon_to_id:
                  desc = "  + " + desc
              if invoice.event.settings.invoice_attendee_name and p.attendee_name:
                  desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(name=p.attendee_name)
              for recv, resp in invoice_line_text.send(sender=invoice.event, position=p):
                  if resp:
                      desc += "<br/>" + resp
  
              for answ in p.answers.all():
                  if not answ.question.print_on_invoice:
                      continue
                  desc += "<br />{}{} {}".format(
                      answ.question.question,
                      "" if str(answ.question.question).endswith("?") else ":",
                      str(answ)
                  )
  
              if invoice.event.has_subevents:
                  desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
              InvoiceLine.objects.create(
                  position=i, invoice=invoice, description=desc,
                  gross_value=p.price, tax_value=p.tax_value,
                  subevent=p.subevent, event_date_from=(p.subevent.date_from if p.subevent else invoice.event.date_from),
                  tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
              )
  
              if p.tax_rule and p.tax_rule.is_reverse_charge(ia) and p.price and not p.tax_value:
                  reverse_charge = True
  
          if reverse_charge:
              if invoice.additional_text:
                  invoice.additional_text += "<br /><br />"
              invoice.additional_text += pgettext(
                  "invoice",
                  "Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
                  "rests with the service recipient."
              )
              invoice.reverse_charge = True
              invoice.save()
  
          offset = len(positions)
          for i, fee in enumerate(invoice.order.fees.all()):
              if fee.fee_type == OrderFee.FEE_TYPE_OTHER and fee.description:
                  fee_title = fee.description
              else:
                  fee_title = _(fee.get_fee_type_display())
                  if fee.description:
                      fee_title += " - " + fee.description
              InvoiceLine.objects.create(
                  position=i + offset,
                  invoice=invoice,
                  description=fee_title,
                  gross_value=fee.value,
                  tax_value=fee.tax_value,
                  tax_rate=fee.tax_rate,
                  tax_name=fee.tax_rule.name if fee.tax_rule else ''
              )
  
          return invoice
  
  
  def build_cancellation(invoice: Invoice):
      invoice.lines.all().delete()
  
      for line in invoice.refers.lines.all():
          line.pk = None
          line.invoice = invoice
          line.gross_value *= -1
          line.tax_value *= -1
          line.save()
      return invoice
  
  
  def generate_cancellation(invoice: Invoice, trigger_pdf=True):
      if invoice.canceled:
          raise ValueError("Invoice should not be canceled twice.")
      cancellation = modelcopy(invoice)
      cancellation.pk = None
      cancellation.invoice_no = None
      cancellation.prefix = None
      cancellation.refers = invoice
      cancellation.is_cancellation = True
      cancellation.date = timezone.now().date()
      cancellation.payment_provider_text = ''
      cancellation.file = None
      cancellation.save()
  
      cancellation = build_cancellation(cancellation)
      if trigger_pdf:
          invoice_pdf(cancellation.pk)
      return cancellation
  
  
  def regenerate_invoice(invoice: Invoice):
      if invoice.shredded:
          return invoice
      if invoice.is_cancellation:
          invoice = build_cancellation(invoice)
      else:
          invoice = build_invoice(invoice)
      invoice_pdf(invoice.pk)
      return invoice
  
  
  def generate_invoice(order: Order, trigger_pdf=True):
      invoice = Invoice(
          order=order,
          event=order.event,
          organizer=order.event.organizer,
          date=timezone.now().date(),
      )
      invoice = build_invoice(invoice)
      if trigger_pdf:
          invoice_pdf(invoice.pk)
  
      if order.status == Order.STATUS_CANCELED:
          generate_cancellation(invoice, trigger_pdf)
  
      return invoice
  
  
  @app.task(base=TransactionAwareTask)
  def invoice_pdf_task(invoice: int):
      with scopes_disabled():
          i = Invoice.objects.get(pk=invoice)
      with scope(organizer=i.order.event.organizer):
          if i.shredded:
              return None
          if i.file:
              i.file.delete()
          with language(i.locale):
              fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
              i.file.save(fname, ContentFile(fcontent))
              i.save()
              return i.file.name
  
  
  def invoice_qualified(order: Order):
      if order.total == Decimal('0.00') or order.require_approval or \
              order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
          return False
      return True
  
  
  def invoice_pdf(*args, **kwargs):
      # We call this task asynchroneously, because otherwise we run into conditions where
      # the task worker tries to generate the PDF even before our database transaction
      # was committed and therefore fails to find the invoice object. The invoice_pdf_task
      # will prevent this kind of race condition.
      invoice_pdf_task.apply_async(args=args, kwargs=kwargs)
  
  
  class DummyRollbackException(Exception):
      pass
  
  
  def build_preview_invoice_pdf(event):
      locale = event.settings.invoice_language
      if not locale or locale == '__user__':
          locale = event.settings.locale
  
      with rolledback_transaction(), language(locale):
          order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
                                      expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
          invoice = Invoice(
              order=order, event=event, invoice_no="PREVIEW",
              date=timezone.now().date(), locale=locale, organizer=event.organizer
          )
          invoice.invoice_from = event.settings.get('invoice_address_from')
          invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
          invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
          invoice.invoice_from_city = invoice.event.settings.get('invoice_address_from_city')
          invoice.invoice_from_country = invoice.event.settings.get('invoice_address_from_country')
          invoice.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id')
          invoice.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id')
  
          introductory = event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
          additional = event.settings.get('invoice_additional_text', as_type=LazyI18nString)
          footer = event.settings.get('invoice_footer_text', as_type=LazyI18nString)
          payment = _("A payment provider specific text might appear here.")
  
          invoice.introductory_text = str(introductory).replace('\n', '<br />')
          invoice.additional_text = str(additional).replace('\n', '<br />')
          invoice.footer_text = str(footer)
          invoice.payment_provider_text = str(payment).replace('\n', '<br />')
          invoice.invoice_to_name = _("John Doe")
          invoice.invoice_to_street = _("214th Example Street")
          invoice.invoice_to_zipcode = _("012345")
          invoice.invoice_to_city = _('Sample city')
          invoice.invoice_to_country = Country('DE')
          invoice.invoice_to = '{}\n{}\n{} {}'.format(
              invoice.invoice_to_name, invoice.invoice_to_street,
              invoice.invoice_to_zipcode, invoice.invoice_to_city
          )
          invoice.invoice_to_beneficiary = ''
          invoice.file = None
          invoice.save()
          invoice.lines.all().delete()
  
          if event.tax_rules.exists():
              for i, tr in enumerate(event.tax_rules.all()):
                  tax = tr.tax(Decimal('100.00'), base_price_is='gross')
                  InvoiceLine.objects.create(
                      invoice=invoice, description=_("Sample product {}").format(i + 1),
                      gross_value=tax.gross, tax_value=tax.tax,
                      tax_rate=tax.rate
                  )
          else:
              InvoiceLine.objects.create(
                  invoice=invoice, description=_("Sample product A"),
                  gross_value=100, tax_value=0, tax_rate=0
              )
  
          return event.invoice_renderer.generate(invoice)
  
  
  @receiver(signal=periodic_task)
  def fetch_ecb_rates(sender, **kwargs):
      if not settings.FETCH_ECB_RATES:
          return
  
      gs = GlobalSettingsObject()
      if gs.settings.ecb_rates_date == now().strftime("%Y-%m-%d"):
          return
  
      try:
          date, rates = vat_moss.exchange_rates.fetch()
          gs.settings.ecb_rates_date = date
          gs.settings.ecb_rates_dict = json.dumps(rates, cls=DjangoJSONEncoder)
      except urllib.error.URLError:
          logger.exception('Could not retrieve rates from ECB')
  