diff --git a/contract_variable_quantity/README.rst b/contract_variable_quantity/README.rst new file mode 100644 index 00000000..4518b00e --- /dev/null +++ b/contract_variable_quantity/README.rst @@ -0,0 +1,76 @@ +.. 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 to 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. + +.. figure:: images/formula_form.png + :alt: Formula form + :width: 600 px + +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/i18n/es.po b/contract_variable_quantity/i18n/es.po new file mode 100644 index 00000000..73ff0684 --- /dev/null +++ b/contract_variable_quantity/i18n/es.po @@ -0,0 +1,188 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * contract_variable_quantity +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-09 16:50+0000\n" +"PO-Revision-Date: 2016-09-09 16:50+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "\n" +" result = env['product.product'].search_count([('sale_ok', '=', True)])\n" +" " +msgstr "\n" +" result = env['product.product'].search_count([('sale_ok', '=', True)])\n" +" " + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "context: Current context dictionary." +msgstr "context: Diccionario de contexto actual." + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "contract: Contract whose line belongs to." +msgstr "contract: Contrato al que pertenece esta línea." + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "env: Environment variable for getting other models." +msgstr "env: Variable de entorno para obtener otros modelos." + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "invoice: Invoice (header) being created." +msgstr "invoice: Factura (cabecera) siendo creada." + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "line: Contract recurring invoice line that triggers this formula." +msgstr "line: Línea del contrato de facturación recurrente que lanza esta fórmula." + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "user: Current user." +msgstr "user: Usuario actual." + +#. module: contract_variable_quantity +#: model:ir.model,name:contract_variable_quantity.model_account_analytic_account +msgid "Analytic Account" +msgstr "Cuenta analítica" + +#. module: contract_variable_quantity +#: model:ir.actions.act_window,help:contract_variable_quantity.action_contract_quantity_formula +msgid "Click to create a new formula for variable quantities." +msgstr "Pulse para crear una nueva fórmula para cantidades variables." + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula_code +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "Code" +msgstr "Código" + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula_create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula_create_date +msgid "Created on" +msgstr "Creado en" + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula_display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: contract_variable_quantity +#: code:addons/contract_variable_quantity/models/contract.py:64 +#, python-format +msgid "Error evaluating code.\n" +"Details: %s" +msgstr "Error evaluando el código.\n" +"Detalles: %s" + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "Example of Python code" +msgstr "Ejemplo del código Python" + +#. module: contract_variable_quantity +#: selection:account.analytic.invoice.line,qty_type:0 +msgid "Fixed quantity" +msgstr "Cantidad fija" + +#. module: contract_variable_quantity +#: model:ir.actions.act_window,name:contract_variable_quantity.action_contract_quantity_formula +#: model:ir.ui.menu,name:contract_variable_quantity.menu_contract_quantity_formula +msgid "Formulas (quantity)" +msgstr "Fórmulas (cantidad)" + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "Help with Python expressions." +msgstr "Ayuda con las expresiones Python." + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula_id +msgid "ID" +msgstr "ID (identificación)" + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula_write_uid +msgid "Last Updated by" +msgstr "Última actualización de" + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula_write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_contract_line_qty_formula_name +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "Name" +msgstr "Nombre" + +#. module: contract_variable_quantity +#: code:addons/contract_variable_quantity/models/contract.py:66 +#, python-format +msgid "No valid result returned." +msgstr "No se devuelve un valor válido." + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_account_analytic_invoice_line_qty_formula_id +msgid "Qty. formula" +msgstr "Formula ctdad." + +#. module: contract_variable_quantity +#: model:ir.model.fields,field_description:contract_variable_quantity.field_account_analytic_invoice_line_qty_type +msgid "Qty. type" +msgstr "Tipo ctdad." + +#. module: contract_variable_quantity +#: selection:account.analytic.invoice.line,qty_type:0 +msgid "Variable quantity" +msgstr "Cantidad variable" + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "You can use these variables to compute your formula:" +msgstr "Puede usar estas variables para calcular su fórmula:" + +#. module: contract_variable_quantity +#: model:ir.ui.view,arch_db:contract_variable_quantity.view_contract_line_qty_formula_form +msgid "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'." +msgstr "Debe insertar un bloque de código Python que almacene en algún momento un valor entero o decimal de la cantidad a facturar en la variable 'result'." + +#. module: contract_variable_quantity +#: model:ir.model,name:contract_variable_quantity.model_account_analytic_invoice_line +msgid "account.analytic.invoice.line" +msgstr "account.analytic.invoice.line" + +#. module: contract_variable_quantity +#: model:ir.model,name:contract_variable_quantity.model_contract_line_formula +msgid "contract.line.formula" +msgstr "contract.line.formula" + +#. module: contract_variable_quantity +#: model:ir.model,name:contract_variable_quantity.model_contract_line_qty_formula +msgid "contract.line.qty.formula" +msgstr "contract.line.qty.formula" + diff --git a/contract_variable_quantity/images/formula_form.png b/contract_variable_quantity/images/formula_form.png new file mode 100644 index 00000000..23d03e5a Binary files /dev/null and b/contract_variable_quantity/images/formula_form.png differ 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. +

+
+
+ + + + +