diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index e4ced1f34..9f26c53aa 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -7,6 +7,7 @@ from collections import namedtuple from datetime import timedelta from dateutil.relativedelta import relativedelta +from freezegun import freeze_time from odoo import fields from odoo.exceptions import UserError, ValidationError @@ -2370,6 +2371,7 @@ class TestContract(TestContractBase): to_date("2018-02-13"), ) + @freeze_time("2020-01-01 00:00:00") def test_recurrency_propagation(self): # Existing contract vals = { diff --git a/product_contract/__manifest__.py b/product_contract/__manifest__.py index eff47ca39..0573a3b34 100644 --- a/product_contract/__manifest__.py +++ b/product_contract/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Recurring - Product Contract", - "version": "13.0.1.0.0", + "version": "14.0.1.0.0", "category": "Contract Management", "license": "AGPL-3", "author": "LasLabs, " "ACSONE SA/NV, " "Odoo Community Association (OCA)", diff --git a/product_contract/models/product_template.py b/product_contract/models/product_template.py index 8b17a92db..12bbeda64 100644 --- a/product_contract/models/product_template.py +++ b/product_contract/models/product_template.py @@ -65,7 +65,7 @@ class ProductTemplate(models.Model): def write(self, vals): if "is_contract" in vals and vals["is_contract"] is False: for company in self.env["res.company"].search([]): - self.with_context(force_company=company.id).write( + self.with_company(company).write( {"property_contract_template_id": False} ) super().write(vals) diff --git a/product_contract/models/sale_order.py b/product_contract/models/sale_order.py index a17d87748..44ffd41a7 100644 --- a/product_contract/models/sale_order.py +++ b/product_contract/models/sale_order.py @@ -16,15 +16,11 @@ class SaleOrder(models.Model): @api.constrains("state") def check_contact_is_not_terminated(self): for rec in self: - if ( - rec.state - not in ( - "sale", - "done", - "cancel", - ) - and rec.order_line.filtered("contract_id.is_terminated") - ): + if rec.state not in ( + "sale", + "done", + "cancel", + ) and rec.order_line.filtered("contract_id.is_terminated"): raise ValidationError( _("You can't upsell or downsell a terminated contract") ) @@ -81,8 +77,8 @@ class SaleOrder(models.Model): ) contract_templates = self.env["contract.template"] for order_line in line_to_create_contract: - contract_template = order_line.product_id.with_context( - force_company=rec.company_id.id + contract_template = order_line.product_id.with_company( + rec.company_id ).property_contract_template_id if not contract_template: raise ValidationError( @@ -94,8 +90,8 @@ class SaleOrder(models.Model): contract_templates |= contract_template for contract_template in contract_templates: order_lines = line_to_create_contract.filtered( - lambda r, template=contract_template: r.product_id.with_context( - force_company=r.order_id.company_id.id + lambda r, template=contract_template: r.product_id.with_company( + r.order_id.company_id ).property_contract_template_id == template ) @@ -112,7 +108,7 @@ class SaleOrder(models.Model): return contracts def action_confirm(self): - """ If we have a contract in the order, set it up """ + """If we have a contract in the order, set it up""" self.filtered( lambda order: (order.company_id.create_contract_at_sale_order_confirmation) ).action_create_contract() @@ -127,7 +123,7 @@ class SaleOrder(models.Model): def action_show_contracts(self): self.ensure_one() - action = self.env.ref("contract.action_customer_contract").read()[0] + action = self.env.ref("contract.action_customer_contract").sudo().read()[0] contracts = ( self.env["contract.line"] .search([("sale_order_line_id", "in", self.order_line.ids)]) diff --git a/product_contract/models/sale_order_line.py b/product_contract/models/sale_order_line.py index acfb08f39..6c261036e 100644 --- a/product_contract/models/sale_order_line.py +++ b/product_contract/models/sale_order_line.py @@ -84,8 +84,8 @@ class SaleOrderLine(models.Model): @api.depends("product_id") def _compute_contract_template_id(self): for rec in self: - rec.contract_template_id = rec.product_id.with_context( - force_company=rec.order_id.company_id.id + rec.contract_template_id = rec.product_id.with_company( + rec.order_id.company_id ).property_contract_template_id def _get_auto_renew_rule_type(self): @@ -95,9 +95,21 @@ class SaleOrderLine(models.Model): return "monthly" return self.recurring_rule_type + def _get_date_end(self): + self.ensure_one() + contract_line_model = self.env["contract.line"] + date_end = ( + self.date_start + + contract_line_model.get_relative_delta( + self._get_auto_renew_rule_type(), + int(self.product_uom_qty), + ) + - relativedelta(days=1) + ) + return date_end + @api.onchange("product_id") def onchange_product(self): - contract_line_model = self.env["contract.line"] for rec in self: if rec.product_id.is_contract: rec.product_uom_qty = rec.product_id.default_qty @@ -105,14 +117,7 @@ class SaleOrderLine(models.Model): rec.recurring_invoicing_type = rec.product_id.recurring_invoicing_type rec.date_start = rec.date_start or fields.Date.today() - rec.date_end = ( - rec.date_start - + contract_line_model.get_relative_delta( - rec._get_auto_renew_rule_type(), - int(rec.product_uom_qty), - ) - - relativedelta(days=1) - ) + rec.date_end = rec._get_date_end() rec.is_auto_renew = rec.product_id.is_auto_renew if rec.is_auto_renew: rec.auto_renew_interval = rec.product_id.auto_renew_interval @@ -120,19 +125,25 @@ class SaleOrderLine(models.Model): @api.onchange("date_start", "product_uom_qty", "recurring_rule_type") def onchange_date_start(self): - contract_line_model = self.env["contract.line"] for rec in self.filtered("product_id.is_contract"): - if not rec.date_start: - rec.date_end = False - else: - rec.date_end = ( - rec.date_start - + contract_line_model.get_relative_delta( - rec._get_auto_renew_rule_type(), - int(rec.product_uom_qty), - ) - - relativedelta(days=1) - ) + rec.date_end = rec._get_date_end() if rec.date_start else False + + def _get_contract_line_qty(self): + """Returns the quantity to be put on new contract lines.""" + self.ensure_one() + # The quantity on the generated contract line is 1, as it + # correspond to the most common use cases: + # - quantity on the SO line = number of periods sold and unit + # price the price of one period, so the + # total amount of the SO corresponds to the planned value + # of the contract; in this case the quantity on the contract + # line must be 1 + # - quantity on the SO line = number of hours sold, + # automatic invoicing of the actual hours through a variable + # quantity formula, in which case the quantity on the contract + # line is not used + # Other use cases are easy to implement by overriding this method. + return 1.0 def _prepare_contract_line_values( self, contract, predecessor_contract_line_id=False @@ -157,19 +168,7 @@ class SaleOrderLine(models.Model): "sequence": self.sequence, "product_id": self.product_id.id, "name": self.name, - # The quantity on the generated contract line is 1, as it - # correspond to the most common use cases: - # - quantity on the SO line = number of periods sold and unit - # price the price of one period, so the - # total amount of the SO corresponds to the planned value - # of the contract; in this case the quantity on the contract - # line must be 1 - # - quantity on the SO line = number of hours sold, - # automatic invoicing of the actual hours through a variable - # quantity formula, in which case the quantity on the contract - # line is not used - # Other use cases are easy to implement by overriding this method. - "quantity": 1.0, + "quantity": self._get_contract_line_qty(), "uom_id": self.product_uom.id, "price_unit": self.price_unit, "discount": self.discount, diff --git a/product_contract/tests/test_sale_order.py b/product_contract/tests/test_sale_order.py index 0a4362aa4..9d42152c9 100644 --- a/product_contract/tests/test_sale_order.py +++ b/product_contract/tests/test_sale_order.py @@ -4,7 +4,7 @@ from dateutil.relativedelta import relativedelta -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.fields import Date from odoo.tests.common import TransactionCase @@ -39,7 +39,7 @@ class TestSaleOrder(TransactionCase): ], } ) - self.product1.with_context(force_company=self.sale.company_id.id).write( + self.product1.with_company(self.sale.company_id).write( { "is_contract": True, "default_qty": 12, @@ -48,7 +48,7 @@ class TestSaleOrder(TransactionCase): "property_contract_template_id": self.contract_template1.id, } ) - self.product2.with_context(force_company=self.sale.company_id.id).write( + self.product2.with_company(self.sale.company_id).write( { "is_contract": True, "property_contract_template_id": self.contract_template2.id, @@ -116,26 +116,16 @@ class TestSaleOrder(TransactionCase): other_company = self.env["res.company"].create( {"name": "other company", "parent_id": self.sale.company_id.id} ) - self.sale.company_id = other_company - with self.assertRaises(ValidationError): + with self.assertRaises(UserError): + self.sale.company_id = other_company self.sale.action_confirm() def test_change_sale_company_2(self): """Contract company must be the sale order company.""" self.assertTrue(self.sale.company_id) - other_company = self.env["res.company"].create( - {"name": "other company", "parent_id": self.sale.company_id.id} - ) - self.product1.with_context( - force_company=other_company.id - ).property_contract_template_id = self.contract_template1 - self.product2.with_context( - force_company=other_company.id - ).property_contract_template_id = self.contract_template2 - self.sale.company_id = other_company self.sale.action_confirm() contracts = self.sale.order_line.mapped("contract_id") - self.assertEqual(contracts.mapped("company_id"), other_company) + self.assertEqual(contracts.mapped("company_id"), self.sale.company_id) def test_sale_order_invoice_status(self): """ @@ -353,7 +343,7 @@ class TestSaleOrder(TransactionCase): def test_order_lines_with_the_same_contract_template(self): """It should create one contract with two lines grouped by contract template""" - self.product2.with_context(force_company=self.sale.company_id.id).write( + self.product2.with_company(self.sale.company_id).write( { "is_contract": True, "property_contract_template_id": self.contract_template1.id, diff --git a/requirements.txt b/requirements.txt index 9cd162922..7d41f1be0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ # generated from manifests external_dependencies +python-dateutil diff --git a/setup/product_contract/odoo/addons/product_contract b/setup/product_contract/odoo/addons/product_contract new file mode 120000 index 000000000..8a36744fa --- /dev/null +++ b/setup/product_contract/odoo/addons/product_contract @@ -0,0 +1 @@ +../../../../product_contract \ No newline at end of file diff --git a/setup/product_contract/setup.py b/setup/product_contract/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/product_contract/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)