T3284 - remove recurring invoicer object - #279
Conversation
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request removes the recurring.invoicer model and its associated wizard, refactoring the invoice generation process to run directly on recurring.contract.group via a new cron method. It also introduces pre- and post-migration scripts to clean up the database. Feedback on the changes suggests deleting from ir_actions instead of ir_act_window in the pre-migration script to avoid leaving orphaned rows due to Odoo's inheritance structure, and adding self.ensure_one() to button_generate_invoices to prevent potential errors when executed on multi-record or empty recordsets.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| 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' | ||
| ) | ||
| """) |
There was a problem hiding this comment.
In Odoo, ir.actions.act_window inherits from ir.actions.actions via _inherits. The parent table is ir_actions and the child table is ir_act_window. Deleting directly from ir_act_window will leave a stale orphaned row in ir_actions because the foreign key cascade only works from parent to child. To clean up both tables properly, you should delete from ir_actions instead.
| 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_actions | |
| 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' | |
| ) | |
| """) |
| 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 | ||
| ) |
There was a problem hiding this comment.
The button_generate_invoices method accesses self.company_id directly. If this method is ever called on an empty recordset or a recordset with multiple records (e.g., from a list view or server action), it will raise an error or behave unexpectedly. Adding self.ensure_one() at the beginning of the method ensures safe execution and adheres to defensive programming practices.
def button_generate_invoices(self):
"""Immediately generate invoices for the contract group."""
self.ensure_one()
before_invoice_ids = set(
self.mapped("active_contract_ids.invoice_line_ids.move_id").ids
)
Confidence Score: 4/5This PR needs a migration fix before it is safe for upgraded databases. The main refactor is localized and consistent, but upgraded databases can keep a scheduled action that points to the removed wizard model.
What T-Rex did
Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User as Accounting User / Cron
participant Action as Server Action / ir.cron
participant Group as recurring.contract.group
participant Queue as queue_job
participant Move as account.move
User->>Action: Launch invoices generation
Action->>Group: generate_from_cron()
Group->>Group: search active, non-suspended groups
Group->>Queue: with_delay_sh(_generate_invoices)
Queue->>Group: _generate_invoices()
Group->>Group: _build_invoice_gen_data(invoicing_date)
Group->>Move: create(inv_data)
Move-->>Group: generated invoice
Group->>Move: action_post()
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User as Accounting User / Cron
participant Action as Server Action / ir.cron
participant Group as recurring.contract.group
participant Queue as queue_job
participant Move as account.move
User->>Action: Launch invoices generation
Action->>Group: generate_from_cron()
Group->>Group: search active, non-suspended groups
Group->>Queue: with_delay_sh(_generate_invoices)
Queue->>Group: _generate_invoices()
Group->>Group: _build_invoice_gen_data(invoicing_date)
Group->>Move: create(inv_data)
Move-->>Group: generated invoice
Group->>Move: action_post()
|
| 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' | ||
| """) |
There was a problem hiding this comment.
Remove stale cron
The migration deletes the old action_invoice_automatic_generation act-window XML record before recreating it, but it leaves the old recurring_invoicer_cron record from the removed daily_invoicer_cron.xml. Existing databases will keep that noupdate cron pointing at model_recurring_invoicer_wizard, while this PR removes the wizard model and adds a new invoice_generation_cron, so upgrades retain a broken and duplicate scheduled action. Delete or retarget the old cron and its XML id during migration.
Artifacts
Repro: focused migration harness that seeds old and new cron XML IDs and runs pre-migration.py
- Contains supporting evidence from the run (text/x-python; charset=utf-8).
- Keeps the command output available without making the summary code-heavy.
The invoice generation code is complex. We historically use a recurring.invoicer model that keeps the history of generated invoices but this has no real benefit to our users and can be safely removed. It will also simplify the code and the database inprint.
See other PR related to it
Module 1 — recurring_contract (compassion-accounting)
The invoicer wizard and its menu were replaced by a server action. Verification:
Module 2 — sponsorship_compassion (compassion-modules)
The invoicer was removed from _generate_invoices and _generate_gifts. Test from a Sponsorships → Sponsorships (S, SC, or SWP contract):
Contract Group form → "Generate invoices" button: invoices generate correctly AND birthday/Christmas gift invoices are still generated alongside regular invoices
Module 3 — sponsorship_switzerland (compassion-switzerland)