diff --git a/.oca/oca-port/blacklist/edi_oca.json b/.oca/oca-port/blacklist/edi_oca.json new file mode 100644 index 0000000000..416159e3d1 --- /dev/null +++ b/.oca/oca-port/blacklist/edi_oca.json @@ -0,0 +1,8 @@ +{ + "pull_requests": { + "OCA/edi-framework#5": "No need", + "OCA/edi-framework#29": "only for > 16.0", + "OCA/edi-framework#63": "FWD port PR from the same version", + "OCA/edi-framework#65": "Only valid for v16" + } +} diff --git a/edi_account_oca/views/res_partner.xml b/edi_account_oca/views/res_partner.xml index 7fffd70055..31a5fcbf44 100644 --- a/edi_account_oca/views/res_partner.xml +++ b/edi_account_oca/views/res_partner.xml @@ -7,6 +7,7 @@ res.partner + diff --git a/edi_oca/__manifest__.py b/edi_oca/__manifest__.py index 6bdcbe77b1..525669478c 100644 --- a/edi_oca/__manifest__.py +++ b/edi_oca/__manifest__.py @@ -26,6 +26,7 @@ "data": [ "wizards/edi_exchange_record_create_wiz.xml", "data/cron.xml", + "data/ir_actions_server.xml", "data/sequence.xml", "data/job_channel.xml", "data/job_function.xml", @@ -36,6 +37,7 @@ "views/edi_exchange_record_views.xml", "views/edi_exchange_type_views.xml", "views/edi_exchange_type_rule_views.xml", + "views/res_partner.xml", "views/menuitems.xml", "templates/exchange_chatter_msg.xml", "templates/exchange_mixin_buttons.xml", diff --git a/edi_oca/data/ir_actions_server.xml b/edi_oca/data/ir_actions_server.xml new file mode 100644 index 0000000000..a74a5c6a65 --- /dev/null +++ b/edi_oca/data/ir_actions_server.xml @@ -0,0 +1,14 @@ + + + + Retry + + + + code + + if records: + action = records.action_retry() + + + diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 504bcd5b72..427c435d61 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -50,6 +50,7 @@ class EDIBackend(models.Model): required=True, ondelete="restrict", ) + backend_type_code = fields.Char(related="backend_type_id.code") output_sent_processed_auto = fields.Boolean( help=""" Automatically set the record as processed after sending. @@ -57,6 +58,7 @@ class EDIBackend(models.Model): """ ) active = fields.Boolean(default=True) + company_id = fields.Many2one("res.company", string="Company") def _get_component(self, exchange_record, key): record_conf = self._get_component_conf_for_record(exchange_record, key) @@ -207,16 +209,23 @@ def exchange_generate(self, exchange_record, store=True, force=False, **kw): :param exchange_record: edi.exchange.record recordset :param store: store output on the record itself - :param force: allow to re-genetate the content + :param force: allow to re-generate the content :param kw: keyword args to be propagated to output generate handler """ self.ensure_one() + if force and exchange_record.exchange_file: + # Remove file to regenerate + exchange_record.exchange_file = False self._check_exchange_generate(exchange_record, force=force) output = self._exchange_generate(exchange_record, **kw) message = None + encoding = exchange_record.type_id.encoding or "UTF-8" + encoding_error_handler = ( + exchange_record.type_id.encoding_out_error_handler or "strict" + ) if output and store: if not isinstance(output, bytes): - output = output.encode() + output = output.encode(encoding, errors=encoding_error_handler) exchange_record.update( { "exchange_file": base64.b64encode(output), @@ -280,6 +289,18 @@ def _exchange_generate(self, exchange_record, **kw): # TODO: add tests def _validate_data(self, exchange_record, value=None, **kw): + if exchange_record.direction == "input" and not exchange_record.exchange_file: + if not exchange_record.type_id.allow_empty_files_on_receive: + raise ValueError( + _( + "Empty files are not allowed for exchange type %(name)s (%(code)s)" + ) + % { + "name": exchange_record.type_id.name, + "code": exchange_record.type_id.code, + } + ) + component = self._get_component(exchange_record, "validate") if component: return component.validate(value) @@ -291,7 +312,7 @@ def exchange_send(self, exchange_record): # In case already sent: skip sending and check the state check = self._output_check_send(exchange_record) if not check: - return "Nothing to do. Likely already sent." + return self._failed_output_check_send_msg() state = exchange_record.edi_exchange_state error = False message = None @@ -389,9 +410,7 @@ def _check_output_exchange_sync( :param skip_sent: ignore records that were already sent. """ # Generate output files - new_records = self.exchange_record_model.search( - self._output_new_records_domain(record_ids=record_ids) - ) + new_records = self._get_new_output_exchange_records(record_ids=record_ids) _logger.info( "EDI Exchange output sync: found %d new records to process.", len(new_records), @@ -422,6 +441,11 @@ def _check_output_exchange_sync( # TODO: run in job as well? self._exchange_output_check_state(rec) + def _get_new_output_exchange_records(self, record_ids=None): + return self.exchange_record_model.search( + self._output_new_records_domain(record_ids=record_ids) + ) + def _output_new_records_domain(self, record_ids=None): """Domain for output records needing output content generation.""" domain = [ @@ -464,7 +488,10 @@ def _exchange_process_check(self, exchange_record): raise exceptions.UserError( _("Record ID=%d is not meant to be processed") % exchange_record.id ) - if not exchange_record.exchange_file: + if ( + not exchange_record.exchange_file + and not exchange_record.type_id.allow_empty_files_on_receive + ): raise exceptions.UserError( _("Record ID=%d has no file to process!") % exchange_record.id ) @@ -535,7 +562,8 @@ def exchange_receive(self, exchange_record): content = None try: content = self._exchange_receive(exchange_record) - if content: + # Ignore result of FileNotFoundError/OSError + if content is not None: exchange_record._set_file_content(content) self._validate_data(exchange_record) except EDIValidationError: @@ -678,3 +706,6 @@ def _is_valid_edi_action(self, action, raise_if_not=False): if raise_if_not: raise return False + + def _failed_output_check_send_msg(self): + return "Nothing to do. Likely already sent." diff --git a/edi_oca/models/edi_exchange_consumer_mixin.py b/edi_oca/models/edi_exchange_consumer_mixin.py index af67a98fe8..773b678bd4 100644 --- a/edi_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_oca/models/edi_exchange_consumer_mixin.py @@ -123,7 +123,9 @@ def fields_view_get( ) if view_type == "form": doc = etree.XML(res["arch"]) - for node in doc.xpath("//sheet"): + # Select main `sheet` only as they can be nested into fields custom forms. + # I'm looking at you `account.view_move_line_form` on v16 :S + for node in doc.xpath("//sheet[not(ancestor::field)]"): # TODO: add a default group group = False if hasattr(self, "_edi_generate_group"): diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index f5243e7128..27649f18f7 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -114,6 +114,7 @@ class EDIExchangeRecord(models.Model): compute="_compute_retryable", help="The record state can be rolled back manually in case of failure.", ) + company_id = fields.Many2one("res.company", string="Company") _sql_constraints = [ ("identifier_uniq", "unique(identifier)", "The identifier must be unique."), @@ -225,11 +226,17 @@ def _get_file_content( ): """Handy method to not have to convert b64 back and forth.""" self.ensure_one() + encoding = self.type_id.encoding or "UTF-8" + decoding_error_handler = self.type_id.encoding_in_error_handler or "strict" if not self[field_name]: return "" if binary: res = base64.b64decode(self[field_name]) - return res.decode() if not as_bytes else res + return ( + res.decode(encoding, errors=decoding_error_handler) + if not as_bytes + else res + ) return self[field_name] def name_get(self): @@ -357,6 +364,10 @@ def _retry_exchange_action(self): self._execute_next_action() return True + def action_regenerate(self): + for rec in self: + rec.action_exchange_generate(force=True) + def action_open_related_record(self): self.ensure_one() if not self.related_record_exists: diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index c29bb70fc1..59e91906af 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -50,7 +50,21 @@ class EDIExchangeType(models.Model): direction = fields.Selection( selection=[("input", "Input"), ("output", "Output")], required=True ) - exchange_filename_pattern = fields.Char(default="{record_name}-{type.code}-{dt}") + exchange_filename_pattern = fields.Char( + default="{record_name}-{type.code}-{dt}", + help="For output exchange types this should be a formatting string " + "with the following variables available (to be used between " + "brackets, `{}`): `exchange_record`, `record_name`, `type`, " + "`dt` and `seq`. For instance, a valid string would be " + "{record_name}-{type.code}-{dt}-{seq}\n" + "For more information:\n" + "- `exchange_record` means exchange record\n" + "- `record_name` means name of the exchange record\n" + "- `type` means code of the exchange record type\n" + "- `dt` means datetime\n" + "- `seq` means sequence. You need a sequence to be defined in " + "`Exchange Filename Sequence` to use `seq`\n", + ) # TODO make required if exchange_filename_pattern is exchange_file_ext = fields.Char() # TODO: this flag should be probably deprecated @@ -152,6 +166,44 @@ class EDIExchangeType(models.Model): "Use it directly or within models rules (domain or snippet)." ), ) + exchange_filename_sequence_id = fields.Many2one( + "ir.sequence", + "Exchange Filename Sequence", + help="If the `Exchange Filename Pattern` has `{seq}`, " + "you should define a sequence in this field to show " + "the sequence in your filename", + ) + # https://docs.python.org/3/library/codecs.html#standard-encodings + encoding = fields.Char( + help="Encoding to be applied to generate/process the exchanged file.\n" + "Example: UTF-8, Windows-1252, ASCII...(default is always 'UTF-8')", + ) + # https://docs.python.org/3/library/codecs.html#codec-base-classes + encoding_out_error_handler = fields.Selection( + string="Encoding Error Handler", + selection=[ + ("strict", "Raise Error"), + ("ignore", "Ignore"), + ("replace", "Replace with Replacement Marker"), + ("backslashreplace", "Replace with Backslashed Escape Sequences"), + ("surrogateescape", "Replace Byte with Individual Surrogate Code"), + ("xmlcharrefreplace", "Replace with XML/HTML Numeric Character Reference"), + ], + help="Handling of encoding errors on generate (default is always 'Raise Error').", + ) + # https://docs.python.org/3/library/codecs.html#codec-base-classes + encoding_in_error_handler = fields.Selection( + string="Decoding Error Handler", + selection=[ + ("strict", "Raise Error"), + ("ignore", "Ignore"), + ("replace", "Replace with Replacement Marker"), + ("backslashreplace", "Replace with Backslashed Escape Sequences"), + ("surrogateescape", "Replace Byte with Individual Surrogate Code"), + ], + help="Handling of decoding errors on process (default is always 'Raise Error').", + ) + allow_empty_files_on_receive = fields.Boolean(string="Allow Empty Files") _sql_constraints = [ ( @@ -216,12 +268,22 @@ def _make_exchange_filename_datetime(self): now = datetime.now(utc).astimezone(tz) return slugify(now.strftime(date_pattern)) + def _make_exchange_filename_sequence(self): + self.ensure_one() + return ( + self.exchange_filename_sequence_id.next_by_id() + if self.exchange_filename_sequence_id + else "" + ) + def _make_exchange_filename(self, exchange_record): """Generate filename.""" pattern = self.exchange_filename_pattern ext = self.exchange_file_ext - pattern = pattern + ".{ext}" + if ext: + pattern += ".{ext}" dt = self._make_exchange_filename_datetime() + seq = self._make_exchange_filename_sequence() record_name = self._get_record_name(exchange_record) record = exchange_record if exchange_record.model and exchange_record.res_id: @@ -232,6 +294,7 @@ def _make_exchange_filename(self, exchange_record): record_name=record_name, type=self, dt=dt, + seq=seq, ext=ext, ) diff --git a/edi_oca/security/ir_model_access.xml b/edi_oca/security/ir_model_access.xml index 83970fb3c5..7920b09069 100644 --- a/edi_oca/security/ir_model_access.xml +++ b/edi_oca/security/ir_model_access.xml @@ -113,4 +113,18 @@ [(1, '=', 1)] + + edi_exchange_record multi-company + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + + + edi_backend multi-company + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + diff --git a/edi_oca/tests/__init__.py b/edi_oca/tests/__init__.py index 30692700a2..b4a8849793 100644 --- a/edi_oca/tests/__init__.py +++ b/edi_oca/tests/__init__.py @@ -13,3 +13,4 @@ from . import test_security from . import test_quick_exec from . import test_exchange_type_deprecated_fields +from . import test_exchange_type_encoding diff --git a/edi_oca/tests/common.py b/edi_oca/tests/common.py index d2b418d865..74047e19f0 100644 --- a/edi_oca/tests/common.py +++ b/edi_oca/tests/common.py @@ -17,7 +17,7 @@ class EDIBackendTestMixin(object): @classmethod def _setup_context(cls, **kw): return dict( - cls.env.context, tracking_disable=True, test_queue_job_no_delay=True, **kw + cls.env.context, tracking_disable=True, queue_job__no_delay=True, **kw ) @classmethod @@ -55,6 +55,14 @@ def _setup_records(cls): cls.exchange_type_out.ack_type_id = cls.exchange_type_out_ack cls.partner = cls.env.ref("base.res_partner_1") cls.partner.ref = "EDI_EXC_TEST" + cls.sequence = cls.env["ir.sequence"].create( + { + "code": "test_sequence", + "name": "Test sequence", + "implementation": "no_gap", + "padding": 7, + } + ) def read_test_file(self, filename): path = os.path.join(os.path.dirname(__file__), "examples", filename) diff --git a/edi_oca/tests/test_backend_input.py b/edi_oca/tests/test_backend_input.py index 63377ba048..0cd8fc48f5 100644 --- a/edi_oca/tests/test_backend_input.py +++ b/edi_oca/tests/test_backend_input.py @@ -43,3 +43,26 @@ def test_receive_record(self): self.backend.with_context(fake_output="yeah!").exchange_receive(self.record) self.assertEqual(self.record._get_file_content(), "yeah!") self.assertRecordValues(self.record, [{"edi_exchange_state": "input_received"}]) + + def test_receive_no_allow_empty_file_record(self): + self.record.edi_exchange_state = "input_pending" + self.backend.with_context( + fake_output="", _edi_receive_break_on_error=False + ).exchange_receive(self.record) + # Check the record + msg = "Empty files are not allowed for exchange type" + self.assertIn(msg, self.record.exchange_error) + self.assertEqual(self.record._get_file_content(), "") + self.assertRecordValues( + self.record, [{"edi_exchange_state": "input_receive_error"}] + ) + + def test_receive_allow_empty_file_record(self): + self.record.edi_exchange_state = "input_pending" + self.record.type_id.allow_empty_files_on_receive = True + self.backend.with_context( + fake_output="", _edi_receive_break_on_error=False + ).exchange_receive(self.record) + # Check the record + self.assertEqual(self.record._get_file_content(), "") + self.assertRecordValues(self.record, [{"edi_exchange_state": "input_received"}]) diff --git a/edi_oca/tests/test_backend_jobs.py b/edi_oca/tests/test_backend_jobs.py index ac23a298ad..1807155381 100644 --- a/edi_oca/tests/test_backend_jobs.py +++ b/edi_oca/tests/test_backend_jobs.py @@ -14,7 +14,7 @@ class EDIBackendTestJobsCase(EDIBackendCommonTestCase, JobMixin): @classmethod def _setup_context(cls): - return dict(super()._setup_context(), test_queue_job_no_delay=None) + return dict(super()._setup_context(), queue_job__no_delay=None) def test_output(self): job_counter = self.job_counter() diff --git a/edi_oca/tests/test_backend_output.py b/edi_oca/tests/test_backend_output.py index a7a63d74ea..5e5a66a055 100644 --- a/edi_oca/tests/test_backend_output.py +++ b/edi_oca/tests/test_backend_output.py @@ -129,7 +129,7 @@ def setUpClass(cls): @classmethod def _setup_context(cls): # Re-enable jobs - return dict(super()._setup_context(), test_queue_job_no_delay=False) + return dict(super()._setup_context(), queue_job__no_delay=False) def test_job(self): with trap_jobs() as trap: diff --git a/edi_oca/tests/test_backend_process.py b/edi_oca/tests/test_backend_process.py index efd11133d2..5aa41a1d34 100644 --- a/edi_oca/tests/test_backend_process.py +++ b/edi_oca/tests/test_backend_process.py @@ -67,9 +67,18 @@ def test_process_record_with_error(self): def test_process_no_file_record(self): self.record.write({"edi_exchange_state": "input_received"}) self.record.exchange_file = False + self.exchange_type_in.allow_empty_files_on_receive = False with self.assertRaises(UserError): self.record.action_exchange_process() + @mute_logger("odoo.models.unlink") + def test_process_allow_no_file_record(self): + self.record.write({"edi_exchange_state": "input_received"}) + self.record.exchange_file = False + self.exchange_type_in.allow_empty_files_on_receive = True + self.record.action_exchange_process() + self.assertEqual(self.record.edi_exchange_state, "input_processed") + def test_process_outbound_record(self): vals = { "model": self.partner._name, diff --git a/edi_oca/tests/test_backend_validate.py b/edi_oca/tests/test_backend_validate.py index b878d005b4..2d244e2137 100644 --- a/edi_oca/tests/test_backend_validate.py +++ b/edi_oca/tests/test_backend_validate.py @@ -91,3 +91,28 @@ def test_generate_validate_record_error(self): ], ) self.assertIn("Data seems wrong!", self.record_out.exchange_error) + + def test_validate_record_error_regenerate(self): + self.record_out.write({"edi_exchange_state": "new"}) + exc = EDIValidationError("Data seems wrong!") + self.backend.with_context(test_break_validate=exc).exchange_generate( + self.record_out + ) + self.assertRecordValues( + self.record_out, + [ + { + "edi_exchange_state": "validate_error", + } + ], + ) + self.record_out.with_context(fake_output="yeah!").action_regenerate() + self.assertEqual(self.record_out._get_file_content(), "yeah!") + self.assertRecordValues( + self.record_out, + [ + { + "edi_exchange_state": "output_pending", + } + ], + ) diff --git a/edi_oca/tests/test_consumer_mixin.py b/edi_oca/tests/test_consumer_mixin.py index ad7dddbe97..c6feaf4406 100644 --- a/edi_oca/tests/test_consumer_mixin.py +++ b/edi_oca/tests/test_consumer_mixin.py @@ -10,7 +10,7 @@ from lxml import etree from odoo_test_helper import FakeModelLoader -from odoo.tests.common import Form +from odoo.tests.common import Form, tagged from .common import EDIBackendCommonTestCase @@ -18,6 +18,7 @@ # This clashes w/ some setup (eg: run tests w/ pytest when edi_storage is installed) # If you still want to run `edi` tests w/ pytest when this happens, set this env var. @unittest.skipIf(os.getenv("SKIP_EDI_CONSUMER_CASE"), "Consumer test case disabled.") +@tagged("at_install", "-post_install") class TestConsumerMixinCase(EDIBackendCommonTestCase): @classmethod def _setup_records(cls): diff --git a/edi_oca/tests/test_exchange_type.py b/edi_oca/tests/test_exchange_type.py index d7bfd8c890..c02db872cb 100644 --- a/edi_oca/tests/test_exchange_type.py +++ b/edi_oca/tests/test_exchange_type.py @@ -95,7 +95,12 @@ def test_filename_pattern_settings(self): # Test without any settings and minimal filename pattern self._test_exchange_filename("Test-File.csv") + # Test without extension for filename pattern + self.exchange_type_out.exchange_file_ext = False + self._test_exchange_filename("Test-File") + # Test with datetime in filename pattern + self.exchange_type_out.exchange_file_ext = "csv" self.exchange_type_out.exchange_filename_pattern = "Test-File-{dt}" self._test_exchange_filename("Test-File-2022-04-28-08-37-24.csv") @@ -126,6 +131,10 @@ def test_filename_pattern_settings(self): date_pattern: '%Y-%m-%d-%H-%M' """ self._test_exchange_filename("Test-File-2022-04-28-10-37.csv") + # Test with sequence in filename pattern + self.exchange_type_out.exchange_filename_pattern = "Test-File-{seq}" + self.exchange_type_out.exchange_filename_sequence_id = self.sequence + self._test_exchange_filename("Test-File-0000001.csv") def test_archive_rules(self): exc_type = self.exchange_type_out diff --git a/edi_oca/tests/test_exchange_type_encoding.py b/edi_oca/tests/test_exchange_type_encoding.py new file mode 100644 index 0000000000..21a3f54dd6 --- /dev/null +++ b/edi_oca/tests/test_exchange_type_encoding.py @@ -0,0 +1,84 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import base64 + +import chardet + +from .common import EDIBackendCommonComponentRegistryTestCase +from .fake_components import FakeOutputGenerator + + +class EDIBackendTestOutputCase(EDIBackendCommonComponentRegistryTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._build_components( + cls, + FakeOutputGenerator, + ) + vals = { + "model": cls.partner._name, + "res_id": cls.partner.id, + } + cls.record = cls.backend.create_record("test_csv_output", vals) + + def setUp(self): + super().setUp() + FakeOutputGenerator.reset_faked() + + def test_encoding_default(self): + """ + Test default output/input encoding (UTF-8). Use string with special + character to test the encoding applied. + """ + self.backend.with_context(fake_output="Palmotićeva").exchange_generate( + self.record + ) + # Test decoding is applied correctly + self.assertEqual(self.record._get_file_content(), "Palmotićeva") + # Test encoding used + content = base64.b64decode(self.record.exchange_file) + encoding = chardet.detect(content)["encoding"].lower() + self.assertEqual(encoding, "utf-8") + + def test_encoding(self): + """ + Test specific output/input encoding. Use string with special + character to test the encoding applied. + """ + self.exchange_type_out.write({"encoding": "UTF-16"}) + self.backend.with_context(fake_output="Palmotićeva").exchange_generate( + self.record + ) + # Test decoding is applied correctly + self.assertEqual(self.record._get_file_content(), "Palmotićeva") + # Test encoding used + content = base64.b64decode(self.record.exchange_file) + encoding = chardet.detect(content)["encoding"].lower() + self.assertEqual(encoding, "utf-16") + + def test_encoding_error_handler(self): + self.exchange_type_out.write({"encoding": "ascii"}) + # By default, error handling raises error + with self.assertRaises(UnicodeEncodeError): + self.backend.with_context(fake_output="Palmotićeva").exchange_generate( + self.record + ) + self.exchange_type_out.write({"encoding_out_error_handler": "ignore"}) + self.backend.with_context(fake_output="Palmotićeva").exchange_generate( + self.record + ) + self.assertEqual(self.record._get_file_content(), "Palmotieva") + + def test_decoding_error_handler(self): + self.backend.with_context(fake_output="Palmotićeva").exchange_generate( + self.record + ) + # Change encoding to ascii to check the decoding + self.exchange_type_out.write({"encoding": "ascii"}) + # By default, error handling raises error + with self.assertRaises(UnicodeDecodeError): + content = self.record._get_file_content() + self.exchange_type_out.write({"encoding_in_error_handler": "ignore"}) + content = self.record._get_file_content() + self.assertEqual(content, "Palmotieva") diff --git a/edi_oca/tests/test_record.py b/edi_oca/tests/test_record.py index f8f00f4506..71eb77907b 100644 --- a/edi_oca/tests/test_record.py +++ b/edi_oca/tests/test_record.py @@ -142,7 +142,7 @@ def test_with_delay_override(self): ) self.exchange_type_in.job_channel_id = channel # re-enable job delayed feature - delayed = record.with_context(test_queue_job_no_delay=False).with_delay() + delayed = record.with_context(queue_job__no_delay=False).with_delay() # Silent useless warning # `Delayable Delayable(edi.exchange.record*) was prepared but never delayed` delayed.delayable._generated_job = object() diff --git a/edi_oca/tests/test_security.py b/edi_oca/tests/test_security.py index 54638f8fbb..1a7f671ff8 100644 --- a/edi_oca/tests/test_security.py +++ b/edi_oca/tests/test_security.py @@ -5,11 +5,13 @@ from odoo_test_helper import FakeModelLoader from odoo.exceptions import AccessError +from odoo.tests.common import tagged from odoo.tools import mute_logger from .common import EDIBackendCommonTestCase +@tagged("at_install", "-post_install") class TestEDIExchangeRecordSecurity(EDIBackendCommonTestCase): @classmethod def _setup_records(cls): diff --git a/edi_oca/views/edi_backend_views.xml b/edi_oca/views/edi_backend_views.xml index 5c78b88f56..c18f3fa735 100644 --- a/edi_oca/views/edi_backend_views.xml +++ b/edi_oca/views/edi_backend_views.xml @@ -6,6 +6,11 @@ + @@ -45,8 +50,11 @@ + + + diff --git a/edi_oca/views/edi_exchange_record_views.xml b/edi_oca/views/edi_exchange_record_views.xml index cd8ba143fb..71100e7d7c 100644 --- a/edi_oca/views/edi_exchange_record_views.xml +++ b/edi_oca/views/edi_exchange_record_views.xml @@ -8,6 +8,11 @@ + @@ -50,6 +55,12 @@ string="Retry" attrs="{'invisible': [('retryable', '=', False)]}" /> +