diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 02a475c7323..9ca5d54d708 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -529,6 +529,7 @@ class AccountBankStatementLine(models.Model): compute='_compute_is_reconciled', help="Technical field indicating if the statement line is already reconciled.") state = fields.Selection(related='statement_id.state', string='Status', readonly=True) + country_code = fields.Char(related='company_id.country_id.code') # ------------------------------------------------------------------------- # HELPERS diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index 2a389ff33a9..9be7d9c68b5 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -101,6 +101,7 @@ class AccountJournal(models.Model): currency_id = fields.Many2one('res.currency', help='The currency used to enter statement', string="Currency") company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, index=True, default=lambda self: self.env.company, help="Company related to this journal") + country_code = fields.Char(related='company_id.country_id.code', readonly=True) refund_sequence = fields.Boolean(string='Dedicated Credit Note Sequence', help="Check this box if you don't want to share the same sequence for invoices and credit notes made from this journal", default=False) sequence_override_regex = fields.Text(help="Technical field used to enforce complex sequence composition that the system would normally misunderstand.\n"\ diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 99753a10e1d..3cea680f502 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -306,6 +306,7 @@ class AccountMove(models.Model): help="Technical field used to display a message when the invoice's accounting date is prior of the tax lock date.") # Technical field to hide Reconciled Entries stat button has_reconciled_entries = fields.Boolean(compute="_compute_has_reconciled_entries") + show_reset_to_draft_button = fields.Boolean(compute='_compute_show_reset_to_draft_button') # ==== Hash Fields ==== restrict_mode_hash_table = fields.Boolean(related='journal_id.restrict_mode_hash_table') @@ -1430,6 +1431,11 @@ class AccountMove(models.Model): else: move.tax_lock_date_message = False + @api.depends('restrict_mode_hash_table', 'state') + def _compute_show_reset_to_draft_button(self): + for move in self: + move.show_reset_to_draft_button = not move.restrict_mode_hash_table and move.state in ('posted', 'cancel') + # ------------------------------------------------------------------------- # BUSINESS MODELS SYNCHRONIZATION # ------------------------------------------------------------------------- diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 1fbd206f050..a3f6d746212 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -125,6 +125,7 @@ class AccountPayment(models.Model): require_partner_bank_account = fields.Boolean( compute='_compute_show_require_partner_bank', help="Technical field used to know whether the field `partner_bank_id` needs to be required or not in the payments form views") + country_code = fields.Char(related='company_id.country_id.code') _sql_constraints = [ ( diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 4e389fcd89f..6d04db8b3c6 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -92,6 +92,7 @@ class AccountTax(models.Model): invoice_repartition_line_ids = fields.One2many(string="Repartition for Invoices", comodel_name="account.tax.repartition.line", inverse_name="invoice_tax_id", copy=True, help="Repartition when the tax is used on an invoice") refund_repartition_line_ids = fields.One2many(string="Repartition for Refund Invoices", comodel_name="account.tax.repartition.line", inverse_name="refund_tax_id", copy=True, help="Repartition when the tax is used on a refund") tax_fiscal_country_id = fields.Many2one(string='Fiscal Country', comodel_name='res.country', related='company_id.account_tax_fiscal_country_id', help="Technical field used to restrict the domain of account tags for tax repartition lines created for this tax.") + country_code = fields.Char(related='company_id.country_id.code', readonly=True) _sql_constraints = [ ('name_company_uniq', 'unique(name, company_id, type_tax_use, tax_scope)', 'Tax names must be unique !'), diff --git a/addons/account/tests/account_test_xml.py b/addons/account/tests/account_test_xml.py deleted file mode 100644 index 04fc717a12c..00000000000 --- a/addons/account/tests/account_test_xml.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo.addons.account.tests.common import AccountTestInvoicingCommon - -import base64 - -from lxml import etree - - -class AccountTestEdiCommon(AccountTestInvoicingCommon): - - def assertXmlTreeEqual(self, xml_tree, expected_xml_tree): - ''' Compare two lxml.etree. - :param xml_tree: The current tree. - :param expected_xml_tree: The expected tree. - ''' - - def _turn_node_as_dict_hierarchy(node): - ''' Turn the node as a python dictionary to be compared later with another one. - Allow to ignore the management of namespaces. - :param node: A node inside an xml tree. - :return: A python dictionary. - ''' - tag_split = node.tag.split('}') - tag_wo_ns = tag_split[-1] - attrib_wo_ns = {k: v for k, v in node.attrib.items() if '}' not in k} - return { - 'tag': tag_wo_ns, - 'namespace': None if len(tag_split) < 2 else tag_split[0], - 'text': (node.text or '').strip(), - 'attrib': attrib_wo_ns, - 'children': [_turn_node_as_dict_hierarchy(child_node) for child_node in node.getchildren()], - } - - def assertNodeDictEqual(node_dict, expected_node_dict): - ''' Compare nodes created by the `_turn_node_as_dict_hierarchy` method. - :param node_dict: The node to compare with. - :param expected_node_dict: The expected node. - ''' - # Check tag. - self.assertEqual(node_dict['tag'], expected_node_dict['tag']) - - # Check attributes. - node_dict_attrib = {k: '___ignore___' if expected_node_dict['attrib'].get(k) == '___ignore___' else v - for k, v in node_dict['attrib'].items()} - expected_node_dict_attrib = {k: v for k, v in expected_node_dict['attrib'].items() if v != '___remove___'} - self.assertDictEqual( - node_dict_attrib, - expected_node_dict_attrib, - "Element attributes are different for node %s" % node_dict['tag'], - ) - - # Check text. - if expected_node_dict['text'] != '___ignore___': - self.assertEqual( - node_dict['text'], - expected_node_dict['text'], - "Element text are different for node %s" % node_dict['tag'], - ) - - # Check children. - self.assertEqual( - [child['tag'] for child in node_dict['children']], - [child['tag'] for child in expected_node_dict['children']], - "Number of children elements for node %s is different." % node_dict['tag'], - ) - - for child_node_dict, expected_child_node_dict in zip(node_dict['children'], expected_node_dict['children']): - assertNodeDictEqual(child_node_dict, expected_child_node_dict) - - assertNodeDictEqual( - _turn_node_as_dict_hierarchy(xml_tree), - _turn_node_as_dict_hierarchy(expected_xml_tree), - ) - - def with_applied_xpath(self, xml_tree, xpath): - ''' Applies the xpath to the xml_tree passed as parameter. - :param xml_tree: An instance of etree. - :param xpath: The xpath to apply as a string. - :return: The resulting etree after applying the xpaths. - ''' - diff_xml_tree = etree.fromstring('%s' % xpath) - return self.env['ir.ui.view'].apply_inheritance_specs(xml_tree, diff_xml_tree) - - def get_xml_tree_from_attachment(self, attachment): - ''' Extract an instance of etree from an ir.attachment. - :param attachment: An ir.attachment. - :return: An instance of etree. - ''' - return etree.fromstring(base64.b64decode(attachment.with_context(bin_size=False).datas)) - - def get_xml_tree_from_string(self, xml_tree_str): - ''' Convert the string passed as parameter to an instance of etree. - :param xml_tree_str: A string representing an xml. - :return: An instance of etree. - ''' - return etree.fromstring(xml_tree_str) diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py index fdb28764ff3..51bcecb9d01 100644 --- a/addons/account/tests/common.py +++ b/addons/account/tests/common.py @@ -4,7 +4,8 @@ from odoo import fields from odoo.tests.common import SavepointCase, HttpSavepointCase, tagged, Form import time - +import base64 +from lxml import etree @tagged('post_install', '-at_install') class AccountTestInvoicingCommon(SavepointCase): @@ -45,7 +46,7 @@ class AccountTestInvoicingCommon(SavepointCase): cls.cr = cls.env.cr cls.company_data_2 = cls.setup_company_data('company_2_data', chart_template=chart_template) - cls.company_data = cls.setup_company_data('company_1_data', chart_template=chart_template) + cls.company_data = cls.setup_company_data('company_1_data', chart_template=chart_template) user.write({ 'company_ids': [(6, 0, (cls.company_data['company'] + cls.company_data_2['company']).ids)], @@ -375,6 +376,97 @@ class AccountTestInvoicingCommon(SavepointCase): self.assertRecordValues(sort_lines(move.invoice_line_ids.sorted()), expected_lines_values[:len(move.invoice_line_ids)]) self.assertRecordValues(move, [expected_move_values]) + #################################################### + # Xml Comparison + #################################################### + + def _turn_node_as_dict_hierarchy(self, node): + ''' Turn the node as a python dictionary to be compared later with another one. + Allow to ignore the management of namespaces. + :param node: A node inside an xml tree. + :return: A python dictionary. + ''' + tag_split = node.tag.split('}') + tag_wo_ns = tag_split[-1] + attrib_wo_ns = {k: v for k, v in node.attrib.items() if '}' not in k} + return { + 'tag': tag_wo_ns, + 'namespace': None if len(tag_split) < 2 else tag_split[0], + 'text': (node.text or '').strip(), + 'attrib': attrib_wo_ns, + 'children': [self._turn_node_as_dict_hierarchy(child_node) for child_node in node.getchildren()], + } + + def assertXmlTreeEqual(self, xml_tree, expected_xml_tree): + ''' Compare two lxml.etree. + :param xml_tree: The current tree. + :param expected_xml_tree: The expected tree. + ''' + + def assertNodeDictEqual(node_dict, expected_node_dict): + ''' Compare nodes created by the `_turn_node_as_dict_hierarchy` method. + :param node_dict: The node to compare with. + :param expected_node_dict: The expected node. + ''' + # Check tag. + self.assertEqual(node_dict['tag'], expected_node_dict['tag']) + + # Check attributes. + node_dict_attrib = {k: '___ignore___' if expected_node_dict['attrib'].get(k) == '___ignore___' else v + for k, v in node_dict['attrib'].items()} + expected_node_dict_attrib = {k: v for k, v in expected_node_dict['attrib'].items() if v != '___remove___'} + self.assertDictEqual( + node_dict_attrib, + expected_node_dict_attrib, + "Element attributes are different for node %s" % node_dict['tag'], + ) + + # Check text. + if expected_node_dict['text'] != '___ignore___': + self.assertEqual( + node_dict['text'], + expected_node_dict['text'], + "Element text are different for node %s" % node_dict['tag'], + ) + + # Check children. + self.assertEqual( + [child['tag'] for child in node_dict['children']], + [child['tag'] for child in expected_node_dict['children']], + "Number of children elements for node %s is different." % node_dict['tag'], + ) + + for child_node_dict, expected_child_node_dict in zip(node_dict['children'], expected_node_dict['children']): + assertNodeDictEqual(child_node_dict, expected_child_node_dict) + + assertNodeDictEqual( + self._turn_node_as_dict_hierarchy(xml_tree), + self._turn_node_as_dict_hierarchy(expected_xml_tree), + ) + + def with_applied_xpath(self, xml_tree, xpath): + ''' Applies the xpath to the xml_tree passed as parameter. + :param xml_tree: An instance of etree. + :param xpath: The xpath to apply as a string. + :return: The resulting etree after applying the xpaths. + ''' + diff_xml_tree = etree.fromstring('%s' % xpath) + return self.env['ir.ui.view'].apply_inheritance_specs(xml_tree, diff_xml_tree) + + def get_xml_tree_from_attachment(self, attachment): + ''' Extract an instance of etree from an ir.attachment. + :param attachment: An ir.attachment. + :return: An instance of etree. + ''' + return etree.fromstring(base64.b64decode(attachment.with_context(bin_size=False).datas)) + + def get_xml_tree_from_string(self, xml_tree_str): + ''' Convert the string passed as parameter to an instance of etree. + :param xml_tree_str: A string representing an xml. + :return: An instance of etree. + ''' + return etree.fromstring(xml_tree_str) + @tagged('post_install', '-at_install') class AccountTestInvoicingHttpCommon(AccountTestInvoicingCommon, HttpSavepointCase): diff --git a/addons/account/views/account_bank_statement_views.xml b/addons/account/views/account_bank_statement_views.xml index 129ececf2cf..55308646a95 100644 --- a/addons/account/views/account_bank_statement_views.xml +++ b/addons/account/views/account_bank_statement_views.xml @@ -196,6 +196,7 @@ + diff --git a/addons/account/views/account_journal_views.xml b/addons/account/views/account_journal_views.xml index 2567bf53215..7a829dfb76f 100644 --- a/addons/account/views/account_journal_views.xml +++ b/addons/account/views/account_journal_views.xml @@ -36,6 +36,7 @@ + diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 2a70e60d9a3..c9e12964093 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -523,7 +523,7 @@