Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] contract: Template lines handling #92

Merged
merged 5 commits into from
Sep 25, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contract/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

{
'name': 'Contracts Management - Recurring',
'version': '10.0.1.1.0',
'version': '10.0.1.1.1',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module version should raise at least to 10.0.1.2.0, but this can be considered a huge change and it should be 10.0.2.0.0

'category': 'Contract Management',
'license': 'AGPL-3',
'author': "OpenERP SA, "
Expand Down
1 change: 1 addition & 0 deletions contract/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
35 changes: 31 additions & 4 deletions contract/models/account_analytic_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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):
Expand All @@ -61,6 +79,15 @@ def _onchange_recurring_invoices(self):
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':
Expand Down
2 changes: 1 addition & 1 deletion contract/models/account_analytic_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
50 changes: 50 additions & 0 deletions contract/models/account_analytic_contract_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, 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',
)

@api.multi
@api.onchange('product_id')
def _onchange_product_id(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this extra method is needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _onchange_product_id in account.analytic.invoice.line uses the partner pretty heavily, which isn't available in the contract.line. This method performs similar operation, just with the user partner instead.

I guess alternatively I could just add some try/excepts in the invoice line. I'll try that

if not self.product_id:
return {'domain': {'uom_id': []}}

vals = {}
domain = {'uom_id': [
('category_id', '=', self.product_id.uom_id.category_id.id)]}
if not self.uom_id or (self.product_id.uom_id.category_id.id !=
self.uom_id.category_id.id):
vals['uom_id'] = self.product_id.uom_id

product = self.product_id.with_context(
lang=self.env.user.partner_id.lang,
partner=self.env.user.partner_id.id,
quantity=self.quantity,
date=fields.Datetime.now(),
pricelist=self.analytic_account_id.pricelist_id.id,
uom=self.uom_id.id,
)

name = product.name_get()[0][1]
if product.description_sale:
name += '\n' + product.description_sale
vals['name'] = name

vals['price_unit'] = product.price
self.update(vals)
return {'domain': domain}
39 changes: 30 additions & 9 deletions contract/models/account_analytic_invoice_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# © 2014 Angel Moya <angel.moya@domatix.com>
# © 2015 Pedro M. Baeza <pedro.baeza@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).

from odoo import api, fields, models
Expand All @@ -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',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewers - sorry I killed the diff here - I was having a hard time auditing the fields for required, etc. so had to clean up the spacing

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')
Expand Down
2 changes: 2 additions & 0 deletions contract/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
119 changes: 106 additions & 13 deletions contract/tests/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,36 @@ def setUp(self):
'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#',
'quantity': 1,
'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):
Expand Down Expand Up @@ -122,10 +133,10 @@ def test_onchange_recurring_invoices(self):

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()
Expand All @@ -134,8 +145,8 @@ def test_onchange_product_id(self):

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()
Expand All @@ -146,7 +157,7 @@ def test_check_journal(self):
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 = {
Expand All @@ -156,6 +167,88 @@ def test_onchange_contract_template_id(self):
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)