From abdf5bb20e79e01f64c809c590a77260ecf07c60 Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Fri, 9 Sep 2016 03:03:38 +0200 Subject: [PATCH] [ADD] contract_variable_quantity: ================================================= Variable quantity in contract recurrent invoicing ================================================= With this module, you will be able to define in recurring contracts some lines with variable quantity according a provided formula. Configuration ============= * Go to Sales > Configuration > Contracts > Formulas (quantity). * Define any formula based on Python code that stores at some moment a float/integer value of the quantity to invoice in the variable 'result'. You can use these variables to compute your formula: * *env*: Environment variable for getting other models. * *context*: Current context dictionary. * *user*: Current user. * *line*: Contract recurring invoice line that triggers this formula. * *contract*: Contract whose line belongs to. * *invoice*: Invoice (header) being created. Usage ===== To use this module, you need to: * Go to Sales -> Contracts and select or create a new contract. * Check *Generate recurring invoices automatically*. * Add a new recurring invoicing line. * Select "Variable quantity" in column "Qty. type". * Select one of the possible formulas to use (previously created). --- contract_variable_quantity/README.rst | 72 +++++ contract_variable_quantity/__init__.py | 4 + contract_variable_quantity/__openerp__.py | 21 ++ contract_variable_quantity/models/__init__.py | 4 + contract_variable_quantity/models/contract.py | 66 ++++ .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 5221 bytes .../static/description/icon.svg | 301 ++++++++++++++++++ contract_variable_quantity/tests/__init__.py | 4 + .../tests/test_contract_variable_quantity.py | 60 ++++ .../views/contract_view.xml | 90 ++++++ 11 files changed, 625 insertions(+) create mode 100644 contract_variable_quantity/README.rst create mode 100644 contract_variable_quantity/__init__.py create mode 100644 contract_variable_quantity/__openerp__.py create mode 100644 contract_variable_quantity/models/__init__.py create mode 100644 contract_variable_quantity/models/contract.py create mode 100644 contract_variable_quantity/security/ir.model.access.csv create mode 100644 contract_variable_quantity/static/description/icon.png create mode 100644 contract_variable_quantity/static/description/icon.svg create mode 100644 contract_variable_quantity/tests/__init__.py create mode 100644 contract_variable_quantity/tests/test_contract_variable_quantity.py create mode 100644 contract_variable_quantity/views/contract_view.xml diff --git a/contract_variable_quantity/README.rst b/contract_variable_quantity/README.rst new file mode 100644 index 00000000..e5786edf --- /dev/null +++ b/contract_variable_quantity/README.rst @@ -0,0 +1,72 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================================================= +Variable quantity in contract recurrent invoicing +================================================= + +With this module, you will be able to define in recurring contracts some +lines with variable quantity according a provided formula. + +Configuration +============= + +#. Go to Sales > Configuration > Contracts > Formulas (quantity). +#. Define any formula based on Python code that stores at some moment a + float/integer value of the quantity to invoice in the variable 'result'. + + You can use these variables to compute your formula: + + * *env*: Environment variable for getting other models. + * *context*: Current context dictionary. + * *user*: Current user. + * *line*: Contract recurring invoice line that triggers this formula. + * *contract*: Contract whose line belongs to. + * *invoice*: Invoice (header) being created. + +Usage +===== + +To use this module, you need to: + +#. Go to Sales -> Contracts and select or create a new contract. +#. Check *Generate recurring invoices automatically*. +#. Add a new recurring invoicing line. +#. Select "Variable quantity" in column "Qty. type". +#. Select one of the possible formulas to use (previously created). + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/110/9.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Pedro M. Baeza + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/contract_variable_quantity/__init__.py b/contract_variable_quantity/__init__.py new file mode 100644 index 00000000..ec50cfc0 --- /dev/null +++ b/contract_variable_quantity/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/contract_variable_quantity/__openerp__.py b/contract_variable_quantity/__openerp__.py new file mode 100644 index 00000000..faad6c56 --- /dev/null +++ b/contract_variable_quantity/__openerp__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# © 2016 Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Variable quantity in contract recurrent invoicing', + 'version': '9.0.1.0.0', + 'category': 'Contract Management', + 'license': 'AGPL-3', + 'author': "Tecnativa," + "Odoo Community Association (OCA)", + 'website': 'https://www.tecnativa.com', + 'depends': [ + 'contract', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/contract_view.xml', + ], + 'installable': True, +} diff --git a/contract_variable_quantity/models/__init__.py b/contract_variable_quantity/models/__init__.py new file mode 100644 index 00000000..35503b27 --- /dev/null +++ b/contract_variable_quantity/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import contract diff --git a/contract_variable_quantity/models/contract.py b/contract_variable_quantity/models/contract.py new file mode 100644 index 00000000..92c6c8d3 --- /dev/null +++ b/contract_variable_quantity/models/contract.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# © 2016 Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import _, api, fields, models, exceptions +from openerp.tools.safe_eval import safe_eval + + +class AccountAnalyticAccount(models.Model): + _inherit = "account.analytic.account" + + @api.model + def _prepare_invoice_line(self, line, invoice_id): + vals = super(AccountAnalyticAccount, self)._prepare_invoice_line( + line, invoice_id) + if line.qty_type == 'variable': + eval_context = { + 'env': self.env, + 'context': self.env.context, + 'user': self.env.user, + 'line': line, + 'contract': line.analytic_account_id, + 'invoice': self.env['account.invoice'].browse(invoice_id), + } + safe_eval(line.qty_formula_id.code.strip(), eval_context, + mode="exec", nocopy=True) # nocopy for returning result + vals['quantity'] = eval_context.get('result', 0) + return vals + + +class AccountAnalyticInvoiceLine(models.Model): + _inherit = 'account.analytic.invoice.line' + + qty_type = fields.Selection( + selection=[ + ('fixed', 'Fixed quantity'), + ('variable', 'Variable quantity'), + ], required=True, default='fixed', string="Qty. type") + qty_formula_id = fields.Many2one( + comodel_name="contract.line.qty.formula", string="Qty. formula") + + +class ContractLineFormula(models.Model): + _name = 'contract.line.qty.formula' + + name = fields.Char(required=True) + code = fields.Text(required=True, default="result = 0") + + @api.constrains('code') + def _check_code(self): + eval_context = { + 'env': self.env, + 'context': self.env.context, + 'user': self.env.user, + 'line': self.env['account.analytic.invoice.line'], + 'contract': self.env['account.analytic.account'], + 'invoice': self.env['account.invoice'], + } + try: + safe_eval( + self.code.strip(), eval_context, mode="exec", nocopy=True) + except Exception as e: + raise exceptions.ValidationError( + _('Error evaluating code.\nDetails: %s') % e) + if 'result' not in eval_context: + raise exceptions.ValidationError(_('No valid result returned.')) diff --git a/contract_variable_quantity/security/ir.model.access.csv b/contract_variable_quantity/security/ir.model.access.csv new file mode 100644 index 00000000..3c87dfeb --- /dev/null +++ b/contract_variable_quantity/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"contract_line_qty_formula_manager","Recurring formula manager","model_contract_line_qty_formula","base.group_sale_manager",1,1,1,1 +"contract_line_qty_formula_user","Recurring formula user","model_contract_line_qty_formula","base.group_sale_salesman",1,0,0,0 diff --git a/contract_variable_quantity/static/description/icon.png b/contract_variable_quantity/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3c00adfec43373940257b2a14fe64e3b59a33e35 GIT binary patch literal 5221 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 literal 0 HcmV?d00001 diff --git a/contract_variable_quantity/static/description/icon.svg b/contract_variable_quantity/static/description/icon.svg new file mode 100644 index 00000000..92888efe --- /dev/null +++ b/contract_variable_quantity/static/description/icon.svg @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Pile of Golden Coins + 2010-04-09T03:27:45 + A pile of hypothetical golden coins, drawn in Inkscape. + https://openclipart.org/detail/43969/pile-of-golden-coins-by-j_alves + + + J_Alves + + + + + coin + currency + gold + money + thaler + + + + + + + + + + + diff --git a/contract_variable_quantity/tests/__init__.py b/contract_variable_quantity/tests/__init__.py new file mode 100644 index 00000000..b772135a --- /dev/null +++ b/contract_variable_quantity/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_contract_variable_quantity diff --git a/contract_variable_quantity/tests/test_contract_variable_quantity.py b/contract_variable_quantity/tests/test_contract_variable_quantity.py new file mode 100644 index 00000000..d87a9f29 --- /dev/null +++ b/contract_variable_quantity/tests/test_contract_variable_quantity.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# © 2016 Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp.tests import common +from openerp import exceptions + + +class TestContractVariableQuantity(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestContractVariableQuantity, cls).setUpClass() + cls.partner = cls.env['res.partner'].create({ + 'name': 'Test partner', + }) + cls.product = cls.env['product.product'].create({ + 'name': 'Test product', + }) + cls.contract = cls.env['account.analytic.account'].create({ + 'name': 'Test Contract', + 'partner_id': cls.partner.id, + 'pricelist_id': cls.partner.property_product_pricelist.id, + 'recurring_invoices': True, + }) + cls.formula = cls.env['contract.line.qty.formula'].create({ + 'name': 'Test formula', + # For testing each of the possible variables + 'code': 'env["res.users"]\n' + 'context.get("lang")\n' + 'user.id\n' + 'line.qty_type\n' + 'contract.id\n' + 'invoice.id\n' + 'result = 12', + }) + cls.contract_line = cls.env['account.analytic.invoice.line'].create({ + 'analytic_account_id': cls.contract.id, + 'product_id': cls.product.id, + 'name': 'Test', + 'qty_type': 'variable', + 'qty_formula_id': cls.formula.id, + 'quantity': 1, + 'uom_id': cls.product.uom_id.id, + 'price_unit': 100, + 'discount': 50, + }) + + def test_check_invalid_code(self): + with self.assertRaises(exceptions.ValidationError): + self.formula.code = "sdsds" + + def test_check_no_return_value(self): + with self.assertRaises(exceptions.ValidationError): + self.formula.code = "user.id" + + def test_check_variable_quantity(self): + self.contract._create_invoice(self.contract) + invoice = self.env['account.invoice'].search( + [('contract_id', '=', self.contract.id)]) + self.assertEqual(invoice.invoice_line_ids[0].quantity, 12) diff --git a/contract_variable_quantity/views/contract_view.xml b/contract_variable_quantity/views/contract_view.xml new file mode 100644 index 00000000..efd6e3a7 --- /dev/null +++ b/contract_variable_quantity/views/contract_view.xml @@ -0,0 +1,90 @@ + + + + + account.analytic.account + + + + + + + + + + {'required': [('qty_type', '=', 'fixed')], 'invisible': [('qty_type', '!=', 'fixed')]} + + + + + + contract.line.qty.formula + + + + + + + + + contract.line.qty.formula + +
+ +
+

+ +

+
+ +
+ +

Help with Python expressions.

+

You have to insert valid Python code block that stores at some moment a float/integer value of the quantity to invoice in the variable 'result'.

+

You can use these variables to compute your formula:

+
    +
  • env: Environment variable for getting other models.
  • +
  • context: Current context dictionary.
  • +
  • user: Current user.
  • +
  • line: Contract recurring invoice line that triggers this formula.
  • +
  • contract: Contract whose line belongs to.
  • +
  • invoice: Invoice (header) being created.
  • +
+
+

Example of Python code

+ + result = env['product.product'].search_count([('sale_ok', '=', True)]) + +
+
+
+
+
+
+
+ + + Formulas (quantity) + contract.line.qty.formula + form + tree,form + +

+ Click to create a new formula for variable quantities. +

+
+
+ + + + +