1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval
REGEX_FORMULA_OBJECT = re.compile(r'((?:product\[\')(?P<field>\w+)(?:\'\]))+')
FORMULA_ALLOWED_TOKENS = {
'(', ')',
'+', '-', '*', '/', ',', '<', '>', '<=', '>=',
'and', 'or', 'None',
'base', 'quantity', 'price_unit',
'min', 'max',
}
class AccountTaxPython(models.Model):
_inherit = "account.tax"
amount_type = fields.Selection(
selection_add=[('code', "Custom Formula")],
ondelete={'code': lambda recs: recs.write({'amount_type': 'percent', 'active': False})},
)
formula = fields.Text(
string="Formula",
default="price_unit * 0.10",
help="Compute the amount of the tax.\n\n"
":param base: float, actual amount on which the tax is applied\n"
":param price_unit: float\n"
":param quantity: float\n"
":param product: A object representing the product\n"
)
formula_decoded_info = fields.Json(compute='_compute_formula_decoded_info')
@api.constrains('amount_type', 'formula')
def _check_amount_type_code_formula(self):
for tax in self:
if tax.amount_type == 'code':
tax._check_formula()
@api.model
def _eval_taxes_computation_prepare_product_fields(self):
# EXTENDS 'account'
field_names = super()._eval_taxes_computation_prepare_product_fields()
for tax in self.filtered(lambda tax: tax.amount_type == 'code'):
field_names.update(tax.formula_decoded_info['product_fields'])
return field_names
@api.depends('formula')
def _compute_formula_decoded_info(self):
for tax in self:
if tax.amount_type != 'code':
tax.formula_decoded_info = None
continue
formula = (tax.formula or '0.0').strip()
formula_decoded_info = {
'js_formula': formula,
'py_formula': formula,
}
product_fields = set()
groups = re.findall(r'((?:product\.)(?P<field>\w+))+', formula) or []
Product = self.env['product.product']
for group in groups:
field_name = group[1]
if field_name in Product and not Product._fields[field_name].relational:
product_fields.add(field_name)
formula_decoded_info['py_formula'] = formula_decoded_info['py_formula'].replace(f"product.{field_name}", f"product['{field_name}']")
formula_decoded_info['product_fields'] = list(product_fields)
tax.formula_decoded_info = formula_decoded_info
def _check_formula(self):
""" Check the formula is passing the minimum check to ensure the compatibility between both evaluation
in python & javascript.
"""
self.ensure_one()
def get_number_size(formula, i):
starting_i = i
seen_separator = False
while i < len(formula):
if formula[i].isnumeric():
i += 1
elif formula[i] == '.' and (i - starting_i) > 0 and not seen_separator:
i += 1
seen_separator = True
else:
break
return i - starting_i
formula_decoded_info = self.formula_decoded_info
allowed_tokens = FORMULA_ALLOWED_TOKENS.union(f"product['{field_name}']" for field_name in formula_decoded_info['product_fields'])
formula = formula_decoded_info['py_formula']
i = 0
while i < len(formula):
if formula[i] == ' ':
i += 1
continue
continue_needed = False
for token in allowed_tokens:
if formula[i:i + len(token)] == token:
i += len(token)
continue_needed = True
break
if continue_needed:
continue
number_size = get_number_size(formula, i)
if number_size > 0:
i += number_size
continue
raise ValidationError(_("Malformed formula '%(formula)s' at position %(position)s", formula=formula, position=i))
@api.model
def _eval_tax_amount_formula(self, raw_base, evaluation_context):
""" Evaluate the formula of the tax passed as parameter.
[!] Mirror of the same method in account_tax.js.
PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.
:param tax_data: The values of a tax returned by '_prepare_taxes_computation'.
:param evaluation_context: The context created by '_eval_taxes_computation_prepare_context'.
:return: The tax base amount.
"""
self._check_formula()
# Safe eval.
formula_context = {
'price_unit': evaluation_context['price_unit'],
'quantity': evaluation_context['quantity'],
'product': evaluation_context['product'],
'base': raw_base,
'min': min,
'max': max,
}
try:
return safe_eval(
self.formula_decoded_info['py_formula'],
globals_dict=formula_context,
locals_dict={},
locals_builtins=False,
nocopy=True,
)
except ZeroDivisionError:
return 0.0
def _eval_tax_amount_fixed_amount(self, batch, raw_base, evaluation_context):
# EXTENDS 'account'
if self.amount_type == 'code':
return self._eval_tax_amount_formula(raw_base, evaluation_context)
return super()._eval_tax_amount_fixed_amount(batch, raw_base, evaluation_context)
|