mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
[FIX] contract: Template lines handling (#92)
Update contract template lines handling to fix #80, and fix #59 #100
This commit is contained in:
committed by
Pedro M. Baeza
parent
2f0c3ad020
commit
48f159085c
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Contracts Management - Recurring',
|
'name': 'Contracts Management - Recurring',
|
||||||
'version': '10.0.1.1.0',
|
'version': '10.0.2.0.0',
|
||||||
'category': 'Contract Management',
|
'category': 'Contract Management',
|
||||||
'license': 'AGPL-3',
|
'license': 'AGPL-3',
|
||||||
'author': "OpenERP SA, "
|
'author': "OpenERP SA, "
|
||||||
|
|||||||
@@ -4,4 +4,5 @@
|
|||||||
from . import account_analytic_contract
|
from . import account_analytic_contract
|
||||||
from . import account_analytic_account
|
from . import account_analytic_account
|
||||||
from . import account_analytic_invoice_line
|
from . import account_analytic_invoice_line
|
||||||
|
from . import account_analytic_contract_line
|
||||||
from . import account_invoice
|
from . import account_invoice
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ class AccountAnalyticAccount(models.Model):
|
|||||||
string='Contract Template',
|
string='Contract Template',
|
||||||
comodel_name='account.analytic.contract',
|
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)
|
date_start = fields.Date(default=fields.Date.context_today)
|
||||||
recurring_invoices = fields.Boolean(
|
recurring_invoices = fields.Boolean(
|
||||||
string='Generate recurring invoices automatically',
|
string='Generate recurring invoices automatically',
|
||||||
@@ -41,16 +47,28 @@ class AccountAnalyticAccount(models.Model):
|
|||||||
|
|
||||||
@api.onchange('contract_template_id')
|
@api.onchange('contract_template_id')
|
||||||
def _onchange_contract_template_id(self):
|
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
|
contract = self.contract_template_id
|
||||||
|
|
||||||
for field_name, field in contract._fields.iteritems():
|
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.compute, field.related, field.automatic,
|
||||||
field.readonly, field.company_dependent,
|
field.readonly, field.company_dependent,
|
||||||
field.name in self.NO_SYNC,
|
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')
|
@api.onchange('recurring_invoices')
|
||||||
def _onchange_recurring_invoices(self):
|
def _onchange_recurring_invoices(self):
|
||||||
@@ -61,6 +79,15 @@ class AccountAnalyticAccount(models.Model):
|
|||||||
def _onchange_partner_id(self):
|
def _onchange_partner_id(self):
|
||||||
self.pricelist_id = self.partner_id.property_product_pricelist.id
|
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
|
@api.model
|
||||||
def get_relative_delta(self, recurring_rule_type, interval):
|
def get_relative_delta(self, recurring_rule_type, interval):
|
||||||
if recurring_rule_type == 'daily':
|
if recurring_rule_type == 'daily':
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class AccountAnalyticContract(models.Model):
|
|||||||
string='Pricelist',
|
string='Pricelist',
|
||||||
)
|
)
|
||||||
recurring_invoice_line_ids = fields.One2many(
|
recurring_invoice_line_ids = fields.One2many(
|
||||||
comodel_name='account.analytic.invoice.line',
|
comodel_name='account.analytic.contract.line',
|
||||||
inverse_name='analytic_account_id',
|
inverse_name='analytic_account_id',
|
||||||
copy=True,
|
copy=True,
|
||||||
string='Invoice Lines',
|
string='Invoice Lines',
|
||||||
|
|||||||
19
contract/models/account_analytic_contract_line.py
Normal file
19
contract/models/account_analytic_contract_line.py
Normal file
@@ -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',
|
||||||
|
)
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
# © 2014 Angel Moya <angel.moya@domatix.com>
|
# © 2014 Angel Moya <angel.moya@domatix.com>
|
||||||
# © 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
# © 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||||
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
|
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||||
# Copyright 2016 LasLabs Inc.
|
# Copyright 2016-2017 LasLabs Inc.
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
@@ -16,23 +16,44 @@ class AccountAnalyticInvoiceLine(models.Model):
|
|||||||
_name = 'account.analytic.invoice.line'
|
_name = 'account.analytic.invoice.line'
|
||||||
|
|
||||||
product_id = fields.Many2one(
|
product_id = fields.Many2one(
|
||||||
'product.product', string='Product', required=True)
|
'product.product',
|
||||||
|
string='Product',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
analytic_account_id = fields.Many2one(
|
analytic_account_id = fields.Many2one(
|
||||||
'account.analytic.account', string='Analytic Account')
|
'account.analytic.account',
|
||||||
name = fields.Text(string='Description', required=True)
|
string='Analytic Account',
|
||||||
quantity = fields.Float(default=1.0, required=True)
|
required=True,
|
||||||
|
ondelete='cascade',
|
||||||
|
)
|
||||||
|
name = fields.Text(
|
||||||
|
string='Description',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
quantity = fields.Float(
|
||||||
|
default=1.0,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
uom_id = fields.Many2one(
|
uom_id = fields.Many2one(
|
||||||
'product.uom', string='Unit of Measure', required=True)
|
'product.uom',
|
||||||
price_unit = fields.Float('Unit Price', required=True)
|
string='Unit of Measure',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
price_unit = fields.Float(
|
||||||
|
'Unit Price',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
price_subtotal = fields.Float(
|
price_subtotal = fields.Float(
|
||||||
compute='_compute_price_subtotal',
|
compute='_compute_price_subtotal',
|
||||||
digits=dp.get_precision('Account'),
|
digits=dp.get_precision('Account'),
|
||||||
string='Sub Total')
|
string='Sub Total',
|
||||||
|
)
|
||||||
discount = fields.Float(
|
discount = fields.Float(
|
||||||
string='Discount (%)',
|
string='Discount (%)',
|
||||||
digits=dp.get_precision('Discount'),
|
digits=dp.get_precision('Discount'),
|
||||||
help='Discount that is applied in generated invoices.'
|
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.multi
|
||||||
@api.depends('quantity', 'price_unit', 'discount')
|
@api.depends('quantity', 'price_unit', 'discount')
|
||||||
@@ -68,14 +89,20 @@ class AccountAnalyticInvoiceLine(models.Model):
|
|||||||
self.uom_id.category_id.id):
|
self.uom_id.category_id.id):
|
||||||
vals['uom_id'] = self.product_id.uom_id
|
vals['uom_id'] = self.product_id.uom_id
|
||||||
|
|
||||||
date = (
|
if self.analytic_account_id._name == 'account.analytic.account':
|
||||||
self.analytic_account_id.recurring_next_date or
|
date = (
|
||||||
fields.Datetime.now()
|
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(
|
product = self.product_id.with_context(
|
||||||
lang=self.analytic_account_id.partner_id.lang,
|
lang=partner.lang,
|
||||||
partner=self.analytic_account_id.partner_id.id,
|
partner=partner.id,
|
||||||
quantity=self.quantity,
|
quantity=self.quantity,
|
||||||
date=date,
|
date=date,
|
||||||
pricelist=self.analytic_account_id.pricelist_id.id,
|
pricelist=self.analytic_account_id.pricelist_id.id,
|
||||||
|
|||||||
@@ -3,3 +3,5 @@
|
|||||||
"account_analytic_contract_user","Recurring user","model_account_analytic_contract","account.group_account_user",1,0,0,0
|
"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_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_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
|
||||||
|
|||||||
|
@@ -31,7 +31,7 @@ class TestContract(TransactionCase):
|
|||||||
'date_start': '2016-02-15',
|
'date_start': '2016-02-15',
|
||||||
'recurring_next_date': '2016-02-29',
|
'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,
|
'analytic_account_id': self.contract.id,
|
||||||
'product_id': self.product.id,
|
'product_id': self.product.id,
|
||||||
'name': 'Services from #START# to #END#',
|
'name': 'Services from #START# to #END#',
|
||||||
@@ -39,17 +39,28 @@ class TestContract(TransactionCase):
|
|||||||
'uom_id': self.product.uom_id.id,
|
'uom_id': self.product.uom_id.id,
|
||||||
'price_unit': 100,
|
'price_unit': 100,
|
||||||
'discount': 50,
|
'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):
|
def test_check_discount(self):
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
self.contract_line.write({'discount': 120})
|
self.acct_line.write({'discount': 120})
|
||||||
|
|
||||||
def test_contract(self):
|
def test_contract(self):
|
||||||
self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0)
|
self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0)
|
||||||
res = self.contract_line._onchange_product_id()
|
res = self.acct_line._onchange_product_id()
|
||||||
self.assertIn('uom_id', res['domain'])
|
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
|
self.contract.partner_id = False
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@@ -122,10 +133,10 @@ class TestContract(TransactionCase):
|
|||||||
|
|
||||||
def test_uom(self):
|
def test_uom(self):
|
||||||
uom_litre = self.env.ref('product.product_uom_litre')
|
uom_litre = self.env.ref('product.product_uom_litre')
|
||||||
self.contract_line.uom_id = uom_litre.id
|
self.acct_line.uom_id = uom_litre.id
|
||||||
self.contract_line._onchange_product_id()
|
self.acct_line._onchange_product_id()
|
||||||
self.assertEqual(self.contract_line.uom_id,
|
self.assertEqual(self.acct_line.uom_id,
|
||||||
self.contract_line.product_id.uom_id)
|
self.acct_line.product_id.uom_id)
|
||||||
|
|
||||||
def test_onchange_product_id(self):
|
def test_onchange_product_id(self):
|
||||||
line = self.env['account.analytic.invoice.line'].new()
|
line = self.env['account.analytic.invoice.line'].new()
|
||||||
@@ -134,8 +145,8 @@ class TestContract(TransactionCase):
|
|||||||
|
|
||||||
def test_no_pricelist(self):
|
def test_no_pricelist(self):
|
||||||
self.contract.pricelist_id = False
|
self.contract.pricelist_id = False
|
||||||
self.contract_line.quantity = 2
|
self.acct_line.quantity = 2
|
||||||
self.assertAlmostEqual(self.contract_line.price_subtotal, 100.0)
|
self.assertAlmostEqual(self.acct_line.price_subtotal, 100.0)
|
||||||
|
|
||||||
def test_check_journal(self):
|
def test_check_journal(self):
|
||||||
contract_no_journal = self.contract.copy()
|
contract_no_journal = self.contract.copy()
|
||||||
@@ -146,7 +157,7 @@ class TestContract(TransactionCase):
|
|||||||
contract_no_journal.recurring_create_invoice()
|
contract_no_journal.recurring_create_invoice()
|
||||||
|
|
||||||
def test_onchange_contract_template_id(self):
|
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.contract_template_id = self.template
|
||||||
self.contract._onchange_contract_template_id()
|
self.contract._onchange_contract_template_id()
|
||||||
res = {
|
res = {
|
||||||
@@ -156,6 +167,88 @@ class TestContract(TransactionCase):
|
|||||||
del self.template_vals['name']
|
del self.template_vals['name']
|
||||||
self.assertDictEqual(res, self.template_vals)
|
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):
|
def test_send_mail_contract(self):
|
||||||
result = self.contract.action_contract_send()
|
result = self.contract.action_contract_send()
|
||||||
self.assertEqual(result['res_model'], 'mail.compose.message')
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user