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)]}"
/>
+
@@ -97,6 +108,10 @@
+
diff --git a/edi_oca/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml
index 2cc4e5db8a..ba28cd3436 100644
--- a/edi_oca/views/edi_exchange_type_views.xml
+++ b/edi_oca/views/edi_exchange_type_views.xml
@@ -41,6 +41,7 @@
+
@@ -48,6 +49,19 @@
+
+
+
+
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
+