From 9e61dd33a9468b77b72276e76d8f9957ce48f10b Mon Sep 17 00:00:00 2001 From: HenningWendtland <156231187+HenningWendtland@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:46:37 +0200 Subject: [PATCH 1/5] feat: ad bieannial and triannial billing frequency --- README.md | 10 +-- simple_subscription/locale/de.po | 74 +++++++++-------- simple_subscription/locale/main.pot | 79 +++++++++++-------- .../simple_subscription.js | 25 ++++++ .../simple_subscription.json | 4 +- .../simple_subscription.py | 41 +++++++--- .../test_simple_subscription.py | 49 +++++++++++- 7 files changed, 198 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 99708c1..cd685aa 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_ (_Biennial_ and _Triennial_ require billing based on _Start Date_) +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..df5a709 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-15 11:45+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,69 +28,80 @@ 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:53 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:128 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" +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:196 +msgid "Frequency {0} is only supported when billing period is based on start date." +msgstr "Die Frequenz {0} ist nur verfügbar, wenn die Abrechnungsperiode auf dem Startdatum basiert." + #. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Halfyearly" @@ -102,17 +113,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 +133,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:50 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:44 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:47 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 +155,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:117 msgid "Sales Invoice already exists for this period: {}" msgstr "Rechnung für diese Periode existiert bereits: {}" @@ -161,7 +167,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,7 +183,7 @@ 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 "" @@ -187,11 +193,16 @@ msgstr "" 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 +231,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..d3eac1f 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-15 11:45+0053\n" +"PO-Revision-Date: 2026-06-15 11:45+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,69 +26,80 @@ 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:53 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:128 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 "" +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:196 +msgid "Frequency {0} is only supported when billing period is based on start date." +msgstr "" + #. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Halfyearly" @@ -100,17 +111,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 +131,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:50 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:44 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:47 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 +153,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:117 msgid "Sales Invoice already exists for this period: {}" msgstr "" @@ -159,7 +165,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,7 +181,7 @@ 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 "" @@ -185,11 +191,16 @@ msgstr "" 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.js b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js index 9f89807..e0db830 100644 --- a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js +++ b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js @@ -1,6 +1,25 @@ // Copyright (c) 2022, ALYF GmbH and contributors // For license information, please see license.txt +const ALL_FREQUENCIES = ["Monthly", "Quarterly", "Halfyearly", "Yearly", "Biennial", "Triennial"]; +const START_DATE_ONLY_FREQUENCIES = ["Biennial", "Triennial"]; + +function set_frequency_options(frm) { + const options = + frm.doc.period_type === "calendar months" + ? ALL_FREQUENCIES.filter((frequency) => !START_DATE_ONLY_FREQUENCIES.includes(frequency)) + : ALL_FREQUENCIES; + + if ( + frm.doc.period_type === "calendar months" && + START_DATE_ONLY_FREQUENCIES.includes(frm.doc.frequency) + ) { + frm.set_value("frequency", ""); + } + + frm.set_df_property("frequency", "options", options.join("\n")); +} + frappe.ui.form.on("Simple Subscription", { setup: function (frm) { frm.set_query("item", "items", function () { @@ -21,7 +40,13 @@ frappe.ui.form.on("Simple Subscription", { }); }, + period_type: function (frm) { + set_frequency_options(frm); + }, + refresh: function (frm) { + set_frequency_options(frm); + if (frm.doc.docstatus !== 1 || frm.doc.disabled === 1) return; const translated_frequency = __(frm.doc.frequency, null, "Frequency of Subscription"); 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..8421d19 100644 --- a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json +++ b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json @@ -68,7 +68,7 @@ "fieldname": "frequency", "fieldtype": "Select", "label": "Frequency", - "options": "Monthly\nQuarterly\nHalfyearly\nYearly" + "options": "Monthly\nQuarterly\nHalfyearly\nYearly\nBiennial\nTriennial" }, { "fieldname": "section_break_5", @@ -189,4 +189,4 @@ "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..0f1eb19 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,11 @@ class Frequency(Enum): Quarterly = 3 Halfyearly = 6 Yearly = 12 + Biennial = 24 + Triennial = 36 + + +START_DATE_ONLY_FREQUENCIES = frozenset({Frequency.Biennial, Frequency.Triennial}) class PeriodType(Enum): @@ -31,6 +35,9 @@ class BillingTime(Enum): class SimpleSubscription(Document): + def validate(self): + validate_start_date_only_frequencies(self.period_type, self.frequency) + def create_invoice(self, from_date: date, to_date: date) -> SalesInvoice: msg = None if self.disabled: @@ -185,8 +192,25 @@ def get_from_and_to_date( return get_calendar_period(current_period_start - timedelta(days=1), frequency) +def get_start_date_only_frequency_error_message(frequency: str) -> str: + return _( + "Frequency {0} is only supported when billing period is based on start date." + ).format(_(frequency, context="Frequency of Subscription")) + + +def validate_start_date_only_frequencies(period_type: str, frequency: str) -> None: + if period_type != PeriodType.CalendarMonths.value or not frequency: + return + + if Frequency[frequency] in START_DATE_ONLY_FREQUENCIES: + frappe.throw(get_start_date_only_frequency_error_message(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`.""" + if frequency in START_DATE_ONLY_FREQUENCIES: + frappe.throw(get_start_date_only_frequency_error_message(frequency.name)) + 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], @@ -207,22 +231,17 @@ def get_calendar_period(eval_date: date, frequency: Frequency) -> tuple[date, da 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..10ad20d 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_start_date_only_frequencies, ) @@ -55,6 +57,31 @@ 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_calendar_period_unsupported_frequencies(self): + with self.assertRaises(frappe.ValidationError): + get_calendar_period(date(2022, 11, 7), Frequency.Biennial) + + with self.assertRaises(frappe.ValidationError): + get_calendar_period(date(2022, 11, 7), Frequency.Triennial) + + def test_validate_start_date_only_frequencies(self): + with self.assertRaises(frappe.ValidationError): + validate_start_date_only_frequencies("calendar months", "Biennial") + + with self.assertRaises(frappe.ValidationError): + validate_start_date_only_frequencies("calendar months", "Triennial") + + validate_start_date_only_frequencies("start date", "Biennial") + validate_start_date_only_frequencies("calendar months", "Yearly") + def test_get_from_and_to_date(self): from_date, to_date = get_from_and_to_date( frequency=Frequency.Monthly, @@ -106,3 +133,23 @@ 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 f12ef6b94a0a311e10dc4aa858ba20147794ca0b Mon Sep 17 00:00:00 2001 From: HenningWendtland <156231187+HenningWendtland@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:59:24 +0200 Subject: [PATCH 2/5] chore: apply auto-created .json changes --- .../doctype/simple_subscription/simple_subscription.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 8421d19..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", @@ -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,6 +186,7 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], From f4c0026fd126c7260b238818070de5168d6c51c0 Mon Sep 17 00:00:00 2001 From: HenningWendtland <156231187+HenningWendtland@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:35:49 +0200 Subject: [PATCH 3/5] feat: implement biennial and trieannial for calendar period --- README.md | 2 +- .../simple_subscription.js | 25 ------ .../simple_subscription.py | 72 +++++++++-------- .../test_simple_subscription.py | 79 ++++++++++++++----- 4 files changed, 100 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index cd685aa..00aa1e6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ 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_ -6. Select a _Frequency_ (_Biennial_ and _Triennial_ require billing based on _Start Date_) +6. Select a _Frequency_ 7. Add items to the table 8. Select a _Sales Taxes and Charges Template_ 9. Click "Save" diff --git a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js index e0db830..9f89807 100644 --- a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js +++ b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js @@ -1,25 +1,6 @@ // Copyright (c) 2022, ALYF GmbH and contributors // For license information, please see license.txt -const ALL_FREQUENCIES = ["Monthly", "Quarterly", "Halfyearly", "Yearly", "Biennial", "Triennial"]; -const START_DATE_ONLY_FREQUENCIES = ["Biennial", "Triennial"]; - -function set_frequency_options(frm) { - const options = - frm.doc.period_type === "calendar months" - ? ALL_FREQUENCIES.filter((frequency) => !START_DATE_ONLY_FREQUENCIES.includes(frequency)) - : ALL_FREQUENCIES; - - if ( - frm.doc.period_type === "calendar months" && - START_DATE_ONLY_FREQUENCIES.includes(frm.doc.frequency) - ) { - frm.set_value("frequency", ""); - } - - frm.set_df_property("frequency", "options", options.join("\n")); -} - frappe.ui.form.on("Simple Subscription", { setup: function (frm) { frm.set_query("item", "items", function () { @@ -40,13 +21,7 @@ frappe.ui.form.on("Simple Subscription", { }); }, - period_type: function (frm) { - set_frequency_options(frm); - }, - refresh: function (frm) { - set_frequency_options(frm); - if (frm.doc.docstatus !== 1 || frm.doc.disabled === 1) return; const translated_frequency = __(frm.doc.frequency, null, "Frequency of Subscription"); 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 0f1eb19..db2fca8 100644 --- a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py +++ b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py @@ -21,7 +21,14 @@ class Frequency(Enum): Triennial = 36 -START_DATE_ONLY_FREQUENCIES = frozenset({Frequency.Biennial, Frequency.Triennial}) +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): @@ -36,7 +43,7 @@ class BillingTime(Enum): class SimpleSubscription(Document): def validate(self): - validate_start_date_only_frequencies(self.period_type, self.frequency) + validate_calendar_frequencies(self.period_type, self.frequency, self.start_date) def create_invoice(self, from_date: date, to_date: date) -> SalesInvoice: msg = None @@ -165,7 +172,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'") @@ -186,47 +194,43 @@ 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_date) 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_start_date_only_frequency_error_message(frequency: str) -> str: - return _( - "Frequency {0} is only supported when billing period is based on start date." - ).format(_(frequency, context="Frequency of Subscription")) + current_period_start, _ = get_calendar_period(eval_date, frequency, start_date) + return get_calendar_period(current_period_start - timedelta(days=1), frequency, start_date) -def validate_start_date_only_frequencies(period_type: str, frequency: str) -> None: +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 START_DATE_ONLY_FREQUENCIES: - frappe.throw(get_start_date_only_frequency_error_message(frequency)) - + 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) -> tuple[date, date]: - """Return the first day and last day of the period containing `from_date`.""" - if frequency in START_DATE_ONLY_FREQUENCIES: - frappe.throw(get_start_date_only_frequency_error_message(frequency.name)) - 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, - } +def get_calendar_period( + eval_date: date, frequency: Frequency, start_date: date | None = None +) -> tuple[date, date]: + """Return the first and last day of the calendar period containing `eval_date`.""" + if frequency in MULTI_YEAR_FREQUENCIES and not start_date: + frappe.throw( + _("Start Date is required for frequency {0}.").format( + _(frequency.name, context="Frequency of Subscription") + ) + ) - 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) + if frequency in MULTI_YEAR_FREQUENCIES: + years_span = frequency.value // 12 + block_start_year = start_date.year + ((eval_date.year - start_date.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 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 10ad20d..b48834e 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 @@ -13,7 +13,7 @@ get_calendar_period, get_date_period, get_from_and_to_date, - validate_start_date_only_frequencies, + validate_calendar_frequencies, ) @@ -37,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_date = date(2022, 6, 25) + + from_date, to_date = get_calendar_period(date(2022, 11, 7), Frequency.Biennial, start_date) + 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_date) + 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_date) + 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_date) + 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_date) + 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_date(self): + with self.assertRaises(frappe.ValidationError): + get_calendar_period(date(2022, 11, 7), Frequency.Biennial) + + with self.assertRaises(frappe.ValidationError): + 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) @@ -65,23 +105,6 @@ def test_get_date_period(self): self.assertEqual(from_date, date(2022, 6, 25)) self.assertEqual(to_date, date(2025, 6, 24)) - def test_get_calendar_period_unsupported_frequencies(self): - with self.assertRaises(frappe.ValidationError): - get_calendar_period(date(2022, 11, 7), Frequency.Biennial) - - with self.assertRaises(frappe.ValidationError): - get_calendar_period(date(2022, 11, 7), Frequency.Triennial) - - def test_validate_start_date_only_frequencies(self): - with self.assertRaises(frappe.ValidationError): - validate_start_date_only_frequencies("calendar months", "Biennial") - - with self.assertRaises(frappe.ValidationError): - validate_start_date_only_frequencies("calendar months", "Triennial") - - validate_start_date_only_frequencies("start date", "Biennial") - validate_start_date_only_frequencies("calendar months", "Yearly") - def test_get_from_and_to_date(self): from_date, to_date = get_from_and_to_date( frequency=Frequency.Monthly, @@ -153,3 +176,23 @@ def test_get_from_and_to_date(self): ) 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)) From 881975ce935e45f86ff17259429fb870d62d46f9 Mon Sep 17 00:00:00 2001 From: HenningWendtland <156231187+HenningWendtland@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:48:28 +0200 Subject: [PATCH 4/5] chore: regenerate and update translation files --- simple_subscription/locale/de.po | 25 +++++++++++++------------ simple_subscription/locale/main.pot | 25 +++++++++++++------------ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/simple_subscription/locale/de.po b/simple_subscription/locale/de.po index df5a709..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: 2026-06-15 11:45+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" @@ -53,7 +53,7 @@ msgstr "Rechnungszeit" msgid "Company" msgstr "Unternehmen" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js:53 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js:28 msgid "Create current {0} invoice" msgstr "Aktuelle {0} Rechnung erstellen" @@ -89,7 +89,7 @@ msgstr "" msgid "Disabled" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:128 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:135 msgid "Failed to create subscription invoice" msgstr "Fehler beim Erstellen der Abo-Rechnung" @@ -98,10 +98,6 @@ msgstr "Fehler beim Erstellen der Abo-Rechnung" msgid "Frequency" msgstr "Frequenz" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:196 -msgid "Frequency {0} is only supported when billing period is based on start date." -msgstr "Die Frequenz {0} ist nur verfügbar, wenn die Abrechnungsperiode auf dem Startdatum basiert." - #. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Halfyearly" @@ -133,15 +129,15 @@ msgstr "" msgid "Monthly" msgstr "Monatlich" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:50 +#: 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:44 +#: 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:47 +#: 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." @@ -155,7 +151,7 @@ msgstr "" msgid "Quarterly" msgstr "Vierteljährlich" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:117 +#: 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: {}" @@ -186,7 +182,12 @@ msgstr "Vertrieb" #. 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 diff --git a/simple_subscription/locale/main.pot b/simple_subscription/locale/main.pot index d3eac1f..a8c2ac1 100644 --- a/simple_subscription/locale/main.pot +++ b/simple_subscription/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Simple Subscription VERSION\n" "Report-Msgid-Bugs-To: hallo@alyf.de\n" -"POT-Creation-Date: 2026-06-15 11:45+0053\n" -"PO-Revision-Date: 2026-06-15 11:45+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" @@ -51,7 +51,7 @@ msgstr "" msgid "Company" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js:53 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.js:28 msgid "Create current {0} invoice" msgstr "" @@ -87,7 +87,7 @@ msgstr "" msgid "Disabled" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:128 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:135 msgid "Failed to create subscription invoice" msgstr "" @@ -96,10 +96,6 @@ msgstr "" msgid "Frequency" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:196 -msgid "Frequency {0} is only supported when billing period is based on start date." -msgstr "" - #. Option for the 'Frequency' (Select) field in DocType 'Simple Subscription' #: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.json msgid "Halfyearly" @@ -131,15 +127,15 @@ msgstr "" msgid "Monthly" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:50 +#: 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:44 +#: 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:47 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:54 msgid "Please submit Subscription {0} before creating a Sales Invoice." msgstr "" @@ -153,7 +149,7 @@ msgstr "" msgid "Quarterly" msgstr "" -#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:117 +#: simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py:124 msgid "Sales Invoice already exists for this period: {}" msgstr "" @@ -186,6 +182,11 @@ msgstr "" 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" From 602c717fdc80bac83d2b3178d47da42d094030cc Mon Sep 17 00:00:00 2001 From: HenningWendtland <156231187+HenningWendtland@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:40:27 +0200 Subject: [PATCH 5/5] fix: pass only start_year and replace frappe.throw in get_calendar_period --- .../simple_subscription.py | 40 ++++++++++--------- .../test_simple_subscription.py | 18 ++++----- 2 files changed, 30 insertions(+), 28 deletions(-) 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 db2fca8..8571c7f 100644 --- a/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py +++ b/simple_subscription/simple_subscription/doctype/simple_subscription/simple_subscription.py @@ -104,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: @@ -184,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: @@ -194,10 +201,10 @@ 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, start_date) + 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, start_date) - return get_calendar_period(current_period_start - timedelta(days=1), frequency, start_date) + 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: @@ -213,19 +220,14 @@ def validate_calendar_frequencies(period_type: str, frequency: str, start_date: def get_calendar_period( - eval_date: date, frequency: Frequency, start_date: date | None = None + 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 and not start_date: - frappe.throw( - _("Start Date is required for frequency {0}.").format( - _(frequency.name, context="Frequency of Subscription") - ) - ) - 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_date.year + ((eval_date.year - start_date.year) // years_span) * years_span + 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]) 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 b48834e..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 @@ -37,33 +37,33 @@ def test_get_calendar_period(self): self.assertEqual(from_date, date(2022, 1, 1)) self.assertEqual(to_date, date(2022, 12, 31)) - start_date = date(2022, 6, 25) + start_year = date(2022, 6, 25).year - from_date, to_date = get_calendar_period(date(2022, 11, 7), Frequency.Biennial, start_date) + 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_date) + 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_date) + 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_date) + 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_date) + 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_date(self): - with self.assertRaises(frappe.ValidationError): + 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(frappe.ValidationError): + with self.assertRaises(ValueError): get_calendar_period(date(2022, 11, 7), Frequency.Triennial) def test_validate_calendar_frequencies(self):