Browse Source

[IMP][10.0] contract: Add templating (#42)

Add template functionality for contracts
pull/202/head
Dave Lasley 8 years ago
committed by Pedro M. Baeza
parent
commit
dcf3ff0877
  1. 1
      contract/README.rst
  2. 8
      contract/__manifest__.py
  3. 7
      contract/models/__init__.py
  4. 159
      contract/models/account_analytic_account.py
  5. 71
      contract/models/account_analytic_contract.py
  6. 87
      contract/models/account_analytic_invoice_line.py
  7. 0
      contract/models/account_invoice.py
  8. 3
      contract/security/ir.model.access.csv
  9. 19
      contract/tests/test_contract.py
  10. 65
      contract/views/account_analytic_account_view.xml
  11. 118
      contract/views/account_analytic_contract_view.xml
  12. 12
      contract/views/account_invoice_view.xml

1
contract/README.rst

@ -67,6 +67,7 @@ Contributors
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
* Carlos Dauden <carlos.dauden@tecnativa.com>
* Angel Moya <angel.moya@domatix.com>
* Dave Lasley <dave@laslabs.com>
Maintainer
----------

8
contract/__manifest__.py

@ -4,19 +4,21 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Contracts Management recurring',
'version': '10.0.1.0.0',
'name': 'Contracts Management - Recurring',
'version': '10.0.1.0.1',
'category': 'Contract Management',
'license': 'AGPL-3',
'author': "OpenERP SA, "
"Tecnativa, "
"LasLabs, "
"Odoo Community Association (OCA)",
'website': 'https://github.com/oca/contract',
'depends': ['base', 'account', 'analytic'],
'data': [
'security/ir.model.access.csv',
'data/contract_cron.xml',
'views/contract.xml',
'views/account_analytic_account_view.xml',
'views/account_analytic_contract_view.xml',
'views/account_invoice_view.xml',
],
'installable': True,

7
contract/models/__init__.py

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import contract
from . import invoice
from . import account_analytic_contract
from . import account_analytic_account
from . import account_analytic_invoice_line
from . import account_invoice

159
contract/models/contract.py → contract/models/account_analytic_account.py

@ -3,157 +3,58 @@
# © 2014 Angel Moya <angel.moya@domatix.com>
# © 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
import logging
from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError
from odoo.tools.translate import _
_logger = logging.getLogger(__name__)
class AccountAnalyticInvoiceLine(models.Model):
_name = 'account.analytic.invoice.line'
product_id = fields.Many2one(
'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)
uom_id = fields.Many2one(
'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')
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')
@api.multi
@api.depends('quantity', 'price_unit', 'discount')
def _compute_price_subtotal(self):
for line in self:
subtotal = line.quantity * line.price_unit
discount = line.discount / 100
subtotal *= 1 - discount
if line.analytic_account_id.pricelist_id:
cur = line.analytic_account_id.pricelist_id.currency_id
line.price_subtotal = cur.round(subtotal)
else:
line.price_subtotal = subtotal
@api.multi
@api.constrains('discount')
def _check_discount(self):
for line in self:
if line.discount > 100:
raise ValidationError(
_("Discount should be less or equal to 100"))
@api.multi
@api.onchange('product_id')
def _onchange_product_id(self):
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.analytic_account_id.partner_id.lang,
partner=self.analytic_account_id.partner_id.id,
quantity=self.quantity,
date=self.analytic_account_id.recurring_next_date,
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}
class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account'
@api.model
def _default_journal(self):
company_id = self.env.context.get(
'company_id', self.env.user.company_id.id)
domain = [
('type', '=', 'sale'),
('company_id', '=', company_id)]
return self.env['account.journal'].search(domain, limit=1)
pricelist_id = fields.Many2one(
comodel_name='product.pricelist',
string='Pricelist')
_name = 'account.analytic.account'
_inherit = ['account.analytic.account',
'account.analytic.contract',
]
contract_template_id = fields.Many2one(
string='Contract Template',
comodel_name='account.analytic.contract',
)
date_start = fields.Date(default=fields.Date.context_today)
recurring_invoice_line_ids = fields.One2many(
comodel_name='account.analytic.invoice.line',
inverse_name='analytic_account_id',
copy=True,
string='Invoice Lines')
recurring_invoices = fields.Boolean(
string='Generate recurring invoices automatically')
recurring_rule_type = fields.Selection(
[('daily', 'Day(s)'),
('weekly', 'Week(s)'),
('monthly', 'Month(s)'),
('monthlylastday', 'Month(s) last day'),
('yearly', 'Year(s)'),
],
default='monthly',
string='Recurrency',
help="Specify Interval for automatic invoice generation.")
recurring_invoicing_type = fields.Selection(
[('pre-paid', 'Pre-paid'),
('post-paid', 'Post-paid'),
],
default='pre-paid',
string='Invoicing type',
help="Specify if process date is 'from' or 'to' invoicing date")
recurring_interval = fields.Integer(
default=1,
string='Repeat Every',
help="Repeat every (Days/Week/Month/Year)")
string='Generate recurring invoices automatically',
)
recurring_next_date = fields.Date(
default=fields.Date.context_today,
copy=False,
string='Date of Next Invoice')
journal_id = fields.Many2one(
'account.journal',
string='Journal',
default=_default_journal,
domain="[('type', '=', 'sale'),('company_id', '=', company_id)]")
string='Date of Next Invoice',
)
@api.onchange('partner_id')
def _onchange_partner_id(self):
self.pricelist_id = self.partner_id.property_product_pricelist.id
@api.onchange('contract_template_id')
def _onchange_contract_template_id(self):
""" It updates contract fields with that of the template """
contract = self.contract_template_id
for field_name, field in contract._fields.iteritems():
if 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]
@api.onchange('recurring_invoices')
def _onchange_recurring_invoices(self):
if self.date_start and self.recurring_invoices:
self.recurring_next_date = self.date_start
@api.onchange('partner_id')
def _onchange_partner_id(self):
self.pricelist_id = self.partner_id.property_product_pricelist.id
@api.model
def get_relative_delta(self, recurring_rule_type, interval):
if recurring_rule_type == 'daily':

71
contract/models/account_analytic_contract.py

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# © 2004-2010 OpenERP SA
# © 2014 Angel Moya <angel.moya@domatix.com>
# © 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2016-2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountAnalyticContract(models.Model):
_name = 'account.analytic.contract'
# These fields will not be synced to the contract
NO_SYNC = [
'name',
]
name = fields.Char(
required=True,
)
pricelist_id = fields.Many2one(
comodel_name='product.pricelist',
string='Pricelist',
)
recurring_invoice_line_ids = fields.One2many(
comodel_name='account.analytic.invoice.line',
inverse_name='analytic_account_id',
copy=True,
string='Invoice Lines',
)
recurring_rule_type = fields.Selection(
[('daily', 'Day(s)'),
('weekly', 'Week(s)'),
('monthly', 'Month(s)'),
('monthlylastday', 'Month(s) last day'),
('yearly', 'Year(s)'),
],
default='monthly',
string='Recurrence',
help="Specify Interval for automatic invoice generation.",
)
recurring_invoicing_type = fields.Selection(
[('pre-paid', 'Pre-paid'),
('post-paid', 'Post-paid'),
],
default='pre-paid',
string='Invoicing type',
help="Specify if process date is 'from' or 'to' invoicing date",
)
recurring_interval = fields.Integer(
default=1,
string='Repeat Every',
help="Repeat every (Days/Week/Month/Year)",
)
journal_id = fields.Many2one(
'account.journal',
string='Journal',
default=lambda s: s._default_journal(),
domain="[('type', '=', 'sale'),('company_id', '=', company_id)]",
)
@api.model
def _default_journal(self):
company_id = self.env.context.get(
'company_id', self.env.user.company_id.id)
domain = [
('type', '=', 'sale'),
('company_id', '=', company_id)]
return self.env['account.journal'].search(domain, limit=1)

87
contract/models/account_analytic_invoice_line.py

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# © 2004-2010 OpenERP SA
# © 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.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError
from odoo.tools.translate import _
class AccountAnalyticInvoiceLine(models.Model):
_name = 'account.analytic.invoice.line'
product_id = fields.Many2one(
'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)
uom_id = fields.Many2one(
'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')
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')
@api.multi
@api.depends('quantity', 'price_unit', 'discount')
def _compute_price_subtotal(self):
for line in self:
subtotal = line.quantity * line.price_unit
discount = line.discount / 100
subtotal *= 1 - discount
if line.analytic_account_id.pricelist_id:
cur = line.analytic_account_id.pricelist_id.currency_id
line.price_subtotal = cur.round(subtotal)
else:
line.price_subtotal = subtotal
@api.multi
@api.constrains('discount')
def _check_discount(self):
for line in self:
if line.discount > 100:
raise ValidationError(
_("Discount should be less or equal to 100"))
@api.multi
@api.onchange('product_id')
def _onchange_product_id(self):
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.analytic_account_id.partner_id.lang,
partner=self.analytic_account_id.partner_id.id,
quantity=self.quantity,
date=self.analytic_account_id.recurring_next_date,
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}

0
contract/models/invoice.py → contract/models/account_invoice.py

3
contract/security/ir.model.access.csv

@ -1,4 +1,5 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"account_analytic_contract_manager","Recurring manager","model_account_analytic_contract","account.group_account_manager",1,1,1,1
"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

19
contract/tests/test_contract.py

@ -15,6 +15,14 @@ class TestContract(TransactionCase):
self.product.taxes_id += self.env['account.tax'].search(
[('type_tax_use', '=', 'sale')], limit=1)
self.product.description_sale = 'Test description sale'
self.template_vals = {
'recurring_rule_type': 'yearly',
'recurring_interval': 12345,
'name': 'Test Contract Template',
}
self.template = self.env['account.analytic.contract'].create(
self.template_vals,
)
self.contract = self.env['account.analytic.account'].create({
'name': 'Test Contract',
'partner_id': self.partner.id,
@ -136,3 +144,14 @@ class TestContract(TransactionCase):
journal.write({'type': 'general'})
with self.assertRaises(ValidationError):
contract_no_journal.recurring_create_invoice()
def test_onchange_contract_template_id(self):
""" It should change the contract values to match the template. """
self.contract.contract_template_id = self.template
self.contract._onchange_contract_template_id()
res = {
'recurring_rule_type': self.contract.recurring_rule_type,
'recurring_interval': self.contract.recurring_interval,
}
del self.template_vals['name']
self.assertDictEqual(res, self.template_vals)

65
contract/views/contract.xml → contract/views/account_analytic_account_view.xml

@ -1,49 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="act_recurring_invoices" model="ir.actions.act_window">
<field name="context">{'search_default_contract_id':
[active_id],
'default_contract_id': active_id}
</field>
<field name="name">Invoices</field>
<field name="res_model">account.invoice</field>
<field name="view_id" ref="account.invoice_tree" />
<field name="search_view_id" ref="account.view_account_invoice_filter"/>
</record>
<record id="account_analytic_account_recurring_form_form" model="ir.ui.view">
<field name="name">account.analytic.account.invoice.recurring.form.inherit</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field eval="40" name="priority"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<group name="main" position="after">
<separator string="Recurring Invoices" attrs="{'invisible': [('recurring_invoices','!=',True)]}"/>
<separator string="Recurring Invoices"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
/>
<div>
<field name="recurring_invoices" class="oe_inline"/>
<label for="recurring_invoices" />
<button name="recurring_create_invoice" type="object"
<button name="recurring_create_invoice"
type="object"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
string="Create invoices" class="oe_link"
groups="base.group_no_one"/>
<button name="%(contract.act_recurring_invoices)d" type="action"
string="Create invoices"
class="oe_link"
groups="base.group_no_one"
/>
<button name="contract.act_recurring_invoices"
type="action"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
string="⇒ Show recurring invoices" class="oe_link"/>
string="⇒ Show recurring invoices"
class="oe_link"
/>
</div>
<group col="4" attrs="{'invisible': [('recurring_invoices','!=',True)]}">
<field name="contract_template_id" />
<field name="journal_id"/>
<field name="pricelist_id"/>
<label for="recurring_interval"/>
<div>
<field name="recurring_interval" class="oe_inline" attrs="{'required': [('recurring_invoices', '=', True)]}"/>
<field name="recurring_rule_type" class="oe_inline" attrs="{'required': [('recurring_invoices', '=', True)]}"/>
<field name="recurring_interval"
class="oe_inline"
attrs="{'required': [('recurring_invoices', '=', True)]}"
/>
<field name="recurring_rule_type"
class="oe_inline"
attrs="{'required': [('recurring_invoices', '=', True)]}"
/>
</div>
<field name="recurring_invoicing_type"/>
<field name="date_start"/>
<field name="recurring_next_date"/>
</group>
<label for="recurring_invoice_line_ids" attrs="{'invisible': [('recurring_invoices','=',False)]}"/>
<label for="recurring_invoice_line_ids"
attrs="{'invisible': [('recurring_invoices','=',False)]}"
/>
<div attrs="{'invisible': [('recurring_invoices','=',False)]}">
<field name="recurring_invoice_line_ids">
<tree string="Account Analytic Lines" editable="bottom">
@ -87,9 +93,15 @@
<field name="name" position="after">
<field name="journal_id"/>
<field name="pricelist_id"/>
<filter name="recurring_invoices" string="Recurring Invoices" domain="[('recurring_invoices','=',True)]"/>
<filter name="recurring_invoices"
string="Recurring Invoices"
domain="[('recurring_invoices','=',True)]"
/>
<group expand="0" string="Group By...">
<filter string="Next Invoice" domain="[]" context="{'group_by':'recurring_next_date'}"/>
<filter string="Next Invoice"
domain="[]"
context="{'group_by':'recurring_next_date'}"
/>
</group>
</field>
</field>
@ -109,6 +121,11 @@
</p>
</field>
</record>
<menuitem action="action_account_analytic_overdue_all" id="menu_action_account_analytic_overdue_all" sequence="99" parent="account.menu_finance_receivables"/>
<menuitem id="menu_action_account_analytic_overdue_all"
parent="account.menu_finance_receivables"
action="action_account_analytic_overdue_all"
sequence="99"
/>
</odoo>

118
contract/views/account_analytic_contract_view.xml

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_contract_view_form" model="ir.ui.view">
<field name="name">Account Analytic Contract Form View</field>
<field name="model">account.analytic.contract</field>
<field name="arch" type="xml">
<form string="Contract Template">
<group name="group_main">
<field name="name" />
<group name="group_main_left">
<field name="journal_id" />
<field name="pricelist_id" />
</group>
<group name="group_main_right">
<field name="recurring_invoicing_type" />
<label for="recurring_interval" />
<div>
<field name="recurring_interval"
class="oe_inline"
required="True"
/>
<field name="recurring_rule_type"
class="oe_inline"
required="True"
/>
</div>
</group>
</group>
<group name="group_invoice_lines" string="Invoice Lines">
<field name="recurring_invoice_line_ids">
<tree string="Account Analytic Lines" editable="bottom">
<field name="product_id" />
<field name="name" />
<field name="quantity" />
<field name="uom_id" />
<field name="price_unit" />
<field name="discount" groups="sale.group_discount_per_so_line" />
<field name="price_subtotal" />
</tree>
</field>
</group>
<group name="group_legend"
string="Legend (for the markers inside invoice lines description)"
>
<p> <strong>#START#</strong>: Start date of the invoiced period</p>
<p> <strong>#END#</strong>: End date of the invoiced period</p>
</group>
</form>
</field>
</record>
<record id="account_analytic_contract_view_tree" model="ir.ui.view">
<field name="name">Account Analytic Contract Tree View</field>
<field name="model">account.analytic.contract</field>
<field name="arch" type="xml">
<tree string="Contract Templates">
<field name="name" />
<field name="recurring_rule_type" />
<field name="recurring_interval" />
<field name="recurring_invoicing_type" />
<field name="pricelist_id" />
</tree>
</field>
</record>
<record id="account_analytic_contract_view_search" model="ir.ui.view">
<field name="name">Account Analytic Contract Search View</field>
<field name="model">account.analytic.contract</field>
<field name="arch" type="xml">
<search string="Contract Templates">
<field name="name" />
<field name="recurring_rule_type" />
<field name="recurring_interval" />
<field name="recurring_invoicing_type" />
<field name="pricelist_id" />
<field name="journal_id" />
<filter string="Recurrence"
context="{'group_by': 'recurring_rule_type'}"
/>
<filter string="Invoicing Type"
context="{'group_by': 'recurring_invoicing_type'}"
/>
<filter string="Pricelist"
context="{'group_by': 'pricelist_id'}"
/>
<filter string="Journal"
context="{'group_by': 'journal_id'}"
/>
</search>
</field>
</record>
<record id="account_analytic_contract_action" model="ir.actions.act_window">
<field name="name">Contract Templates</field>
<field name="res_model">account.analytic.contract</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract template.
</p>
</field>
</record>
<menuitem id="menu_config_contract"
name="Contracts"
sequence="1"
parent="account.menu_finance_configuration"
/>
<menuitem id="account_analytic_contract_menu"
parent="menu_config_contract"
action="account_analytic_contract_action"
sequence="1"
/>
</odoo>

12
contract/views/account_invoice_view.xml

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Invoice search view with contract -->
<record id="view_account_invoice_filter_contract" model="ir.ui.view">
<field name="name">account.invoice.select.contract</field>
<field name="model">account.invoice</field>
@ -14,4 +13,15 @@
</field>
</record>
<record id="act_recurring_invoices" model="ir.actions.act_window">
<field name="context">{'search_default_contract_id':
[active_id],
'default_contract_id': active_id}
</field>
<field name="name">Invoices</field>
<field name="res_model">account.invoice</field>
<field name="view_id" ref="account.invoice_tree" />
<field name="search_view_id" ref="account.view_account_invoice_filter"/>
</record>
</odoo>
Loading…
Cancel
Save