forked from CGM_Public/pretix_original
Configurable invoice prefixes
This commit is contained in:
47
src/pretix/base/migrations/0069_invoice_prefix.py
Normal file
47
src/pretix/base/migrations/0069_invoice_prefix.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.2 on 2017-07-17 19:29
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import deletion
|
||||||
|
|
||||||
|
|
||||||
|
def fwd(app, schema_editor):
|
||||||
|
Event = app.get_model('pretixbase', 'Event')
|
||||||
|
for e in Event.objects.select_related('organizer').all():
|
||||||
|
e.invoices.all().update(prefix=e.slug.upper() + '-', organizer=e.organizer)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0068_subevent_frontpage_text'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='prefix',
|
||||||
|
field=models.CharField(db_index=True, default='', max_length=160),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='organizer',
|
||||||
|
field=models.ForeignKey(null=True,
|
||||||
|
on_delete=deletion.PROTECT,
|
||||||
|
related_name='invoices', to='pretixbase.Organizer'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
fwd, migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='invoice',
|
||||||
|
unique_together=set([('organizer', 'prefix', 'invoice_no')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='organizer',
|
||||||
|
field=models.ForeignKey(on_delete=deletion.PROTECT, related_name='invoices', to='pretixbase.Organizer'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -29,6 +29,8 @@ class Invoice(models.Model):
|
|||||||
:type order: Order
|
:type order: Order
|
||||||
:param event: The event this belongs to (for convenience)
|
:param event: The event this belongs to (for convenience)
|
||||||
:type event: Event
|
:type event: Event
|
||||||
|
:param organizer: The organizer this belongs to (redundant, for enforcing uniqueness)
|
||||||
|
:type organizer: Organizer
|
||||||
:param invoice_no: The human-readable, event-unique invoice number
|
:param invoice_no: The human-readable, event-unique invoice number
|
||||||
:type invoice_no: int
|
:type invoice_no: int
|
||||||
:param is_cancellation: Whether or not this is a cancellation instead of an invoice
|
:param is_cancellation: Whether or not this is a cancellation instead of an invoice
|
||||||
@@ -55,7 +57,9 @@ class Invoice(models.Model):
|
|||||||
:type file: File
|
:type file: File
|
||||||
"""
|
"""
|
||||||
order = models.ForeignKey('Order', related_name='invoices', db_index=True)
|
order = models.ForeignKey('Order', related_name='invoices', db_index=True)
|
||||||
|
organizer = models.ForeignKey('Organizer', related_name='invoices', db_index=True, on_delete=models.PROTECT)
|
||||||
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
|
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
|
||||||
|
prefix = models.CharField(max_length=160, db_index=True)
|
||||||
invoice_no = models.CharField(max_length=19, db_index=True)
|
invoice_no = models.CharField(max_length=19, db_index=True)
|
||||||
is_cancellation = models.BooleanField(default=False)
|
is_cancellation = models.BooleanField(default=False)
|
||||||
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
|
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
|
||||||
@@ -74,7 +78,10 @@ class Invoice(models.Model):
|
|||||||
return '{:05d}'.format(int(number))
|
return '{:05d}'.format(int(number))
|
||||||
|
|
||||||
def _get_numeric_invoice_number(self):
|
def _get_numeric_invoice_number(self):
|
||||||
numeric_invoices = Invoice.objects.filter(event=self.event).exclude(invoice_no__contains='-')
|
numeric_invoices = Invoice.objects.filter(
|
||||||
|
event__organizer=self.event.organizer,
|
||||||
|
prefix=self.prefix,
|
||||||
|
).exclude(invoice_no__contains='-')
|
||||||
return self._to_numeric_invoice_number(numeric_invoices.count() + 1)
|
return self._to_numeric_invoice_number(numeric_invoices.count() + 1)
|
||||||
|
|
||||||
def _get_invoice_number_from_order(self):
|
def _get_invoice_number_from_order(self):
|
||||||
@@ -88,6 +95,10 @@ class Invoice(models.Model):
|
|||||||
raise ValueError('Every invoice needs to be connected to an order')
|
raise ValueError('Every invoice needs to be connected to an order')
|
||||||
if not self.event:
|
if not self.event:
|
||||||
self.event = self.order.event
|
self.event = self.order.event
|
||||||
|
if not self.organizer:
|
||||||
|
self.organizer = self.order.event.organizer
|
||||||
|
if not self.prefix:
|
||||||
|
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
|
||||||
if not self.invoice_no:
|
if not self.invoice_no:
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
if self.event.settings.get('invoice_numbers_consecutive'):
|
if self.event.settings.get('invoice_numbers_consecutive'):
|
||||||
@@ -116,8 +127,8 @@ class Invoice(models.Model):
|
|||||||
"""
|
"""
|
||||||
Returns the invoice number in a human-readable string with the event slug prepended.
|
Returns the invoice number in a human-readable string with the event slug prepended.
|
||||||
"""
|
"""
|
||||||
return '{event}-{code}'.format(
|
return '{prefix}{code}'.format(
|
||||||
event=self.event.slug.upper(),
|
prefix=self.prefix,
|
||||||
code=self.invoice_no
|
code=self.invoice_no
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,7 +137,7 @@ class Invoice(models.Model):
|
|||||||
return self.refered.filter(is_cancellation=True).exists()
|
return self.refered.filter(is_cancellation=True).exists()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('event', 'invoice_no')
|
unique_together = ('organizer', 'prefix', 'invoice_no')
|
||||||
ordering = ('invoice_no',)
|
ordering = ('invoice_no',)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ def generate_cancellation(invoice: Invoice):
|
|||||||
cancellation = copy.copy(invoice)
|
cancellation = copy.copy(invoice)
|
||||||
cancellation.pk = None
|
cancellation.pk = None
|
||||||
cancellation.invoice_no = None
|
cancellation.invoice_no = None
|
||||||
|
cancellation.prefix = None
|
||||||
cancellation.refers = invoice
|
cancellation.refers = invoice
|
||||||
cancellation.is_cancellation = True
|
cancellation.is_cancellation = True
|
||||||
cancellation.date = timezone.now().date()
|
cancellation.date = timezone.now().date()
|
||||||
@@ -125,6 +126,7 @@ def generate_invoice(order: Order):
|
|||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
order=order,
|
order=order,
|
||||||
event=order.event,
|
event=order.event,
|
||||||
|
organizer=order.event.organizer,
|
||||||
date=timezone.now().date(),
|
date=timezone.now().date(),
|
||||||
locale=locale
|
locale=locale
|
||||||
)
|
)
|
||||||
@@ -171,7 +173,7 @@ def build_preview_invoice_pdf(event):
|
|||||||
expires=timezone.now(), code="PREVIEW", total=119)
|
expires=timezone.now(), code="PREVIEW", total=119)
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
order=order, event=event, invoice_no="PREVIEW",
|
order=order, event=event, invoice_no="PREVIEW",
|
||||||
date=timezone.now().date(), locale=locale
|
date=timezone.now().date(), locale=locale, organizer=event.organizer
|
||||||
)
|
)
|
||||||
invoice.invoice_from = event.settings.get('invoice_address_from')
|
invoice.invoice_from = event.settings.get('invoice_address_from')
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ DEFAULTS = {
|
|||||||
'default': 'True',
|
'default': 'True',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
},
|
},
|
||||||
|
'invoice_numbers_prefix': {
|
||||||
|
'default': '',
|
||||||
|
'type': str,
|
||||||
|
},
|
||||||
'invoice_renderer': {
|
'invoice_renderer': {
|
||||||
'default': 'classic',
|
'default': 'classic',
|
||||||
'type': str,
|
'type': str,
|
||||||
|
|||||||
@@ -453,6 +453,14 @@ class InvoiceSettingsForm(SettingsForm):
|
|||||||
help_text=_("If deactivated, the order code will be used in the invoice number."),
|
help_text=_("If deactivated, the order code will be used in the invoice number."),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
invoice_numbers_prefix = forms.CharField(
|
||||||
|
label=_("Invoice number prefix"),
|
||||||
|
help_text=_("This will be prepended to invoice numbers. If you leave this field empty, your event slug will "
|
||||||
|
"be used followed by a dash. Attention: If multiple events within the same organization use the "
|
||||||
|
"same value in this field, they will share their number range, i.e. every full number will be "
|
||||||
|
"used at most once over all of your events. This setting only affects future invoices."),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
invoice_generate = forms.ChoiceField(
|
invoice_generate = forms.ChoiceField(
|
||||||
label=_("Generate invoices"),
|
label=_("Generate invoices"),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -511,6 +519,7 @@ class InvoiceSettingsForm(SettingsForm):
|
|||||||
self.fields['invoice_renderer'].choices = [
|
self.fields['invoice_renderer'].choices = [
|
||||||
(r.identifier, r.verbose_name) for r in event.get_invoice_renderers().values()
|
(r.identifier, r.verbose_name) for r in event.get_invoice_renderers().values()
|
||||||
]
|
]
|
||||||
|
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
|
||||||
|
|
||||||
|
|
||||||
class MailSettingsForm(SettingsForm):
|
class MailSettingsForm(SettingsForm):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
{% bootstrap_field form.invoice_address_required layout="horizontal" %}
|
{% bootstrap_field form.invoice_address_required layout="horizontal" %}
|
||||||
{% bootstrap_field form.invoice_address_vatid layout="horizontal" %}
|
{% bootstrap_field form.invoice_address_vatid layout="horizontal" %}
|
||||||
{% bootstrap_field form.invoice_numbers_consecutive layout="horizontal" %}
|
{% bootstrap_field form.invoice_numbers_consecutive layout="horizontal" %}
|
||||||
|
{% bootstrap_field form.invoice_numbers_prefix layout="horizontal" %}
|
||||||
{% bootstrap_field form.invoice_generate layout="horizontal" %}
|
{% bootstrap_field form.invoice_generate layout="horizontal" %}
|
||||||
{% bootstrap_field form.invoice_renderer layout="horizontal" %}
|
{% bootstrap_field form.invoice_renderer layout="horizontal" %}
|
||||||
{% bootstrap_field form.invoice_language layout="horizontal" %}
|
{% bootstrap_field form.invoice_language layout="horizontal" %}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from datetime import timedelta
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.db import DatabaseError
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -219,3 +220,58 @@ def test_invoice_numbers(env):
|
|||||||
# test Invoice.number, too
|
# test Invoice.number, too
|
||||||
assert inv1.number == '{}-00001'.format(event.slug.upper())
|
assert inv1.number == '{}-00001'.format(event.slug.upper())
|
||||||
assert inv3.number == '{}-{}-3'.format(event.slug.upper(), order.code)
|
assert inv3.number == '{}-{}-3'.format(event.slug.upper(), order.code)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_invoice_number_prefixes(env):
|
||||||
|
event, order = env
|
||||||
|
event2 = Event.objects.create(
|
||||||
|
organizer=event.organizer, name='Dummy', slug='dummy2',
|
||||||
|
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||||
|
)
|
||||||
|
order2 = Order.objects.create(
|
||||||
|
event=event2, email='dummy2@dummy.test',
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
datetime=now(), expires=now() + timedelta(days=10),
|
||||||
|
total=0, payment_provider='banktransfer',
|
||||||
|
payment_fee=Decimal('0.25'), payment_fee_tax_rate=0,
|
||||||
|
payment_fee_tax_value=0, locale='en'
|
||||||
|
)
|
||||||
|
event.settings.set('invoice_numbers_consecutive', False)
|
||||||
|
event2.settings.set('invoice_numbers_consecutive', False)
|
||||||
|
assert generate_invoice(order).number == 'DUMMY-{}-1'.format(order.code)
|
||||||
|
assert generate_invoice(order2).number == 'DUMMY2-{}-1'.format(order2.code)
|
||||||
|
|
||||||
|
event.settings.set('invoice_numbers_consecutive', True)
|
||||||
|
event2.settings.set('invoice_numbers_consecutive', True)
|
||||||
|
event.settings.set('invoice_numbers_prefix', '')
|
||||||
|
event2.settings.set('invoice_numbers_prefix', '')
|
||||||
|
|
||||||
|
assert generate_invoice(order).number == 'DUMMY-00001'
|
||||||
|
assert generate_invoice(order).number == 'DUMMY-00002'
|
||||||
|
assert generate_invoice(order2).number == 'DUMMY2-00001'
|
||||||
|
assert generate_invoice(order2).number == 'DUMMY2-00002'
|
||||||
|
|
||||||
|
event.settings.set('invoice_numbers_prefix', 'shared_')
|
||||||
|
event2.settings.set('invoice_numbers_prefix', 'shared_')
|
||||||
|
|
||||||
|
assert generate_invoice(order).number == 'shared_00001'
|
||||||
|
assert generate_invoice(order2).number == 'shared_00002'
|
||||||
|
assert generate_invoice(order).number == 'shared_00003'
|
||||||
|
assert generate_invoice(order2).number == 'shared_00004'
|
||||||
|
|
||||||
|
event.settings.set('invoice_numbers_consecutive', False)
|
||||||
|
event2.settings.set('invoice_numbers_consecutive', False)
|
||||||
|
assert generate_invoice(order).number == 'shared_{}-6'.format(order.code)
|
||||||
|
assert generate_invoice(order2).number == 'shared_{}-6'.format(order2.code)
|
||||||
|
|
||||||
|
# Test database uniqueness check
|
||||||
|
with pytest.raises(DatabaseError):
|
||||||
|
Invoice.objects.create(
|
||||||
|
order=order,
|
||||||
|
event=order.event,
|
||||||
|
organizer=order.event.organizer,
|
||||||
|
date=now().date(),
|
||||||
|
locale='en',
|
||||||
|
invoice_no='00001',
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user