From 97a379446a1a88264eeeb1a4ac154f3547b0c2ab Mon Sep 17 00:00:00 2001 From: Daniel Palumbo Date: Wed, 1 Jul 2026 13:47:26 +0200 Subject: [PATCH 1/3] T3284 Remove recurring.invoicer model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recurring.invoicer model stored batches of generated invoices but provided no real user value — it was just a log of cron runs. Remove the model, wizard, views, cron, and all related code. Invoice generation now runs directly without creating an invoicer record. Migration script drops the recurring_invoicer_id column from account_move and drops the recurring_invoicer table. Co-Authored-By: Claude Sonnet 4.6 --- recurring_contract/__manifest__.py | 5 +- .../data/daily_invoicer_cron.xml | 19 ------ .../migrations/18.0.1.1.0/post-migration.py | 17 +++++ recurring_contract/models/__init__.py | 1 - recurring_contract/models/contract_group.py | 29 +++++---- recurring_contract/models/move.py | 3 - .../models/recurring_invoicer.py | 55 ---------------- .../security/ir.model.access.csv | 2 - .../views/recurring_invoicer_view.xml | 65 ------------------- .../views/recurring_invoicer_wizard_view.xml | 49 -------------- recurring_contract/wizards/__init__.py | 1 - .../wizards/recurring_invoicer_wizard.py | 50 -------------- 12 files changed, 33 insertions(+), 263 deletions(-) delete mode 100644 recurring_contract/data/daily_invoicer_cron.xml create mode 100644 recurring_contract/migrations/18.0.1.1.0/post-migration.py delete mode 100644 recurring_contract/models/recurring_invoicer.py delete mode 100644 recurring_contract/views/recurring_invoicer_view.xml delete mode 100644 recurring_contract/views/recurring_invoicer_wizard_view.xml delete mode 100644 recurring_contract/wizards/recurring_invoicer_wizard.py diff --git a/recurring_contract/__manifest__.py b/recurring_contract/__manifest__.py index 266119ed7..07d18d87d 100644 --- a/recurring_contract/__manifest__.py +++ b/recurring_contract/__manifest__.py @@ -29,7 +29,7 @@ { "name": "Recurring contract", "summary": "Contract for recurring invoicing", - "version": "18.0.1.0.1", + "version": "18.0.1.1.0", "license": "AGPL-3", "author": "Compassion CH", "development_status": "Production/Stable", @@ -48,8 +48,6 @@ "views/activate_contract_view.xml", "views/contract_group_view.xml", "views/recurring_contract_view.xml", - "views/recurring_invoicer_view.xml", - "views/recurring_invoicer_wizard_view.xml", "views/res_config_settings_view.xml", "views/utm_medium_view.xml", "views/account_move_view.xml", @@ -57,7 +55,6 @@ "data/recurring_contract_sequence.xml", "data/contract_expire_cron.xml", "data/pricelist_item_base_automation.xml", - "data/daily_invoicer_cron.xml", "data/utm_data.xml", "data/queue_job.xml", "security/ir.model.access.csv", diff --git a/recurring_contract/data/daily_invoicer_cron.xml b/recurring_contract/data/daily_invoicer_cron.xml deleted file mode 100644 index 9040fd1ee..000000000 --- a/recurring_contract/data/daily_invoicer_cron.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - Launch daily invoice generation - - - 1 - days - - model.generate_from_cron() - - - diff --git a/recurring_contract/migrations/18.0.1.1.0/post-migration.py b/recurring_contract/migrations/18.0.1.1.0/post-migration.py new file mode 100644 index 000000000..767806e66 --- /dev/null +++ b/recurring_contract/migrations/18.0.1.1.0/post-migration.py @@ -0,0 +1,17 @@ +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + """Drop recurring.invoicer: remove FK column from account_move and drop table.""" + cr.execute( + "ALTER TABLE account_move DROP COLUMN IF EXISTS recurring_invoicer_id" + ) + _logger.info( + "post-migration: dropped recurring_invoicer_id column (%s rows affected)", + cr.rowcount, + ) + + cr.execute("DROP TABLE IF EXISTS recurring_invoicer CASCADE") + _logger.info("post-migration: dropped recurring_invoicer table") diff --git a/recurring_contract/models/__init__.py b/recurring_contract/models/__init__.py index a089a5cd4..02ed27f52 100644 --- a/recurring_contract/models/__init__.py +++ b/recurring_contract/models/__init__.py @@ -1,7 +1,6 @@ from . import contract_group from . import move from . import move_line -from . import recurring_invoicer from . import product_pricelist_item from . import recurring_contract from . import recurring_contract_line diff --git a/recurring_contract/models/contract_group.py b/recurring_contract/models/contract_group.py index 6f142e0f1..c36d0e729 100644 --- a/recurring_contract/models/contract_group.py +++ b/recurring_contract/models/contract_group.py @@ -242,15 +242,19 @@ def open_invoices(self): def button_generate_invoices(self): """Immediately generate invoices for the contract group.""" - invoicer = ( - self.with_context(queue_job__no_delay=True) - .with_company(self.company_id) - .generate_invoices() + before_invoice_ids = set( + self.mapped("active_contract_ids.invoice_line_ids.move_id").ids + ) + self.with_context(queue_job__no_delay=True).with_company( + self.company_id + ).generate_invoices() + after_invoice_ids = set( + self.mapped("active_contract_ids.invoice_line_ids.move_id").ids ) notification = { "type": "ir.actions.client", } - if invoicer.invoice_ids: + if after_invoice_ids - before_invoice_ids: notification["tag"] = "reload" else: msg = _( @@ -273,15 +277,13 @@ def button_generate_invoices(self): # PRIVATE METHODS # ########################################################################## def generate_invoices(self): - invoicer = self.env["recurring.invoicer"].create({}) for group in self: group.with_delay( priority=100, identity_key=self._name + ".generate_invoices." + str(group.id), - )._generate_invoices(invoicer) - return invoicer + )._generate_invoices() - def _generate_invoices(self, invoicer): + def _generate_invoices(self): """Checks all contracts and generate invoices if needed. Create an invoice per contract group per date. """ @@ -318,7 +320,7 @@ def _generate_invoices(self, invoicer): if invoice_key not in processed_invoices: # Process invoice generation if not already processed group.with_company(group.company_id)._process_invoice_generation( - invoicer, current_invoicing_date + current_invoicing_date ) # Add the invoice key to the set of processed invoices processed_invoices.add(invoice_key) @@ -386,7 +388,7 @@ def _should_skip_invoice_generation( ) return has_all_invoices - def _process_invoice_generation(self, invoicer, invoicing_date): + def _process_invoice_generation(self, invoicing_date): self.ensure_one() active_contracts = self.active_contract_ids open_invoices = active_contracts.mapped("open_invoice_ids").filtered( @@ -459,7 +461,7 @@ def _process_invoice_generation(self, invoicer, invoicing_date): open_invoice.action_post() else: # Building invoices data - inv_data = self._build_invoice_gen_data(invoicing_date, invoicer) + inv_data = self._build_invoice_gen_data(invoicing_date) # Creating the actual invoice _logger.info(f"Generating invoice : {inv_data}") invoice = self.env["account.move"].create(inv_data) @@ -473,7 +475,7 @@ def _process_invoice_generation(self, invoicer, invoicing_date): ) invoice.unlink() - def _build_invoice_gen_data(self, invoicing_date, invoicer, gift_wizard=False): + def _build_invoice_gen_data(self, invoicing_date, gift_wizard=False): """Setup a dict with data passed to invoice.create. If any custom data is wanted in invoice from contract group, just inherit this method. @@ -516,7 +518,6 @@ def _build_invoice_gen_data(self, invoicing_date, invoicer, gift_wizard=False): "journal_id": journal.id, "currency_id": self.currency_id.id, "invoice_date": invoicing_date, # Accountant date - "recurring_invoicer_id": invoicer.id, "pricelist_id": self.pricelist_id.id, "payment_mode_id": self.payment_mode_id.id, "company_id": self.company_id.id, diff --git a/recurring_contract/models/move.py b/recurring_contract/models/move.py index 7a531baf2..74b87a7b6 100644 --- a/recurring_contract/models/move.py +++ b/recurring_contract/models/move.py @@ -23,9 +23,6 @@ class AccountMove(models.Model): last_payment = fields.Date( "Paid on", compute="_compute_last_payment", store=True, tracking=True ) - recurring_invoicer_id = fields.Many2one( - "recurring.invoicer", "Invoicer", readonly=False - ) @api.depends("partner_id", "company_id") def _compute_pricelist_id(self): diff --git a/recurring_contract/models/recurring_invoicer.py b/recurring_contract/models/recurring_invoicer.py deleted file mode 100644 index 2ece00fcc..000000000 --- a/recurring_contract/models/recurring_invoicer.py +++ /dev/null @@ -1,55 +0,0 @@ -############################################################################## -# -# Copyright (C) 2014-2017 Compassion CH (http://www.compassion.ch) -# Releasing children from poverty in Jesus' name -# @author: Cyril Sester -# -# The licence is in the file __manifest__.py -# -############################################################################## - -import logging - -from odoo import _, fields, models - -logger = logging.getLogger(__name__) - - -class RecurringInvoicer(models.Model): - """An invoicer holds a bunch of invoices that have been generated - in the same context. It also makes the validating or cancelling process - of these contracts easy. - """ - - _name = "recurring.invoicer" - _order = "generation_date desc" - _description = "Recurring invoicer" - - generation_date = fields.Datetime(default=fields.Datetime.now) - invoice_ids = fields.One2many( - "account.move", "recurring_invoicer_id", "Generated invoices", readonly=False - ) - - def cancel_invoices(self): - """ - Cancel created invoices (set state from open to cancelled) - :return: True - """ - invoice_to_cancel = self.mapped("invoice_ids").filtered( - lambda invoice: invoice.state != "cancel" - ) - invoice_to_cancel.button_draft() - invoice_to_cancel.button_cancel() - return True - - def show_invoices(self): - return { - "type": "ir.actions.act_window", - "name": _("Invoices"), - "view_mode": "list,form", - "views": [[False, "list"], [False, "form"]], - "res_model": "account.move", - "domain": [("id", "in", self.invoice_ids.ids)], - "target": "current", - "context": self.env.context, - } diff --git a/recurring_contract/security/ir.model.access.csv b/recurring_contract/security/ir.model.access.csv index 289c2f8c0..5e34db48b 100644 --- a/recurring_contract/security/ir.model.access.csv +++ b/recurring_contract/security/ir.model.access.csv @@ -2,11 +2,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink write_access_recurring_contract,Write access on recurring.contract,model_recurring_contract,account.group_account_invoice,1,1,1,0 access_recurring_contract_line,Full access on recurring.contract.line,model_recurring_contract_line,account.group_account_invoice,1,1,1,1 write_access_recurring_contract_group,Write access on recurring.contract.group,model_recurring_contract_group,account.group_account_invoice,1,1,1,0 -access_recurring_invoicer,Full access on recurring.invoicer,model_recurring_invoicer,account.group_account_invoice,1,1,1,1 access_recurring_contract,Full access on recurring.contract,model_recurring_contract,account.group_account_manager,1,1,1,1 access_recurring_contract_group,Full access on recurring.contract.group,model_recurring_contract_group,account.group_account_manager,1,1,1,1 read_access_end_reason,Read access on recurring.contract.end.reason,model_recurring_contract_end_reason,account.group_account_invoice,1,0,0,0 full_access_end_reason,Full access on recurring.contract.end.reason,model_recurring_contract_end_reason,account.group_account_manager,1,1,1,1 -access_recurring_invoicer_wizard,access_recurring_invoicer_wizard,model_recurring_invoicer_wizard,base.group_user,1,0,0,0 access_recurring_contract_activate_wizard,access_recurring_contract_activate_wizard,model_recurring_contract_activate_wizard,base.group_user,1,1,1,1 access_end_contract_wizard,access_end_contract_wizard,model_end_contract_wizard,base.group_user,1,0,1,0 diff --git a/recurring_contract/views/recurring_invoicer_view.xml b/recurring_contract/views/recurring_invoicer_view.xml deleted file mode 100644 index 3aa19f242..000000000 --- a/recurring_contract/views/recurring_invoicer_view.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - recurring.invoicer.list - recurring.invoicer - - - - - - - - - - - - recurring.invoicer.form - recurring.invoicer - -
-
-
- -

- - -
-
-
-
- - - - Generated invoices - ir.actions.act_window - recurring.invoicer - list,form - - - -
diff --git a/recurring_contract/views/recurring_invoicer_wizard_view.xml b/recurring_contract/views/recurring_invoicer_wizard_view.xml deleted file mode 100644 index 34afab4e5..000000000 --- a/recurring_contract/views/recurring_invoicer_wizard_view.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - Recurring invoicer wizard view - recurring.invoicer.wizard - -
- -
-
- -
-
- - - Automatic invoice generation from contracts - recurring.invoicer.wizard - ir.actions.act_window - form - - {} - new - - - -
diff --git a/recurring_contract/wizards/__init__.py b/recurring_contract/wizards/__init__.py index afcc5482d..402a8f210 100644 --- a/recurring_contract/wizards/__init__.py +++ b/recurring_contract/wizards/__init__.py @@ -1,3 +1,2 @@ -from . import recurring_invoicer_wizard from . import contract_activation_wizard from . import end_contract_wizard diff --git a/recurring_contract/wizards/recurring_invoicer_wizard.py b/recurring_contract/wizards/recurring_invoicer_wizard.py deleted file mode 100644 index 360f74122..000000000 --- a/recurring_contract/wizards/recurring_invoicer_wizard.py +++ /dev/null @@ -1,50 +0,0 @@ -############################################################################## -# -# Copyright (C) 2014-2017 Compassion CH (http://www.compassion.ch) -# Releasing children from poverty in Jesus' name -# @author: Cyril Sester -# -# The licence is in the file __manifest__.py -# -############################################################################## - -from odoo import api, fields, models - - -class InvoicerWizard(models.TransientModel): - """This wizard generate invoices from contract groups when launched. - By default, all contract groups are used. - """ - - _name = "recurring.invoicer.wizard" - _description = "Recurring invoicer wizard" - - generation_date = fields.Date(readonly=True) - - def generate(self): - groups = self.env["recurring.contract.group"].search( - [ - "|", - ("invoice_suspended_until", "=", False), - ("invoice_suspended_until", "<", fields.Date.today()), - ("has_active_contracts", "=", True), - ] - ) - - # Add a job for all groups and start the job when all jobs are created. - invoicer = groups.generate_invoices() - res_id = False - if invoicer: - res_id = invoicer.id - return { - "name": "recurring.invoicer.form", - "view_mode": "form", - "res_id": res_id, # id of the object to which to redirect - "res_model": "recurring.invoicer", # object name - "type": "ir.actions.act_window", - } - - @api.model - def generate_from_cron(self): - self.generate() - return True From 07752e9c68716e2ebbf6053ef78d10820168f7a6 Mon Sep 17 00:00:00 2001 From: Daniel Palumbo Date: Wed, 1 Jul 2026 13:54:11 +0200 Subject: [PATCH 2/3] T3284 Restore invoice generation cron and menu action Add generate_from_cron() directly to ContractGroup to replace the deleted InvoicerWizard, create a new daily cron targeting that method, and replace the wizard-based menu action with an ir.actions.server action. Co-Authored-By: Claude Sonnet 4.6 --- recurring_contract/__manifest__.py | 1 + .../data/daily_invoice_generation_cron.xml | 19 +++++++++++++++++++ .../migrations/18.0.1.1.0/post-migration.py | 4 +--- recurring_contract/models/contract_group.py | 13 +++++++++++++ .../views/recurring_contract_view.xml | 16 ++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 recurring_contract/data/daily_invoice_generation_cron.xml diff --git a/recurring_contract/__manifest__.py b/recurring_contract/__manifest__.py index 07d18d87d..3bca2020c 100644 --- a/recurring_contract/__manifest__.py +++ b/recurring_contract/__manifest__.py @@ -54,6 +54,7 @@ "data/balance_product_for_migr.xml", "data/recurring_contract_sequence.xml", "data/contract_expire_cron.xml", + "data/daily_invoice_generation_cron.xml", "data/pricelist_item_base_automation.xml", "data/utm_data.xml", "data/queue_job.xml", diff --git a/recurring_contract/data/daily_invoice_generation_cron.xml b/recurring_contract/data/daily_invoice_generation_cron.xml new file mode 100644 index 000000000..3f2a264d5 --- /dev/null +++ b/recurring_contract/data/daily_invoice_generation_cron.xml @@ -0,0 +1,19 @@ + + + + + + Launch daily invoice generation + + + 1 + days + + model.generate_from_cron() + + + diff --git a/recurring_contract/migrations/18.0.1.1.0/post-migration.py b/recurring_contract/migrations/18.0.1.1.0/post-migration.py index 767806e66..b3ab86771 100644 --- a/recurring_contract/migrations/18.0.1.1.0/post-migration.py +++ b/recurring_contract/migrations/18.0.1.1.0/post-migration.py @@ -5,9 +5,7 @@ def migrate(cr, version): """Drop recurring.invoicer: remove FK column from account_move and drop table.""" - cr.execute( - "ALTER TABLE account_move DROP COLUMN IF EXISTS recurring_invoicer_id" - ) + cr.execute("ALTER TABLE account_move DROP COLUMN IF EXISTS recurring_invoicer_id") _logger.info( "post-migration: dropped recurring_invoicer_id column (%s rows affected)", cr.rowcount, diff --git a/recurring_contract/models/contract_group.py b/recurring_contract/models/contract_group.py index c36d0e729..b3cad0dc0 100644 --- a/recurring_contract/models/contract_group.py +++ b/recurring_contract/models/contract_group.py @@ -276,6 +276,19 @@ def button_generate_invoices(self): ########################################################################## # PRIVATE METHODS # ########################################################################## + @api.model + def generate_from_cron(self): + """Entry point for the daily invoice generation cron.""" + groups = self.search( + [ + "|", + ("invoice_suspended_until", "=", False), + ("invoice_suspended_until", "<", fields.Date.today()), + ("has_active_contracts", "=", True), + ] + ) + groups.generate_invoices() + def generate_invoices(self): for group in self: group.with_delay( diff --git a/recurring_contract/views/recurring_contract_view.xml b/recurring_contract/views/recurring_contract_view.xml index eb76e202c..0950f4282 100644 --- a/recurring_contract/views/recurring_contract_view.xml +++ b/recurring_contract/views/recurring_contract_view.xml @@ -319,4 +319,20 @@ action="action_recurring_contract_form" sequence="5" /> + + + Launch invoices generation + + code + model.generate_from_cron() + + + From 8876eca6b4d378eed5fe2f4437d3100e7b418fc4 Mon Sep 17 00:00:00 2001 From: Daniel Palumbo Date: Wed, 1 Jul 2026 15:57:19 +0200 Subject: [PATCH 3/3] T3284 Remove stale act_window before recreating as server action The XML ID action_invoice_automatic_generation existed in the DB as ir.actions.act_window from the deleted wizard view. Pre-migration deletes it so Odoo can recreate it as ir.actions.server during the upgrade. Co-Authored-By: Claude Sonnet 4.6 --- .../migrations/18.0.1.1.0/pre-migration.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 recurring_contract/migrations/18.0.1.1.0/pre-migration.py diff --git a/recurring_contract/migrations/18.0.1.1.0/pre-migration.py b/recurring_contract/migrations/18.0.1.1.0/pre-migration.py new file mode 100644 index 000000000..ebc305a94 --- /dev/null +++ b/recurring_contract/migrations/18.0.1.1.0/pre-migration.py @@ -0,0 +1,29 @@ +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + """Remove stale ir.actions.act_window for action_invoice_automatic_generation. + + This action is recreated as ir.actions.server in recurring_contract_view.xml. + Odoo refuses to update a record if the model type changes, so we delete it first. + """ + cr.execute(""" + DELETE FROM ir_act_window + WHERE id IN ( + SELECT res_id FROM ir_model_data + WHERE module = 'recurring_contract' + AND name = 'action_invoice_automatic_generation' + AND model = 'ir.actions.act_window' + ) + """) + cr.execute(""" + DELETE FROM ir_model_data + WHERE module = 'recurring_contract' + AND name = 'action_invoice_automatic_generation' + AND model = 'ir.actions.act_window' + """) + _logger.info( + "pre-migration: removed stale act_window action_invoice_automatic_generation" + )