mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-19 13:02:02 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
|
|
@ -1,3 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_tax
|
||||
from . import common
|
||||
from . import test_taxes_computation
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
from odoo.addons.account.tests.test_tax import TestTaxCommon
|
||||
|
||||
|
||||
class TestTaxCommonAccountTaxPython(TestTaxCommon):
|
||||
|
||||
def _jsonify_tax(self, tax):
|
||||
values = super()._jsonify_tax(tax)
|
||||
values['formula_decoded_info'] = tax.formula_decoded_info
|
||||
return values
|
||||
|
||||
def assert_python_taxes_computation(
|
||||
self,
|
||||
formula,
|
||||
price_unit,
|
||||
expected_values,
|
||||
product_values=None,
|
||||
product_uom_values=None,
|
||||
price_include_override='tax_excluded',
|
||||
):
|
||||
tax = self.python_tax(formula, price_include_override=price_include_override)
|
||||
if product_values:
|
||||
product = self.env['product.product'].create({
|
||||
'name': "assert_python_taxes_computation",
|
||||
**product_values,
|
||||
})
|
||||
else:
|
||||
product = None
|
||||
if product_uom_values:
|
||||
uom = self.env['uom.uom'].create({
|
||||
'name': "assert_python_taxes_computation",
|
||||
'relative_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
**product_uom_values,
|
||||
})
|
||||
else:
|
||||
uom = None
|
||||
return self.assert_taxes_computation(tax, price_unit, expected_values, product=product, product_uom=uom)
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.test_tax import TestTaxCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxPython(TestTaxCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestTaxPython, cls).setUpClass()
|
||||
cls.python_tax = cls.env['account.tax'].create({
|
||||
'name': 'Python TAx',
|
||||
'amount_type': 'code',
|
||||
'amount': 0.0,
|
||||
'python_compute': 'result = ((price_unit * quantity) - ((price_unit * quantity) / 1.12)) * 0.5',
|
||||
'sequence': 1,
|
||||
})
|
||||
|
||||
def test_tax_python_basic(self):
|
||||
res = self.python_tax.compute_all(130.0)
|
||||
self._check_compute_all_results(
|
||||
136.96, # 'total_included'
|
||||
130.0, # 'total_excluded'
|
||||
[
|
||||
# base , amount | seq | amount | incl | incl_base
|
||||
# --------------------------------------------------
|
||||
(130.0, 6.96), # | 1 | 6% | t |
|
||||
# --------------------------------------------------
|
||||
],
|
||||
res
|
||||
)
|
||||
|
||||
def test_tax_python_price_include(self):
|
||||
self.python_tax.price_include = True
|
||||
res = self.python_tax.compute_all(130.0)
|
||||
self._check_compute_all_results(
|
||||
130, # 'total_included'
|
||||
123.04, # 'total_excluded'
|
||||
[
|
||||
# base , amount | seq | amount | incl | incl_base
|
||||
# ---------------------------------------------------
|
||||
(123.04, 6.96), # | 1 | 6% | t |
|
||||
# ---------------------------------------------------
|
||||
],
|
||||
res
|
||||
)
|
||||
|
||||
python_tax_2 = self.python_tax.copy()
|
||||
res = (self.python_tax + python_tax_2).compute_all(130.0)
|
||||
self._check_compute_all_results(
|
||||
130, # 'total_included'
|
||||
116.08, # 'total_excluded'
|
||||
[
|
||||
# base , amount | seq | amount | incl | incl_base
|
||||
# ---------------------------------------------------
|
||||
(116.08, 6.96), # | 1 | 6% | t |
|
||||
(116.08, 6.96), # | 1 | 6% | t |
|
||||
# ---------------------------------------------------
|
||||
],
|
||||
res
|
||||
)
|
||||
|
||||
def test_price_included_multi_taxes_with_python_tax_1(self):
|
||||
""" Test multiple price-included taxes with a Python code tax applied last
|
||||
to ensure the total matches the price, and cached tax didn't bypassing the rounding correction.
|
||||
"""
|
||||
tax_12_percent = self.env['account.tax'].create({
|
||||
'name': "Tax 12%",
|
||||
'amount_type': 'percent',
|
||||
'amount': 12.0,
|
||||
'price_include': True,
|
||||
'include_base_amount': False,
|
||||
'sequence': 1, # Ensure this tax is applied first
|
||||
})
|
||||
|
||||
tax_python = self.env['account.tax'].create({
|
||||
'name': "Python Tax",
|
||||
'amount_type': 'code',
|
||||
'python_compute': "result = 22.503",
|
||||
'price_include': True,
|
||||
'include_base_amount': False,
|
||||
'sequence': 2, # Ensure this tax is applied after the 12% tax
|
||||
})
|
||||
|
||||
taxes = tax_12_percent + tax_python
|
||||
res = taxes.compute_all(516.00)
|
||||
|
||||
self._check_compute_all_results(
|
||||
516.0, # total_included
|
||||
440.63, # total_excluded
|
||||
[
|
||||
(440.63, 52.87),
|
||||
(440.63, 22.5),
|
||||
],
|
||||
res
|
||||
)
|
||||
|
||||
def test_price_included_multi_taxes_with_python_tax_2(self):
|
||||
tax_python = self.env['account.tax'].create({
|
||||
'name': "Python Tax",
|
||||
'amount_type': 'code',
|
||||
'python_compute': "result = 5",
|
||||
'price_include': True,
|
||||
'include_base_amount': True,
|
||||
'sequence': 1, # Ensure this tax is applied first
|
||||
})
|
||||
|
||||
tax_12_percent = self.env['account.tax'].create({
|
||||
'name': "Tax 12%",
|
||||
'amount_type': 'percent',
|
||||
'amount': 15.0,
|
||||
'price_include': True,
|
||||
'include_base_amount': False,
|
||||
'sequence': 2, # Ensure this tax is applied after the 12% tax
|
||||
})
|
||||
|
||||
taxes = tax_python + tax_12_percent
|
||||
res = taxes.compute_all(100.00)
|
||||
|
||||
self._check_compute_all_results(
|
||||
total_included=100.0,
|
||||
total_excluded=81.96,
|
||||
taxes=[
|
||||
(81.96, 5.0),
|
||||
(86.96, 13.04),
|
||||
],
|
||||
res=res,
|
||||
)
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
from odoo.addons.account_tax_python.tests.common import TestTaxCommonAccountTaxPython
|
||||
from odoo.tests import tagged
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.account_tax_python.tools.formula_utils import check_formula, normalize_formula
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxesComputation(TestTaxCommonAccountTaxPython):
|
||||
|
||||
def test_formula(self):
|
||||
self.assert_python_taxes_computation(
|
||||
"max(quantity * price_unit * 0.21, quantity * 4.17)",
|
||||
130.0,
|
||||
{
|
||||
'total_included': 157.3,
|
||||
'total_excluded': 130.0,
|
||||
'taxes_data': (
|
||||
(130.0, 27.3),
|
||||
),
|
||||
},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"max(quantity * price_unit * 0.21, quantity * 4.17)",
|
||||
130.0,
|
||||
{
|
||||
'total_included': 130.0,
|
||||
'total_excluded': 102.7,
|
||||
'taxes_data': (
|
||||
(102.7, 27.3),
|
||||
),
|
||||
},
|
||||
price_include_override='tax_included',
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"product.volume * quantity * 0.35",
|
||||
100.0,
|
||||
{
|
||||
'total_included': 135.0,
|
||||
'total_excluded': 100.0,
|
||||
'taxes_data': (
|
||||
(100.0, 35.0),
|
||||
),
|
||||
},
|
||||
product_values={'volume': 100.0},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
'product["volume"] > 100 and 10 or 5',
|
||||
100.0,
|
||||
{
|
||||
'total_included': 110.0,
|
||||
'total_excluded': 100.0,
|
||||
'taxes_data': (
|
||||
(100.0, 10.0),
|
||||
),
|
||||
},
|
||||
product_values={'volume': 105.0},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"product['volume'] > 100 and 10 or 5",
|
||||
100.0,
|
||||
{
|
||||
'total_included': 105.0,
|
||||
'total_excluded': 100.0,
|
||||
'taxes_data': (
|
||||
(100.0, 5.0),
|
||||
),
|
||||
},
|
||||
product_values={'volume': 50.0},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"product.volume > 100 and 5 or None",
|
||||
100.0,
|
||||
{
|
||||
'total_included': 100.0,
|
||||
'total_excluded': 100.0,
|
||||
'taxes_data': [],
|
||||
},
|
||||
product_values={'volume': 50.0},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"(product.volume or 5.0) and 0.0 or 10.0",
|
||||
100.0,
|
||||
{
|
||||
'total_included': 110.0,
|
||||
'total_excluded': 100.0,
|
||||
'taxes_data': (
|
||||
(100.0, 10.0),
|
||||
),
|
||||
},
|
||||
product_values={'volume': 0.0},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"max(product.volume, 5.0) + 0.0 + -.0",
|
||||
100.0,
|
||||
{
|
||||
'total_included': 105.0,
|
||||
'total_excluded': 100.0,
|
||||
'taxes_data': (
|
||||
(100.0, 5.0),
|
||||
),
|
||||
},
|
||||
product_values={'volume': 0.0},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"(max(product.volume, 5.0) + base * 0.05) and None",
|
||||
100.0,
|
||||
{
|
||||
"total_included": 100.0,
|
||||
"total_excluded": 100.0,
|
||||
"taxes_data": (),
|
||||
},
|
||||
product_values={"volume": 0.0},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"min(max(price_unit, quantity), base) * 0.10 + (5 < product['volume'] < 10 and 1.0 or 0.0)",
|
||||
20.0,
|
||||
{
|
||||
"total_excluded": 20.0,
|
||||
"total_included": 23.0,
|
||||
"taxes_data": (
|
||||
(20.0, 3.0),
|
||||
),
|
||||
},
|
||||
product_values={"volume": 7.0},
|
||||
)
|
||||
self.assert_python_taxes_computation(
|
||||
"uom.relative_factor",
|
||||
100.0,
|
||||
{
|
||||
'total_included': 142.0,
|
||||
'total_excluded': 100.0,
|
||||
'taxes_data': (
|
||||
(100.0, 42.0),
|
||||
),
|
||||
},
|
||||
product_uom_values={'relative_factor': 42.0},
|
||||
)
|
||||
self._run_js_tests()
|
||||
|
||||
def test_invalid_formula(self):
|
||||
invalid_formulas = [
|
||||
'product.product_tmpl_id', # no relational fields
|
||||
'product.sudo()', # You don't have access to any record.
|
||||
'tuple(1, 2, 3)', # only min/max functions, no other callables
|
||||
'set(1, 2, 3)',
|
||||
'[1, 2, 3]',
|
||||
'1,',
|
||||
'{1, 2}',
|
||||
'{1: 2}',
|
||||
'(i for _ in product)',
|
||||
'"test"', # strings are only allowed in subscripts of product
|
||||
'product[min("volume", "price")]',
|
||||
'product()',
|
||||
'product[0]',
|
||||
'product[:10]',
|
||||
'product["field_that_does_not_exist"]',
|
||||
'product.field_that_does_not_exist',
|
||||
'product.ids',
|
||||
'product._fields',
|
||||
'product.env.cr',
|
||||
'range(1, 10)',
|
||||
]
|
||||
|
||||
for formula in invalid_formulas:
|
||||
with self.subTest(formula=formula):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.python_tax(formula=formula)
|
||||
|
||||
def test_ast_transformer_normalizes(self):
|
||||
# this test simply checks that the AST transformer does not raise any error when
|
||||
# collecting attributes and rewriting the formula, and also to list a couple of edge cases.
|
||||
# of course all these weird edge cases must be filtered out by the validator later on
|
||||
valid_cases = [
|
||||
(
|
||||
"((10_000 / product.__dunders__) * (product['shall'] - product[\"n0t\"] + ((product._pass)))) -.01",
|
||||
"10000 / product['__dunders__'] * (product['shall'] - product['n0t'] + product['_pass']) - 0.01",
|
||||
{"__dunders__", "shall", "n0t", "_pass"}
|
||||
),
|
||||
(
|
||||
"-bob[eats(product . sandwich)] + +product. \\\nwith_fries_inside and product['IS_THE_WAY']",
|
||||
"-bob[eats(product['sandwich'])] + +product['with_fries_inside'] and product['IS_THE_WAY']",
|
||||
{"sandwich", "with_fries_inside", "IS_THE_WAY"}
|
||||
),
|
||||
(
|
||||
"(product.help_youself, product['with some']) if product[None] else product.tarte_al_djote['grault']",
|
||||
"(product['help_youself'], product['with some']) if product[None] else product['tarte_al_djote']['grault']",
|
||||
{"help_youself", "with some", "tarte_al_djote"}
|
||||
),
|
||||
]
|
||||
|
||||
for formula, expected_normalized, expected_fields in valid_cases:
|
||||
with self.subTest(code=formula):
|
||||
normalized_formula, accessed_fields = normalize_formula(self.env, formula)
|
||||
self.assertEqual(accessed_fields['product.product'], expected_fields)
|
||||
self.assertEqual(normalized_formula, expected_normalized)
|
||||
|
||||
def test_ast_validator(self):
|
||||
to_fail = [
|
||||
# no attributes
|
||||
# transformer pass before validation rewrote attrs to subscripts,
|
||||
# so we don't allow attributes in validation step
|
||||
"product.field",
|
||||
"isinstance",
|
||||
"product.env",
|
||||
"(None for _ in ()).gi_frame.f_builtins['__import__']",
|
||||
|
||||
# only whitelisted nodes (no tuples, sets, dicts, lists, etc)
|
||||
"1,",
|
||||
"product,",
|
||||
"min(1, 2),",
|
||||
"()",
|
||||
"{}",
|
||||
"[]",
|
||||
"{1: product}",
|
||||
"{1, 2}",
|
||||
"[product]",
|
||||
"(None for _ in product)",
|
||||
|
||||
# only string subscripts of product are allowed
|
||||
"product[None]",
|
||||
"product[1]",
|
||||
"product[:]",
|
||||
"not_product['field']",
|
||||
|
||||
# no arbitrary function calls
|
||||
"product['a_callable']()",
|
||||
"product()",
|
||||
"(min or max)(1, 2)",
|
||||
"isinstance(1, ())",
|
||||
|
||||
# no arbitrary name load
|
||||
"a",
|
||||
"__builtins__",
|
||||
"isinstance",
|
||||
]
|
||||
for formula in to_fail:
|
||||
with self.subTest(code=formula):
|
||||
with self.assertRaises(ValidationError):
|
||||
check_formula(self.env, formula)
|
||||
Loading…
Add table
Add a link
Reference in a new issue