Browse Source

[FIX] contract: Template lines handling (#92)

Update contract template lines handling to fix #80, and fix #59 #100
13.0-mig-contract
Dave Lasley 7 years ago
committed by Administrator
parent
commit
cc9c583f92
  1. 2
      contract/__manifest__.py
  2. 1
      contract/models/__init__.py
  3. 33
      contract/models/account_analytic_account.py
  4. 2
      contract/models/account_analytic_contract.py
  5. 19
      contract/models/account_analytic_contract_line.py
  6. 49
      contract/models/account_analytic_invoice_line.py
  7. 2
      contract/security/ir.model.access.csv
  8. 117
      contract/tests/test_contract.py

2
contract/__manifest__.py

@ -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, "

1
contract/models/__init__.py

@ -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

33
contract/models/account_analytic_account.py

@ -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,15 +47,27 @@ 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')
@ -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':

2
contract/models/account_analytic_contract.py

@ -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

@ -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',
)

49
contract/models/account_analytic_invoice_line.py

@ -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')
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( 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( 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
if self.analytic_account_id._name == 'account.analytic.account':
date = ( date = (
self.analytic_account_id.recurring_next_date or self.analytic_account_id.recurring_next_date or
fields.Datetime.now() 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,
partner=self.analytic_account_id.partner_id.id,
lang=partner.lang,
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,

2
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_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

117
contract/tests/test_contract.py

@ -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)
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.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.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): 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.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): def test_check_journal(self):
contract_no_journal = self.contract.copy() contract_no_journal = self.contract.copy()
@ -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)
Loading…
Cancel
Save