Skip to content

Commit

Permalink
Merge PR #1052 into 14.0
Browse files Browse the repository at this point in the history
Signed-off-by simahawk
  • Loading branch information
OCA-git-bot committed Nov 15, 2024
2 parents acb4d94 + 9155585 commit 678319d
Show file tree
Hide file tree
Showing 26 changed files with 380 additions and 17 deletions.
8 changes: 8 additions & 0 deletions .oca/oca-port/blacklist/edi_oca.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions edi_account_oca/views/res_partner.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form" />
<field name="arch" type="xml">
<!-- TODO: Move this conf inside `edi` page from edi_oca. -->
<group name="accounting_entries" position="after">
<group name="edi_configuration" string="EDI Configuration" />
</group>
Expand Down
2 changes: 2 additions & 0 deletions edi_oca/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions edi_oca/data/ir_actions_server.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.actions.server" id="action_retry_edi_exchange_record">
<field name="name">Retry</field>
<field name="groups_id" eval="[(4, ref('base_edi.group_edi_manager'))]" />
<field name="model_id" ref="model_edi_exchange_record" />
<field name="binding_model_id" ref="model_edi_exchange_record" />
<field name="state">code</field>
<field name="code">
if records:
action = records.action_retry()
</field>
</record>
</odoo>
47 changes: 39 additions & 8 deletions edi_oca/models/edi_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ 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.
Usecase: the web service you send the file to processes it on the fly.
"""
)
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)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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."
4 changes: 3 additions & 1 deletion edi_oca/models/edi_exchange_consumer_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
13 changes: 12 additions & 1 deletion edi_oca/models/edi_exchange_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."),
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
67 changes: 65 additions & 2 deletions edi_oca/models/edi_exchange_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = [
(
Expand Down Expand Up @@ -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:
Expand All @@ -232,6 +294,7 @@ def _make_exchange_filename(self, exchange_record):
record_name=record_name,
type=self,
dt=dt,
seq=seq,
ext=ext,
)

Expand Down
14 changes: 14 additions & 0 deletions edi_oca/security/ir_model_access.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,18 @@
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('base_edi.group_edi_manager'))]" />
</record>
<record id="rule_edi_exchange_record_multi_company" model="ir.rule">
<field name="name">edi_exchange_record multi-company</field>
<field name="model_id" ref="model_edi_exchange_record" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="rule_edi_backend_multi_company" model="ir.rule">
<field name="name">edi_backend multi-company</field>
<field name="model_id" ref="model_edi_backend" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
</odoo>
1 change: 1 addition & 0 deletions edi_oca/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 9 additions & 1 deletion edi_oca/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions edi_oca/tests/test_backend_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}])
Loading

0 comments on commit 678319d

Please sign in to comment.