File: account_tax.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (161 lines) | stat: -rw-r--r-- 5,949 bytes parent folder | download
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)