forked from CGM_Public/pretix_original
Allow to exclude free products from invoices
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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."),
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user