forked from CGM_Public/pretix_original
Add setting determining invoice number format (#193)
This commit is contained in:
committed by
Raphael Michel
parent
6628d65f9a
commit
4191f93ece
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-08-16 06:36
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0028_auto_20160816_1242'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='invoice_no_charfield',
|
||||||
|
field=models.CharField(db_index=True, default='', max_length=19),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
23
src/pretix/base/migrations/0029_invoice_no_data.py
Normal file
23
src/pretix/base/migrations/0029_invoice_no_data.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-08-16 06:37
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forwards(apps, schema_editor):
|
||||||
|
Invoice = apps.get_model('pretixbase', 'Invoice')
|
||||||
|
for invoice in Invoice.objects.all():
|
||||||
|
invoice.invoice_no_charfield = '{:05d}'.format(invoice.invoice_no)
|
||||||
|
invoice.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0028_invoice_invoice_no_charfield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forwards, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
23
src/pretix/base/migrations/0030_auto_20160816_0646.py
Normal file
23
src/pretix/base/migrations/0030_auto_20160816_0646.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-08-16 06:46
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0029_invoice_no_data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='invoice',
|
||||||
|
unique_together=set([('event', 'invoice_no_charfield')]),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='invoice_no',
|
||||||
|
),
|
||||||
|
]
|
||||||
24
src/pretix/base/migrations/0031_auto_20160816_0648.py
Normal file
24
src/pretix/base/migrations/0031_auto_20160816_0648.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-08-16 06:48
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0030_auto_20160816_0646'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='invoice',
|
||||||
|
old_name='invoice_no_charfield',
|
||||||
|
new_name='invoice_no',
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='invoice',
|
||||||
|
unique_together=set([('event', 'invoice_no')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -10,10 +10,9 @@ from django.utils.functional import cached_property
|
|||||||
|
|
||||||
def invoice_filename(instance, filename: str) -> str:
|
def invoice_filename(instance, filename: str) -> str:
|
||||||
secret = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
|
secret = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
|
||||||
return 'invoices/{org}/{ev}/{ev}-{no:05d}-{code}-{secret}.pdf'.format(
|
return 'invoices/{org}/{ev}/{no}-{code}-{secret}.pdf'.format(
|
||||||
org=instance.event.organizer.slug, ev=instance.event.slug,
|
org=instance.event.organizer.slug, ev=instance.event.slug,
|
||||||
no=instance.invoice_no, code=instance.order.code,
|
no=instance.number, code=instance.order.code, secret=secret
|
||||||
secret=secret
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ class Invoice(models.Model):
|
|||||||
"""
|
"""
|
||||||
order = models.ForeignKey('Order', related_name='invoices', db_index=True)
|
order = models.ForeignKey('Order', related_name='invoices', db_index=True)
|
||||||
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
|
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
|
||||||
invoice_no = models.PositiveIntegerField(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)
|
||||||
invoice_from = models.TextField()
|
invoice_from = models.TextField()
|
||||||
@@ -57,6 +56,20 @@ class Invoice(models.Model):
|
|||||||
additional_text = models.TextField(blank=True)
|
additional_text = models.TextField(blank=True)
|
||||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
|
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_numeric_invoice_number(number):
|
||||||
|
return '{:05d}'.format(int(number))
|
||||||
|
|
||||||
|
def _get_numeric_invoice_number(self):
|
||||||
|
numeric_invoices = Invoice.objects.filter(event=self.event).exclude(invoice_no__contains='-')
|
||||||
|
return self._to_numeric_invoice_number(numeric_invoices.count() + 1)
|
||||||
|
|
||||||
|
def _get_invoice_number_from_order(self):
|
||||||
|
return '{order}-{count}'.format(
|
||||||
|
order=self.order.code,
|
||||||
|
count=Invoice.objects.filter(event=self.event, order=self.order).count() + 1,
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.order:
|
if not self.order:
|
||||||
raise ValueError('Every invoice needs to be connected to an order')
|
raise ValueError('Every invoice needs to be connected to an order')
|
||||||
@@ -64,8 +77,10 @@ class Invoice(models.Model):
|
|||||||
self.event = self.order.event
|
self.event = self.order.event
|
||||||
if not self.invoice_no:
|
if not self.invoice_no:
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
self.invoice_no = (Invoice.objects.filter(
|
if self.event.settings.get('invoice_numbers_consecutive'):
|
||||||
event=self.event).aggregate(m=Max('invoice_no'))['m'] or 0) + 1
|
self.invoice_no = self._get_numeric_invoice_number()
|
||||||
|
else:
|
||||||
|
self.invoice_no = self._get_invoice_number_from_order()
|
||||||
try:
|
try:
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
except DatabaseError:
|
except DatabaseError:
|
||||||
@@ -74,12 +89,23 @@ class Invoice(models.Model):
|
|||||||
raise
|
raise
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Deleting an Invoice would allow for the creation of another Invoice object
|
||||||
|
with the same invoice_no as the deleted one. For various reasons, invoice_no
|
||||||
|
should be reliably unique for an event.
|
||||||
|
"""
|
||||||
|
raise Exception('Invoices cannot be deleted, to guarantee uniqueness of Invoice.invoice_no in any event.')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def number(self):
|
def number(self):
|
||||||
"""
|
"""
|
||||||
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 '%s-%05d' % (self.event.slug.upper(), self.invoice_no)
|
return '{event}-{code}'.format(
|
||||||
|
event=self.event.slug.upper(),
|
||||||
|
code=self.invoice_no
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def canceled(self):
|
def canceled(self):
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class Order(LoggedModel):
|
|||||||
An order code which is unique among all events of a single organizer,
|
An order code which is unique among all events of a single organizer,
|
||||||
built by contatenating the event slug and the order code.
|
built by contatenating the event slug and the order code.
|
||||||
"""
|
"""
|
||||||
return self.event.slug.upper() + self.code
|
return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.code:
|
if not self.code:
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ DEFAULTS = {
|
|||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
},
|
},
|
||||||
|
'invoice_numbers_consecutive': {
|
||||||
|
'default': 'True',
|
||||||
|
'type': bool,
|
||||||
|
},
|
||||||
'reservation_time': {
|
'reservation_time': {
|
||||||
'default': '30',
|
'default': '30',
|
||||||
'type': int
|
'type': int
|
||||||
|
|||||||
@@ -252,6 +252,11 @@ class InvoiceSettingsForm(SettingsForm):
|
|||||||
help_text=_("Does only work if an invoice address is asked for. VAT ID is not required."),
|
help_text=_("Does only work if an invoice address is asked for. VAT ID is not required."),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
invoice_numbers_consecutive = forms.BooleanField(
|
||||||
|
label=_("Generate invoices with consecutive numbers"),
|
||||||
|
help_text=_("If deactivated, the order code will be used in the invoice number."),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
invoice_generate = forms.ChoiceField(
|
invoice_generate = forms.ChoiceField(
|
||||||
label=_("Generate invoices"),
|
label=_("Generate invoices"),
|
||||||
required=False,
|
required=False,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
{% bootstrap_field form.invoice_address_asked layout="horizontal" %}
|
{% bootstrap_field form.invoice_address_asked layout="horizontal" %}
|
||||||
{% 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_generate layout="horizontal" %}
|
{% bootstrap_field form.invoice_generate layout="horizontal" %}
|
||||||
{% bootstrap_field form.invoice_language layout="horizontal" %}
|
{% bootstrap_field form.invoice_language layout="horizontal" %}
|
||||||
{% bootstrap_field form.invoice_address_from layout="horizontal" %}
|
{% bootstrap_field form.invoice_address_from layout="horizontal" %}
|
||||||
|
|||||||
@@ -91,3 +91,45 @@ def test_positions(env):
|
|||||||
assert last.tax_rate == order.payment_fee_tax_rate
|
assert last.tax_rate == order.payment_fee_tax_rate
|
||||||
assert last.tax_value == order.payment_fee_tax_value
|
assert last.tax_value == order.payment_fee_tax_value
|
||||||
assert inv.invoice_to == ""
|
assert inv.invoice_to == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_invoice_numbers(env):
|
||||||
|
event, order = env
|
||||||
|
order2 = Order.objects.create(
|
||||||
|
code='BAR', event=event, 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'
|
||||||
|
)
|
||||||
|
inv1 = generate_invoice(order)
|
||||||
|
inv2 = generate_invoice(order)
|
||||||
|
|
||||||
|
event.settings.set('invoice_numbers_consecutive', False)
|
||||||
|
inv3 = generate_invoice(order)
|
||||||
|
inv4 = generate_invoice(order)
|
||||||
|
inv21 = generate_invoice(order2)
|
||||||
|
inv22 = generate_invoice(order2)
|
||||||
|
|
||||||
|
event.settings.set('invoice_numbers_consecutive', True)
|
||||||
|
inv5 = generate_invoice(order)
|
||||||
|
inv23 = generate_invoice(order2)
|
||||||
|
|
||||||
|
# expected behaviour for switching between numbering formats
|
||||||
|
assert inv1.invoice_no == '00001'
|
||||||
|
assert inv2.invoice_no == '00002'
|
||||||
|
assert inv3.invoice_no == '{}-3'.format(order.code)
|
||||||
|
assert inv4.invoice_no == '{}-4'.format(order.code)
|
||||||
|
assert inv5.invoice_no == '00003'
|
||||||
|
|
||||||
|
# test that separate orders are counted separately in this mode
|
||||||
|
assert inv21.invoice_no == '{}-1'.format(order2.code)
|
||||||
|
assert inv22.invoice_no == '{}-2'.format(order2.code)
|
||||||
|
# but consecutively in this mode
|
||||||
|
assert inv23.invoice_no == '00004'
|
||||||
|
|
||||||
|
# test Invoice.number, too
|
||||||
|
assert inv1.number == '{}-00001'.format(event.slug.upper())
|
||||||
|
assert inv3.number == '{}-{}-3'.format(event.slug.upper(), order.code)
|
||||||
|
|||||||
Reference in New Issue
Block a user