Add setting determining invoice number format (#193)

This commit is contained in:
Tobias Kunze
2016-08-16 21:18:40 +02:00
committed by Raphael Michel
parent 6628d65f9a
commit 4191f93ece
11 changed files with 177 additions and 8 deletions

View File

View File

@@ -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,
),
]

View 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),
]

View 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',
),
]

View 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')]),
),
]

View File

@@ -10,10 +10,9 @@ from django.utils.functional import cached_property
def invoice_filename(instance, filename: str) -> str:
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,
no=instance.invoice_no, code=instance.order.code,
secret=secret
no=instance.number, code=instance.order.code, secret=secret
)
@@ -47,7 +46,7 @@ class Invoice(models.Model):
"""
order = models.ForeignKey('Order', 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)
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
invoice_from = models.TextField()
@@ -57,6 +56,20 @@ class Invoice(models.Model):
additional_text = models.TextField(blank=True)
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):
if not self.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
if not self.invoice_no:
for i in range(10):
self.invoice_no = (Invoice.objects.filter(
event=self.event).aggregate(m=Max('invoice_no'))['m'] or 0) + 1
if self.event.settings.get('invoice_numbers_consecutive'):
self.invoice_no = self._get_numeric_invoice_number()
else:
self.invoice_no = self._get_invoice_number_from_order()
try:
return super().save(*args, **kwargs)
except DatabaseError:
@@ -74,12 +89,23 @@ class Invoice(models.Model):
raise
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
def number(self):
"""
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
def canceled(self):

View File

@@ -175,7 +175,7 @@ class Order(LoggedModel):
An order code which is unique among all events of a single organizer,
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):
if not self.code:

View File

@@ -37,6 +37,10 @@ DEFAULTS = {
'default': 'False',
'type': bool,
},
'invoice_numbers_consecutive': {
'default': 'True',
'type': bool,
},
'reservation_time': {
'default': '30',
'type': int

View File

@@ -252,6 +252,11 @@ class InvoiceSettingsForm(SettingsForm):
help_text=_("Does only work if an invoice address is asked for. VAT ID is not required."),
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(
label=_("Generate invoices"),
required=False,

View File

@@ -10,6 +10,7 @@
{% bootstrap_field form.invoice_address_asked layout="horizontal" %}
{% bootstrap_field form.invoice_address_required 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_language layout="horizontal" %}
{% bootstrap_field form.invoice_address_from layout="horizontal" %}

View File

@@ -91,3 +91,45 @@ def test_positions(env):
assert last.tax_rate == order.payment_fee_tax_rate
assert last.tax_value == order.payment_fee_tax_value
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)