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