Skip to content

Commit

Permalink
[IMP] account_product_fiscal_classification :
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
bealdav authored and legalsylvain committed Jul 4, 2024
1 parent c2f474e commit da630aa
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 79 deletions.
79 changes: 61 additions & 18 deletions account_product_fiscal_classification/models/product_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import json
import logging

from lxml import etree

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo import api, fields, models

_logger = logging.getLogger(__name__)


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

@api.constrains("categ_id", "fiscal_classification_id")
def _check_rules_fiscal_classification(self):
self.env["account.product.fiscal.rule"].check_product_templates_integrity(self)

# Overload Section
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self._update_vals_fiscal_classification(vals)
return super().create(vals_list)
self._fiscal_classification_update_taxes(vals)
templates = super().create(vals_list)
for template in templates.filtered(lambda x: not x.fiscal_classification_id):
template.fiscal_classification_id = (
template._fiscal_classification_get_or_create()[0]
)
return templates

def write(self, vals):
self._update_vals_fiscal_classification(vals)
self._fiscal_classification_update_taxes(vals)
res = super().write(vals)
if ({"supplier_taxes_id", "taxes_id"} & vals.keys()) and (
"fiscal_classification_id" not in vals.keys()
):
for template in self:
new_classification = template._fiscal_classification_get_or_create()[0]

Check warning on line 56 in account_product_fiscal_classification/models/product_template.py

View check run for this annotation

Codecov / codecov/patch

account_product_fiscal_classification/models/product_template.py#L56

Added line #L56 was not covered by tests
if template.fiscal_classification_id != new_classification:
template.fiscal_classification_id = new_classification

Check warning on line 58 in account_product_fiscal_classification/models/product_template.py

View check run for this annotation

Codecov / codecov/patch

account_product_fiscal_classification/models/product_template.py#L58

Added line #L58 was not covered by tests
return res

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

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

# Custom Section
def _update_vals_fiscal_classification(self, vals):
def _fiscal_classification_update_taxes(self, vals):
"""if fiscal classification is in vals, update vals to set
according purchase and sale taxes"""
FiscalClassification = self.env["account.product.fiscal.classification"]
if vals.get("fiscal_classification_id", False):
if vals.get("fiscal_classification_id"):
# We use sudo to have access to all the taxes, even taxes that belong
# to companies that the user can't access in the current context
classification = FiscalClassification.sudo().browse(
Expand All @@ -74,16 +97,36 @@ def _update_vals_fiscal_classification(self, vals):
"taxes_id": [(6, 0, classification.sale_tax_ids.ids)],
}
)
elif vals.get("supplier_taxes_id") or vals.get("taxes_id"):
raise ValidationError(
_(
"You can not create or write products with"
" 'Customer Taxes' or 'Supplier Taxes'\n."
"Please, use instead the 'Fiscal Classification' field."
)
)
return vals

@api.constrains("categ_id", "fiscal_classification_id")
def _check_rules_fiscal_classification(self):
self.env["account.product.fiscal.rule"].check_product_templates_integrity(self)
def _fiscal_classification_get_or_create(self):
"""get the classification(s) that matches with the fiscal settings
of the current product.
If no configuration is found, create a new one.
This will raise an error, if current user doesn't have the access right
to create one classification."""

self.ensure_one()

FiscalClassification = self.env["account.product.fiscal.classification"]
FiscalClassificationSudo = found_classifications = self.env[
"account.product.fiscal.classification"
].sudo()
all_classifications = FiscalClassificationSudo.search(
[("company_id", "in", [self.company_id.id, False])]
)

for classification in all_classifications:
if sorted(self.supplier_taxes_id.ids) == sorted(
classification.purchase_tax_ids.ids
) and sorted(self.taxes_id.ids) == sorted(classification.sale_tax_ids.ids):
found_classifications |= classification

if len(found_classifications) == 0:
vals = FiscalClassification._prepare_vals_from_taxes(
self.supplier_taxes_id, self.taxes_id
)
_logger.info(f"Creating new Fiscal Classification '{vals['name']}' ...")
return FiscalClassification.create(vals)

return found_classifications
192 changes: 131 additions & 61 deletions account_product_fiscal_classification/tests/test_module.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,115 @@
# Copyright (C) 2014-Today GRAP (http://www.grap.coop)
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo.exceptions import ValidationError
from odoo import Command
from odoo.exceptions import AccessError, ValidationError
from odoo.tests.common import TransactionCase


class Tests(TransactionCase):
def setUp(self):
super().setUp()
self.ProductTemplate = self.env["product.template"]
self.ResCompany = self.env["res.company"]
self.FiscalClassification = self.env["account.product.fiscal.classification"]
self.WizardChange = self.env["wizard.change.fiscal.classification"]

self.main_company = self.env.ref("base.main_company")
self.group_accountant = self.env.ref("account.group_account_manager")
self.group_system = self.env.ref("base.group_system")
self.company_2 = self.env.ref("account_product_fiscal_classification.company_2")
self.user_demo = self.env.ref("base.user_demo")

self.fiscal_classification_A_company_1 = self.env.ref(
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ProductTemplate = cls.env["product.template"]
cls.ResCompany = cls.env["res.company"]
cls.FiscalClassification = cls.env["account.product.fiscal.classification"]
cls.WizardChange = cls.env["wizard.change.fiscal.classification"]

cls.main_company = cls.env.ref("base.main_company")
cls.group_accountant = cls.env.ref("account.group_account_manager")
cls.group_system = cls.env.ref("base.group_system")
cls.company_2 = cls.env.ref("account_product_fiscal_classification.company_2")

cls.classification_A_company_1 = cls.env.ref(
"account_product_fiscal_classification.fiscal_classification_A_company_1"
)
self.fiscal_classification_B_company_1 = self.env.ref(
cls.classification_B_company_1 = cls.env.ref(
"account_product_fiscal_classification.fiscal_classification_B_company_1"
)
self.fiscal_classification_D_global = self.env.ref(
cls.classification_D_global = cls.env.ref(
"account_product_fiscal_classification.fiscal_classification_D_global"
)
self.product_template_A_company_1 = self.env.ref(
cls.product_template_A_company_1 = cls.env.ref(
"account_product_fiscal_classification.product_template_A_company_1"
)
self.account_tax_purchase_20_company_1 = self.env.ref(
cls.account_tax_purchase_20_company_1 = cls.env.ref(
"account_product_fiscal_classification.account_tax_purchase_20_company_1"
)
self.account_tax_sale_20_company_1 = self.env.ref(
cls.account_tax_sale_5_company_1 = cls.env.ref(
"account_product_fiscal_classification.account_tax_sale_5_company_1"
)
cls.account_tax_sale_20_company_1 = cls.env.ref(
"account_product_fiscal_classification.account_tax_sale_20_company_1"
)
self.account_tax_purchase_7_company_2 = self.env.ref(
cls.account_tax_purchase_7_company_2 = cls.env.ref(
"account_product_fiscal_classification.account_tax_purchase_7_company_2"
)
self.chart_template = self.env.ref(
cls.chart_template = cls.env.ref(
"account_product_fiscal_classification.chart_template"
)
# self.sale_tax_2 = self.env.ref(
# "account_product_fiscal_classification.account_tax_sale_5_company_1"
# )

self.category_all = self.env.ref("product.product_category_all")
self.category_wine = self.env.ref(
cls.category_all = cls.env.ref("product.product_category_all")
cls.category_wine = cls.env.ref(
"account_product_fiscal_classification.category_wine"
)

# # Group to create product
# self.product_group = self.env.ref("account.group_account_manager")
# self.restricted_group = self.env.ref("base.group_system")
cls.initial_classif_count = cls.FiscalClassification.search_count([])

# Test Section
vals = {
"company_id": cls.env.ref("base.main_company").id,
"name": "New User",
"login": "new_user",
"email": "[email protected]",
"groups_id": [Command.set(cls.env.ref("base.group_system").ids)],
}
cls.user_demo = cls.env["res.users"].create(vals)

def _create_product(self, extra_vals, user=False):
if not user:
user = self.env.user
vals = {
"name": "Test Product",
"company_id": self.main_company.id,
"categ_id": self.category_all.id,
}
vals.update(extra_vals)
return self.ProductTemplate.with_user(user).create(vals)

# # Test Section
def test_01_change_classification(self):
"""Test the behaviour when we change Fiscal Classification for
products."""
wizard = self.WizardChange.create(
{
"old_fiscal_classification_id": self.fiscal_classification_A_company_1.id,
"new_fiscal_classification_id": self.fiscal_classification_B_company_1.id,
"old_fiscal_classification_id": self.classification_A_company_1.id,
"new_fiscal_classification_id": self.classification_B_company_1.id,
}
)
wizard.button_change_fiscal_classification()
self.assertEqual(
self.product_template_A_company_1.fiscal_classification_id,
self.fiscal_classification_B_company_1,
self.classification_B_company_1,
"Fiscal Classification change has failed for products via Wizard.",
)

def test_02_create_product(self):
"""Test if creating a product with fiscal classification set correct taxes"""
vals = {
"name": "Product Product Name",
"company_id": self.main_company.id,
"fiscal_classification_id": self.fiscal_classification_D_global.id,
}
newTemplate = self.ProductTemplate.create(vals)
newTemplate = self._create_product(
{"fiscal_classification_id": self.classification_D_global.id}
)
# Test that all taxes are set (in sudo mode)
self.assertEqual(
set(newTemplate.sudo().taxes_id.ids),
set(self.fiscal_classification_D_global.sudo().sale_tax_ids.ids),
set(self.classification_D_global.sudo().sale_tax_ids.ids),
)
self.assertEqual(
set(newTemplate.sudo().supplier_taxes_id.ids),
set(self.fiscal_classification_D_global.sudo().purchase_tax_ids.ids),
set(self.classification_D_global.sudo().purchase_tax_ids.ids),
)

def test_03_update_fiscal_classification(self):
"""Test if changing a Configuration of a Fiscal Classification changes
the product."""
self.fiscal_classification_A_company_1.write(
self.classification_A_company_1.write(
{"sale_tax_ids": [(6, 0, [self.account_tax_sale_20_company_1.id])]}
)
self.assertEqual(
Expand All @@ -107,7 +121,7 @@ def test_03_update_fiscal_classification(self):
def test_05_unlink_fiscal_classification(self):
"""Test if unlinking a Fiscal Classification with products fails."""
with self.assertRaises(ValidationError):
self.fiscal_classification_A_company_1.unlink()
self.classification_A_company_1.unlink()

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

# Create a product without rules should success
self._create_product(
self.env.user, self.category_all, self.fiscal_classification_B_company_1
{"fiscal_classification_id": self.classification_B_company_1.id}
)
self._create_product(
self.user_demo, self.category_all, self.fiscal_classification_B_company_1
{"fiscal_classification_id": self.classification_B_company_1.id},
user=self.user_demo,
)

vals = {
"categ_id": self.category_wine.id,
"fiscal_classification_id": self.classification_B_company_1.id,
}
# create a product not respecting rules should succeed with accountant perfil
self._create_product(
self.env.user, self.category_wine, self.fiscal_classification_B_company_1
)
self._create_product(vals)

# create a product not respecting rules should fail without accountant perfil
with self.assertRaises(ValidationError):
self._create_product(
self.user_demo,
self.category_wine,
self.fiscal_classification_B_company_1,
)
self._create_product(vals, user=self.user_demo)

def _create_product(self, user, category, classification):
def test_no_classification_and_find_one(self):
vals = {
"name": "Test Product",
"categ_id": category.id,
"fiscal_classification_id": classification.id,
"taxes_id": self.classification_B_company_1.sale_tax_ids.ids,
"supplier_taxes_id": self.classification_B_company_1.purchase_tax_ids.ids,
}
product = self._create_product(vals, user=self.user_demo)

# no new classification is created
classif_count_after = self.FiscalClassification.search_count([])
self.assertEqual(classif_count_after, self.initial_classif_count)
# product is linked to the correct classification
self.assertEqual(
product.fiscal_classification_id, self.classification_B_company_1
)

def test_no_classification_one_more_tax_and_create_one(self):
"""Create a product with fiscal settings that looks like
classification_B_company_1 but with an additional supplier tax.
"""

vals = {
"taxes_id": self.classification_B_company_1.sale_tax_ids.ids,
"supplier_taxes_id": self.account_tax_purchase_20_company_1.ids,
}

# create a product with fiscal settings that doesn't match
# existing classification should fail with non accountant user
with self.assertRaises(AccessError):
self._create_product(vals, user=self.user_demo)

# create a product with fiscal settings that doesn't match
# existing classification should success with accountant user
product = self._create_product(vals)
self.assertNotEqual(product.fiscal_classification_id, False)
classif_count_after = self.FiscalClassification.search_count([])
self.assertEqual(classif_count_after, self.initial_classif_count + 1)

def test_no_classification_one_less_tax_and_create_one(self):
"""Create a product with fiscal settings that looks like
classification_B_company_1 but with one less tax
"""

vals = {
"taxes_id": self.account_tax_sale_5_company_1.ids,
"supplier_taxes_id": [],
}
self.ProductTemplate.with_user(user).create(vals)
# create a product with fiscal settings that doesn't match
# existing classification should fail with non accountant user
with self.assertRaises(AccessError):
self._create_product(vals, user=self.user_demo)

# create a product with fiscal settings that doesn't match
# existing classification should success with accountant user
product = self._create_product(vals)

self.assertNotEqual(product.fiscal_classification_id, False)
classif_count_after = self.FiscalClassification.search_count([])
self.assertEqual(classif_count_after, self.initial_classif_count + 1)

def test_no_tax_nor_classification(self):
product = self._create_product({"taxes_id": [], "supplier_taxes_id": []})
classif = product.fiscal_classification_id
self.assertEqual(classif.purchase_tax_ids.ids, [])
self.assertEqual(classif.sale_tax_ids.ids, [])

0 comments on commit da630aa

Please sign in to comment.