Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions recurring_contract/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -48,16 +48,14 @@
"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",
"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/daily_invoicer_cron.xml",
"data/utm_data.xml",
"security/ir.model.access.csv",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
-->
<odoo>
<data noupdate="1">
<record id="recurring_invoicer_cron" model="ir.cron">
<record id="invoice_generation_cron" model="ir.cron">
<field name="name">Launch daily invoice generation</field>
<field name="active" eval="True" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="model_id" ref="model_recurring_invoicer_wizard" />
<field name="model_id" ref="model_recurring_contract_group" />
<field name="code">model.generate_from_cron()</field>
</record>
</data>
Expand Down
15 changes: 15 additions & 0 deletions recurring_contract/migrations/18.0.1.1.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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")
29 changes: 29 additions & 0 deletions recurring_contract/migrations/18.0.1.1.0/pre-migration.py
Original file line number Diff line number Diff line change
@@ -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'
)
""")
Comment on lines +12 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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'
)
""")

cr.execute("""
DELETE FROM ir_model_data
WHERE module = 'recurring_contract'
AND name = 'action_invoice_automatic_generation'
AND model = 'ir.actions.act_window'
""")
Comment on lines +12 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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).

Repro: command transcript and before-after SQL rows showing recurring_invoicer_cron survives after migration

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

_logger.info(
"pre-migration: removed stale act_window action_invoice_automatic_generation"
)
1 change: 0 additions & 1 deletion recurring_contract/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
46 changes: 27 additions & 19 deletions recurring_contract/models/contract_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Comment on lines 243 to +247

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
        )

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 = _(
Expand All @@ -272,29 +276,35 @@ 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):
invoicer = self.env["recurring.invoicer"].create({})
for group in self:
group.with_delay_sh(
"_generate_invoices",
invoicer.id,
channel="root.accounting",
priority=100,
identity_key=self._name + ".generate_invoices." + str(group.id),
)
return invoicer

def _generate_invoices(self, invoicer_id=False):
def _generate_invoices(self):
"""Checks all contracts and generate invoices if needed.
Create an invoice per contract group per date.
"""
_logger.info(
f"Starting generation of invoices for contract groups : {self.ids}"
)
if invoicer_id:
invoicer = self.env["recurring.invoicer"].browse(invoicer_id)
else:
invoicer = self.env["recurring.invoicer"].create({})

# Set to track processed invoices to avoid duplication
processed_invoices = set()
Expand Down Expand Up @@ -325,15 +335,14 @@ def _generate_invoices(self, invoicer_id=False):
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)

# Refresh state to check whether invoices are missing in some contracts
self.mapped("active_contract_ids")._compute_missing_invoices()
_logger.info("Process successfully generated invoices")
return invoicer

def _calculate_start_date_and_offset(self):
"""
Expand Down Expand Up @@ -393,7 +402,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(
Expand Down Expand Up @@ -466,7 +475,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)
Expand All @@ -480,7 +489,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.
Expand Down Expand Up @@ -523,7 +532,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,
Expand Down
3 changes: 0 additions & 3 deletions recurring_contract/models/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
55 changes: 0 additions & 55 deletions recurring_contract/models/recurring_invoicer.py

This file was deleted.

2 changes: 0 additions & 2 deletions recurring_contract/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions recurring_contract/views/recurring_contract_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,20 @@
action="action_recurring_contract_form"
sequence="5"
/>

<record id="action_invoice_automatic_generation" model="ir.actions.server">
<field name="name">Launch invoices generation</field>
<field name="model_id" ref="model_recurring_contract_group" />
<field name="state">code</field>
<field name="code">model.generate_from_cron()</field>
</record>

<menuitem
id="menu_invoice_automatic_generation"
name="Launch invoices generation"
parent="menu_contracts_section"
sequence="10"
action="action_invoice_automatic_generation"
groups="account.group_account_manager"
/>
</odoo>
65 changes: 0 additions & 65 deletions recurring_contract/views/recurring_invoicer_view.xml

This file was deleted.

Loading
Loading