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:
|
||||
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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -37,6 +37,10 @@ DEFAULTS = {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
},
|
||||
'invoice_numbers_consecutive': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
},
|
||||
'reservation_time': {
|
||||
'default': '30',
|
||||
'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."),
|
||||
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,
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user