Skip to content

Commit a09deb3

Browse files
JordiMForgeFlowHviorForgeFlow
authored andcommitted
[IMP] edi_oca: add encoding management for exchange type
1 parent a51be65 commit a09deb3

6 files changed

+136
-2
lines changed

edi_oca/models/edi_backend.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,13 @@ def exchange_generate(self, exchange_record, store=True, force=False, **kw):
214214
self._check_exchange_generate(exchange_record, force=force)
215215
output = self._exchange_generate(exchange_record, **kw)
216216
message = None
217+
encoding = exchange_record.type_id.encoding or "UTF-8"
218+
encoding_error_handler = (
219+
exchange_record.type_id.encoding_out_error_handler or "strict"
220+
)
217221
if output and store:
218222
if not isinstance(output, bytes):
219-
output = output.encode()
223+
output = output.encode(encoding, errors=encoding_error_handler)
220224
exchange_record.update(
221225
{
222226
"exchange_file": base64.b64encode(output),

edi_oca/models/edi_exchange_record.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,17 @@ def _get_file_content(
225225
):
226226
"""Handy method to not have to convert b64 back and forth."""
227227
self.ensure_one()
228+
encoding = self.type_id.encoding or "UTF-8"
229+
decoding_error_handler = self.type_id.encoding_in_error_handler or "strict"
228230
if not self[field_name]:
229231
return ""
230232
if binary:
231233
res = base64.b64decode(self[field_name])
232-
return res.decode() if not as_bytes else res
234+
return (
235+
res.decode(encoding, errors=decoding_error_handler)
236+
if not as_bytes
237+
else res
238+
)
233239
return self[field_name]
234240

235241
def name_get(self):

edi_oca/models/edi_exchange_type.py

+30
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,36 @@ class EDIExchangeType(models.Model):
152152
"Use it directly or within models rules (domain or snippet)."
153153
),
154154
)
155+
# https://docs.python.org/3/library/codecs.html#standard-encodings
156+
encoding = fields.Char(
157+
help="Encoding to be applied to generate/process the exchanged file.\n"
158+
"Example: UTF-8, Windows-1252, ASCII...(default is always 'UTF-8')",
159+
)
160+
# https://docs.python.org/3/library/codecs.html#codec-base-classes
161+
encoding_out_error_handler = fields.Selection(
162+
string="Encoding Error Handler",
163+
selection=[
164+
("strict", "Raise Error"),
165+
("ignore", "Ignore"),
166+
("replace", "Replace with Replacement Marker"),
167+
("backslashreplace", "Replace with Backslashed Escape Sequences"),
168+
("surrogateescape", "Replace Byte with Individual Surrogate Code"),
169+
("xmlcharrefreplace", "Replace with XML/HTML Numeric Character Reference"),
170+
],
171+
help="Handling of encoding errors on generate (default is always 'Raise Error').",
172+
)
173+
# https://docs.python.org/3/library/codecs.html#codec-base-classes
174+
encoding_in_error_handler = fields.Selection(
175+
string="Decoding Error Handler",
176+
selection=[
177+
("strict", "Raise Error"),
178+
("ignore", "Ignore"),
179+
("replace", "Replace with Replacement Marker"),
180+
("backslashreplace", "Replace with Backslashed Escape Sequences"),
181+
("surrogateescape", "Replace Byte with Individual Surrogate Code"),
182+
],
183+
help="Handling of decoding errors on process (default is always 'Raise Error').",
184+
)
155185

156186
_sql_constraints = [
157187
(

edi_oca/tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
from . import test_security
1414
from . import test_quick_exec
1515
from . import test_exchange_type_deprecated_fields
16+
from . import test_exchange_type_encoding
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com)
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
3+
import base64
4+
5+
import chardet
6+
7+
from .common import EDIBackendCommonComponentRegistryTestCase
8+
from .fake_components import FakeOutputGenerator
9+
10+
11+
class EDIBackendTestOutputCase(EDIBackendCommonComponentRegistryTestCase):
12+
@classmethod
13+
def setUpClass(cls):
14+
super().setUpClass()
15+
cls._build_components(
16+
cls,
17+
FakeOutputGenerator,
18+
)
19+
vals = {
20+
"model": cls.partner._name,
21+
"res_id": cls.partner.id,
22+
}
23+
cls.record = cls.backend.create_record("test_csv_output", vals)
24+
25+
def setUp(self):
26+
super().setUp()
27+
FakeOutputGenerator.reset_faked()
28+
29+
def test_encoding_default(self):
30+
"""
31+
Test default output/input encoding (UTF-8). Use string with special
32+
character to test the encoding applied.
33+
"""
34+
self.backend.with_context(fake_output="Palmotićeva").exchange_generate(
35+
self.record
36+
)
37+
# Test decoding is applied correctly
38+
self.assertEqual(self.record._get_file_content(), "Palmotićeva")
39+
# Test encoding used
40+
content = base64.b64decode(self.record.exchange_file)
41+
encoding = chardet.detect(content)["encoding"].lower()
42+
self.assertEqual(encoding, "utf-8")
43+
44+
def test_encoding(self):
45+
"""
46+
Test specific output/input encoding. Use string with special
47+
character to test the encoding applied.
48+
"""
49+
self.exchange_type_out.write({"encoding": "UTF-16"})
50+
self.backend.with_context(fake_output="Palmotićeva").exchange_generate(
51+
self.record
52+
)
53+
# Test decoding is applied correctly
54+
self.assertEqual(self.record._get_file_content(), "Palmotićeva")
55+
# Test encoding used
56+
content = base64.b64decode(self.record.exchange_file)
57+
encoding = chardet.detect(content)["encoding"].lower()
58+
self.assertEqual(encoding, "utf-16")
59+
60+
def test_encoding_error_handler(self):
61+
self.exchange_type_out.write({"encoding": "ascii"})
62+
# By default, error handling raises error
63+
with self.assertRaises(UnicodeEncodeError):
64+
self.backend.with_context(fake_output="Palmotićeva").exchange_generate(
65+
self.record
66+
)
67+
self.exchange_type_out.write({"encoding_out_error_handler": "ignore"})
68+
self.backend.with_context(fake_output="Palmotićeva").exchange_generate(
69+
self.record
70+
)
71+
self.assertEqual(self.record._get_file_content(), "Palmotieva")
72+
73+
def test_decoding_error_handler(self):
74+
self.backend.with_context(fake_output="Palmotićeva").exchange_generate(
75+
self.record
76+
)
77+
# Change encoding to ascii to check the decoding
78+
self.exchange_type_out.write({"encoding": "ascii"})
79+
# By default, error handling raises error
80+
with self.assertRaises(UnicodeDecodeError):
81+
content = self.record._get_file_content()
82+
self.exchange_type_out.write({"encoding_in_error_handler": "ignore"})
83+
content = self.record._get_file_content()
84+
self.assertEqual(content, "Palmotieva")

edi_oca/views/edi_exchange_type_views.xml

+9
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@
4141
<field name="partner_ids" widget="many2many_tags" />
4242
<field name="job_channel_id" />
4343
<field name="quick_exec" />
44+
<field name="encoding" />
45+
<field
46+
name="encoding_out_error_handler"
47+
attrs="{'invisible': [('direction', '=', 'input')]}"
48+
/>
49+
<field
50+
name="encoding_in_error_handler"
51+
attrs="{'invisible': [('direction', '=', 'output')]}"
52+
/>
4453
</group>
4554
</group>
4655
<field name="deprecated_rule_fields_still_used" invisible="1" />

0 commit comments

Comments
 (0)