Allow to exclude free products from invoices

This commit is contained in:
Raphael Michel
2017-07-14 12:07:29 +02:00
parent 0ab6ac569e
commit 6ea798e55b
7 changed files with 66 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ from decimal import Decimal
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction from django.db import transaction
from django.db.models import Count
from django.utils import timezone from django.utils import timezone
from django.utils.translation import pgettext, ugettext as _ from django.utils.translation import pgettext, ugettext as _
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
@@ -47,9 +48,16 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.save() invoice.save()
invoice.lines.all().delete() invoice.lines.all().delete()
positions = list(invoice.order.positions.select_related('addon_to', 'item', 'variation')) positions = list(
invoice.order.positions.select_related('addon_to', 'item', 'variation').annotate(
addon_c=Count('addons')
)
)
positions.sort(key=lambda p: p.sort_key) positions.sort(key=lambda p: p.sort_key)
for p in positions: for p in 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) desc = str(p.item.name)
if p.variation: if p.variation:
desc += " - " + str(p.variation.value) desc += " - " + str(p.variation.value)

View File

@@ -545,6 +545,7 @@ class OrderChangeManager:
self._totaldiff = 0 self._totaldiff = 0
self._quotadiff = Counter() self._quotadiff = Counter()
self._operations = [] self._operations = []
self._invoice_dirty = False
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]): def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk): if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
@@ -560,6 +561,9 @@ class OrderChangeManager:
if not new_quotas: if not new_quotas:
raise OrderError(self.error_messages['quota_missing']) raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff = price - position.price self._totaldiff = price - position.price
self._quotadiff.update(new_quotas) self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas) self._quotadiff.subtract(position.quotas)
@@ -576,6 +580,9 @@ class OrderChangeManager:
if not new_quotas: if not new_quotas:
raise OrderError(self.error_messages['quota_missing']) raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff = price - position.price self._totaldiff = price - position.price
self._quotadiff.update(new_quotas) self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas) self._quotadiff.subtract(position.quotas)
@@ -583,6 +590,10 @@ class OrderChangeManager:
def change_price(self, position: OrderPosition, price: Decimal): def change_price(self, position: OrderPosition, price: Decimal):
self._totaldiff = price - position.price self._totaldiff = price - position.price
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._operations.append(self.PriceOperation(position, price)) self._operations.append(self.PriceOperation(position, price))
def cancel(self, position: OrderPosition): def cancel(self, position: OrderPosition):
@@ -590,6 +601,9 @@ class OrderChangeManager:
self._quotadiff.subtract(position.quotas) self._quotadiff.subtract(position.quotas)
self._operations.append(self.CancelOperation(position)) self._operations.append(self.CancelOperation(position))
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
self._invoice_dirty = True
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order = None, def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order = None,
subevent: SubEvent = None): subevent: SubEvent = None):
if price is None: if price is None:
@@ -609,6 +623,9 @@ class OrderChangeManager:
if not new_quotas: if not new_quotas:
raise OrderError(self.error_messages['quota_missing']) raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff = price self._totaldiff = price
self._quotadiff.update(new_quotas) self._quotadiff.update(new_quotas)
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent)) self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent))
@@ -730,7 +747,7 @@ class OrderChangeManager:
def _reissue_invoice(self): def _reissue_invoice(self):
i = self.order.invoices.filter(is_cancellation=False).last() i = self.order.invoices.filter(is_cancellation=False).last()
if i: if i and self._invoice_dirty:
generate_cancellation(i) generate_cancellation(i)
generate_invoice(self.order) generate_invoice(self.order)

View File

@@ -53,6 +53,10 @@ DEFAULTS = {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,
}, },
'invoice_include_free': {
'default': 'True',
'type': bool,
},
'invoice_numbers_consecutive': { 'invoice_numbers_consecutive': {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool,

View File

@@ -440,6 +440,12 @@ class InvoiceSettingsForm(SettingsForm):
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}), widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
required=False required=False
) )
invoice_include_free = forms.BooleanField(
label=_("Show free products on invoices"),
help_text=_("Note that invoices will never be generated for orders that contain only free "
"products."),
required=False
)
invoice_numbers_consecutive = forms.BooleanField( invoice_numbers_consecutive = forms.BooleanField(
label=_("Generate invoices with consecutive numbers"), label=_("Generate invoices with consecutive numbers"),
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."),

View File

@@ -14,6 +14,7 @@
{% 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" %}
{% bootstrap_field form.invoice_include_free layout="horizontal" %}
{% bootstrap_field form.invoice_address_from layout="horizontal" %} {% bootstrap_field form.invoice_address_from layout="horizontal" %}
{% bootstrap_field form.invoice_introductory_text layout="horizontal" %} {% bootstrap_field form.invoice_introductory_text layout="horizontal" %}
{% bootstrap_field form.invoice_additional_text layout="horizontal" %} {% bootstrap_field form.invoice_additional_text layout="horizontal" %}

View File

@@ -92,6 +92,17 @@ def test_address_vat_id(env):
assert inv.invoice_to == "Acme Company\nSherlock Holmes\n221B Baker Street\n12345 London\nUK\nVAT-ID: UK1234567" assert inv.invoice_to == "Acme Company\nSherlock Holmes\n221B Baker Street\n12345 London\nUK\nVAT-ID: UK1234567"
@pytest.mark.django_db
def test_positions_skip_free(env):
event, order = env
event.settings.invoice_include_free = False
op1 = order.positions.first()
op1.price = Decimal('0.00')
op1.save()
inv = generate_invoice(order)
assert inv.lines.count() == 2
@pytest.mark.django_db @pytest.mark.django_db
def test_positions(env): def test_positions(env):
event, order = env event, order = env

View File

@@ -13,6 +13,7 @@ from pretix.base.models import (
from pretix.base.models.items import SubEventItem from pretix.base.models.items import SubEventItem
from pretix.base.payment import FreeOrderProvider from pretix.base.payment import FreeOrderProvider
from pretix.base.reldate import RelativeDate, RelativeDateWrapper from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.invoices import generate_invoice
from pretix.base.services.orders import ( from pretix.base.services.orders import (
OrderChangeManager, OrderError, _create_order, expire_orders, OrderChangeManager, OrderError, _create_order, expire_orders,
) )
@@ -184,7 +185,7 @@ class OrderChangeManagerTests(TestCase):
self.event = Event.objects.create(organizer=o, name='Dummy', slug='dummy', date_from=now(), plugins='pretix.plugins.banktransfer') self.event = Event.objects.create(organizer=o, name='Dummy', slug='dummy', date_from=now(), plugins='pretix.plugins.banktransfer')
self.order = Order.objects.create( self.order = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test', code='FOO', event=self.event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10), datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('46.00'), payment_provider='banktransfer' total=Decimal('46.00'), payment_provider='banktransfer'
) )
@@ -510,3 +511,18 @@ class OrderChangeManagerTests(TestCase):
assert nop.item == self.ticket assert nop.item == self.ticket
assert nop.price == Decimal('12.00') assert nop.price == Decimal('12.00')
assert nop.subevent == se1 assert nop.subevent == se1
def test_reissue_invoice(self):
generate_invoice(self.order)
assert self.order.invoices.count() == 1
self.ocm.add_position(self.ticket, None, Decimal('0.00'))
self.ocm.commit()
assert self.order.invoices.count() == 3
def test_dont_reissue_invoice_on_free_product_changes(self):
self.event.settings.invoice_include_free = False
generate_invoice(self.order)
assert self.order.invoices.count() == 1
self.ocm.add_position(self.ticket, None, Decimal('0.00'))
self.ocm.commit()
assert self.order.invoices.count() == 1