From 5b4172213ec83efa03e0c6bac65ff5afc0ec7688 Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Mon, 25 Sep 2017 05:14:21 -0700 Subject: [PATCH] [FIX] contract: Template lines handling (#92) Update contract template lines handling to fix #80, and fix #59 #100 --- contract/__manifest__.py | 2 +- contract/models/__init__.py | 1 + contract/models/account_analytic_account.py | 35 +++++- contract/models/account_analytic_contract.py | 2 +- .../models/account_analytic_contract_line.py | 19 +++ .../models/account_analytic_invoice_line.py | 57 ++++++--- contract/security/ir.model.access.csv | 2 + contract/tests/test_contract.py | 119 ++++++++++++++++-- 8 files changed, 203 insertions(+), 34 deletions(-) create mode 100644 contract/models/account_analytic_contract_line.py diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 97383127..7bddc43d 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Contracts Management - Recurring', - 'version': '10.0.1.1.0', + 'version': '10.0.2.0.0', 'category': 'Contract Management', 'license': 'AGPL-3', 'author': "OpenERP SA, " diff --git a/contract/models/__init__.py b/contract/models/__init__.py index 7edc9a63..35a1de5e 100644 --- a/contract/models/__init__.py +++ b/contract/models/__init__.py @@ -4,4 +4,5 @@ from . import account_analytic_contract from . import account_analytic_account from . import account_analytic_invoice_line +from . import account_analytic_contract_line from . import account_invoice diff --git a/contract/models/account_analytic_account.py b/contract/models/account_analytic_account.py index 27340cac..b9b8355d 100644 --- a/contract/models/account_analytic_account.py +++ b/contract/models/account_analytic_account.py @@ -23,6 +23,12 @@ class AccountAnalyticAccount(models.Model): string='Contract Template', comodel_name='account.analytic.contract', ) + recurring_invoice_line_ids = fields.One2many( + string='Invoice Lines', + comodel_name='account.analytic.invoice.line', + inverse_name='analytic_account_id', + copy=True, + ) date_start = fields.Date(default=fields.Date.context_today) recurring_invoices = fields.Boolean( string='Generate recurring invoices automatically', @@ -41,16 +47,28 @@ class AccountAnalyticAccount(models.Model): @api.onchange('contract_template_id') def _onchange_contract_template_id(self): - """ It updates contract fields with that of the template """ + """Update the contract fields with that of the template. + + Take special consideration with the `recurring_invoice_line_ids`, + which must be created using the data from the contract lines. Cascade + deletion ensures that any errant lines that are created are also + deleted. + """ + contract = self.contract_template_id + for field_name, field in contract._fields.iteritems(): - if any(( + + if field.name == 'recurring_invoice_line_ids': + lines = self._convert_contract_lines(contract) + self.recurring_invoice_line_ids = lines + + elif not any(( field.compute, field.related, field.automatic, field.readonly, field.company_dependent, field.name in self.NO_SYNC, )): - continue - self[field_name] = self.contract_template_id[field_name] + self[field_name] = self.contract_template_id[field_name] @api.onchange('recurring_invoices') def _onchange_recurring_invoices(self): @@ -61,6 +79,15 @@ class AccountAnalyticAccount(models.Model): def _onchange_partner_id(self): self.pricelist_id = self.partner_id.property_product_pricelist.id + @api.multi + def _convert_contract_lines(self, contract): + self.ensure_one() + new_lines = [] + for contract_line in contract.recurring_invoice_line_ids: + vals = contract_line._convert_to_write(contract_line.read()[0]) + new_lines.append((0, 0, vals)) + return new_lines + @api.model def get_relative_delta(self, recurring_rule_type, interval): if recurring_rule_type == 'daily': diff --git a/contract/models/account_analytic_contract.py b/contract/models/account_analytic_contract.py index 6e46894a..5749f650 100644 --- a/contract/models/account_analytic_contract.py +++ b/contract/models/account_analytic_contract.py @@ -25,7 +25,7 @@ class AccountAnalyticContract(models.Model): string='Pricelist', ) recurring_invoice_line_ids = fields.One2many( - comodel_name='account.analytic.invoice.line', + comodel_name='account.analytic.contract.line', inverse_name='analytic_account_id', copy=True, string='Invoice Lines', diff --git a/contract/models/account_analytic_contract_line.py b/contract/models/account_analytic_contract_line.py new file mode 100644 index 00000000..b2222c9b --- /dev/null +++ b/contract/models/account_analytic_contract_line.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountAnalyticContractLine(models.Model): + + _name = 'account.analytic.contract.line' + _description = 'Contract Lines' + _inherit = 'account.analytic.invoice.line' + + analytic_account_id = fields.Many2one( + string='Contract', + comodel_name='account.analytic.contract', + required=True, + ondelete='cascade', + ) diff --git a/contract/models/account_analytic_invoice_line.py b/contract/models/account_analytic_invoice_line.py index 6467bbf2..8c87c062 100644 --- a/contract/models/account_analytic_invoice_line.py +++ b/contract/models/account_analytic_invoice_line.py @@ -3,7 +3,7 @@ # © 2014 Angel Moya # © 2015 Pedro M. Baeza # © 2016 Carlos Dauden -# Copyright 2016 LasLabs Inc. +# Copyright 2016-2017 LasLabs Inc. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import api, fields, models @@ -16,23 +16,44 @@ class AccountAnalyticInvoiceLine(models.Model): _name = 'account.analytic.invoice.line' product_id = fields.Many2one( - 'product.product', string='Product', required=True) + 'product.product', + string='Product', + required=True, + ) analytic_account_id = fields.Many2one( - 'account.analytic.account', string='Analytic Account') - name = fields.Text(string='Description', required=True) - quantity = fields.Float(default=1.0, required=True) + 'account.analytic.account', + string='Analytic Account', + required=True, + ondelete='cascade', + ) + name = fields.Text( + string='Description', + required=True, + ) + quantity = fields.Float( + default=1.0, + required=True, + ) uom_id = fields.Many2one( - 'product.uom', string='Unit of Measure', required=True) - price_unit = fields.Float('Unit Price', required=True) + 'product.uom', + string='Unit of Measure', + required=True, + ) + price_unit = fields.Float( + 'Unit Price', + required=True, + ) price_subtotal = fields.Float( compute='_compute_price_subtotal', digits=dp.get_precision('Account'), - string='Sub Total') + string='Sub Total', + ) discount = fields.Float( string='Discount (%)', digits=dp.get_precision('Discount'), help='Discount that is applied in generated invoices.' - ' It should be less or equal to 100') + ' It should be less or equal to 100', + ) @api.multi @api.depends('quantity', 'price_unit', 'discount') @@ -68,14 +89,20 @@ class AccountAnalyticInvoiceLine(models.Model): self.uom_id.category_id.id): vals['uom_id'] = self.product_id.uom_id - date = ( - self.analytic_account_id.recurring_next_date or - fields.Datetime.now() - ) + if self.analytic_account_id._name == 'account.analytic.account': + date = ( + self.analytic_account_id.recurring_next_date or + fields.Datetime.now() + ) + partner = self.analytic_account_id.partner_id + + else: + date = fields.Datetime.now() + partner = self.env.user.partner_id product = self.product_id.with_context( - lang=self.analytic_account_id.partner_id.lang, - partner=self.analytic_account_id.partner_id.id, + lang=partner.lang, + partner=partner.id, quantity=self.quantity, date=date, pricelist=self.analytic_account_id.pricelist_id.id, diff --git a/contract/security/ir.model.access.csv b/contract/security/ir.model.access.csv index 75ca4b72..937da35f 100644 --- a/contract/security/ir.model.access.csv +++ b/contract/security/ir.model.access.csv @@ -3,3 +3,5 @@ "account_analytic_contract_user","Recurring user","model_account_analytic_contract","account.group_account_user",1,0,0,0 "account_analytic_invoice_line_manager","Recurring manager","model_account_analytic_invoice_line","account.group_account_manager",1,1,1,1 "account_analytic_invoice_line_user","Recurring user","model_account_analytic_invoice_line","account.group_account_user",1,0,0,0 +"account_analytic_contract_line_manager","Recurring manager","model_account_analytic_contract_line","account.group_account_manager",1,1,1,1 +"account_analytic_contract_line_user","Recurring user","model_account_analytic_contract_line","account.group_account_user",1,0,0,0 diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index a1acb758..44fff4e9 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -31,7 +31,7 @@ class TestContract(TransactionCase): 'date_start': '2016-02-15', 'recurring_next_date': '2016-02-29', }) - self.contract_line = self.env['account.analytic.invoice.line'].create({ + self.line_vals = { 'analytic_account_id': self.contract.id, 'product_id': self.product.id, 'name': 'Services from #START# to #END#', @@ -39,17 +39,28 @@ class TestContract(TransactionCase): 'uom_id': self.product.uom_id.id, 'price_unit': 100, 'discount': 50, - }) + } + self.acct_line = self.env['account.analytic.invoice.line'].create( + self.line_vals, + ) + + def _add_template_line(self, overrides=None): + if overrides is None: + overrides = {} + vals = self.line_vals.copy() + vals['analytic_account_id'] = self.template.id + vals.update(overrides) + return self.env['account.analytic.contract.line'].create(vals) def test_check_discount(self): with self.assertRaises(ValidationError): - self.contract_line.write({'discount': 120}) + self.acct_line.write({'discount': 120}) def test_contract(self): - self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0) - res = self.contract_line._onchange_product_id() + self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0) + res = self.acct_line._onchange_product_id() self.assertIn('uom_id', res['domain']) - self.contract_line.price_unit = 100.0 + self.acct_line.price_unit = 100.0 self.contract.partner_id = False with self.assertRaises(ValidationError): @@ -122,10 +133,10 @@ class TestContract(TransactionCase): def test_uom(self): uom_litre = self.env.ref('product.product_uom_litre') - self.contract_line.uom_id = uom_litre.id - self.contract_line._onchange_product_id() - self.assertEqual(self.contract_line.uom_id, - self.contract_line.product_id.uom_id) + self.acct_line.uom_id = uom_litre.id + self.acct_line._onchange_product_id() + self.assertEqual(self.acct_line.uom_id, + self.acct_line.product_id.uom_id) def test_onchange_product_id(self): line = self.env['account.analytic.invoice.line'].new() @@ -134,8 +145,8 @@ class TestContract(TransactionCase): def test_no_pricelist(self): self.contract.pricelist_id = False - self.contract_line.quantity = 2 - self.assertAlmostEqual(self.contract_line.price_subtotal, 100.0) + self.acct_line.quantity = 2 + self.assertAlmostEqual(self.acct_line.price_subtotal, 100.0) def test_check_journal(self): contract_no_journal = self.contract.copy() @@ -146,7 +157,7 @@ class TestContract(TransactionCase): contract_no_journal.recurring_create_invoice() def test_onchange_contract_template_id(self): - """ It should change the contract values to match the template. """ + """It should change the contract values to match the template.""" self.contract.contract_template_id = self.template self.contract._onchange_contract_template_id() res = { @@ -156,6 +167,88 @@ class TestContract(TransactionCase): del self.template_vals['name'] self.assertDictEqual(res, self.template_vals) + def test_onchange_contract_template_id_lines(self): + """It should create invoice lines for the contract lines.""" + + self.acct_line.unlink() + self.line_vals['analytic_account_id'] = self.template.id + self.env['account.analytic.contract.line'].create(self.line_vals) + self.contract.contract_template_id = self.template + + self.assertFalse(self.contract.recurring_invoice_line_ids, + 'Recurring lines were not removed.') + + self.contract._onchange_contract_template_id() + del self.line_vals['analytic_account_id'] + + self.assertEqual(len(self.contract.recurring_invoice_line_ids), 1) + + for key, value in self.line_vals.items(): + test_value = self.contract.recurring_invoice_line_ids[0][key] + try: + test_value = test_value.id + except AttributeError: + pass + self.assertEqual(test_value, value) + def test_send_mail_contract(self): result = self.contract.action_contract_send() self.assertEqual(result['res_model'], 'mail.compose.message') + + def test_contract_onchange_product_id_domain_blank(self): + """It should return a blank UoM domain when no product.""" + line = self.env['account.analytic.contract.line'].new() + res = line._onchange_product_id() + self.assertFalse(res['domain']['uom_id']) + + def test_contract_onchange_product_id_domain(self): + """It should return UoM category domain.""" + line = self._add_template_line() + res = line._onchange_product_id() + self.assertEqual( + res['domain']['uom_id'][0], + ('category_id', '=', self.product.uom_id.category_id.id), + ) + + def test_contract_onchange_product_id_uom(self): + """It should update the UoM for the line.""" + line = self._add_template_line( + {'uom_id': self.env.ref('product.product_uom_litre').id} + ) + line.product_id.uom_id = self.env.ref('product.product_uom_day').id + line._onchange_product_id() + self.assertEqual(line.uom_id, + line.product_id.uom_id) + + def test_contract_onchange_product_id_name(self): + """It should update the name for the line.""" + line = self._add_template_line() + line.product_id.description_sale = 'Test' + line._onchange_product_id() + self.assertEqual(line.name, + '\n'.join([line.product_id.name, + line.product_id.description_sale, + ])) + + def test_contract(self): + self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0) + res = self.acct_line._onchange_product_id() + self.assertIn('uom_id', res['domain']) + self.acct_line.price_unit = 100.0 + + self.contract.partner_id = False + with self.assertRaises(ValidationError): + self.contract.recurring_create_invoice() + self.contract.partner_id = self.partner.id + + self.contract.recurring_create_invoice() + self.invoice_monthly = self.env['account.invoice'].search( + [('contract_id', '=', self.contract.id)]) + self.assertTrue(self.invoice_monthly) + self.assertEqual(self.contract.recurring_next_date, '2016-03-29') + + self.inv_line = self.invoice_monthly.invoice_line_ids[0] + self.assertTrue(self.inv_line.invoice_line_tax_ids) + self.assertAlmostEqual(self.inv_line.price_subtotal, 50.0) + self.assertEqual(self.contract.partner_id.user_id, + self.invoice_monthly.user_id)