diff --git a/recurring_contract/__manifest__.py b/recurring_contract/__manifest__.py
index 1f0f6536..0fecc2e2 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,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",
],
diff --git a/recurring_contract/data/daily_invoicer_cron.xml b/recurring_contract/data/daily_invoice_generation_cron.xml
similarity index 81%
rename from recurring_contract/data/daily_invoicer_cron.xml
rename to recurring_contract/data/daily_invoice_generation_cron.xml
index 9040fd1e..3f2a264d 100644
--- a/recurring_contract/data/daily_invoicer_cron.xml
+++ b/recurring_contract/data/daily_invoice_generation_cron.xml
@@ -6,13 +6,13 @@
-->
-
+
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 00000000..b3ab8677
--- /dev/null
+++ b/recurring_contract/migrations/18.0.1.1.0/post-migration.py
@@ -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")
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 00000000..ebc305a9
--- /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"
+ )
diff --git a/recurring_contract/models/__init__.py b/recurring_contract/models/__init__.py
index a089a5cd..02ed27f5 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 6c754ac1..d9b21cef 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 = _(
@@ -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()
@@ -325,7 +335,7 @@ 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)
@@ -333,7 +343,6 @@ def _generate_invoices(self, invoicer_id=False):
# 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):
"""
@@ -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(
@@ -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)
@@ -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.
@@ -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,
diff --git a/recurring_contract/models/move.py b/recurring_contract/models/move.py
index 7a531baf..74b87a7b 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 2ece00fc..00000000
--- 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 289c2f8c..5e34db48 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_contract_view.xml b/recurring_contract/views/recurring_contract_view.xml
index eb76e202..0950f428 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()
+
+
+
diff --git a/recurring_contract/views/recurring_invoicer_view.xml b/recurring_contract/views/recurring_invoicer_view.xml
deleted file mode 100644
index 3aa19f24..00000000
--- 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 34afab4e..00000000
--- 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 afcc5482..402a8f21 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 360f7412..00000000
--- 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