Skip to content

Commit da630aa

Browse files
bealdavlegalsylvain
authored andcommitted
[IMP] account_product_fiscal_classification :
- In a context of product creation, if classification is not provided (in import context for exemple), link the products to an existing classification, if the taxes match an existing one. If no classification match, try to create a new classification, based on the product fiscal settings. (It will fail if user is not member of Accountant group that can create / write on fiscal classifications) - In a context of product update, apply same logic, linking to existing classification, or creating new classification This will fix current limitation if account_product_fiscal_classification and pos_sale (for exemple) is installed, there is an error when making an update, because loading data / demo is failing for the time being if the fields taxes_id or supplier_taxes_id is defined. See for exemple : pos_sale.default_downpayment_product that contains {'taxes_id': '[(5,)]'} write.
1 parent c2f474e commit da630aa

File tree

2 files changed

+192
-79
lines changed

2 files changed

+192
-79
lines changed

account_product_fiscal_classification/models/product_template.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
55

66
import json
7+
import logging
78

89
from lxml import etree
910

10-
from odoo import _, api, fields, models
11-
from odoo.exceptions import ValidationError
11+
from odoo import api, fields, models
12+
13+
_logger = logging.getLogger(__name__)
1214

1315

1416
class ProductTemplate(models.Model):
@@ -28,16 +30,32 @@ class ProductTemplate(models.Model):
2830
" manager if you don't have the access right.",
2931
)
3032

33+
@api.constrains("categ_id", "fiscal_classification_id")
34+
def _check_rules_fiscal_classification(self):
35+
self.env["account.product.fiscal.rule"].check_product_templates_integrity(self)
36+
3137
# Overload Section
3238
@api.model_create_multi
3339
def create(self, vals_list):
3440
for vals in vals_list:
35-
self._update_vals_fiscal_classification(vals)
36-
return super().create(vals_list)
41+
self._fiscal_classification_update_taxes(vals)
42+
templates = super().create(vals_list)
43+
for template in templates.filtered(lambda x: not x.fiscal_classification_id):
44+
template.fiscal_classification_id = (
45+
template._fiscal_classification_get_or_create()[0]
46+
)
47+
return templates
3748

3849
def write(self, vals):
39-
self._update_vals_fiscal_classification(vals)
50+
self._fiscal_classification_update_taxes(vals)
4051
res = super().write(vals)
52+
if ({"supplier_taxes_id", "taxes_id"} & vals.keys()) and (
53+
"fiscal_classification_id" not in vals.keys()
54+
):
55+
for template in self:
56+
new_classification = template._fiscal_classification_get_or_create()[0]
57+
if template.fiscal_classification_id != new_classification:
58+
template.fiscal_classification_id = new_classification
4159
return res
4260

4361
# View Section
@@ -48,6 +66,9 @@ def _onchange_fiscal_classification_id(self):
4866

4967
@api.model
5068
def get_view(self, view_id=None, view_type="form", **options):
69+
"""Set fiscal_classification_id required on all views.
70+
We don't set the field required by field definition to avoid
71+
incompatibility with other modules, errors on import, etc..."""
5172
result = super().get_view(view_id=view_id, view_type=view_type, **options)
5273
doc = etree.fromstring(result["arch"])
5374
nodes = doc.xpath("//field[@name='fiscal_classification_id']")
@@ -60,9 +81,11 @@ def get_view(self, view_id=None, view_type="form", **options):
6081
return result
6182

6283
# Custom Section
63-
def _update_vals_fiscal_classification(self, vals):
84+
def _fiscal_classification_update_taxes(self, vals):
85+
"""if fiscal classification is in vals, update vals to set
86+
according purchase and sale taxes"""
6487
FiscalClassification = self.env["account.product.fiscal.classification"]
65-
if vals.get("fiscal_classification_id", False):
88+
if vals.get("fiscal_classification_id"):
6689
# We use sudo to have access to all the taxes, even taxes that belong
6790
# to companies that the user can't access in the current context
6891
classification = FiscalClassification.sudo().browse(
@@ -74,16 +97,36 @@ def _update_vals_fiscal_classification(self, vals):
7497
"taxes_id": [(6, 0, classification.sale_tax_ids.ids)],
7598
}
7699
)
77-
elif vals.get("supplier_taxes_id") or vals.get("taxes_id"):
78-
raise ValidationError(
79-
_(
80-
"You can not create or write products with"
81-
" 'Customer Taxes' or 'Supplier Taxes'\n."
82-
"Please, use instead the 'Fiscal Classification' field."
83-
)
84-
)
85100
return vals
86101

87-
@api.constrains("categ_id", "fiscal_classification_id")
88-
def _check_rules_fiscal_classification(self):
89-
self.env["account.product.fiscal.rule"].check_product_templates_integrity(self)
102+
def _fiscal_classification_get_or_create(self):
103+
"""get the classification(s) that matches with the fiscal settings
104+
of the current product.
105+
If no configuration is found, create a new one.
106+
This will raise an error, if current user doesn't have the access right
107+
to create one classification."""
108+
109+
self.ensure_one()
110+
111+
FiscalClassification = self.env["account.product.fiscal.classification"]
112+
FiscalClassificationSudo = found_classifications = self.env[
113+
"account.product.fiscal.classification"
114+
].sudo()
115+
all_classifications = FiscalClassificationSudo.search(
116+
[("company_id", "in", [self.company_id.id, False])]
117+
)
118+
119+
for classification in all_classifications:
120+
if sorted(self.supplier_taxes_id.ids) == sorted(
121+
classification.purchase_tax_ids.ids
122+
) and sorted(self.taxes_id.ids) == sorted(classification.sale_tax_ids.ids):
123+
found_classifications |= classification
124+
125+
if len(found_classifications) == 0:
126+
vals = FiscalClassification._prepare_vals_from_taxes(
127+
self.supplier_taxes_id, self.taxes_id
128+
)
129+
_logger.info(f"Creating new Fiscal Classification '{vals['name']}' ...")
130+
return FiscalClassification.create(vals)
131+
132+
return found_classifications
Lines changed: 131 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,115 @@
11
# Copyright (C) 2014-Today GRAP (http://www.grap.coop)
22
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
33
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
4-
5-
from odoo.exceptions import ValidationError
4+
from odoo import Command
5+
from odoo.exceptions import AccessError, ValidationError
66
from odoo.tests.common import TransactionCase
77

88

99
class Tests(TransactionCase):
10-
def setUp(self):
11-
super().setUp()
12-
self.ProductTemplate = self.env["product.template"]
13-
self.ResCompany = self.env["res.company"]
14-
self.FiscalClassification = self.env["account.product.fiscal.classification"]
15-
self.WizardChange = self.env["wizard.change.fiscal.classification"]
16-
17-
self.main_company = self.env.ref("base.main_company")
18-
self.group_accountant = self.env.ref("account.group_account_manager")
19-
self.group_system = self.env.ref("base.group_system")
20-
self.company_2 = self.env.ref("account_product_fiscal_classification.company_2")
21-
self.user_demo = self.env.ref("base.user_demo")
22-
23-
self.fiscal_classification_A_company_1 = self.env.ref(
10+
@classmethod
11+
def setUpClass(cls):
12+
super().setUpClass()
13+
cls.ProductTemplate = cls.env["product.template"]
14+
cls.ResCompany = cls.env["res.company"]
15+
cls.FiscalClassification = cls.env["account.product.fiscal.classification"]
16+
cls.WizardChange = cls.env["wizard.change.fiscal.classification"]
17+
18+
cls.main_company = cls.env.ref("base.main_company")
19+
cls.group_accountant = cls.env.ref("account.group_account_manager")
20+
cls.group_system = cls.env.ref("base.group_system")
21+
cls.company_2 = cls.env.ref("account_product_fiscal_classification.company_2")
22+
23+
cls.classification_A_company_1 = cls.env.ref(
2424
"account_product_fiscal_classification.fiscal_classification_A_company_1"
2525
)
26-
self.fiscal_classification_B_company_1 = self.env.ref(
26+
cls.classification_B_company_1 = cls.env.ref(
2727
"account_product_fiscal_classification.fiscal_classification_B_company_1"
2828
)
29-
self.fiscal_classification_D_global = self.env.ref(
29+
cls.classification_D_global = cls.env.ref(
3030
"account_product_fiscal_classification.fiscal_classification_D_global"
3131
)
32-
self.product_template_A_company_1 = self.env.ref(
32+
cls.product_template_A_company_1 = cls.env.ref(
3333
"account_product_fiscal_classification.product_template_A_company_1"
3434
)
35-
self.account_tax_purchase_20_company_1 = self.env.ref(
35+
cls.account_tax_purchase_20_company_1 = cls.env.ref(
3636
"account_product_fiscal_classification.account_tax_purchase_20_company_1"
3737
)
38-
self.account_tax_sale_20_company_1 = self.env.ref(
38+
cls.account_tax_sale_5_company_1 = cls.env.ref(
39+
"account_product_fiscal_classification.account_tax_sale_5_company_1"
40+
)
41+
cls.account_tax_sale_20_company_1 = cls.env.ref(
3942
"account_product_fiscal_classification.account_tax_sale_20_company_1"
4043
)
41-
self.account_tax_purchase_7_company_2 = self.env.ref(
44+
cls.account_tax_purchase_7_company_2 = cls.env.ref(
4245
"account_product_fiscal_classification.account_tax_purchase_7_company_2"
4346
)
44-
self.chart_template = self.env.ref(
47+
cls.chart_template = cls.env.ref(
4548
"account_product_fiscal_classification.chart_template"
4649
)
47-
# self.sale_tax_2 = self.env.ref(
48-
# "account_product_fiscal_classification.account_tax_sale_5_company_1"
49-
# )
50-
51-
self.category_all = self.env.ref("product.product_category_all")
52-
self.category_wine = self.env.ref(
50+
cls.category_all = cls.env.ref("product.product_category_all")
51+
cls.category_wine = cls.env.ref(
5352
"account_product_fiscal_classification.category_wine"
5453
)
5554

56-
# # Group to create product
57-
# self.product_group = self.env.ref("account.group_account_manager")
58-
# self.restricted_group = self.env.ref("base.group_system")
55+
cls.initial_classif_count = cls.FiscalClassification.search_count([])
5956

60-
# Test Section
57+
vals = {
58+
"company_id": cls.env.ref("base.main_company").id,
59+
"name": "New User",
60+
"login": "new_user",
61+
"email": "[email protected]",
62+
"groups_id": [Command.set(cls.env.ref("base.group_system").ids)],
63+
}
64+
cls.user_demo = cls.env["res.users"].create(vals)
65+
66+
def _create_product(self, extra_vals, user=False):
67+
if not user:
68+
user = self.env.user
69+
vals = {
70+
"name": "Test Product",
71+
"company_id": self.main_company.id,
72+
"categ_id": self.category_all.id,
73+
}
74+
vals.update(extra_vals)
75+
return self.ProductTemplate.with_user(user).create(vals)
76+
77+
# # Test Section
6178
def test_01_change_classification(self):
6279
"""Test the behaviour when we change Fiscal Classification for
6380
products."""
6481
wizard = self.WizardChange.create(
6582
{
66-
"old_fiscal_classification_id": self.fiscal_classification_A_company_1.id,
67-
"new_fiscal_classification_id": self.fiscal_classification_B_company_1.id,
83+
"old_fiscal_classification_id": self.classification_A_company_1.id,
84+
"new_fiscal_classification_id": self.classification_B_company_1.id,
6885
}
6986
)
7087
wizard.button_change_fiscal_classification()
7188
self.assertEqual(
7289
self.product_template_A_company_1.fiscal_classification_id,
73-
self.fiscal_classification_B_company_1,
90+
self.classification_B_company_1,
7491
"Fiscal Classification change has failed for products via Wizard.",
7592
)
7693

7794
def test_02_create_product(self):
7895
"""Test if creating a product with fiscal classification set correct taxes"""
79-
vals = {
80-
"name": "Product Product Name",
81-
"company_id": self.main_company.id,
82-
"fiscal_classification_id": self.fiscal_classification_D_global.id,
83-
}
84-
newTemplate = self.ProductTemplate.create(vals)
96+
newTemplate = self._create_product(
97+
{"fiscal_classification_id": self.classification_D_global.id}
98+
)
8599
# Test that all taxes are set (in sudo mode)
86100
self.assertEqual(
87101
set(newTemplate.sudo().taxes_id.ids),
88-
set(self.fiscal_classification_D_global.sudo().sale_tax_ids.ids),
102+
set(self.classification_D_global.sudo().sale_tax_ids.ids),
89103
)
90104
self.assertEqual(
91105
set(newTemplate.sudo().supplier_taxes_id.ids),
92-
set(self.fiscal_classification_D_global.sudo().purchase_tax_ids.ids),
106+
set(self.classification_D_global.sudo().purchase_tax_ids.ids),
93107
)
94108

95109
def test_03_update_fiscal_classification(self):
96110
"""Test if changing a Configuration of a Fiscal Classification changes
97111
the product."""
98-
self.fiscal_classification_A_company_1.write(
112+
self.classification_A_company_1.write(
99113
{"sale_tax_ids": [(6, 0, [self.account_tax_sale_20_company_1.id])]}
100114
)
101115
self.assertEqual(
@@ -107,7 +121,7 @@ def test_03_update_fiscal_classification(self):
107121
def test_05_unlink_fiscal_classification(self):
108122
"""Test if unlinking a Fiscal Classification with products fails."""
109123
with self.assertRaises(ValidationError):
110-
self.fiscal_classification_A_company_1.unlink()
124+
self.classification_A_company_1.unlink()
111125

112126
def test_10_chart_template(self):
113127
"""Test if installing new CoA creates correct classification"""
@@ -148,29 +162,85 @@ def test_30_rules(self):
148162

149163
# Create a product without rules should success
150164
self._create_product(
151-
self.env.user, self.category_all, self.fiscal_classification_B_company_1
165+
{"fiscal_classification_id": self.classification_B_company_1.id}
152166
)
153167
self._create_product(
154-
self.user_demo, self.category_all, self.fiscal_classification_B_company_1
168+
{"fiscal_classification_id": self.classification_B_company_1.id},
169+
user=self.user_demo,
155170
)
156171

172+
vals = {
173+
"categ_id": self.category_wine.id,
174+
"fiscal_classification_id": self.classification_B_company_1.id,
175+
}
157176
# create a product not respecting rules should succeed with accountant perfil
158-
self._create_product(
159-
self.env.user, self.category_wine, self.fiscal_classification_B_company_1
160-
)
177+
self._create_product(vals)
161178

162179
# create a product not respecting rules should fail without accountant perfil
163180
with self.assertRaises(ValidationError):
164-
self._create_product(
165-
self.user_demo,
166-
self.category_wine,
167-
self.fiscal_classification_B_company_1,
168-
)
181+
self._create_product(vals, user=self.user_demo)
169182

170-
def _create_product(self, user, category, classification):
183+
def test_no_classification_and_find_one(self):
171184
vals = {
172-
"name": "Test Product",
173-
"categ_id": category.id,
174-
"fiscal_classification_id": classification.id,
185+
"taxes_id": self.classification_B_company_1.sale_tax_ids.ids,
186+
"supplier_taxes_id": self.classification_B_company_1.purchase_tax_ids.ids,
187+
}
188+
product = self._create_product(vals, user=self.user_demo)
189+
190+
# no new classification is created
191+
classif_count_after = self.FiscalClassification.search_count([])
192+
self.assertEqual(classif_count_after, self.initial_classif_count)
193+
# product is linked to the correct classification
194+
self.assertEqual(
195+
product.fiscal_classification_id, self.classification_B_company_1
196+
)
197+
198+
def test_no_classification_one_more_tax_and_create_one(self):
199+
"""Create a product with fiscal settings that looks like
200+
classification_B_company_1 but with an additional supplier tax.
201+
"""
202+
203+
vals = {
204+
"taxes_id": self.classification_B_company_1.sale_tax_ids.ids,
205+
"supplier_taxes_id": self.account_tax_purchase_20_company_1.ids,
206+
}
207+
208+
# create a product with fiscal settings that doesn't match
209+
# existing classification should fail with non accountant user
210+
with self.assertRaises(AccessError):
211+
self._create_product(vals, user=self.user_demo)
212+
213+
# create a product with fiscal settings that doesn't match
214+
# existing classification should success with accountant user
215+
product = self._create_product(vals)
216+
self.assertNotEqual(product.fiscal_classification_id, False)
217+
classif_count_after = self.FiscalClassification.search_count([])
218+
self.assertEqual(classif_count_after, self.initial_classif_count + 1)
219+
220+
def test_no_classification_one_less_tax_and_create_one(self):
221+
"""Create a product with fiscal settings that looks like
222+
classification_B_company_1 but with one less tax
223+
"""
224+
225+
vals = {
226+
"taxes_id": self.account_tax_sale_5_company_1.ids,
227+
"supplier_taxes_id": [],
175228
}
176-
self.ProductTemplate.with_user(user).create(vals)
229+
# create a product with fiscal settings that doesn't match
230+
# existing classification should fail with non accountant user
231+
with self.assertRaises(AccessError):
232+
self._create_product(vals, user=self.user_demo)
233+
234+
# create a product with fiscal settings that doesn't match
235+
# existing classification should success with accountant user
236+
product = self._create_product(vals)
237+
238+
self.assertNotEqual(product.fiscal_classification_id, False)
239+
classif_count_after = self.FiscalClassification.search_count([])
240+
self.assertEqual(classif_count_after, self.initial_classif_count + 1)
241+
242+
def test_no_tax_nor_classification(self):
243+
product = self._create_product({"taxes_id": [], "supplier_taxes_id": []})
244+
classif = product.fiscal_classification_id
245+
self.assertEqual(classif.purchase_tax_ids.ids, [])
246+
self.assertEqual(classif.sale_tax_ids.ids, [])

0 commit comments

Comments
 (0)