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 00000000..3c00adfe Binary files /dev/null and b/contract_variable_quantity/static/description/icon.png differ 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. +

+
+
+ + + + +