diff --git a/README.md b/README.md index 99708c1..00aa1e6 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,17 @@ Easy setup: existing **Subscriptions** get copied from ERPNext (you have to canc 3. Set a _Start Date_ 4. Select if billing is based on _calendar months_ or _Start Date_ 5. Select if billing is supposed to happen _at the beginning of period_ or _after end of period_ -4. Select a _Frequency_ -5. Add items to the table -6. Select a _Sales Taxes and Charges Template_ -7. Click "Save" +6. Select a _Frequency_ +7. Add items to the table +8. Select a _Sales Taxes and Charges Template_ +9. Click "Save" ### Actions - Submit a **Simple Subscription** to start generating invoices. - New invoices for the current period will automatically be generated once per day if they don't yet exist - - If you need an invoice immediately, click the button "Create current {Monthly / Quarterly / Yearly} invoice". + - If you need an invoice immediately, click the button "Create current {frequency} invoice". - Mark a **Simple Subscription** as _disabled_ to stop generating invoices. - Duplicate a disabled **Simple Subscription** to change any values. diff --git a/simple_subscription/locale/de.po b/simple_subscription/locale/de.po index 046c230..5872677 100644 --- a/simple_subscription/locale/de.po +++ b/simple_subscription/locale/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Simple Subscription VERSION\n" "Report-Msgid-Bugs-To: hallo@alyf.de\n" -"POT-Creation-Date: 2024-12-02 17:26+0053\n" +"POT-Creation-Date: 2026-06-16 11:37+0053\n" "PO-Revision-Date: 2024-12-02 17:26+0053\n" "Last-Translator: hallo@alyf.de\n" "Language: de\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.13.1\n" +"Generated-By: Babel 2.16.0\n" #. Name of a role #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json @@ -28,65 +28,72 @@ msgstr "" msgid "Accounts User" msgstr "" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the amended_from (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Amended From" msgstr "" -#. Label of a Select field in DocType 'Simple Subscription' +#. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json +msgid "Biennial" +msgstr "Zweijährlich" + +#. Label of the period_type (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Billing Period is based on" msgstr "Rechnungsperiode basiert auf" -#. Label of a Select field in DocType 'Simple Subscription' +#. Label of the billing_time (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Billing Time" msgstr "Rechnungszeit" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the company (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Company" msgstr "Unternehmen" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js:32 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js:28 msgid "Create current {0} invoice" msgstr "Aktuelle {0} Rechnung erstellen" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the currency (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Currency" msgstr "" -#. Label of a Text Editor field in DocType 'Simple Subscription Item' +#. Label of the current_description (Text Editor) field in DocType 'Simple +#. Subscription Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Current Description" msgstr "Aktuelle Beschreibung" -#. Label of a Currency field in DocType 'Simple Subscription Item' +#. Label of the current_rate (Currency) field in DocType 'Simple Subscription +#. Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Current Rate" msgstr "Aktueller Einzelpreis" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the customer (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Customer" msgstr "" -#. Label of a Data field in DocType 'Simple Subscription' +#. Label of the customer_name (Data) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Customer Name" msgstr "" -#. Label of a Check field in DocType 'Simple Subscription' +#. Label of the disabled (Check) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Disabled" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:105 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:135 msgid "Failed to create subscription invoice" msgstr "Fehler beim Erstellen der Abo-Rechnung" -#. Label of a Select field in DocType 'Simple Subscription' +#. Label of the frequency (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Frequency" msgstr "Frequenz" @@ -102,17 +109,17 @@ msgstr "Halbjährlich" msgid "Invoices will be generated for periods starting on or after this date." msgstr "Rechnungen werden für Perioden erstellt, die an oder nach diesem Datum beginnen." -#. Label of a Link field in DocType 'Simple Subscription Item' +#. Label of the item (Link) field in DocType 'Simple Subscription Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Item" msgstr "" -#. Label of a Data field in DocType 'Simple Subscription Item' +#. Label of the item_name (Data) field in DocType 'Simple Subscription Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Item Name" msgstr "" -#. Label of a Table field in DocType 'Simple Subscription' +#. Label of the items (Table) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Items" msgstr "" @@ -122,19 +129,19 @@ msgstr "" msgid "Monthly" msgstr "Monatlich" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:42 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:57 msgid "Please amend Subscription {0} before creating a Sales Invoice." msgstr "Bitte ändern Sie das Abo {0} bevor Sie eine Rechnung erstellen." -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:36 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:51 msgid "Please enable Subscription {0} before creating a Sales Invoice." msgstr "Bitte aktivieren Sie das Abo {0} bevor Sie eine Rechnung erstellen." -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:39 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:54 msgid "Please submit Subscription {0} before creating a Sales Invoice." msgstr "Bitte buchen Sie das Abo {0} bevor Sie eine Rechnung erstellen." -#. Label of a Float field in DocType 'Simple Subscription Item' +#. Label of the qty (Float) field in DocType 'Simple Subscription Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Qty" msgstr "" @@ -144,12 +151,7 @@ msgstr "" msgid "Quarterly" msgstr "Vierteljährlich" -#. Linked DocType in Simple Subscription's connections -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json -msgid "Sales Invoice" -msgstr "" - -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:91 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:124 msgid "Sales Invoice already exists for this period: {}" msgstr "Rechnung für diese Periode existiert bereits: {}" @@ -161,7 +163,7 @@ msgstr "" #. Name of a DocType #. Label of a Link in the Simple Subscription Selling Workspace #. Label of a shortcut in the Simple Subscription Selling Workspace -#: simple_subscription/config/desktop.py:10 +#: simple_subscription/config/desktop.py:11 #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json #: simple_subscription/simple_subscription/workspace/simple_subscription_selling/simple_subscription_selling.json msgid "Simple Subscription" @@ -177,21 +179,31 @@ msgstr "Einfacher Abo-Artikel" msgid "Simple Subscription Selling" msgstr "Vertrieb" -#. Label of a Date field in DocType 'Simple Subscription' +#. Label of the start_date (Date) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Start Date" -msgstr "" +msgstr "Startdatum" + +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:209 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:221 +msgid "Start Date is required for frequency {0}." +msgstr "Startdatum ist für die Frequenz {0} erforderlich." #. Name of a role #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "System Manager" msgstr "" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the taxes_and_charges (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Taxes and Charges" msgstr "" +#. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json +msgid "Triennial" +msgstr "Dreijährlich" + #. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Yearly" @@ -220,3 +232,4 @@ msgstr "Kalendermonate" #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "start date" msgstr "Startdatum" + diff --git a/simple_subscription/locale/main.pot b/simple_subscription/locale/main.pot index 040419f..a8c2ac1 100644 --- a/simple_subscription/locale/main.pot +++ b/simple_subscription/locale/main.pot @@ -1,20 +1,20 @@ # Translations template for Simple Subscription. -# Copyright (C) 2024 ALYF GmbH +# Copyright (C) 2026 ALYF GmbH # This file is distributed under the same license as the Simple Subscription project. -# FIRST AUTHOR , 2024. +# FIRST AUTHOR , 2026. # msgid "" msgstr "" "Project-Id-Version: Simple Subscription VERSION\n" "Report-Msgid-Bugs-To: hallo@alyf.de\n" -"POT-Creation-Date: 2024-12-02 17:26+0053\n" -"PO-Revision-Date: 2024-12-02 17:26+0053\n" +"POT-Creation-Date: 2026-06-16 11:37+0053\n" +"PO-Revision-Date: 2026-06-16 11:37+0053\n" "Last-Translator: hallo@alyf.de\n" "Language-Team: hallo@alyf.de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.13.1\n" +"Generated-By: Babel 2.16.0\n" #. Name of a role #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json @@ -26,65 +26,72 @@ msgstr "" msgid "Accounts User" msgstr "" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the amended_from (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Amended From" msgstr "" -#. Label of a Select field in DocType 'Simple Subscription' +#. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json +msgid "Biennial" +msgstr "" + +#. Label of the period_type (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Billing Period is based on" msgstr "" -#. Label of a Select field in DocType 'Simple Subscription' +#. Label of the billing_time (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Billing Time" msgstr "" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the company (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Company" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js:32 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js:28 msgid "Create current {0} invoice" msgstr "" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the currency (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Currency" msgstr "" -#. Label of a Text Editor field in DocType 'Simple Subscription Item' +#. Label of the current_description (Text Editor) field in DocType 'Simple +#. Subscription Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Current Description" msgstr "" -#. Label of a Currency field in DocType 'Simple Subscription Item' +#. Label of the current_rate (Currency) field in DocType 'Simple Subscription +#. Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Current Rate" msgstr "" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the customer (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Customer" msgstr "" -#. Label of a Data field in DocType 'Simple Subscription' +#. Label of the customer_name (Data) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Customer Name" msgstr "" -#. Label of a Check field in DocType 'Simple Subscription' +#. Label of the disabled (Check) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Disabled" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:105 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:135 msgid "Failed to create subscription invoice" msgstr "" -#. Label of a Select field in DocType 'Simple Subscription' +#. Label of the frequency (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Frequency" msgstr "" @@ -100,17 +107,17 @@ msgstr "" msgid "Invoices will be generated for periods starting on or after this date." msgstr "" -#. Label of a Link field in DocType 'Simple Subscription Item' +#. Label of the item (Link) field in DocType 'Simple Subscription Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Item" msgstr "" -#. Label of a Data field in DocType 'Simple Subscription Item' +#. Label of the item_name (Data) field in DocType 'Simple Subscription Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Item Name" msgstr "" -#. Label of a Table field in DocType 'Simple Subscription' +#. Label of the items (Table) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Items" msgstr "" @@ -120,19 +127,19 @@ msgstr "" msgid "Monthly" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:42 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:57 msgid "Please amend Subscription {0} before creating a Sales Invoice." msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:36 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:51 msgid "Please enable Subscription {0} before creating a Sales Invoice." msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:39 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:54 msgid "Please submit Subscription {0} before creating a Sales Invoice." msgstr "" -#. Label of a Float field in DocType 'Simple Subscription Item' +#. Label of the qty (Float) field in DocType 'Simple Subscription Item' #: simple_subscription/simple_subscription/doctype/simple_subscription_item/simple_subscription_item.json msgid "Qty" msgstr "" @@ -142,12 +149,7 @@ msgstr "" msgid "Quarterly" msgstr "" -#. Linked DocType in Simple Subscription's connections -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json -msgid "Sales Invoice" -msgstr "" - -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:91 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:124 msgid "Sales Invoice already exists for this period: {}" msgstr "" @@ -159,7 +161,7 @@ msgstr "" #. Name of a DocType #. Label of a Link in the Simple Subscription Selling Workspace #. Label of a shortcut in the Simple Subscription Selling Workspace -#: simple_subscription/config/desktop.py:10 +#: simple_subscription/config/desktop.py:11 #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json #: simple_subscription/simple_subscription/workspace/simple_subscription_selling/simple_subscription_selling.json msgid "Simple Subscription" @@ -175,21 +177,31 @@ msgstr "" msgid "Simple Subscription Selling" msgstr "" -#. Label of a Date field in DocType 'Simple Subscription' +#. Label of the start_date (Date) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Start Date" msgstr "" +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:209 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:221 +msgid "Start Date is required for frequency {0}." +msgstr "" + #. Name of a role #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "System Manager" msgstr "" -#. Label of a Link field in DocType 'Simple Subscription' +#. Label of the taxes_and_charges (Link) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Taxes and Charges" msgstr "" +#. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json +msgid "Triennial" +msgstr "" + #. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Yearly" diff --git a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json index 7fb27ab..4c0d406 100644 --- a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json +++ b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "autoname": "ABO-.#####", "creation": "2022-02-04 16:13:31.955134", "doctype": "DocType", @@ -68,7 +69,7 @@ "fieldname": "frequency", "fieldtype": "Select", "label": "Frequency", - "options": "Monthly\nQuarterly\nHalfyearly\nYearly" + "options": "Monthly\nQuarterly\nHalfyearly\nYearly\nBiennial\nTriennial" }, { "fieldname": "section_break_5", @@ -133,11 +134,11 @@ "link_fieldname": "simple_subscription" } ], - "modified": "2024-09-12 12:54:00.717315", + "modified": "2026-06-15 11:53:49.870013", "modified_by": "Administrator", "module": "Simple Subscription", "name": "Simple Subscription", - "naming_rule": "Expression (old style)", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -185,8 +186,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py index 93a2a30..8571c7f 100644 --- a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py +++ b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py @@ -2,7 +2,6 @@ # For license information, please see license.txt from datetime import date, timedelta from enum import Enum -from typing import Union import frappe from dateutil.relativedelta import relativedelta @@ -18,6 +17,18 @@ class Frequency(Enum): Quarterly = 3 Halfyearly = 6 Yearly = 12 + Biennial = 24 + Triennial = 36 + + +MULTI_YEAR_FREQUENCIES = frozenset({Frequency.Biennial, Frequency.Triennial}) + +INVOICE_MONTH_MAP: dict[Frequency, list[int]] = { + Frequency.Monthly: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + Frequency.Quarterly: [1, 1, 1, 4, 4, 4, 7, 7, 7, 10, 10, 10], + Frequency.Halfyearly: [1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 7, 7], + Frequency.Yearly: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], +} class PeriodType(Enum): @@ -31,6 +42,9 @@ class BillingTime(Enum): class SimpleSubscription(Document): + def validate(self): + validate_calendar_frequencies(self.period_type, self.frequency, self.start_date) + def create_invoice(self, from_date: date, to_date: date) -> SalesInvoice: msg = None if self.disabled: @@ -90,13 +104,18 @@ def get_price_list(self) -> str | None: @frappe.whitelist() def create_current_invoice(subscription_name: str, silent=False): subscription = frappe.get_doc("Simple Subscription", subscription_name) - from_date, to_date = get_from_and_to_date( - frequency=Frequency[subscription.frequency], - eval_date=date.today(), - period_type=PeriodType(subscription.period_type), - billing_time=BillingTime(subscription.billing_time), - start_date=subscription.start_date, - ) + try: + from_date, to_date = get_from_and_to_date( + frequency=Frequency[subscription.frequency], + eval_date=date.today(), + period_type=PeriodType(subscription.period_type), + billing_time=BillingTime(subscription.billing_time), + start_date=subscription.start_date, + ) + except ValueError as e: + if not silent: + frappe.throw(str(e)) + return # check that start_date is not in the future if subscription.start_date and subscription.start_date > from_date: @@ -158,7 +177,8 @@ def get_from_and_to_date( :param eval_date: Date to evaluate the period for :param period_type: Type of period to evaluate, defaults to CalendarMonths :param billing_time: Time to bill the subscription, defaults to AfterEndOfPeriod - :param start_date: Start date of the subscription, required only for PeriodType.StartDate + :param start_date: Start date of the subscription, required for PeriodType.StartDate + and for multi-year frequencies with calendar months """ if period_type == PeriodType.StartDate and not start_date: raise ValueError("start_date is required for period_type 'start date'") @@ -169,6 +189,8 @@ def get_from_and_to_date( if not billing_time: billing_time = BillingTime.AfterEndOfPeriod + start_year = start_date.year if start_date else None + if period_type == PeriodType.StartDate and billing_time == BillingTime.AtBeginningOfPeriod: return get_date_period(eval_date, frequency, start_date) elif period_type == PeriodType.StartDate and billing_time == BillingTime.AfterEndOfPeriod: @@ -179,50 +201,53 @@ def get_from_and_to_date( start_date, ) elif period_type == PeriodType.CalendarMonths and billing_time == BillingTime.AtBeginningOfPeriod: - return get_calendar_period(eval_date, frequency) + return get_calendar_period(eval_date, frequency, start_year) elif period_type == PeriodType.CalendarMonths and billing_time == BillingTime.AfterEndOfPeriod: - current_period_start, _ = get_calendar_period(eval_date, frequency) - return get_calendar_period(current_period_start - timedelta(days=1), frequency) - - -def get_calendar_period(eval_date: date, frequency: Frequency) -> tuple[date, date]: - """Return the first day and last day of the period containing `from_date`.""" - invoice_month_map = { - Frequency.Monthly: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - Frequency.Quarterly: [1, 1, 1, 4, 4, 4, 7, 7, 7, 10, 10, 10], - Frequency.Halfyearly: [1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 7, 7], - Frequency.Yearly: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - } - no_of_month_map = { - Frequency.Monthly: 1, - Frequency.Quarterly: 3, - Frequency.Halfyearly: 6, - Frequency.Yearly: 12, - } - - from_date = eval_date.replace(day=1, month=invoice_month_map[frequency][eval_date.month - 1]) - to_date = from_date + relativedelta(months=no_of_month_map[frequency]) - relativedelta(days=1) + current_period_start, _ = get_calendar_period(eval_date, frequency, start_year) + return get_calendar_period(current_period_start - timedelta(days=1), frequency, start_year) + + +def validate_calendar_frequencies(period_type: str, frequency: str, start_date: date | None) -> None: + if period_type != PeriodType.CalendarMonths.value or not frequency: + return + + if Frequency[frequency] in MULTI_YEAR_FREQUENCIES and not start_date: + frappe.throw( + _("Start Date is required for frequency {0}.").format( + _(frequency, context="Frequency of Subscription") + ) + ) + + +def get_calendar_period( + eval_date: date, frequency: Frequency, start_year: int | None = None +) -> tuple[date, date]: + """Return the first and last day of the calendar period containing `eval_date`.""" + if frequency in MULTI_YEAR_FREQUENCIES: + if not start_year: + raise ValueError("start_year is required for multi-year frequencies") + years_span = frequency.value // 12 + block_start_year = start_year + ((eval_date.year - start_year) // years_span) * years_span + from_date = date(block_start_year, 1, 1) + else: + from_date = eval_date.replace(day=1, month=INVOICE_MONTH_MAP[frequency][eval_date.month - 1]) + to_date = from_date + relativedelta(months=frequency.value) - relativedelta(days=1) return from_date, to_date def get_date_period(eval_date: date, frequency: Frequency, initial_date: date) -> tuple[date, date]: - no_of_month_map = { - Frequency.Monthly: 1, - Frequency.Quarterly: 3, - Frequency.Halfyearly: 6, - Frequency.Yearly: 12, - } + months = frequency.value delta = relativedelta(eval_date, initial_date) # determine no of period eval_date lies in when starting on initial_date if eval_date >= initial_date: - month_detla_floor = (delta.years * 12 + delta.months) // no_of_month_map[frequency] + month_detla_floor = (delta.years * 12 + delta.months) // months else: - month_detla_floor = (delta.years * 12 + delta.months - 1) // no_of_month_map[frequency] + month_detla_floor = (delta.years * 12 + delta.months - 1) // months - from_date = initial_date + relativedelta(months=(no_of_month_map[frequency] * month_detla_floor)) - to_date = from_date + relativedelta(months=no_of_month_map[frequency]) - relativedelta(days=1) + from_date = initial_date + relativedelta(months=(months * month_detla_floor)) + to_date = from_date + relativedelta(months=months) - relativedelta(days=1) return from_date, to_date diff --git a/simple_subscription/simple_subscription/doctype/simple_subscription/test_simple_subscription.py b/simple_subscription/simple_subscription/doctype/simple_subscription/test_simple_subscription.py index a6e44b9..efa1d60 100644 --- a/simple_subscription/simple_subscription/doctype/simple_subscription/test_simple_subscription.py +++ b/simple_subscription/simple_subscription/doctype/simple_subscription/test_simple_subscription.py @@ -1,10 +1,11 @@ # Copyright (c) 2022, ALYF GmbH and Contributors # See license.txt -# import frappe import unittest from datetime import date +import frappe + from .simple_subscription import ( BillingTime, Frequency, @@ -12,6 +13,7 @@ get_calendar_period, get_date_period, get_from_and_to_date, + validate_calendar_frequencies, ) @@ -35,6 +37,46 @@ def test_get_calendar_period(self): self.assertEqual(from_date, date(2022, 1, 1)) self.assertEqual(to_date, date(2022, 12, 31)) + start_year = date(2022, 6, 25).year + + from_date, to_date = get_calendar_period(date(2022, 11, 7), Frequency.Biennial, start_year) + self.assertEqual(from_date, date(2022, 1, 1)) + self.assertEqual(to_date, date(2023, 12, 31)) + + from_date, to_date = get_calendar_period(date(2023, 6, 15), Frequency.Biennial, start_year) + self.assertEqual(from_date, date(2022, 1, 1)) + self.assertEqual(to_date, date(2023, 12, 31)) + + from_date, to_date = get_calendar_period(date(2024, 3, 1), Frequency.Biennial, start_year) + self.assertEqual(from_date, date(2024, 1, 1)) + self.assertEqual(to_date, date(2025, 12, 31)) + + from_date, to_date = get_calendar_period(date(2024, 6, 15), Frequency.Triennial, start_year) + self.assertEqual(from_date, date(2022, 1, 1)) + self.assertEqual(to_date, date(2024, 12, 31)) + + from_date, to_date = get_calendar_period(date(2025, 1, 1), Frequency.Triennial, start_year) + self.assertEqual(from_date, date(2025, 1, 1)) + self.assertEqual(to_date, date(2027, 12, 31)) + + def test_get_calendar_period_multi_year_requires_start_year(self): + with self.assertRaises(ValueError): + get_calendar_period(date(2022, 11, 7), Frequency.Biennial) + + with self.assertRaises(ValueError): + get_calendar_period(date(2022, 11, 7), Frequency.Triennial) + + def test_validate_calendar_frequencies(self): + with self.assertRaises(frappe.ValidationError): + validate_calendar_frequencies("calendar months", "Biennial", None) + + with self.assertRaises(frappe.ValidationError): + validate_calendar_frequencies("calendar months", "Triennial", None) + + validate_calendar_frequencies("calendar months", "Biennial", date(2022, 6, 25)) + validate_calendar_frequencies("start date", "Biennial", None) + validate_calendar_frequencies("calendar months", "Yearly", None) + def test_get_date_period(self): eval_date = date(2022, 11, 7) initial_date = date(2022, 6, 25) @@ -55,6 +97,14 @@ def test_get_date_period(self): self.assertEqual(from_date, date(2022, 6, 25)) self.assertEqual(to_date, date(2023, 6, 24)) + from_date, to_date = get_date_period(eval_date, Frequency.Biennial, initial_date) + self.assertEqual(from_date, date(2022, 6, 25)) + self.assertEqual(to_date, date(2024, 6, 24)) + + from_date, to_date = get_date_period(eval_date, Frequency.Triennial, initial_date) + self.assertEqual(from_date, date(2022, 6, 25)) + self.assertEqual(to_date, date(2025, 6, 24)) + def test_get_from_and_to_date(self): from_date, to_date = get_from_and_to_date( frequency=Frequency.Monthly, @@ -106,3 +156,43 @@ def test_get_from_and_to_date(self): ) self.assertEqual(from_date, date(2022, 10, 5)) self.assertEqual(to_date, date(2022, 11, 4)) + + from_date, to_date = get_from_and_to_date( + frequency=Frequency.Biennial, + period_type=PeriodType.StartDate, + billing_time=BillingTime.AtBeginningOfPeriod, + eval_date=date(2024, 11, 7), + start_date=date(2022, 6, 25), + ) + self.assertEqual(from_date, date(2024, 6, 25)) + self.assertEqual(to_date, date(2026, 6, 24)) + + from_date, to_date = get_from_and_to_date( + frequency=Frequency.Triennial, + period_type=PeriodType.StartDate, + billing_time=BillingTime.AfterEndOfPeriod, + eval_date=date(2026, 1, 1), + start_date=date(2022, 6, 25), + ) + self.assertEqual(from_date, date(2022, 6, 25)) + self.assertEqual(to_date, date(2025, 6, 24)) + + from_date, to_date = get_from_and_to_date( + frequency=Frequency.Biennial, + period_type=PeriodType.CalendarMonths, + billing_time=BillingTime.AtBeginningOfPeriod, + eval_date=date(2023, 6, 15), + start_date=date(2022, 6, 25), + ) + self.assertEqual(from_date, date(2022, 1, 1)) + self.assertEqual(to_date, date(2023, 12, 31)) + + from_date, to_date = get_from_and_to_date( + frequency=Frequency.Biennial, + period_type=PeriodType.CalendarMonths, + billing_time=BillingTime.AfterEndOfPeriod, + eval_date=date(2023, 12, 31), + start_date=date(2022, 6, 25), + ) + self.assertEqual(from_date, date(2020, 1, 1)) + self.assertEqual(to_date, date(2021, 12, 31))