From e32e5faf35a74472f814028eafca37c26132a6e2 Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera <hugo@whynothugo.nl> Date: Fri, 17 Nov 2023 05:55:37 +0800 Subject: [PATCH] Add a helper to approximate receipt dates --- django_afip/models.py | 53 ++++++++++++++++++++++ docs/changelog.rst | 3 ++ tests/test_models.py | 103 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) diff --git a/django_afip/models.py b/django_afip/models.py index b6ce2a9a..09da9eeb 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1336,6 +1336,59 @@ def revalidate(self) -> ReceiptValidation | None: return validation return None + def approximate_date(receipt: models.Receipt) -> bool: + """Approximate the date of the receipt as close as possible. + + If a receipt should have been validated in a past date, adjust its date as close + as possible: + + - Receipts can only be validated with dates as far as 14 days ago. If the + receipt date is older than that, set it to 14 days ago. + - If other receipts have been validated on a more recent date, the receipt + cannot be older than the most recent one. + + If the ``issued_date`` needs to be changed, the field in the input receipt will + be updated and atomically saved to the database. + + Returns ``True`` if the date has been changed. + """ + today = datetime.now(TZ_AR).date() + + if receipt.issued_date == today: + return False + + most_recent = ( + Receipt.objects.filter( + point_of_sales=receipt.point_of_sales, + receipt_type=receipt.receipt_type, + validation__result=ReceiptValidation.RESULT_APPROVED, + ) + .order_by("issued_date") + .last() + ) + + fortnight_ago = today - timedelta(days=14) + if most_recent is not None: + oldest_possible = max(most_recent.issued_date, fortnight_ago) + else: + oldest_possible = fortnight_ago + + if receipt.issued_date >= oldest_possible: + return False + + # Commit this atomically to avoid race conditions. + Receipt.objects.filter( + pk=receipt.id, + receipt_number__isnull=True, + ).update( + issued_date=oldest_possible, + ) + + # Mutate the input object to avoid inconsistency issues. + receipt.issued_date = oldest_possible + + return True + def __repr__(self) -> str: return "<Receipt {}: {} {} for {}>".format( self.pk, diff --git a/docs/changelog.rst b/docs/changelog.rst index 1a8c5928..0296bece 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,6 +25,9 @@ acá. - **BREAKING**: The signal that auto-generated receipt pdfs for validated ReceiptPDFs has been removed. Applications now need to explicitly call :meth:`~.ReceiptPDF.save_pdf()`. +- Add a new helper helper method :meth:`~.Receipt.approximate_date`. It is + intended to be used to automatically approximate dates on systems which + perform automatic or unattended receipt validation. 11.3.1 ------ diff --git a/tests/test_models.py b/tests/test_models.py index c7cf8115..4f28ec52 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,8 @@ from __future__ import annotations +from datetime import date +from datetime import datetime +from datetime import timedelta from decimal import Decimal from typing import TYPE_CHECKING from unittest.mock import MagicMock @@ -8,11 +11,13 @@ import pytest from django.db.models import DecimalField +from freezegun import freeze_time from pytest_django.asserts import assertQuerysetEqual from django_afip import exceptions from django_afip import factories from django_afip import models +from django_afip.clients import TZ_AR from django_afip.factories import ReceiptFactory from django_afip.factories import ReceiptFCEAWithVatAndTaxFactory from django_afip.factories import ReceiptFCEAWithVatTaxAndOptionalsFactory @@ -456,3 +461,101 @@ def test_receipt_entry_manage_decimal_quantities() -> None: last = models.ReceiptEntry.objects.last() assert last is not None assert last.quantity == Decimal("5.23") + + +@pytest.mark.django_db() +@freeze_time("2023-11-16 18:39:40") +def test_approximate_noop_today() -> None: + today = datetime.now(TZ_AR).date() + + factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=20)) + + receipt = factories.ReceiptFactory(issued_date=today) + changed = receipt.approximate_date() + + assert changed is False + assert receipt.issued_date == today + + +@pytest.mark.django_db() +@freeze_time("2023-11-16 18:39:40") +def test_approximate_noop_two_days_ago() -> None: + today = datetime.now(TZ_AR).date() + two_days_ago = date(2023, 11, 14) + + factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=20)) + + receipt = factories.ReceiptFactory(issued_date=two_days_ago) + changed = receipt.approximate_date() + + assert changed is False + assert receipt.issued_date == two_days_ago + + +@pytest.mark.django_db() +@freeze_time("2023-11-16 18:39:40") +def test_approximate_date_today_with_most_recent() -> None: + today = datetime.now(TZ_AR).date() + + factories.ReceiptWithApprovedValidation(issued_date=today) + + receipt = factories.ReceiptFactory(issued_date=today - timedelta(days=30)) + changed = receipt.approximate_date() + + assert changed is True + assert receipt.issued_date == date(2023, 11, 16) + + +@pytest.mark.django_db() +@freeze_time("2023-11-16 18:39:40") +def test_approximate_date_yesterday_with_most_recent() -> None: + today = datetime.now(TZ_AR).date() + + factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=1)) + + receipt = factories.ReceiptFactory(issued_date=today - timedelta(days=30)) + changed = receipt.approximate_date() + + assert changed is True + assert receipt.issued_date == date(2023, 11, 15) + + +@pytest.mark.django_db() +@freeze_time("2023-11-16 18:39:40") +def test_approximate_date_30_days_ago_with_most_recent_20_days_ago() -> None: + today = datetime.now(TZ_AR).date() + + factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=20)) + + receipt = factories.ReceiptFactory(issued_date=today - timedelta(days=30)) + changed = receipt.approximate_date() + + assert changed is True + assert receipt.issued_date == date(2023, 11, 2) + + +@pytest.mark.django_db() +@freeze_time("2023-11-16 18:39:40") +def test_approximate_date_2_days_ago_with_most_recent_20_days_ago() -> None: + today = datetime.now(TZ_AR).date() + two_days_ago = date(2023, 11, 14) + + factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=20)) + + receipt = factories.ReceiptFactory(issued_date=two_days_ago) + changed = receipt.approximate_date() + + assert changed is False + assert receipt.issued_date == two_days_ago + + +@pytest.mark.django_db() +@freeze_time("2023-11-16 18:39:40") +def test_approximate_date_two_days_ago_without_most_recent() -> None: + two_days_ago = date(2023, 11, 14) + + receipt = factories.ReceiptFactory(issued_date=two_days_ago) + changed = receipt.approximate_date() + + assert changed is False + assert receipt.issued_date == two_days_ago