From 767341d4ec6aaa4fbd379827da4baf04e561eb32 Mon Sep 17 00:00:00 2001 From: "Ali Alfie (alal)" Date: Tue, 20 Jun 2023 13:16:24 +0300 Subject: [PATCH] [IMP] account: default taxes for multi-company product creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: When a new product was created in a multi-company setting, if the company field is empty, the product is available for all companies. The default sale and purchase tax of the company was set on the product. The problem is that only the default taxes of the currently active company was set on the product, so viewing the product in other companies showed an empty field for the tax. Now: - When creating a new product with the company field empty, the default taxes of the other companies are set on the product as well. - Tax display_name now shows company name if in a multi-company environment and more than one company is selected to make it easier to know which tax belongs to which company. task-3375286 closes odoo/odoo#127196 Related: odoo/enterprise#45075 Signed-off-by: William André (wan) --- addons/account/models/account_move_line.py | 12 +++----- addons/account/models/account_tax.py | 2 ++ addons/account/models/product.py | 27 +++++++++++++++-- addons/account/tests/__init__.py | 1 + addons/account/tests/test_product.py | 29 +++++++++++++++++++ addons/membership/models/partner.py | 11 ++++++- addons/point_of_sale/tests/common.py | 2 +- .../tests/test_project_profitability.py | 12 ++++---- ...st_anglo_saxon_valuation_reconciliation.py | 1 + 9 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 addons/account/tests/test_product.py diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index baf779c2fd4..b5d8178053f 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -866,17 +866,13 @@ class AccountMoveLine(models.Model): company_domain = self.env['account.tax']._check_company_domain(self.move_id.company_id) if self.move_id.is_sale_document(include_receipts=True): # Out invoice. - if self.product_id.taxes_id: - tax_ids = self.product_id.taxes_id.filtered_domain(company_domain) - else: - tax_ids = self.account_id.tax_ids.filtered(lambda tax: tax.type_tax_use == 'sale') + filtered_taxes_id = self.product_id.taxes_id.filtered_domain(company_domain) + tax_ids = filtered_taxes_id or self.account_id.tax_ids.filtered(lambda tax: tax.type_tax_use == 'sale') elif self.move_id.is_purchase_document(include_receipts=True): # In invoice. - if self.product_id.supplier_taxes_id: - tax_ids = self.product_id.supplier_taxes_id.filtered_domain(company_domain) - else: - tax_ids = self.account_id.tax_ids.filtered(lambda tax: tax.type_tax_use == 'purchase') + filtered_supplier_taxes_id = self.product_id.supplier_taxes_id.filtered_domain(company_domain) + tax_ids = filtered_supplier_taxes_id or self.account_id.tax_ids.filtered(lambda tax: tax.type_tax_use == 'purchase') else: tax_ids = self.account_id.tax_ids diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index acafbf14f0c..476c56d7b87 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -381,6 +381,8 @@ class AccountTax(models.Model): name += ' (%s)' % type_tax_use.get(record.type_tax_use) if record.tax_scope: name += ' (%s)' % tax_scope.get(record.tax_scope) + if len(self.env.companies) > 1 and self.env.context.get('params', {}).get('model') == 'product.template': + name += ' (%s)' % record.company_id.display_name record.display_name = name @api.onchange('amount') diff --git a/addons/account/models/product.py b/addons/account/models/product.py index faf9e35437f..509181c8bed 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from odoo import api, fields, models, _ +from odoo import api, Command, fields, models, _ from odoo.exceptions import ValidationError from odoo.osv import expression from odoo.tools import format_amount @@ -27,12 +27,12 @@ class ProductTemplate(models.Model): taxes_id = fields.Many2many('account.tax', 'product_taxes_rel', 'prod_id', 'tax_id', help="Default taxes used when selling the product.", string='Customer Taxes', domain=[('type_tax_use', '=', 'sale')], - default=lambda self: self.env.company.account_sale_tax_id or self.env.company.root_id.account_sale_tax_id, + default=lambda self: self.env.companies.account_sale_tax_id or self.env.companies.root_id.account_sale_tax_id, ) tax_string = fields.Char(compute='_compute_tax_string') supplier_taxes_id = fields.Many2many('account.tax', 'product_supplier_taxes_rel', 'prod_id', 'tax_id', string='Vendor Taxes', help='Default taxes used when buying the product.', domain=[('type_tax_use', '=', 'purchase')], - default=lambda self: self.env.company.account_purchase_tax_id or self.env.company.root_id.account_purchase_tax_id, + default=lambda self: self.env.companies.account_purchase_tax_id or self.env.companies.root_id.account_purchase_tax_id, ) property_account_income_id = fields.Many2one('account.account', company_dependent=True, string="Income Account", @@ -118,6 +118,27 @@ class ProductTemplate(models.Model): "If you want to change its Unit of Measure, please archive this product and create a new one." )) + @api.model_create_multi + def create(self, vals_list): + products = super().create(vals_list) + # If no company was set for the product, the product will be available for all companies and therefore should + # have the default taxes of the other companies as well. sudo() is used since we're going to need to fetch all + # the other companies default taxes which the user may not have access to. + other_companies = self.env['res.company'].sudo().search([('id', 'not in', self.env.companies.ids)]) + if not other_companies: + return products + + default_customer_tax_ids = other_companies.filtered('account_sale_tax_id').account_sale_tax_id.ids + default_supplier_tax_ids = other_companies.filtered('account_purchase_tax_id').account_purchase_tax_id.ids + + products_without_company = products.filtered(lambda p: not p.company_id).sudo() + products_without_company.taxes_id = [Command.link(tax) for tax in default_customer_tax_ids] + products_without_company.supplier_taxes_id = [Command.link(tax) for tax in default_supplier_tax_ids] + + products_without_company.invalidate_recordset(['taxes_id', 'supplier_taxes_id']) + products.invalidate_recordset(['taxes_id', 'supplier_taxes_id']) + return products + class ProductProduct(models.Model): _inherit = "product.product" diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index f97496fa462..8bb201b73d5 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -42,3 +42,4 @@ from . import test_multivat from . import test_account_partner from . import test_setup_wizard from . import test_structured_reference +from . import test_product diff --git a/addons/account/tests/test_product.py b/addons/account/tests/test_product.py new file mode 100644 index 00000000000..4d3e3c4872c --- /dev/null +++ b/addons/account/tests/test_product.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestProduct(AccountTestInvoicingCommon): + + def test_multi_company_product_tax(self): + """ Ensure default taxes are set for all companies on products with no company set. """ + product_without_company = self.env['product.template'].with_context(allowed_company_ids=self.env.company.ids).create({ + 'name': 'Product Without a Company', + }) + product_with_company = self.env['product.template'].with_context(allowed_company_ids=self.env.company.ids).create({ + 'name': 'Product With a Company', + 'company_id': self.company_data['company'].id, + }) + companies = self.env['res.company'].sudo().search([]) + # Product should have all the default taxes of the other companies. + self.assertRecordValues(product_without_company.sudo(), [{ + 'taxes_id': companies.account_sale_tax_id.ids, + 'supplier_taxes_id': companies.account_purchase_tax_id.ids, + }]) + # Product should have only the default tax of the company it belongs to. + self.assertRecordValues(product_with_company.sudo(), [{ + 'taxes_id': self.company_data['company'].account_sale_tax_id.ids, + 'supplier_taxes_id': self.company_data['company'].account_purchase_tax_id.ids, + }]) diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py index 8204176e4ee..a35e9c974e8 100644 --- a/addons/membership/models/partner.py +++ b/addons/membership/models/partner.py @@ -110,7 +110,16 @@ class Partner(models.Model): 'move_type': 'out_invoice', 'partner_id': partner.id, 'invoice_line_ids': [ - (0, None, {'product_id': product.id, 'quantity': 1, 'price_unit': amount, 'tax_ids': [(6, 0, product.taxes_id.ids)]}) + ( + 0, + None, + { + 'product_id': product.id, + 'quantity': 1, + 'price_unit': amount, + 'tax_ids': [(6, 0, product.taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(self.env.company)).ids)] + } + ) ] }) diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index a6ab74afd34..2115f8968ad 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -486,7 +486,7 @@ class TestPoSCommon(ValuationReconciliationTestCommon): def create_order_line(product, quantity, discount=0.0): price_unit = self.pricelist._get_product_price(product, quantity) - tax_ids = fiscal_position.map_tax(product.taxes_id) + tax_ids = fiscal_position.map_tax(product.taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(self.env.company))) price_unit_after_discount = price_unit * (1 - discount / 100.0) tax_values = ( tax_ids.compute_all(price_unit_after_discount, self.currency, quantity) diff --git a/addons/project_sale_expense/tests/test_project_profitability.py b/addons/project_sale_expense/tests/test_project_profitability.py index 5c11555401d..30967abfd8b 100644 --- a/addons/project_sale_expense/tests/test_project_profitability.py +++ b/addons/project_sale_expense/tests/test_project_profitability.py @@ -81,7 +81,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr ) self.assertDictEqual( expense_profitability['costs'], - {'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0 - expense_foreign.untaxed_amount * 0.2, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-280.0 - expense_foreign.untaxed_amount * 0.2), 'to_bill': 0.0}, ) expense_sheet.action_sheet_move_create() @@ -109,7 +109,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr ) self.assertDictEqual( expense_profitability['costs'], - {'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0 - expense_foreign.untaxed_amount * 0.2, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-280.0 - expense_foreign.untaxed_amount * 0.2), 'to_bill': 0.0}, ) self.assertDictEqual( @@ -135,7 +135,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr ) self.assertDictEqual( expense_profitability['costs'], - {'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0 - expense_foreign.untaxed_amount * 0.2, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-280.0 - expense_foreign.untaxed_amount * 0.2), 'to_bill': 0.0}, ) self.assertDictEqual( @@ -183,7 +183,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr ) self.assertDictEqual( expense_profitability['costs'], - {'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0 - expense_foreign.untaxed_amount * 0.2, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-280.0 - expense_foreign.untaxed_amount * 0.2), 'to_bill': 0.0}, ) expense_sheet._do_refuse('Test Cancel Expense') @@ -194,7 +194,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr ) self.assertDictEqual( expense_profitability.get('costs', {}), - {'id': 'expenses', 'sequence': expense_sequence, 'billed': -expense_foreign.untaxed_amount * 0.2, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-expense_foreign.untaxed_amount * 0.2), 'to_bill': 0.0}, ) invoice = self.env['sale.advance.payment.inv'].with_context({ @@ -227,7 +227,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr ) self.assertDictEqual( expense_profitability['costs'], - {'id': 'expenses', 'sequence': expense_sequence, 'billed': -expense_foreign.untaxed_amount * 0.2, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-expense_foreign.untaxed_amount * 0.2), 'to_bill': 0.0}, ) expense_sheet_foreign._do_refuse('Test Cancel Expense') diff --git a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py index 05a43e4ee0a..0e24f3544ee 100644 --- a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py +++ b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py @@ -316,6 +316,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCommon): 'amount': 33.3333, 'company_id': self.company_data['company'].id, 'cash_basis_transition_account_id': cash_basis_transfer_account.id, + 'type_tax_use': 'purchase', 'tax_exigibility': 'on_payment', 'invoice_repartition_line_ids': [ (0, 0, {