From 6c2d916778b4db38b7dbd1585f1e4d0c8922a656 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 3 Oct 2023 13:10:54 +0200 Subject: [PATCH 01/17] edi: fix consumer get_view w/ nested forms Select main only as they can be nested into field custom forms. I'm looking at you on v16 :S --- edi_oca/models/edi_exchange_consumer_mixin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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"): From 8501c635aadf3975c40b584409817295c19011be Mon Sep 17 00:00:00 2001 From: OriolMForgeFlow Date: Wed, 25 Oct 2023 11:55:29 +0200 Subject: [PATCH 02/17] [IMP] edi_oca: add company_id field with multi-company rule --- edi_oca/models/edi_exchange_record.py | 1 + edi_oca/security/ir_model_access.xml | 7 +++++++ edi_oca/views/edi_exchange_record_views.xml | 9 +++++++++ 3 files changed, 17 insertions(+) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index f5243e7128..356da5b134 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."), diff --git a/edi_oca/security/ir_model_access.xml b/edi_oca/security/ir_model_access.xml index 83970fb3c5..551101e030 100644 --- a/edi_oca/security/ir_model_access.xml +++ b/edi_oca/security/ir_model_access.xml @@ -113,4 +113,11 @@ [(1, '=', 1)] + + edi_exchange_record multi-company + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + diff --git a/edi_oca/views/edi_exchange_record_views.xml b/edi_oca/views/edi_exchange_record_views.xml index cd8ba143fb..63302588ea 100644 --- a/edi_oca/views/edi_exchange_record_views.xml +++ b/edi_oca/views/edi_exchange_record_views.xml @@ -8,6 +8,11 @@ + @@ -97,6 +102,10 @@ + From c49fbaa992ed9fd964d5cecd3b41fa39507774cf Mon Sep 17 00:00:00 2001 From: duongtq Date: Thu, 14 Dec 2023 16:27:53 +0700 Subject: [PATCH 03/17] [IMP] edi_oca: support exchange filename without extension on exchange record --- edi_oca/models/edi_exchange_type.py | 3 ++- edi_oca/tests/test_exchange_type.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index c29bb70fc1..5e13577d91 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -220,7 +220,8 @@ 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() record_name = self._get_record_name(exchange_record) record = exchange_record diff --git a/edi_oca/tests/test_exchange_type.py b/edi_oca/tests/test_exchange_type.py index d7bfd8c890..7f1e53a65a 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") From 73352c01844e2570b01d7eac799fa401c77b2d4f Mon Sep 17 00:00:00 2001 From: duongtq Date: Wed, 10 Jan 2024 10:16:46 +0700 Subject: [PATCH 04/17] [IMP] edi_oca: add partner form page --- edi_account_oca/views/res_partner.xml | 1 + edi_oca/__manifest__.py | 1 + edi_oca/views/res_partner.xml | 17 +++++++++++++++++ edi_stock_oca/views/res_partner.xml | 1 + 4 files changed, 20 insertions(+) create mode 100644 edi_oca/views/res_partner.xml 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..eba762c77d 100644 --- a/edi_oca/__manifest__.py +++ b/edi_oca/__manifest__.py @@ -36,6 +36,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/views/res_partner.xml b/edi_oca/views/res_partner.xml new file mode 100644 index 0000000000..904a75caef --- /dev/null +++ b/edi_oca/views/res_partner.xml @@ -0,0 +1,17 @@ + + + + + res.partner.view.form + res.partner + + + + + + + + + + diff --git a/edi_stock_oca/views/res_partner.xml b/edi_stock_oca/views/res_partner.xml index 81ace2803d..be4ece87f9 100644 --- a/edi_stock_oca/views/res_partner.xml +++ b/edi_stock_oca/views/res_partner.xml @@ -7,6 +7,7 @@ res.partner + From 130ac8fba0a1efe0a11af3182d8ffca29ce95f14 Mon Sep 17 00:00:00 2001 From: Nils Hamerlinck Date: Mon, 15 Jan 2024 15:34:51 +0700 Subject: [PATCH 05/17] [FIX] edi_oca: tests using FakeModelLoader should not be post_install --- edi_oca/tests/test_consumer_mixin.py | 3 ++- edi_oca/tests/test_security.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) 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_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): From 258c0cc45e632bee2dc301c1e67bf8104f3b002a Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Tue, 16 Jan 2024 11:24:55 +0700 Subject: [PATCH 06/17] [IMP] edi_oca: allow use of sequence for exchange filename pattern --- edi_oca/models/edi_exchange_type.py | 33 ++++++++++++++++++++++- edi_oca/tests/common.py | 8 ++++++ edi_oca/tests/test_exchange_type.py | 4 +++ edi_oca/views/edi_exchange_type_views.xml | 1 + 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index 5e13577d91..f9b38d01a8 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,13 @@ 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", + ) _sql_constraints = [ ( @@ -216,6 +237,14 @@ 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 @@ -223,6 +252,7 @@ def _make_exchange_filename(self, exchange_record): 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: @@ -233,6 +263,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/tests/common.py b/edi_oca/tests/common.py index d2b418d865..f4e45f7a03 100644 --- a/edi_oca/tests/common.py +++ b/edi_oca/tests/common.py @@ -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_exchange_type.py b/edi_oca/tests/test_exchange_type.py index 7f1e53a65a..c02db872cb 100644 --- a/edi_oca/tests/test_exchange_type.py +++ b/edi_oca/tests/test_exchange_type.py @@ -131,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/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml index 2cc4e5db8a..0e03ee93e0 100644 --- a/edi_oca/views/edi_exchange_type_views.xml +++ b/edi_oca/views/edi_exchange_type_views.xml @@ -41,6 +41,7 @@ + From 89de1b21489704483fe574d0e0430dd32604ac5b Mon Sep 17 00:00:00 2001 From: JordiMForgeFlow Date: Fri, 2 Feb 2024 08:35:44 +0100 Subject: [PATCH 07/17] [IMP] edi_oca: edi_backend multi-company --- edi_oca/models/edi_backend.py | 1 + edi_oca/security/ir_model_access.xml | 7 +++++++ edi_oca/views/edi_backend_views.xml | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 504bcd5b72..dfb01b8df1 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -57,6 +57,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) diff --git a/edi_oca/security/ir_model_access.xml b/edi_oca/security/ir_model_access.xml index 551101e030..7920b09069 100644 --- a/edi_oca/security/ir_model_access.xml +++ b/edi_oca/security/ir_model_access.xml @@ -120,4 +120,11 @@ name="domain_force" >['|',('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/views/edi_backend_views.xml b/edi_oca/views/edi_backend_views.xml index 5c78b88f56..57a6ee466b 100644 --- a/edi_oca/views/edi_backend_views.xml +++ b/edi_oca/views/edi_backend_views.xml @@ -6,6 +6,11 @@ + @@ -47,6 +52,7 @@ + From fa184cd9a6336b0b8ff6a860f7f9d2d304ff5bf5 Mon Sep 17 00:00:00 2001 From: JordiMForgeFlow Date: Tue, 16 Jan 2024 12:20:48 +0100 Subject: [PATCH 08/17] [IMP] edi_oca: add encoding management for exchange type --- edi_oca/models/edi_backend.py | 6 +- edi_oca/models/edi_exchange_record.py | 8 +- edi_oca/models/edi_exchange_type.py | 30 +++++++ edi_oca/tests/__init__.py | 1 + edi_oca/tests/test_exchange_type_encoding.py | 84 ++++++++++++++++++++ edi_oca/views/edi_exchange_type_views.xml | 9 +++ 6 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 edi_oca/tests/test_exchange_type_encoding.py diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index dfb01b8df1..385f2b0214 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -215,9 +215,13 @@ def exchange_generate(self, exchange_record, store=True, force=False, **kw): 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), diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 356da5b134..232fd06df1 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -226,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): diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index f9b38d01a8..1b1820163d 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -173,6 +173,36 @@ class EDIExchangeType(models.Model): "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').", + ) _sql_constraints = [ ( 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/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/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml index 0e03ee93e0..d9280ff3eb 100644 --- a/edi_oca/views/edi_exchange_type_views.xml +++ b/edi_oca/views/edi_exchange_type_views.xml @@ -49,6 +49,15 @@ + + + From f314e3636d70e227bf578e83a2e1e83ffd2eb9a4 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Wed, 6 Mar 2024 10:49:59 +0100 Subject: [PATCH 09/17] [IMP] edi_oca: add a backend_type_code field on the backend this field can be used in backend views to show/hide configuration elements depending on the backend type --- edi_oca/models/edi_backend.py | 1 + edi_oca/views/edi_backend_views.xml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 385f2b0214..839326bece 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. diff --git a/edi_oca/views/edi_backend_views.xml b/edi_oca/views/edi_backend_views.xml index 57a6ee466b..c18f3fa735 100644 --- a/edi_oca/views/edi_backend_views.xml +++ b/edi_oca/views/edi_backend_views.xml @@ -50,6 +50,8 @@ + + From 3b0fe312a0f5efe00bda0f1d6bc960a29ebf10d6 Mon Sep 17 00:00:00 2001 From: duongtq Date: Mon, 27 May 2024 16:18:05 +0700 Subject: [PATCH 10/17] [IMP] edi_oca: allow to receive empty files Currently when we receive an empty file: - Receive step doesn't end up in state error_on_receive : that's because we just ignore the content here - But the process step will raise an error - So this commit goal tries to improve that - By default, this option is disabled so we still consider that an empty file is an error case --- edi_oca/models/edi_backend.py | 14 ++++++++++++-- edi_oca/models/edi_exchange_type.py | 1 + edi_oca/tests/test_backend_input.py | 23 +++++++++++++++++++++++ edi_oca/tests/test_backend_process.py | 9 +++++++++ edi_oca/views/edi_exchange_type_views.xml | 4 ++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 839326bece..655a74df8a 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -286,6 +286,12 @@ 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 this exchange type") + ) + component = self._get_component(exchange_record, "validate") if component: return component.validate(value) @@ -470,7 +476,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 ) @@ -541,7 +550,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: diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index 1b1820163d..59e91906af 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -203,6 +203,7 @@ class EDIExchangeType(models.Model): ], 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 = [ ( diff --git a/edi_oca/tests/test_backend_input.py b/edi_oca/tests/test_backend_input.py index 63377ba048..757f7fd5ef 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 this 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_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/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml index d9280ff3eb..ba28cd3436 100644 --- a/edi_oca/views/edi_exchange_type_views.xml +++ b/edi_oca/views/edi_exchange_type_views.xml @@ -58,6 +58,10 @@ name="encoding_in_error_handler" attrs="{'invisible': [('direction', '=', 'output')]}" /> + From b51a3e5cb9ad151ebfd201fcfdbf46002fe0a1b3 Mon Sep 17 00:00:00 2001 From: duongtq Date: Wed, 8 May 2024 14:13:23 +0700 Subject: [PATCH 11/17] [IMP] edi_oca: Add Retry action on edi.exchange.record list view --- edi_oca/__manifest__.py | 1 + edi_oca/data/ir_actions_server.xml | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 edi_oca/data/ir_actions_server.xml diff --git a/edi_oca/__manifest__.py b/edi_oca/__manifest__.py index eba762c77d..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", 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() + + + From 460195b3b5fc301e80469da785b9227db2a529ef Mon Sep 17 00:00:00 2001 From: JordiMForgeFlow Date: Tue, 4 Jun 2024 12:42:54 +0200 Subject: [PATCH 12/17] [IMP] edi_oca: add button to regenerate file after error on validation --- edi_oca/models/edi_backend.py | 5 ++++- edi_oca/models/edi_exchange_record.py | 4 ++++ edi_oca/tests/test_backend_validate.py | 25 +++++++++++++++++++++ edi_oca/views/edi_exchange_record_views.xml | 6 +++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 655a74df8a..693ea70236 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -209,10 +209,13 @@ 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 diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 232fd06df1..27649f18f7 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -364,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/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/views/edi_exchange_record_views.xml b/edi_oca/views/edi_exchange_record_views.xml index 63302588ea..71100e7d7c 100644 --- a/edi_oca/views/edi_exchange_record_views.xml +++ b/edi_oca/views/edi_exchange_record_views.xml @@ -55,6 +55,12 @@ string="Retry" attrs="{'invisible': [('retryable', '=', False)]}" /> +