From 3e876611f5db35ddf8d58f062427dd24e1659c8c Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Sat, 1 Apr 2017 08:14:55 -0700 Subject: [PATCH] [IMP][10.0] contract: Add templating (#42) Add template functionality for contracts --- contract/README.rst | 1 + contract/__manifest__.py | 12 +- contract/models/__init__.py | 7 +- ...ontract.py => account_analytic_account.py} | 161 ++++-------------- contract/models/account_analytic_contract.py | 71 ++++++++ .../models/account_analytic_invoice_line.py | 87 ++++++++++ .../models/{invoice.py => account_invoice.py} | 0 contract/security/ir.model.access.csv | 3 +- contract/tests/test_contract.py | 19 +++ ....xml => account_analytic_account_view.xml} | 65 ++++--- .../views/account_analytic_contract_view.xml | 118 +++++++++++++ contract/views/account_invoice_view.xml | 12 +- .../views/contract_view.xml | 5 - 13 files changed, 392 insertions(+), 169 deletions(-) rename contract/models/{contract.py => account_analytic_account.py} (58%) create mode 100644 contract/models/account_analytic_contract.py create mode 100644 contract/models/account_analytic_invoice_line.py rename contract/models/{invoice.py => account_invoice.py} (100%) rename contract/views/{contract.xml => account_analytic_account_view.xml} (69%) create mode 100644 contract/views/account_analytic_contract_view.xml diff --git a/contract/README.rst b/contract/README.rst index c073600c..496aa415 100644 --- a/contract/README.rst +++ b/contract/README.rst @@ -67,6 +67,7 @@ Contributors * Pedro M. Baeza * Carlos Dauden * Angel Moya +* Dave Lasley Maintainer ---------- diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 173be551..1b22ea13 100644 --- a/contract/__manifest__.py +++ b/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," + '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, diff --git a/contract/models/__init__.py b/contract/models/__init__.py index 8deef410..7edc9a63 100644 --- a/contract/models/__init__.py +++ b/contract/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -# © 2016 Carlos Dauden # 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 diff --git a/contract/models/contract.py b/contract/models/account_analytic_account.py similarity index 58% rename from contract/models/contract.py rename to contract/models/account_analytic_account.py index 89bd5fbe..986effc8 100644 --- a/contract/models/contract.py +++ b/contract/models/account_analytic_account.py @@ -3,157 +3,58 @@ # © 2014 Angel Moya # © 2015 Pedro M. Baeza # © 2016 Carlos Dauden +# 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)]") - - @api.onchange('partner_id') - def _onchange_partner_id(self): - self.pricelist_id = self.partner_id.property_product_pricelist.id + string='Date of Next Invoice', + ) + + @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': diff --git a/contract/models/account_analytic_contract.py b/contract/models/account_analytic_contract.py new file mode 100644 index 00000000..6e46894a --- /dev/null +++ b/contract/models/account_analytic_contract.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# © 2004-2010 OpenERP SA +# © 2014 Angel Moya +# © 2015 Pedro M. Baeza +# © 2016 Carlos Dauden +# 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) diff --git a/contract/models/account_analytic_invoice_line.py b/contract/models/account_analytic_invoice_line.py new file mode 100644 index 00000000..e3a0487a --- /dev/null +++ b/contract/models/account_analytic_invoice_line.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# © 2004-2010 OpenERP SA +# © 2014 Angel Moya +# © 2015 Pedro M. Baeza +# © 2016 Carlos Dauden +# 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} diff --git a/contract/models/invoice.py b/contract/models/account_invoice.py similarity index 100% rename from contract/models/invoice.py rename to contract/models/account_invoice.py diff --git a/contract/security/ir.model.access.csv b/contract/security/ir.model.access.csv index d477bb78..75ca4b72 100644 --- a/contract/security/ir.model.access.csv +++ b/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 - diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index be2d17e6..8cef9512 100644 --- a/contract/tests/test_contract.py +++ b/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) diff --git a/contract/views/contract.xml b/contract/views/account_analytic_account_view.xml similarity index 69% rename from contract/views/contract.xml rename to contract/views/account_analytic_account_view.xml index d9360d58..e416bc57 100644 --- a/contract/views/contract.xml +++ b/contract/views/account_analytic_account_view.xml @@ -1,49 +1,55 @@ - - {'search_default_contract_id': - [active_id], - 'default_contract_id': active_id} - - Invoices - account.invoice - - - - account.analytic.account.invoice.recurring.form.inherit account.analytic.account - + primary - +
+ -