From ba03b40feec68dc5be0ff4289ff7b83348e1491e Mon Sep 17 00:00:00 2001 From: Bogdan Petrea Date: Mon, 18 Dec 2023 13:20:36 +0200 Subject: [PATCH 1/4] Bump some dependencies versions --- requirements/common.txt | 16 ++++++++-------- requirements/optionals.txt | 4 ++-- silver/api/urls.py | 2 +- silver/models/documents/invoice.py | 3 +++ silver/models/documents/proforma.py | 3 +++ silver/urls.py | 3 ++- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/requirements/common.txt b/requirements/common.txt index 8f90f64a..5331ba83 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -3,29 +3,29 @@ # UNMAINTAINED means the package owner is no longer maintaining it # Core -Django>=3.1,<3.3 # ------------------------------------------------------------ (bumped 2021-06-24) +Django>=3.2,<5.0 # ------------------------------------------------------------ (bumped 2023-09-18) sqlparse>=0.2,<0.5 # (required for some old migrations) ----------------------- (bumped 2021-04-15) # Django Utils django-fsm>=2.7,<2.8 # -------------------------------------------------------- (bumped 2021-04-15) django-filter>=2.4,<2.5 # ----------------------------------------------------- (bumped 2021-04-15) django-livefield>=3.3,<3.4 # -------------------------------------------------- (bumped 2021-04-15) -django-model-utils>=4.1,<4.2 # ------------------------------------------------ (bumped 2021-04-15) +django-model-utils>=4.3.1,<4.4 # ---------------------------------------------- (bumped 2023-09-18) django-annoying>=0.10,<0.11 # (various Django helpers) ----------------------- (checked 2021-04-15) -django-autocomplete-light>=3.8,<3.9 # ----------------------------------------- (bumped 2021-04-15) +django-autocomplete-light>=3.9.7,<3.10 # -------------------------------------- (bumped 2023-09-18) # API -djangorestframework>=3.12,<3.13 # --------------------------------------------- (bumped 2021-04-15) +djangorestframework>=3.14,<3.15 # --------------------------------------------- (bumped 2023-09-18) djangorestframework-bulk<0.3 # ----------------------------------------------- (checked 2021-04-15) # I18n -pycountry>=20.7.3 # ----------------------------------------------------------- (bumped 2021-04-15) -python-dateutil>=2.8,<2.9 # --------------------------------------------------- (bumped 2021-04-15) +pycountry>=22.3.5 # ----------------------------------------------------------- (bumped 2023-09-18) +python-dateutil>=2.8,<2.9 # -------------------------------------------------- (checked 2023-09-18) pyvat>=1.3,<1.4 # ------------------------------------------------------------ (checked 2021-04-15) # Crypto -cryptography>=3.3,<3.4 # ------------------------------------------------------ (bumped 2021-04-15) -PyJWT>=2.0,<2.1 # ------------------------------------------------------------- (bumped 2021-04-15) +cryptography>=41.0.3,<42.0 # -------------------------------------------------- (bumped 2023-09-18) +PyJWT>=2.8,<2.9 # ------------------------------------------------------------- (bumped 2023-09-18) # Other furl>=1.2,<1.3 # (URL parsing and manipulation) NOT_LATEST -------------------- (bumped 2018-08-10) diff --git a/requirements/optionals.txt b/requirements/optionals.txt index 2db3205f..4c3168b5 100644 --- a/requirements/optionals.txt +++ b/requirements/optionals.txt @@ -1,3 +1,3 @@ -celery>=4.0,<5.1 # ------------------------------------------------------------ (bumped 2021-04-15) -redis>=2.10,<2.11 # ----------------------------------------------------------- (bumped 2018-06-07) +celery>=5.3.4,<5.4 # ---------------------------------------------------------- (bumped 2023-09-18) +redis>=5,<5.1 # --------------------------------------------------------------- (bumped 2023-09-18) celery-once>=1.2,<3.1 # ------------------------------------------------------- (bumped 2021-04-15) diff --git a/silver/api/urls.py b/silver/api/urls.py index afa7db2c..4ad90c9f 100644 --- a/silver/api/urls.py +++ b/silver/api/urls.py @@ -14,7 +14,7 @@ from __future__ import absolute_import -from django.conf.urls import re_path +from django.urls import re_path from silver import views as silver_views from silver.api.views import billing_entities_views, documents_views, payment_method_views, \ diff --git a/silver/models/documents/invoice.py b/silver/models/documents/invoice.py index 8764f24d..51329c63 100644 --- a/silver/models/documents/invoice.py +++ b/silver/models/documents/invoice.py @@ -50,6 +50,9 @@ def __init__(self, *args, **kwargs): @property def transactions(self): + if not self.pk: + return self.invoice_transactions.model.objects.none() + return self.invoice_transactions.all() @locking_atomic_transition(field='state', source=BillingDocumentBase.STATES.DRAFT, diff --git a/silver/models/documents/proforma.py b/silver/models/documents/proforma.py index 3a57ac8c..711e67d1 100644 --- a/silver/models/documents/proforma.py +++ b/silver/models/documents/proforma.py @@ -52,6 +52,9 @@ def __init__(self, *args, **kwargs): @property def transactions(self): + if not self.pk: + return self.proforma_transactions.model.objects.none() + return self.proforma_transactions.all() def clean(self): diff --git a/silver/urls.py b/silver/urls.py index 7b6073e7..4b2be622 100644 --- a/silver/urls.py +++ b/silver/urls.py @@ -17,8 +17,9 @@ from __future__ import absolute_import -from django.conf.urls import include, re_path +from django.conf.urls import include from django.contrib import admin +from django.urls import re_path from silver.views import (pay_transaction_view, complete_payment_view, InvoiceAutocomplete, ProformaAutocomplete, From 0f260f2a66a1f6cb64a857adbed7082a81d818c8 Mon Sep 17 00:00:00 2001 From: Bogdan Petrea Date: Mon, 18 Dec 2023 13:23:31 +0200 Subject: [PATCH 2/4] Also automatically clean DocumentEntries before saving --- silver/models/documents/entries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/silver/models/documents/entries.py b/silver/models/documents/entries.py index 9965067b..e9f69e36 100644 --- a/silver/models/documents/entries.py +++ b/silver/models/documents/entries.py @@ -24,9 +24,10 @@ from django.db import models from silver.utils.decorators import require_transaction_currency_and_xe_rate +from silver.utils.models import AutoCleanModelMixin -class DocumentEntry(models.Model): +class DocumentEntry(AutoCleanModelMixin, models.Model): description = models.TextField() unit = models.CharField(max_length=1024, blank=True, null=True) quantity = models.DecimalField(max_digits=19, decimal_places=4, From 0eca5dbee3dcfd91a4d998c19c3c0729065494e7 Mon Sep 17 00:00:00 2001 From: Bogdan Petrea Date: Mon, 18 Dec 2023 13:29:34 +0200 Subject: [PATCH 3/4] Fix force_generate param and handle subs canceled before start_date --- silver/documents_generator.py | 43 +++++++++++++++++++-- silver/management/commands/generate_docs.py | 15 ++++--- silver/tests/commands/test_generate_docs.py | 41 ++++++++++++++++++++ 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/silver/documents_generator.py b/silver/documents_generator.py index 982e9ac0..5c525457 100644 --- a/silver/documents_generator.py +++ b/silver/documents_generator.py @@ -56,7 +56,7 @@ def generate(self, subscription=None, billing_date=None, customers=None, :param customers: the customers for which one wants to generate the proformas/invoices. :param force_generate: if True, invoices are generated at the date - indicated by `billing_date` instead of the normal end of billing + indicated by `billing_date` instead of after the normal end of billing cycle. :note @@ -114,7 +114,16 @@ def get_subscriptions_prepared_for_billing(self, customer, billing_date, force_g criteria = {'state__in': [Subscription.STATES.ACTIVE, Subscription.STATES.CANCELED]} for subscription in customer.subscriptions.filter(**criteria): - if subscription.should_be_billed(billing_date) or force_generate: + to_bill = subscription.should_be_billed(billing_date) or force_generate + + if not to_bill and subscription.cancel_date: + billing_up_to_dates = subscription.billed_up_to_dates + to_bill = ( + subscription.cancel_date < billing_up_to_dates["metered_features_billed_up_to"] and + subscription.cancel_date < billing_up_to_dates["plan_billed_up_to"] + ) + + if to_bill: subs_to_bill.append(subscription) return subs_to_bill @@ -135,7 +144,6 @@ def _bill_subscription_into_document(self, subscription, billing_date, document= }) billing_log, entries_info = self.add_subscription_cycles_to_document(**kwargs) - if subscription.state == Subscription.STATES.CANCELED: subscription.end() subscription.save() @@ -382,6 +390,12 @@ def _generate_for_user_with_consolidated_billing(self, customer, billing_date, f self._create_discount_entries(**kwargs) + # TODO: Creating and then deleting the document in the DB is not ideal and this whole logic + # should be refactored. + if not document.entries.exists(): + document.delete() + continue + if provider.default_document_state == Provider.DEFAULT_DOC_STATE.ISSUED: document.issue() @@ -405,6 +419,12 @@ def _generate_for_user_without_consolidated_billing(self, customer, billing_date self._create_discount_entries(**kwargs) + # TODO: Creating and then deleting the document in the DB is not ideal and this whole logic + # should be refactored. + if not document.entries.exists(): + document.delete() + continue + if provider.default_document_state == Provider.DEFAULT_DOC_STATE.ISSUED: document.issue() @@ -419,7 +439,16 @@ def _generate_for_single_subscription(self, subscription=None, billing_date=None provider = subscription.provider - if not subscription.should_be_billed(billing_date) or force_generate: + to_bill = subscription.should_be_billed(billing_date) or force_generate + + if not to_bill and subscription.cancel_date: + billing_up_to_dates = subscription.billed_up_to_dates + to_bill = ( + subscription.cancel_date < billing_up_to_dates["metered_features_billed_up_to"] and + subscription.cancel_date < billing_up_to_dates["plan_billed_up_to"] + ) + + if not to_bill: return document, discount_amounts = self._bill_subscription_into_document(subscription, billing_date) @@ -427,6 +456,12 @@ def _generate_for_single_subscription(self, subscription=None, billing_date=None kwargs = {'entries_info': discount_amounts, provider.flow: document} + # TODO: Creating and then deleting the document in the DB is not ideal and this whole logic + # should be refactored. + if not document.entries.exists(): + document.delete() + return + self._create_discount_entries(**kwargs) if provider.default_document_state == Provider.DEFAULT_DOC_STATE.ISSUED: diff --git a/silver/management/commands/generate_docs.py b/silver/management/commands/generate_docs.py index 78efd3cb..9d014c35 100644 --- a/silver/management/commands/generate_docs.py +++ b/silver/management/commands/generate_docs.py @@ -48,30 +48,35 @@ def add_arguments(self, parser): parser.add_argument('--date', action='store', dest='billing_date', type=date, help='The billing date (format YYYY-MM-DD).') + parser.add_argument('--force', + action='store', dest='force_generate', type=bool, + help='Bill subscriptions even in situations when they would be skipped.') def handle(self, *args, **options): translation.activate('en-us') billing_date = options['billing_date'] + force_generate = options.get('force_generate', False) docs_generator = DocumentsGenerator() if options['subscription_id']: try: subscription_id = options['subscription_id'] logger.info('Generating for subscription with id=%s; ' - 'billing_date=%s.', subscription_id, - billing_date) + 'billing_date=%s; force_generate=%s.', subscription_id, + billing_date, force_generate) subscription = Subscription.objects.get(id=subscription_id) docs_generator.generate(subscription=subscription, - billing_date=billing_date) + billing_date=billing_date, + force_generate=force_generate) self.stdout.write('Done. You can have a Club-Mate now. :)') except Subscription.DoesNotExist: msg = 'The subscription with the provided id does not exist.' self.stdout.write(msg) else: logger.info('Generating for all the available subscriptions; ' - 'billing_date=%s.', billing_date) + 'billing_date=%s; force_generate=%s.', billing_date, force_generate) - docs_generator.generate(billing_date=billing_date) + docs_generator.generate(billing_date=billing_date, force_generate=force_generate) self.stdout.write('Done. You can have a Club-Mate now. :)') diff --git a/silver/tests/commands/test_generate_docs.py b/silver/tests/commands/test_generate_docs.py index ac7d7701..9c7e977d 100644 --- a/silver/tests/commands/test_generate_docs.py +++ b/silver/tests/commands/test_generate_docs.py @@ -1726,6 +1726,47 @@ def test_gen_active_and_canceled_selection(self): assert Subscription.objects.filter(state='ended').count() == 3 + def test_subscription_with_cancel_date_before_start_date(self): + """ + Should not produce anything, just end the subscription. + """ + + subscription = SubscriptionFactory.create() + subscription.activate() + subscription.cancel(when=subscription.start_date - dt.timedelta(days=2)) + subscription.save() + + assert subscription.state == "canceled" + + call_command('generate_docs', + billing_date=subscription.start_date + dt.timedelta(days=9999), + stdout=self.output) + assert Invoice.objects.all().count() == Proforma.objects.all().count() == 0 + + subscription.refresh_from_db() + assert subscription.state == "ended" + + def test_subscription_with_cancel_date_before_start_date_and_with_specified_subscription_id(self): + """ + Should not produce anything, just end the subscription. + """ + + subscription = SubscriptionFactory.create() + subscription.activate() + subscription.cancel(when=subscription.start_date - dt.timedelta(days=2)) + subscription.save() + + assert subscription.state == "canceled" + + call_command('generate_docs', + billing_date=subscription.start_date + dt.timedelta(days=9999), + subscription=str(subscription.id), + stdout=self.output) + assert Invoice.objects.all().count() == Proforma.objects.all().count() == 0 + + subscription.refresh_from_db() + assert subscription.state == "ended" + def test_subscription_with_separate_cycles_during_trial(self): separate_cycles_during_trial = True prebill_plan = False From 90fb1747d524bce2d0e1e863ed3c4e95d4ff98f8 Mon Sep 17 00:00:00 2001 From: Bogdan Petrea Date: Mon, 18 Dec 2023 13:39:39 +0200 Subject: [PATCH 4/4] More dependencies updating in various places --- .drone.yml | 30 +++++++++++++++--------------- Dockerfile | 2 +- requirements/common.txt | 2 +- requirements/test.txt | 2 +- setup.py | 7 ++++--- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/.drone.yml b/.drone.yml index 4779d5e5..df0963af 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,7 +1,7 @@ --- { "kind": "pipeline", - "name": "python:3.7,django>=3.1,<3.2", + "name": "python:3.10,django>=4.2,<4.3", "platform": { "arch": "amd64", "os": "linux" @@ -19,7 +19,7 @@ "MYSQL_ROOT_PASSWORD": "secret", "MYSQL_USER": "silver" }, - "image": "mysql:5.7", + "image": "mysql:8.0", "name": "mysql", "pull": "if-not-exists" }, @@ -33,7 +33,7 @@ { "commands": [ "make dependencies", - "pip install -U \"Django>=3.1,<3.2\"", + "pip install -U \"django>=4.2,<4.3\"", "mkdir /var/log/silver", "make lint", "make test" @@ -41,7 +41,7 @@ "environment": { "SILVER_DB_URL": "mysql://silver:silver@mysql/db" }, - "image": "python:3.7", + "image": "python:3.10", "name": "test", "pull": "always", "settings": { @@ -105,7 +105,7 @@ --- { "kind": "pipeline", - "name": "python:3.8,django>=3.1,<3.2", + "name": "python:3.11,django>=4.2,<4.3", "platform": { "arch": "amd64", "os": "linux" @@ -123,7 +123,7 @@ "MYSQL_ROOT_PASSWORD": "secret", "MYSQL_USER": "silver" }, - "image": "mysql:5.7", + "image": "mysql:8.0", "name": "mysql", "pull": "if-not-exists" }, @@ -137,7 +137,7 @@ { "commands": [ "make dependencies", - "pip install -U \"Django>=3.1,<3.2\"", + "pip install -U \"django>=4.2,<4.3\"", "mkdir /var/log/silver", "make lint", "make test" @@ -145,7 +145,7 @@ "environment": { "SILVER_DB_URL": "mysql://silver:silver@mysql/db" }, - "image": "python:3.8", + "image": "python:3.11", "name": "test", "pull": "always", "settings": { @@ -209,7 +209,7 @@ --- { "kind": "pipeline", - "name": "python:3.7,django>=3.2,<3.3", + "name": "python:3.10,django>=3.2,<3.3", "platform": { "arch": "amd64", "os": "linux" @@ -227,7 +227,7 @@ "MYSQL_ROOT_PASSWORD": "secret", "MYSQL_USER": "silver" }, - "image": "mysql:5.7", + "image": "mysql:8.0", "name": "mysql", "pull": "if-not-exists" }, @@ -249,7 +249,7 @@ "environment": { "SILVER_DB_URL": "mysql://silver:silver@mysql/db" }, - "image": "python:3.7", + "image": "python:3.10", "name": "test", "pull": "always", "settings": { @@ -313,7 +313,7 @@ --- { "kind": "pipeline", - "name": "python:3.8,django>=3.2,<3.3", + "name": "python:3.11,django>=3.2,<3.3", "platform": { "arch": "amd64", "os": "linux" @@ -331,7 +331,7 @@ "MYSQL_ROOT_PASSWORD": "secret", "MYSQL_USER": "silver" }, - "image": "mysql:5.7", + "image": "mysql:8.0", "name": "mysql", "pull": "if-not-exists" }, @@ -353,7 +353,7 @@ "environment": { "SILVER_DB_URL": "mysql://silver:silver@mysql/db" }, - "image": "python:3.8", + "image": "python:3.11", "name": "test", "pull": "always", "settings": { @@ -416,6 +416,6 @@ } --- kind: signature -hmac: beee66896031cb6802633da0a02d083cff16b93f6787729fd5d58b5710e543e0 +hmac: 1231c01a60b201851df1d8bb5856897afcbde610438a08a84aa6321a58e7452a ... diff --git a/Dockerfile b/Dockerfile index e902fcce..f383aad9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-alpine +FROM python:3.11-alpine MAINTAINER Presslabs ping@presslabs.com # Ensure that Python outputs everything that's printed inside diff --git a/requirements/common.txt b/requirements/common.txt index 5331ba83..dcc013aa 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -9,7 +9,7 @@ sqlparse>=0.2,<0.5 # (required for some old migrations) ----------------------- # Django Utils django-fsm>=2.7,<2.8 # -------------------------------------------------------- (bumped 2021-04-15) django-filter>=2.4,<2.5 # ----------------------------------------------------- (bumped 2021-04-15) -django-livefield>=3.3,<3.4 # -------------------------------------------------- (bumped 2021-04-15) +django-livefield>=4.0,<4.1 # -------------------------------------------------- (bumped 2023-12-18) django-model-utils>=4.3.1,<4.4 # ---------------------------------------------- (bumped 2023-09-18) django-annoying>=0.10,<0.11 # (various Django helpers) ----------------------- (checked 2021-04-15) django-autocomplete-light>=3.9.7,<3.10 # -------------------------------------- (bumped 2023-09-18) diff --git a/requirements/test.txt b/requirements/test.txt index c8d9ff2c..4c2a5723 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -12,4 +12,4 @@ factory-boy==3.2.0 pep8==1.7.1 faker==8.2.1 django-environ==0.7 -PyMySQL==1.0.2 +PyMySQL==1.1.0 diff --git a/setup.py b/setup.py index 43cfba90..28dbeb9c 100644 --- a/setup.py +++ b/setup.py @@ -80,11 +80,12 @@ def read(fname): entry_points={"pytest11": ["django-silver = silver.fixtures.pytest_fixtures"]}, classifiers=[ 'Environment :: Web Environment', - 'Framework :: Django :: 3.1', 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.1', + 'Framework :: Django :: 4.2', 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8' + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11' ] )