Repository: OCA/donation (language: python)

Issue:
Preparing a big reorganisation of the code for v9
For v9, I'm preparing a big re-organisation of the code of the donation project. Let me explain the idea and what I plan to do.

**Main idea**

The donation module is based on dedicated objects donation and donation.line ; so a donation is not related to a sale order nor an invoice. There are several advantages for this approach:
- the user can encode both the donation and the related payment in a single form, so it's fast and easy.
- we can generate an accounting entry that go directly from the bank account to the revenue account, without going through a receivable account, which reduces the number of accounting entries and avoids reconciliation.
- the donation module only depends on the account module

In one of my new projects, I need to support:
- online donation paid by credit card
- mix of regular products (T-shirt, with VAT) and donations in the same order ; the whole order is paid by a single credit card transaction

For this, I need to support donations inside a sale.order (for the online shop, the basket is a draft sale.order in Odoo). So it is quite different from the current implementation and it's not easy to have those 2 implementations "compatible".

In fact, I think it is the same kind of problem between the "sale" module that provide the sale.order/sale.order.line objects and the "point_of_sale" module that provide the pos.order/pos.order.line objects. The sale.order and pos.order objects are technically independant but they are both related to the sale of goods to a customer. Why did the point_of_sale module created a dedicated object "pos.order" instead of using the "sale.order" object ? I guess it was for simplicity reasons: they wanted to have an object that could handle sale+payment+delivery at the same time (whereas the sale.order object only handles the sale and doesn't directly handle the payment and delivery).

So I plan to make this project evolve to the same kind of approach:
1) First solution: the donation/donation.line objects will be used when you have donation+payment at the same time (equivalent to the "point_of_sale"), because it is very simple and we can have a journal item that go directly from the bank account to the revenue account (no reconciliation because we don't use a receivable account).
2) Second solution: the sale.order/sale.order.line object will be used when you have a mix of products and donations in the same transaction and/or when you want to have donations in an online shop. In this solution, an invoice is generated so there are 2 journal items: one from the receivable account to the income account and a second one from the bank account to the receivable account and it require a reconciliation in the receivable account.
You should be able to use only the first solution or only the second solution... or use both ! But, if you use both, you will still have the constraints linked to the fact that donation.line and sale.order.line are different objects. The main difficulty will be to be able to handle annual tax receipts in the scenario where you use the 2 solutions.

**Proposed implementation**

So here are my plans for the code base of this project for v9:
- create a new module "donation_base" that will have the properties on the product object
- create a new module "donation_sale" that will depend on donation_base
- the "donation" module will depend on "donation_base"

I know that this change will introduce more modules and more complexity and I don't want to make this project too complex, so I propose the following simplification: the handling of tax receipts will not be handled in a separate module any more ; the handling of tax receipts will be made in the base module (the donation.tax.receipt object will be defined in the donation_base module). For associations that receive donations but are not allowed to generate tax receipts, the drawback is that they will have menu entries and object fields that they don't need ; so I plan to create a group "Donation Tax Receipt" and you will have to be in that group to see all tax-receipt related info (just like the group "Analytic Accounting").

Please give your feedback on this new idea NOW, because I'm starting the development on this new implementation tomorrow ! :)



Discussion / hints:

Fix patch:
diff --git a/donation_recurring/README.rst b/donation_recurring/README.rst
new file mode 100644
index 000000000..ce59e78ac
--- /dev/null
+++ b/donation_recurring/README.rst
@@ -0,0 +1,67 @@
+.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
+   :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+   :alt: License: AGPL-3
+
+==================
+Donation Recurring
+==================
+
+This module handles recurring donations. For example, if you have
+setup a donation plan where donors can donate monthly via direct
+debit, you can use this module in combination with the module
+*account_banking_sepa_direct_debit* to organise your recurring donations
+(and you may also use the module *account_direct_debit*).
+
+Configuration
+=============
+
+In the menu Donations > Donations > Recurring Donations, create a
+recurring donation template for each donor that commited to donate
+monthly via direct debit.
+
+Usage
+=====
+
+Every month, execute the wizard *Generate Recurring Donations* : it will
+duplicate each recurring donation template to create a regular donation.
+Then select all these new draft donations generated by the wizard and
+validate them.
+
+.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
+   :alt: Try me on Runbot
+   :target: https://runbot.odoo-community.org/runbot/180/11.0
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues
+<https://github.com/OCA/donation/issues>`_. In case of trouble, please
+check there if your issue has already been reported. If you spotted it first,
+help us smashing it by providing a detailed and welcomed feedback.
+
+Credits
+=======
+
+Contributors
+------------
+
+* Brother Bernard <informatique - at - barroux.org>
+* Brother Irénée (Barroux Abbey)
+* Alexis de Lattre <alexis.delattre@akretion.com>
+* Serpent Consulting Services Pvt. Ltd. <support@serpentcs.com>
+* Nikul Chaudhary <nikul.chaudhary.serpentcs@gmail.com>
+
+Maintainer
+----------
+
+.. image:: https://odoo-community.org/logo.png
+   :alt: Odoo Community Association
+   :target: https://odoo-community.org
+
+This module is maintained by the OCA.
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+To contribute to this module, please visit https://odoo-community.org.
diff --git a/donation_recurring/__init__.py b/donation_recurring/__init__.py
new file mode 100644
index 000000000..9b4296142
--- /dev/null
+++ b/donation_recurring/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/donation_recurring/__manifest__.py b/donation_recurring/__manifest__.py
new file mode 100644
index 000000000..681784ae6
--- /dev/null
+++ b/donation_recurring/__manifest__.py
@@ -0,0 +1,25 @@
+# Copyright 2014-2021 Barroux Abbey (http://www.barroux.org)
+# Copyright 2014-2021 Akretion France (http://www.akretion.com/)
+# @author: Alexis de Lattre <alexis.delattre@akretion.com>
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+    "name": "Donation Recurring",
+    "version": "14.0.1.0.0",
+    "category": "Accounting",
+    "license": "AGPL-3",
+    "summary": "Manage recurring donations",
+    "author": "Barroux Abbey, Akretion, Odoo Community Association (OCA)",
+    "maintainers": ["alexis-via"],
+    "website": "https://github.com/OCA/donation",
+    "depends": [
+        "donation",
+    ],
+    "data": [
+        "views/donation.xml",
+        "wizard/donation_recurring_generate_view.xml",
+        "security/ir.model.access.csv",
+    ],
+    "demo": ["demo/donation_recurring_demo.xml"],
+    "installable": True,
+}
diff --git a/donation_recurring/demo/donation_recurring_demo.xml b/donation_recurring/demo/donation_recurring_demo.xml
new file mode 100644
index 000000000..1280b8c7a
--- /dev/null
+++ b/donation_recurring/demo/donation_recurring_demo.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+  Copyright 2014-2021 Barroux Abbey (http://www.barroux.org)
+  Copyright 2014-2021 Akretion France (http://www.akretion.com/)
+  @author: Alexis de Lattre <alexis.delattre@akretion.com>
+  License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+-->
+<odoo noupdate="1">
+
+    <record id="donor_rec1" model="res.partner">
+        <field name="name">Emilie Legrand</field>
+        <field name="donor_rank" eval="1" />
+        <field name="street">24 chemin des oliviers</field>
+        <field name="zip">74210</field>
+        <field name="city">Seythenex</field>
+        <field name="country_id" ref="base.fr" />
+        <field name="email">emilie.legrand@laposte.example.com</field>
+        <field name="tax_receipt_option">annual</field>
+    </record>
+
+    <record id="donor_rec2" model="res.partner">
+        <field name="name">Marie Durand</field>
+        <field name="donor_rank" eval="1" />
+        <field name="street">9 avenue du Général de Lattre</field>
+        <field name="zip">75017</field>
+        <field name="city">Paris</field>
+        <field name="country_id" ref="base.fr" />
+        <field name="email">marie.durand@free.example.com</field>
+        <field name="tax_receipt_option">annual</field>
+    </record>
+
+    <record id="donor_rec3" model="res.partner">
+        <field name="name">Olivier Dumesnil</field>
+        <field name="donor_rank" eval="1" />
+        <field name="street">9 avenue Foch</field>
+        <field name="zip">92400</field>
+        <field name="city">Courbevoie</field>
+        <field name="country_id" ref="base.fr" />
+        <field name="email">olivier.dumesnil@free.example.com</field>
+        <field name="tax_receipt_option">annual</field>
+    </record>
+
+</odoo>
diff --git a/donation_recurring/i18n/de.po b/donation_recurring/i18n/de.po
new file mode 100644
index 000000000..8ed7d1b4c
--- /dev/null
+++ b/donation_recurring/i18n/de.po
@@ -0,0 +1,200 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# 	* donation_recurring
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 10.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2019-02-08 16:50+0000\n"
+"Last-Translator: kempleton <kimdoelling@gmail.com>\n"
+"Language-Team: none\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 3.4\n"
+
+#. module: donation_recurring
+#: selection:donation.donation,recurring_template:0
+msgid "Active"
+msgstr "Aktiv"
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "Active Recurring Template"
+msgstr "Aktive wiederkehrende Vorlage"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:82
+#, python-format
+msgid ""
+"As it is a recurring donation, the Tax Receipt Option has been changed from "
+"Each to Annual. You may want to change it also on the Donor form."
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Cancel"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:70
+#, python-format
+msgid "Cancelled Donation of %s"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_create_date
+msgid "Created on"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_date
+msgid "Date"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model,name:donation_recurring.model_donation_donation
+msgid "Donation"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:68
+#, python-format
+msgid "Draft Donation of %s"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "From Recurring"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Generate"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.actions.act_window,name:donation_recurring.donation_recurring_generate_action
+#: model:ir.model,name:donation_recurring.model_donation_recurring_generate
+#: model:ir.ui.menu,name:donation_recurring.donation_recurring_generate_menu
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Generate Recurring Donations"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_form
+msgid "History"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_id
+msgid "ID"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate___last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_recurring_donation_ids
+msgid "Past Recurring Donations"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_payment_ref
+msgid "Payment Reference"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:62
+#, python-format
+msgid "Recurring Donation of %s"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.actions.act_window,name:donation_recurring.donation_recurring_action
+#: model:ir.ui.menu,name:donation_recurring.donation_recurring_menu
+msgid "Recurring Donations"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_recurring_template
+msgid "Recurring Template"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/wizard/donation_recurring_generate.py:43
+#, python-format
+msgid "Recurring donations have already been generated for %s."
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_source_recurring_id
+msgid "Source Recurring Template"
+msgstr ""
+
+#. module: donation_recurring
+#: selection:donation.donation,recurring_template:0
+msgid "Suspended"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:65
+#, python-format
+msgid "Suspended Recurring Donation of %s"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "Suspended Recurring Template"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:51
+#, python-format
+msgid "The recurring donation of %s cannot have a tax receipt option 'Each'."
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:44
+#, python-format
+msgid ""
+"The recurring donation template of '%s' cannot have a Source Recurring "
+"Template"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:40
+#, python-format
+msgid "The recurring donation template of '%s' must stay in draft state."
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:81
+#, python-format
+msgid "Update of Tax Receipt Option"
+msgstr ""
diff --git a/donation_recurring/i18n/donation_recurring.pot b/donation_recurring/i18n/donation_recurring.pot
new file mode 100644
index 000000000..767450ac8
--- /dev/null
+++ b/donation_recurring/i18n/donation_recurring.pot
@@ -0,0 +1,194 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+#	* donation_recurring
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: donation_recurring
+#: selection:donation.donation,recurring_template:0
+msgid "Active"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "Active Recurring Template"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:82
+#, python-format
+msgid "As it is a recurring donation, the Tax Receipt Option has been changed from Each to Annual. You may want to change it also on the Donor form."
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Cancel"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:70
+#, python-format
+msgid "Cancelled Donation of %s"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_create_date
+msgid "Created on"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_date
+msgid "Date"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model,name:donation_recurring.model_donation_donation
+msgid "Donation"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:68
+#, python-format
+msgid "Draft Donation of %s"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "From Recurring"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Generate"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.actions.act_window,name:donation_recurring.donation_recurring_generate_action
+#: model:ir.model,name:donation_recurring.model_donation_recurring_generate
+#: model:ir.ui.menu,name:donation_recurring.donation_recurring_generate_menu
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Generate Recurring Donations"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_form
+msgid "History"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_id
+msgid "ID"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate___last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_recurring_donation_ids
+msgid "Past Recurring Donations"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_payment_ref
+msgid "Payment Reference"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:62
+#, python-format
+msgid "Recurring Donation of %s"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.actions.act_window,name:donation_recurring.donation_recurring_action
+#: model:ir.ui.menu,name:donation_recurring.donation_recurring_menu
+msgid "Recurring Donations"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_recurring_template
+msgid "Recurring Template"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/wizard/donation_recurring_generate.py:43
+#, python-format
+msgid "Recurring donations have already been generated for %s."
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_source_recurring_id
+msgid "Source Recurring Template"
+msgstr ""
+
+#. module: donation_recurring
+#: selection:donation.donation,recurring_template:0
+msgid "Suspended"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:65
+#, python-format
+msgid "Suspended Recurring Donation of %s"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "Suspended Recurring Template"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:51
+#, python-format
+msgid "The recurring donation of %s cannot have a tax receipt option 'Each'."
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:44
+#, python-format
+msgid "The recurring donation template of '%s' cannot have a Source Recurring Template"
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:40
+#, python-format
+msgid "The recurring donation template of '%s' must stay in draft state."
+msgstr ""
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:81
+#, python-format
+msgid "Update of Tax Receipt Option"
+msgstr ""
+
diff --git a/donation_recurring/i18n/fr.po b/donation_recurring/i18n/fr.po
new file mode 100644
index 000000000..1de78f03a
--- /dev/null
+++ b/donation_recurring/i18n/fr.po
@@ -0,0 +1,209 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# 	* donation_recurring
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 8.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-02-17 22:43+0000\n"
+"PO-Revision-Date: 2015-02-17 22:43+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: donation_recurring
+#: selection:donation.donation,recurring_template:0
+msgid "Active"
+msgstr "Active"
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "Active Recurring Template"
+msgstr "Active Recurring Template"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:82
+#, python-format
+msgid ""
+"As it is a recurring donation, the Tax Receipt Option has been changed from "
+"Each to Annual. You may want to change it also on the Donor form."
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Cancel"
+msgstr "Cancel"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:70
+#, fuzzy, python-format
+msgid "Cancelled Donation of %s"
+msgstr "Draft Donation of %s"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_create_uid
+msgid "Created by"
+msgstr "Created by"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_create_date
+msgid "Created on"
+msgstr "Created on"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_date
+msgid "Date"
+msgstr "Date"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: donation_recurring
+#: model:ir.model,name:donation_recurring.model_donation_donation
+#, fuzzy
+msgid "Donation"
+msgstr "Dons"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:68
+#, python-format
+msgid "Draft Donation of %s"
+msgstr "Draft Donation of %s"
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "From Recurring"
+msgstr "From Recurring"
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Generate"
+msgstr "Generate"
+
+#. module: donation_recurring
+#: model:ir.actions.act_window,name:donation_recurring.donation_recurring_generate_action
+#: model:ir.model,name:donation_recurring.model_donation_recurring_generate
+#: model:ir.ui.menu,name:donation_recurring.donation_recurring_generate_menu
+#: model:ir.ui.view,arch_db:donation_recurring.donation_recurring_generate_form
+msgid "Generate Recurring Donations"
+msgstr "Generate Recurring Donations"
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_form
+msgid "History"
+msgstr "History"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_id
+msgid "ID"
+msgstr "ID"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate___last_update
+#, fuzzy
+msgid "Last Modified on"
+msgstr "Last Updated on"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_write_uid
+msgid "Last Updated by"
+msgstr "Last Updated by"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_write_date
+msgid "Last Updated on"
+msgstr "Last Updated on"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_recurring_donation_ids
+msgid "Past Recurring Donations"
+msgstr "Past Recurring Donations"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_recurring_generate_payment_ref
+msgid "Payment Reference"
+msgstr "Payment Reference"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:62
+#, python-format
+msgid "Recurring Donation of %s"
+msgstr "Recurring Donation of %s"
+
+#. module: donation_recurring
+#: model:ir.actions.act_window,name:donation_recurring.donation_recurring_action
+#: model:ir.ui.menu,name:donation_recurring.donation_recurring_menu
+msgid "Recurring Donations"
+msgstr "Recurring Donations"
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_recurring_template
+msgid "Recurring Template"
+msgstr "Recurring Template"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/wizard/donation_recurring_generate.py:43
+#, python-format
+msgid "Recurring donations have already been generated for %s."
+msgstr "Recurring donations have already been generated for %s."
+
+#. module: donation_recurring
+#: model:ir.model.fields,field_description:donation_recurring.field_donation_donation_source_recurring_id
+msgid "Source Recurring Template"
+msgstr "Source Recurring Template"
+
+#. module: donation_recurring
+#: selection:donation.donation,recurring_template:0
+msgid "Suspended"
+msgstr "Suspended"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:65
+#, python-format
+msgid "Suspended Recurring Donation of %s"
+msgstr "Suspended Recurring Donation of %s"
+
+#. module: donation_recurring
+#: model:ir.ui.view,arch_db:donation_recurring.donation_search
+msgid "Suspended Recurring Template"
+msgstr "Suspended Recurring Template"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:51
+#, fuzzy, python-format
+msgid "The recurring donation of %s cannot have a tax receipt option 'Each'."
+msgstr ""
+"The recurring donation template of '%s' cannot have a Source Recurring "
+"Template"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:44
+#, python-format
+msgid ""
+"The recurring donation template of '%s' cannot have a Source Recurring "
+"Template"
+msgstr ""
+"The recurring donation template of '%s' cannot have a Source Recurring "
+"Template"
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:40
+#, python-format
+msgid "The recurring donation template of '%s' must stay in draft state."
+msgstr "The recurring donation template of '%s' must stay in draft state."
+
+#. module: donation_recurring
+#: code:addons/donation_recurring/models/donation.py:81
+#, python-format
+msgid "Update of Tax Receipt Option"
+msgstr ""
+
+#~ msgid "red:recurring_template=='suspended';blue:state=='draft'"
+#~ msgstr "red:recurring_template=='suspended';blue:state=='draft'"
diff --git a/donation_recurring/models/__init__.py b/donation_recurring/models/__init__.py
new file mode 100644
index 000000000..b1847a5ab
--- /dev/null
+++ b/donation_recurring/models/__init__.py
@@ -0,0 +1,1 @@
+from . import donation
diff --git a/donation_recurring/models/donation.py b/donation_recurring/models/donation.py
new file mode 100644
index 000000000..afd6b71a6
--- /dev/null
+++ b/donation_recurring/models/donation.py
@@ -0,0 +1,110 @@
+# Copyright 2014-2021 Barroux Abbey (http://www.barroux.org)
+# Copyright 2014-2021 Akretion France (http://www.akretion.com/)
+# @author: Alexis de Lattre <alexis.delattre@akretion.com>
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError, ValidationError
+
+
+class DonationDonation(models.Model):
+    _inherit = "donation.donation"
+
+    recurring_template = fields.Selection(
+        [("active", "Active"), ("suspended", "Suspended")],
+        string="Recurring Template",
+        copy=False,
+        index=True,
+        tracking=True,
+    )
+    source_recurring_id = fields.Many2one(
+        "donation.donation",
+        string="Source Recurring Template",
+        states={"done": [("readonly", True)]},
+    )
+    recurring_donation_ids = fields.One2many(
+        "donation.donation",
+        "source_recurring_id",
+        string="Past Recurring Donations",
+        readonly=True,
+        copy=False,
+    )
+
+    @api.constrains(
+        "recurring_template", "source_recurring_id", "state", "tax_receipt_option"
+    )
+    def _check_recurring_donation(self):
+        for donation in self:
+            if donation.recurring_template and donation.state != "draft":
+                raise ValidationError(
+                    _("The recurring donation template %s must stay in " "draft state.")
+                    % donation.number
+                )
+            if donation.source_recurring_id and donation.recurring_template:
+                raise ValidationError(
+                    _(
+                        "The recurring donation template %s cannot have "
+                        "a Source Recurring Template"
+                    )
+                    % donation.number
+                )
+            if donation.recurring_template and donation.tax_receipt_option == "each":
+                raise ValidationError(
+                    _(
+                        "The recurring donation %s cannot have a tax "
+                        "receipt option 'Each'."
+                    )
+                    % donation.number
+                )
+
+    @api.depends("state", "partner_id", "move_id", "recurring_template")
+    def name_get(self):
+        res = []
+        for donation in self:
+            if donation.recurring_template == "active":
+                name = _("Recurring Donation %s") % (donation.number)
+            elif donation.recurring_template == "suspended":
+                name = _("Suspended Recurring Donation %s") % (donation.number)
+            else:
+                name = super(DonationDonation, donation).name_get()[0][1]
+            res.append((donation.id, name))
+        return res
+
+    @api.onchange("recurring_template")
+    def recurring_template_change(self):
+        res = {"warning": {}}
+        if self.recurring_template and self.tax_receipt_option == "each":
+            self.tax_receipt_option = "annual"
+            res["warning"]["title"] = _("Update of Tax Receipt Option")
+            res["warning"]["message"] = _(
+                "As it is a recurring donation, "
+                "the Tax Receipt Option has been changed from Each to "
+                "Annual. You may want to change it also on the Donor "
+                "form."
+            )
+        if not self.recurring_template and self.commercial_partner_id:
+            if self.commercial_partner_id.tax_receipt_option != self.tax_receipt_option:
+                self.tax_receipt_option = self.commercial_partner_id.tax_receipt_option
+        return res
+
+    def active2suspended(self):
+        self.ensure_one()
+        assert self.recurring_template == "active"
+        self.write({"recurring_template": "suspended"})
+
+    def suspended2active(self):
+        self.ensure_one()
+        assert self.recurring_template == "suspended"
+        self.write({"recurring_template": "active"})
+
+    def unlink(self):
+        for donation in self:
+            # To avoid accidents !
+            if donation.recurring_template == "active":
+                raise UserError(
+                    _(
+                        "You cannot delete an active recurring donation. "
+                        "You must suspend it first."
+                    )
+                )
+        return super().unlink()
diff --git a/donation_recurring/readme/CONFIGURE.rst b/donation_recurring/readme/CONFIGURE.rst
new file mode 100644
index 000000000..e6c7698f9
--- /dev/null
+++ b/donation_recurring/readme/CONFIGURE.rst
@@ -0,0 +1,5 @@
+In the menu *Donation > Donations > Recurring Donations*, create a
+recurring donation template for each donor that commited to donate
+monthly via direct debit.
+
+If a donor ask to suspend it's recurring donation, click on the *Suspend* button. If he later asks to restart his recurring donations, click on the *Re-activate* button.
diff --git a/donation_recurring/readme/CONTRIBUTORS.rst b/donation_recurring/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000..41affeb19
--- /dev/null
+++ b/donation_recurring/readme/CONTRIBUTORS.rst
@@ -0,0 +1,5 @@
+* Brother Bernard <informatique - at - barroux.org>
+* Brother Irénée (Barroux Abbey)
+* Alexis de Lattre <alexis.delattre@akretion.com>
+* Serpent Consulting Services Pvt. Ltd. <support@serpentcs.com>
+* Nikul Chaudhary <nikul.chaudhary.serpentcs@gmail.com>
diff --git a/donation_recurring/readme/DESCRIPTION.rst b/donation_recurring/readme/DESCRIPTION.rst
new file mode 100644
index 000000000..c6dd21e2a
--- /dev/null
+++ b/donation_recurring/readme/DESCRIPTION.rst
@@ -0,0 +1,4 @@
+This module handles recurring donations. For example, if you have
+setup a donation plan where donors can donate monthly via direct
+debit, you can use this module in combination with the module
+*account_banking_sepa_direct_debit* to organise your recurring donations.
diff --git a/donation_recurring/readme/USAGE.rst b/donation_recurring/readme/USAGE.rst
new file mode 100644
index 000000000..c926a0e67
--- /dev/null
+++ b/donation_recurring/readme/USAGE.rst
@@ -0,0 +1,4 @@
+Every month, execute the wizard *Generate Recurring Donations* : it will
+duplicate each recurring donation template to create a regular donation.
+Then select all these new draft donations generated by the wizard and
+validate them.
diff --git a/donation_recurring/security/ir.model.access.csv b/donation_recurring/security/ir.model.access.csv
new file mode 100644
index 000000000..c72213375
--- /dev/null
+++ b/donation_recurring/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_donation_recurring_generate,Full access on donation.recurring.generate wizard,model_donation_recurring_generate,donation.group_donation_user,1,1,1,1
diff --git a/donation_recurring/static/description/icon.png b/donation_recurring/static/description/icon.png
new file mode 100644
index 000000000..dbbc26199
Binary files /dev/null and b/donation_recurring/static/description/icon.png differ
diff --git a/donation_recurring/views/donation.xml b/donation_recurring/views/donation.xml
new file mode 100644
index 000000000..5696d8e3f
--- /dev/null
+++ b/donation_recurring/views/donation.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+  Copyright 2014-2021 Barroux Abbey (http://www.barroux.org)
+  Copyright 2014-2021 Akretion France (http://www.akretion.com/)
+  @author: Alexis de Lattre <alexis.delattre@akretion.com>
+  License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+-->
+<odoo>
+
+    <record id="donation_form" model="ir.ui.view">
+        <field name="name">donation.recurring.form</field>
+        <field name="model">donation.donation</field>
+        <field name="inherit_id" ref="donation.donation_form" />
+        <field name="arch" type="xml">
+            <group name="other-right" position="inside">
+                <field
+                    name="recurring_template"
+                    attrs="{'invisible': [('source_recurring_id', '!=', False)]}"
+                />
+                <field
+                    name="source_recurring_id"
+                    attrs="{'invisible': [('recurring_template', '!=', False)]}"
+                />
+            </group>
+            <page name="lines" position="after">
+                <page
+                    name="recurring"
+                    string="History"
+                    attrs="{'invisible': [('recurring_template', '=', False)]}"
+                >
+                    <field name="recurring_donation_ids" nolabel="1" />
+                </page>
+            </page>
+            <button name="done2cancel" position="after">
+                    <button
+                    name="active2suspended"
+                    type="object"
+                    string="Suspend"
+                    attrs="{'invisible': [('recurring_template', '!=', 'active')]}"
+                    groups="donation.group_donation_user"
+                />
+                    <button
+                    name="suspended2active"
+                    type="object"
+                    string="Re-activate"
+                    attrs="{'invisible': [('recurring_template', '!=', 'suspended')]}"
+                    groups="donation.group_donation_user"
+                />
+            </button>
+            <div class="oe_title" position="before">
+                <widget
+                    name="web_ribbon"
+                    title="Suspended"
+                    bg_color="bg-danger"
+                    attrs="{'invisible': [('recurring_template', '!=', 'suspended')]}"
+                />
+            </div>
+        </field>
+    </record>
+
+    <record id="donation_tree" model="ir.ui.view">
+        <field name="name">donation.recurring.tree</field>
+        <field name="model">donation.donation</field>
+        <field name="inherit_id" ref="donation.donation_tree" />
+        <field name="arch" type="xml">
+            <field name="state" position="after">
+                <field
+                    name="recurring_template"
+                    invisible="not context.get('recurring_view')"
+                    widget="badge"
+                    decoration-danger="recurring_template == 'suspended'"
+                    decoration-info="recurring_template == 'active'"
+                />
+            </field>
+        </field>
+    </record>
+
+    <record id="donation_search" model="ir.ui.view">
+        <field name="name">donation.search</field>
+        <field name="model">donation.donation</field>
+        <field name="inherit_id" ref="donation.donation_search" />
+        <field name="arch" type="xml">
+            <filter name="cancel" position="after">
+                <separator />
+                <filter
+                    name="from_recurring"
+                    string="From Recurring"
+                    domain="[('source_recurring_id', '!=', False)]"
+                />
+                <filter
+                    name="recurring_active"
+                    string="Active Recurring Template"
+                    domain="[('recurring_template', '=', 'active')]"
+                />
+                <filter
+                    name="recurring_suspended"
+                    string="Suspended Recurring Template"
+                    domain="[('recurring_template', '=', 'suspended')]"
+                />
+            </filter>
+        </field>
+    </record>
+
+    <record id="donation.donation_action" model="ir.actions.act_window">
+        <field name="domain">[('recurring_template', '=', False)]</field>
+    </record>
+
+    <record id="donation_recurring_action" model="ir.actions.act_window">
+        <field name="name">Recurring Donations</field>
+        <field name="res_model">donation.donation</field>
+        <field name="view_mode">tree,form,pivot,graph</field>
+        <field
+            name="context"
+        >{'default_recurring_template': 'active', 'recurring_view': True}</field>
+        <field name="domain">[('recurring_template', '!=', False)]</field>
+    </record>
+
+    <menuitem
+        id="donation_recurring_menu"
+        action="donation_recurring_action"
+        parent="donation.donation_title_menu"
+        sequence="20"
+    />
+
+</odoo>
diff --git a/donation_recurring/wizard/__init__.py b/donation_recurring/wizard/__init__.py
new file mode 100644
index 000000000..da5768580
--- /dev/null
+++ b/donation_recurring/wizard/__init__.py
@@ -0,0 +1,1 @@
+from . import donation_recurring_generate
diff --git a/donation_recurring/wizard/donation_recurring_generate.py b/donation_recurring/wizard/donation_recurring_generate.py
new file mode 100644
index 000000000..b474125a9
--- /dev/null
+++ b/donation_recurring/wizard/donation_recurring_generate.py
@@ -0,0 +1,67 @@
+# Copyright 2014-2021 Barroux Abbey (http://www.barroux.org)
+# Copyright 2014-2021 Akretion France (http://www.akretion.com/)
+# @author: Alexis de Lattre <alexis.delattre@akretion.com>
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+from odoo.tools.misc import format_date
+
+
+class DonationRecurringGenerate(models.TransientModel):
+    _name = "donation.recurring.generate"
+    _description = "Generate Recurring Donations"
+    _rec_name = "date"
+
+    date = fields.Date(required=True, default=fields.Date.context_today)
+    payment_ref = fields.Char(string="Payment Reference")
+    company_id = fields.Many2one(
+        "res.company",
+        required=True,
+        string="Company",
+        default=lambda self: self.env.company,
+    )
+
+    @api.model
+    def _prepare_donation_default(self, donation):
+        default = {
+            "donation_date": self.date,
+            "source_recurring_id": donation.id,
+            "payment_ref": self.payment_ref,
+        }
+        return default
+
+    def generate(self):
+        self.ensure_one()
+        doo = self.env["donation.donation"]
+        donations = doo.search(
+            [
+                ("recurring_template", "=", "active"),
+                ("company_id", "=", self.company_id.id),
+            ]
+        )
+        new_donation_ids = []
+        existing_recur_donations = doo.search(
+            [
+                ("donation_date", "=", self.date),
+                ("source_recurring_id", "!=", False),
+                ("company_id", "=", self.company_id.id),
+            ]
+        )
+        if existing_recur_donations:
+            raise UserError(
+                _("Recurring donations have already been generated for %s.")
+                % format_date(self.env, self.date)
+            )
+        for donation in donations:
+            default = self._prepare_donation_default(donation)
+            new_donation = donation.copy(default=default)
+            new_donation_ids.append(new_donation.id)
+        action = self.env.ref("donation.donation_action").sudo().read([])[0]
+        action.update(
+            {
+                "domain": [("id", "in", new_donation_ids)],
+                "limit": 500,
+            }
+        )
+        return action
diff --git a/donation_recurring/wizard/donation_recurring_generate_view.xml b/donation_recurring/wizard/donation_recurring_generate_view.xml
new file mode 100644
index 000000000..bfeef8672
--- /dev/null
+++ b/donation_recurring/wizard/donation_recurring_generate_view.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+  Copyright 2014-2021 Barroux Abbey (http://www.barroux.org)
+  Copyright 2014-2021 Akretion France (http://www.akretion.com/)
+  @author: Alexis de Lattre <alexis.delattre@akretion.com>
+  License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+-->
+<odoo>
+
+    <record id="donation_recurring_generate_form" model="ir.ui.view">
+        <field name="name">donation_recurring_generate.form</field>
+        <field name="model">donation.recurring.generate</field>
+        <field name="arch" type="xml">
+            <form string="Generate Recurring Donations">
+                <group name="main">
+                    <field name="date" />
+                    <field name="payment_ref" />
+                    <field name="company_id" groups="base.group_multi_company" />
+                </group>
+                <footer>
+                    <button
+                        type="object"
+                        name="generate"
+                        string="Generate"
+                        class="btn-primary"
+                    />
+                    <button special="cancel" string="Cancel" />
+                </footer>
+            </form>
+        </field>
+    </record>
+
+    <record id="donation_recurring_generate_action" model="ir.actions.act_window">
+        <field name="name">Generate Recurring Donations</field>
+        <field name="res_model">donation.recurring.generate</field>
+        <field name="view_mode">form</field>
+        <field name="target">new</field>
+    </record>
+
+    <menuitem
+        id="donation_recurring_generate_menu"
+        sequence="30"
+        action="donation_recurring_generate_action"
+        parent="donation.donation_title_menu"
+        groups="donation.group_donation_user"
+    />
+
+</odoo>
diff --git a/setup/donation_recurring/odoo/addons/donation_recurring b/setup/donation_recurring/odoo/addons/donation_recurring
new file mode 120000
index 000000000..82e73d869
--- /dev/null
+++ b/setup/donation_recurring/odoo/addons/donation_recurring
@@ -0,0 +1,1 @@
+../../../../donation_recurring
\ No newline at end of file
diff --git a/setup/donation_recurring/setup.py b/setup/donation_recurring/setup.py
new file mode 100644
index 000000000..28c57bb64
--- /dev/null
+++ b/setup/donation_recurring/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+    setup_requires=['setuptools-odoo'],
+    odoo_addon=True,
+)


Test patch:
diff --git a/donation_recurring/tests/__init__.py b/donation_recurring/tests/__init__.py
new file mode 100644
index 000000000..85fde9768
--- /dev/null
+++ b/donation_recurring/tests/__init__.py
@@ -0,0 +1,1 @@
+from . import test_recurring_donations
diff --git a/donation_recurring/tests/test_recurring_donations.py b/donation_recurring/tests/test_recurring_donations.py
new file mode 100644
index 000000000..1444a97ff
--- /dev/null
+++ b/donation_recurring/tests/test_recurring_donations.py
@@ -0,0 +1,122 @@
+# Copyright 2016-2021 Akretion France (http://www.akretion.com/)
+# @author: Alexis de Lattre <alexis.delattre@akretion.com>
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+import time
+
+from odoo.exceptions import UserError
+from odoo.tests.common import TransactionCase
+
+
+class TestDonationRecurring(TransactionCase):
+
+    at_install = False
+    post_install = True
+
+    def setUp(self):
+        super().setUp()
+        self.bank_journal = self.env["account.journal"].create(
+            {
+                "type": "bank",
+                "name": "test bank journal",
+            }
+        )
+        self.payment_mode = self.env["account.payment.mode"].create(
+            {
+                "name": "test_payment_mode",
+                "donation": True,
+                "bank_account_link": "fixed",
+                "fixed_journal_id": self.bank_journal.id,
+                "payment_method_id": self.env.ref(
+                    "account.account_payment_method_manual_in"
+                ).id,
+            }
+        )
+        self.product = self.env.ref("donation_base.product_product_donation")
+        self.ddo = self.env["donation.donation"]
+        self.don_rec1 = self.ddo.create(
+            {
+                "check_total": 30,
+                "partner_id": self.env.ref("donation_recurring.donor_rec1").id,
+                "donation_date": time.strftime("%Y-01-01"),
+                "payment_mode_id": self.payment_mode.id,
+                "tax_receipt_option": "annual",
+                "recurring_template": "active",
+                "line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_id": self.product.id,
+                            "quantity": 1,
+                            "unit_price": 30,
+                        },
+                    )
+                ],
+            }
+        )
+        self.don_rec2 = self.ddo.create(
+            {
+                "check_total": 25,
+                "partner_id": self.env.ref("donation_recurring.donor_rec2").id,
+                "donation_date": time.strftime("%Y-01-01"),
+                "payment_mode_id": self.payment_mode.id,
+                "tax_receipt_option": "annual",
+                "recurring_template": "active",
+                "line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_id": self.product.id,
+                            "quantity": 1,
+                            "unit_price": 25,
+                        },
+                    )
+                ],
+            }
+        )
+        self.don_rec3 = self.ddo.create(
+            {
+                "check_total": 35,
+                "partner_id": self.env.ref("donation_recurring.donor_rec3").id,
+                "donation_date": time.strftime("%Y-01-01"),
+                "payment_mode_id": self.payment_mode.id,
+                "tax_receipt_option": "annual",
+                "recurring_template": "suspended",
+                "line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_id": self.product.id,
+                            "quantity": 1,
+                            "unit_price": 35,
+                        },
+                    )
+                ],
+            }
+        )
+
+    def test_donation_recurring(self):
+        wizard = self.env["donation.recurring.generate"].create(
+            {"payment_ref": "Don Abbaye Sainte Madeleine"}
+        )
+        action = wizard.generate()
+        active_don_recs = self.don_rec1 + self.don_rec2
+        for don_rec in active_don_recs:
+            self.assertTrue(don_rec.recurring_donation_ids)
+            don = don_rec.recurring_donation_ids[0]
+            self.assertEqual(don.state, "draft")
+            self.assertEqual(don.payment_ref, "Don Abbaye Sainte Madeleine")
+            self.assertEqual(don.campaign_id, don_rec.campaign_id)
+        self.assertFalse(self.don_rec3.recurring_donation_ids)
+        active_ids = action["domain"][0][2]
+        wizard_val = (
+            self.env["donation.validate"]
+            .with_context(active_ids=active_ids, active_model="donation.donation")
+            .create({})
+        )
+        with self.assertRaises(UserError):
+            wizard_val.run()
+        self.don_rec1.recurring_template_change()