Commit 6c772e15 authored by Patrick Kimber's avatar Patrick Kimber

Exclude 'on_account' transactions from invoice batches

parent a45ab198
Pipeline #138591114 passed with stage
in 5 minutes and 14 seconds
......@@ -318,6 +318,39 @@ class PaymentProcessor(TimeStampedModel):
class InvoiceManager(TimedCreateModifyDeleteVersionModelManager):
def _batch_invoices(self, invoice_date):
"""List of invoices which can be included in a batch.
.. tip:: For details, see ``on_account_invoices``.
"""
on_account_contact_pks = InvoiceContact.objects.filter(
on_account=True
).values_list("contact__pk", flat=True)
return (
self.model.objects.current(invoice_date=invoice_date)
.exclude(is_credit=True)
.filter(upfront_payment=True,)
.exclude(contact__pk__in=on_account_contact_pks)
)
def batch_invoices(self, invoice_date, currency, payment_processor):
"""Invoices which can be included in a batch.
Invoices can be included in a batch if they are paid, but not on-account
and have the same invoice date, currency and payment processor.
.. tip:: For details, see ``on_account_invoices`` and
``_batch_invoices``.
.. note:: Moved from ``Batch.objects._batch_invoices``.
"""
qs = self._batch_invoices(invoice_date)
return qs.filter(
currency=currency, upfront_payment_processor=payment_processor,
)
def credit_notes(self, invoice_date):
return self.current(invoice_date=invoice_date).filter(is_credit=True)
......@@ -329,19 +362,36 @@ class InvoiceManager(TimedCreateModifyDeleteVersionModelManager):
qs = qs.filter(invoice_date=invoice_date)
return qs
def not_upfront_payment(self, invoice_date):
"""List of invoices which cannot be added to a batch.
def exchange_rates(self, invoice_date, currency, payment_processor):
"""A set of the exchange rates for invoices on this date."""
qs = (
self.batch_invoices(invoice_date, currency, payment_processor)
.order_by("exchange_rate")
.distinct("exchange_rate")
)
return {x.exchange_rate for x in qs}
def on_account_invoices(self, invoice_date):
"""List of on-account invoices (which cannot be added to a batch).
- Exclude credit notes.
- We use a ``Batch`` to group invoices which were paid for *on order*
- A ``Batch`` is used to group invoices which were paid for *on order*
(``upfront_payment=True``).
- ``on_account`` customers should not be included in a batch.
(``InvoiceContact``).
- Exclude credit notes - they are handled differently.
.. tip:: For details, see ``batch_invoices``.
.. note:: 22/04/2020, Renamed from ``not_upfront_payment``.
"""
batch_invoice_pks = self._batch_invoices(invoice_date).values_list(
"pk", flat=True
)
return (
self.model.objects.current()
.filter(invoice_date=invoice_date)
self.model.objects.current(invoice_date=invoice_date)
.exclude(is_credit=True)
.exclude(upfront_payment=True)
.exclude(pk__in=batch_invoice_pks)
)
......@@ -717,19 +767,14 @@ class InvoiceIssueLine(TimeStampedModel):
class BatchManager(models.Manager):
def _batch_invoices(self, batch_date, currency, payment_processor):
return Invoice.objects.filter(
invoice_date=batch_date,
currency=currency,
upfront_payment_processor=payment_processor,
upfront_payment=True,
)
def create_batch(
self, batch_date, currency, exchange_rate, payment_processor
):
result = None
qs = self._batch_invoices(batch_date, currency, payment_processor)
# qs = self._batch_invoices(batch_date, currency, payment_processor)
qs = Invoice.objects.batch_invoices(
batch_date, currency, payment_processor
)
invoices = qs.filter(exchange_rate=exchange_rate)
# only create the batch if we have invoices to go in it!
if invoices.count():
......@@ -747,15 +792,6 @@ class BatchManager(models.Manager):
BatchInvoice(batch=result, invoice=invoice).save()
return result
def exchange_rates(self, batch_date, currency, payment_processor):
"""A set of the exchange rates for invoices on this date."""
qs = (
self._batch_invoices(batch_date, currency, payment_processor)
.order_by("exchange_rate")
.distinct("exchange_rate")
)
return {x.exchange_rate for x in qs}
def for_date(self, batch_date):
return self.model.objects.filter(batch_date=batch_date)
......
......@@ -6,10 +6,11 @@ from decimal import Decimal
from django.db import IntegrityError
from finance.models import Currency, VatCode
from invoice.models import Batch, BatchInvoice
from invoice.models import Batch, BatchInvoice, Invoice
from invoice.tests.factories import (
BatchFactory,
BatchInvoiceFactory,
InvoiceContactFactory,
InvoiceFactory,
InvoiceLineFactory,
PaymentProcessorFactory,
......@@ -70,6 +71,49 @@ def test_create_batch_exchange_rate():
batch_date, currency, exchange_rate, payment_processor
)
assert [invoice_2.pk] == [x.invoice.pk for x in batch.invoices()]
assert [] == [x.pk for x in Invoice.objects.on_account_invoices(batch_date)]
@pytest.mark.django_db
def test_create_batch_on_account():
"""
.. note:: ``invoice_2`` is an ``on_account`` invoice so must not be
included in a batch:
https://www.kbsoftware.co.uk/crm/ticket/4924/
"""
batch_date = date(2018, 2, 22)
currency = Currency.objects.get(slug="GBP")
payment_processor = PaymentProcessorFactory()
invoice_1 = InvoiceFactory(
invoice_date=batch_date,
currency=currency,
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
invoice_2 = InvoiceFactory(
invoice_date=batch_date,
currency=currency,
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceContactFactory(contact=invoice_2.contact, on_account=True)
invoice_3 = InvoiceFactory(
invoice_date=batch_date,
currency=currency,
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
batch = Batch.objects.create_batch(
batch_date, currency, Decimal(), payment_processor
)
assert set([invoice_1.pk, invoice_3.pk]) == set(
[x.invoice.pk for x in batch.invoices()]
)
assert [invoice_2.pk] == [
x.pk for x in Invoice.objects.on_account_invoices(batch_date)
]
@pytest.mark.django_db
......@@ -112,58 +156,6 @@ def test_duplicate():
)
@pytest.mark.django_db
def test_exchange_rates():
currency = Currency.objects.get(slug=Currency.EURO)
payment_processor = PaymentProcessorFactory()
invoice_date = date.today()
InvoiceFactory(
invoice_date=invoice_date,
currency=currency,
exchange_rate=Decimal(),
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceFactory(
currency=currency,
invoice_date=invoice_date,
exchange_rate=Decimal("0.999"),
upfront_payment=True,
upfront_payment_processor=PaymentProcessorFactory(),
)
InvoiceFactory(
currency=currency,
invoice_date=invoice_date,
exchange_rate=Decimal("0.888"),
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceFactory(
currency=Currency.objects.get(slug=Currency.POUND),
invoice_date=invoice_date,
exchange_rate=Decimal("0.777"),
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceFactory(
currency=currency,
invoice_date=invoice_date,
exchange_rate=Decimal("0.888"),
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceFactory(
currency=currency,
invoice_date=invoice_date,
exchange_rate=Decimal("0.555"),
upfront_payment=False,
upfront_payment_processor=payment_processor,
)
assert set([Decimal(), Decimal("0.888")]) == Batch.objects.exchange_rates(
invoice_date, currency, payment_processor
)
@pytest.mark.django_db
def test_net_and_vat():
vat_code_standard = VatCode.objects.get(slug=VatCode.STANDARD)
......
......@@ -12,10 +12,12 @@ from finance.tests.factories import VatSettingsFactory
from invoice.models import Invoice, InvoiceLine, format_minutes
from invoice.service import InvoicePrint
from invoice.tests.factories import (
InvoiceContactFactory,
InvoiceCreditFactory,
InvoiceFactory,
InvoiceLineFactory,
InvoiceSettingsFactory,
PaymentProcessorFactory,
TimeRecordFactory,
)
from login.tests.factories import UserFactory
......@@ -165,6 +167,58 @@ def test_description():
assert "Invoice" == invoice.description
@pytest.mark.django_db
def test_exchange_rates():
currency = Currency.objects.get(slug=Currency.EURO)
payment_processor = PaymentProcessorFactory()
invoice_date = date.today()
InvoiceFactory(
invoice_date=invoice_date,
currency=currency,
exchange_rate=Decimal(),
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceFactory(
currency=currency,
invoice_date=invoice_date,
exchange_rate=Decimal("0.999"),
upfront_payment=True,
upfront_payment_processor=PaymentProcessorFactory(),
)
InvoiceFactory(
currency=currency,
invoice_date=invoice_date,
exchange_rate=Decimal("0.888"),
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceFactory(
currency=Currency.objects.get(slug=Currency.POUND),
invoice_date=invoice_date,
exchange_rate=Decimal("0.777"),
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceFactory(
currency=currency,
invoice_date=invoice_date,
exchange_rate=Decimal("0.888"),
upfront_payment=True,
upfront_payment_processor=payment_processor,
)
InvoiceFactory(
currency=currency,
invoice_date=invoice_date,
exchange_rate=Decimal("0.555"),
upfront_payment=False,
upfront_payment_processor=payment_processor,
)
assert set([Decimal(), Decimal("0.888")]) == Invoice.objects.exchange_rates(
invoice_date, currency, payment_processor
)
def test_format_minutes():
assert "02:00" == format_minutes(120)
......@@ -365,22 +419,6 @@ def test_next_number_set_deleted_fill_gap():
assert 2 == n4
@pytest.mark.django_db
def test_not_upfront_payment():
date_before = date(2019, 3, 10)
invoice_date = date(2019, 3, 11)
date_after = date(2019, 3, 12)
invoice_1 = InvoiceFactory(invoice_date=invoice_date)
InvoiceFactory(invoice_date=date_before)
InvoiceFactory(invoice_date=invoice_date, upfront_payment=True)
InvoiceFactory(invoice_date=invoice_date, is_credit=True)
invoice_5 = InvoiceFactory(invoice_date=invoice_date)
InvoiceFactory(invoice_date=date_after)
assert [invoice_1.pk, invoice_5.pk] == [
x.pk for x in Invoice.objects.not_upfront_payment(invoice_date)
]
@pytest.mark.django_db
def test_number_prefix():
"""Check the invoice prefix (category) is working as expected.
......@@ -488,6 +526,38 @@ def test_number_prefix():
).save()
@pytest.mark.django_db
def test_on_account_invoices():
date_before = date(2019, 3, 10)
invoice_date = date(2019, 3, 11)
date_after = date(2019, 3, 12)
invoice_1 = InvoiceFactory(number=1, invoice_date=invoice_date)
InvoiceFactory(number=2, invoice_date=date_before)
InvoiceFactory(number=3, invoice_date=invoice_date, upfront_payment=True)
InvoiceFactory(number=4, invoice_date=invoice_date, is_credit=True)
invoice_5 = InvoiceFactory(number=5, invoice_date=invoice_date)
# on account
on_account_contact = ContactFactory()
InvoiceContactFactory(contact=on_account_contact, on_account=True)
invoice_6 = InvoiceFactory(
number=6,
contact=on_account_contact,
invoice_date=invoice_date,
upfront_payment=True,
)
invoice_7 = InvoiceFactory(
contact=on_account_contact,
number=7,
invoice_date=invoice_date,
is_credit=True,
)
# the day after...
InvoiceFactory(number=8, invoice_date=date_after)
assert [invoice_1.number, invoice_5.number, invoice_6.number] == [
x.number for x in Invoice.objects.on_account_invoices(invoice_date)
]
@pytest.mark.django_db
def test_user_can_edit():
line = InvoiceLineFactory()
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment