From ec3f2514949226e8c7a19c7742b57da84a1f41dd Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Wed, 3 Nov 2021 18:20:54 +0100 Subject: [PATCH 01/16] contract: Improve invoice generation through cron Avoid too much sql queries by iterating on first search Avoid performances problems through invoices ids isntead of recordset --- contract/models/contract.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/contract/models/contract.py b/contract/models/contract.py index 4b588375e..f0928fca9 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -611,19 +611,22 @@ class ContractContract(models.Model): if not date_ref: date_ref = fields.Date.context_today(self) domain = self._get_contracts_to_invoice_domain(date_ref) - invoices = self.env["account.move"] + invoice_obj = self.env["account.move"] + + contracts = self.search(domain) + companies = set(contracts.mapped("company_id")) + invoice_ids = [] # Invoice by companies, so assignation emails get correct context - companies_to_invoice = self.read_group(domain, ["company_id"], ["company_id"]) - for row in companies_to_invoice: - contracts_to_invoice = ( - self.search(row["__domain"]) - .with_context(allowed_company_ids=[row["company_id"][0]]) - .filtered( - lambda a: not a.date_end or a.recurring_next_date <= a.date_end - ) + # Use ids instead of recordset for perfomances reasons + for company in companies: + contracts_to_invoice = contracts.filtered( + lambda c: c.company_id == company + and (not c.date_end or c.recurring_next_date <= c.date_end) + ).with_company(company) + invoice_ids.extend( + contracts_to_invoice._recurring_create_invoice(date_ref).ids ) - invoices |= contracts_to_invoice._recurring_create_invoice(date_ref) - return invoices + return invoice_obj.browse(invoice_ids) def action_terminate_contract(self): self.ensure_one() From 92e551cd22249ad82fd213162b7faedf863193f1 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 4 Nov 2021 11:10:53 +0100 Subject: [PATCH 02/16] contract: Improve tests Adds multi company tests --- contract/tests/__init__.py | 3 +- contract/tests/test_contract.py | 6 +- contract/tests/test_multicompany.py | 113 ++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 contract/tests/test_multicompany.py diff --git a/contract/tests/__init__.py b/contract/tests/__init__.py index 9de1b60ae..9a3586ace 100644 --- a/contract/tests/__init__.py +++ b/contract/tests/__init__.py @@ -1,5 +1,4 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - from . import test_contract from . import test_contract_manually_create_invoice from . import test_portal +from . import test_multicompany diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 37eea84f6..087cea7ac 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -18,7 +18,7 @@ def to_date(date): return fields.Date.to_date(date) -class TestContractBase(common.TransactionCase): +class TestContractBase: @classmethod def setUpClass(cls): super().setUpClass() @@ -2370,3 +2370,7 @@ class TestContract(TestContractBase): action = self.contract.action_preview() self.assertIn("/my/contracts/", action["url"]) self.assertIn("access_token=", action["url"]) + + +class TestContractCase(TestContract, common.SavepointCase): + """ TESTS """ diff --git a/contract/tests/test_multicompany.py b/contract/tests/test_multicompany.py new file mode 100644 index 000000000..734e6c81c --- /dev/null +++ b/contract/tests/test_multicompany.py @@ -0,0 +1,113 @@ +# Copyright 2021 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + +from .test_contract import TestContractBase + + +class ContractMulticompanyCase(TestContractBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.company_obj = cls.env["res.company"] + cls.company_1 = cls.env.ref("base.main_company") + cls.company_2 = cls.company_data_2["company"] + cls.env.user.company_ids |= cls.company_2 + + cls.contract_mc = ( + cls.env["contract.contract"] + .with_company(cls.company_2) + .create( + { + "name": "Test Contract MC", + "partner_id": cls.partner.id, + "pricelist_id": cls.partner.property_product_pricelist.id, + "line_recurrence": True, + "contract_type": "purchase", + "contract_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product_1.id, + "name": "Services from #START# to #END#", + "quantity": 1, + "uom_id": cls.product_1.uom_id.id, + "price_unit": 100, + "discount": 50, + "recurring_rule_type": "monthly", + "recurring_interval": 1, + "date_start": "2018-02-15", + "recurring_next_date": "2018-02-22", + }, + ) + ], + } + ) + ) + cls.line_vals = { + "contract_id": cls.contract_mc.id, + "product_id": cls.product_1.id, + "name": "Services from #START# to #END#", + "quantity": 1, + "uom_id": cls.product_1.uom_id.id, + "price_unit": 100, + "discount": 50, + "recurring_rule_type": "monthly", + "recurring_interval": 1, + "date_start": "2018-01-01", + "recurring_next_date": "2018-01-15", + "is_auto_renew": False, + } + cls.acct_line_mc = ( + cls.env["contract.line"].with_company(cls.company_2).create(cls.line_vals) + ) + + def test_cron_recurring_create_invoice_multi_company(self): + self.acct_line.date_start = "2018-01-01" + self.acct_line.recurring_invoicing_type = "post-paid" + self.acct_line.date_end = "2018-03-15" + + self.acct_line_mc.date_start = "2018-01-01" + self.acct_line_mc.recurring_invoicing_type = "post-paid" + self.acct_line_mc.date_end = "2018-03-15" + + contracts = self.contract2 + contracts_company_2 = self.env["contract.contract"].browse() + for _i in range(10): + contracts |= self.contract.copy() + for _i in range(10): + vals = ( + self.contract_mc.with_company(company=self.company_2) + .with_context(active_test=False) + .copy_data({"company_id": self.company_2.id}) + ) + contracts_company_2 |= self.contract_mc.with_company( + company=self.company_2 + ).create(vals) + self.env["contract.contract"].cron_recurring_create_invoice() + # Check company 1 + invoice_lines_company_1 = self.env["account.move.line"].search( + [("contract_line_id", "in", contracts.mapped("contract_line_ids").ids)] + ) + invoice_lines_company_2 = self.env["account.move.line"].search( + [ + ( + "contract_line_id", + "in", + contracts_company_2.mapped("contract_line_ids").ids, + ) + ] + ) + self.assertEqual( + len(contracts.mapped("contract_line_ids")), len(invoice_lines_company_1) + ) + self.assertEqual( + len(contracts_company_2.mapped("contract_line_ids")), + len(invoice_lines_company_2), + ) + + +class TestContractMultiCompany(ContractMulticompanyCase, AccountTestInvoicingCommon): + """ TESTS """ From a161a88387a595df93e7a501c51d0d9b819dba37 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 4 Nov 2021 11:43:35 +0100 Subject: [PATCH 03/16] contract: Add a generation type on contract Add a generation type on contract that allows to generate other document than invoice (e.g. sale order) --- contract/models/abstract_contract.py | 14 ++++++++++++++ contract/models/contract.py | 13 ++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/contract/models/abstract_contract.py b/contract/models/abstract_contract.py index 377f32f14..cb951f227 100644 --- a/contract/models/abstract_contract.py +++ b/contract/models/abstract_contract.py @@ -48,6 +48,20 @@ class ContractAbstractContract(models.AbstractModel): help="Mark this check if you want to control recurrrence at line level instead" " of all together for the whole contract.", ) + generation_type = fields.Selection( + string="Generation Type", + selection=lambda self: self._get_generation_type_selection(), + default=lambda self: self._get_default_generation_type(), + help="Choose the document that will be automatically generated by cron.", + ) + + @api.model + def _get_generation_type_selection(self): + return [("invoice", "Invoice")] + + @api.model + def _get_default_generation_type(self): + return "invoice" @api.onchange("contract_type") def _onchange_contract_type(self): diff --git a/contract/models/contract.py b/contract/models/contract.py index f0928fca9..c2a78d2af 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -9,6 +9,7 @@ from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError +from odoo.osv import expression from odoo.tests import Form from odoo.tools.translate import _ @@ -607,10 +608,16 @@ class ContractContract(models.Model): return moves @api.model - def cron_recurring_create_invoice(self, date_ref=None): + def _cron_recurring_create(self, date_ref=False, create_type="invoice"): if not date_ref: date_ref = fields.Date.context_today(self) domain = self._get_contracts_to_invoice_domain(date_ref) + domain = expression.AND( + [ + domain, + [("generation_type", "=", create_type)], + ] + ) invoice_obj = self.env["account.move"] contracts = self.search(domain) @@ -628,6 +635,10 @@ class ContractContract(models.Model): ) return invoice_obj.browse(invoice_ids) + @api.model + def cron_recurring_create_invoice(self, date_ref=None): + return self._cron_recurring_create(date_ref) + def action_terminate_contract(self): self.ensure_one() context = {"default_contract_id": self.id} From ce38e335fabfdaeaf614c6a70a66afec861de67a Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 4 Nov 2021 13:27:41 +0100 Subject: [PATCH 04/16] contract: Allows to get several functions to create recurring documents --- contract/models/contract.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/contract/models/contract.py b/contract/models/contract.py index c2a78d2af..d00ee0c05 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -6,6 +6,7 @@ # Copyright 2018 ACSONE SA/NV # Copyright 2021 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError @@ -13,6 +14,8 @@ from odoo.osv import expression from odoo.tests import Form from odoo.tools.translate import _ +_logger = logging.getLogger(__name__) + class ContractContract(models.Model): _name = "contract.contract" @@ -609,6 +612,20 @@ class ContractContract(models.Model): @api.model def _cron_recurring_create(self, date_ref=False, create_type="invoice"): + """ + The cron function in order to create recurrent documents + from contracts. + """ + _recurring_create_func = f"_recurring_create_{create_type}" + if not hasattr(self, _recurring_create_func): + _logger.info( + _( + "No function to create %s documents automatically is " + "declared in contract.contract model. Passing." + ), + create_type, + ) + return False if not date_ref: date_ref = fields.Date.context_today(self) domain = self._get_contracts_to_invoice_domain(date_ref) @@ -618,22 +635,16 @@ class ContractContract(models.Model): [("generation_type", "=", create_type)], ] ) - invoice_obj = self.env["account.move"] - contracts = self.search(domain) companies = set(contracts.mapped("company_id")) - invoice_ids = [] # Invoice by companies, so assignation emails get correct context - # Use ids instead of recordset for perfomances reasons for company in companies: contracts_to_invoice = contracts.filtered( lambda c: c.company_id == company and (not c.date_end or c.recurring_next_date <= c.date_end) ).with_company(company) - invoice_ids.extend( - contracts_to_invoice._recurring_create_invoice(date_ref).ids - ) - return invoice_obj.browse(invoice_ids) + getattr(contracts_to_invoice, _recurring_create_func)(date_ref) + return True @api.model def cron_recurring_create_invoice(self, date_ref=None): From eedcdcd3d7869c939afcbaa6feb3147779d73bd5 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Sun, 7 Nov 2021 10:33:29 +0100 Subject: [PATCH 05/16] contract: Set visibility on button to show invoices --- contract/views/contract.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/contract/views/contract.xml b/contract/views/contract.xml index b788f4289..19b79a2d5 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -82,6 +82,7 @@ type="object" icon="fa-list" class="oe_stat_button" + attrs="{'invisible': [('generation_type','!=','invoice')]}" > Date: Sun, 7 Nov 2021 10:57:12 +0100 Subject: [PATCH 06/16] contract: Add generation_type field --- contract/views/contract.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contract/views/contract.xml b/contract/views/contract.xml index 19b79a2d5..6a516c68a 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -176,6 +176,9 @@ + + + Date: Mon, 8 Nov 2021 18:24:02 +0100 Subject: [PATCH 07/16] contract: Update button visibility --- contract/views/contract.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract/views/contract.xml b/contract/views/contract.xml index 6a516c68a..b28e6f8a1 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -40,7 +40,7 @@