diff --git a/edi_oca/__manifest__.py b/edi_oca/__manifest__.py
index 9582f270c6..ac52a4376a 100644
--- a/edi_oca/__manifest__.py
+++ b/edi_oca/__manifest__.py
@@ -27,6 +27,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()
+
+
+
diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py
index 3276c8528c..24f35f320f 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.
@@ -207,10 +208,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
@@ -286,6 +290,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)
@@ -471,7 +481,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
)
@@ -540,7 +553,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:
@@ -637,7 +651,7 @@ def _input_pending_records_domain(self, record_ids=None):
return domain
def _input_pending_process_records_domain(self, record_ids=None):
- states = ("input_received", "input_processed_error")
+ states = ("input_received",)
domain = [
("backend_id", "=", self.id),
("type_id.direction", "=", "input"),
diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py
index 3a62f1d539..13b3c94322 100644
--- a/edi_oca/models/edi_exchange_record.py
+++ b/edi_oca/models/edi_exchange_record.py
@@ -363,6 +363,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 f94e388789..2ff2a185aa 100644
--- a/edi_oca/models/edi_exchange_type.py
+++ b/edi_oca/models/edi_exchange_type.py
@@ -184,6 +184,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 3cc095df11..456682068a 100644
--- a/edi_oca/tests/test_backend_input.py
+++ b/edi_oca/tests/test_backend_input.py
@@ -44,3 +44,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_jobs.py b/edi_oca/tests/test_backend_jobs.py
index 70c835dabd..ac23a298ad 100644
--- a/edi_oca/tests/test_backend_jobs.py
+++ b/edi_oca/tests/test_backend_jobs.py
@@ -94,3 +94,24 @@ def test_input(self):
job = self.backend.with_delay().exchange_process(record)
created = job_counter.search_created()
self.assertEqual(created[0].name, "Process an incoming document.")
+
+ def test_input_processed_error(self):
+ vals = {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ "edi_exchange_state": "input_received",
+ }
+ record = self.backend.create_record("test_csv_input", vals)
+ record._set_file_content("ABC")
+ # Process `input_received` records
+ job_counter = self.job_counter()
+ self.backend._check_input_exchange_sync()
+ created = job_counter.search_created()
+ # Create job
+ self.assertEqual(len(created), 1)
+ record.edi_exchange_state = "input_processed_error"
+ # Don't re-process `input_processed_error` records
+ self.backend._check_input_exchange_sync()
+ new_created = job_counter.search_created() - created
+ # Should not create new job
+ self.assertEqual(len(new_created), 0)
diff --git a/edi_oca/tests/test_backend_process.py b/edi_oca/tests/test_backend_process.py
index 47c87148a7..80a62f781c 100644
--- a/edi_oca/tests/test_backend_process.py
+++ b/edi_oca/tests/test_backend_process.py
@@ -69,9 +69,18 @@ def test_process_no_file_record(self):
self.record.write({"edi_exchange_state": "input_received"})
self.record._onchange_edi_exchange_state()
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 97f5bc8401..5e44c17346 100644
--- a/edi_oca/tests/test_backend_validate.py
+++ b/edi_oca/tests/test_backend_validate.py
@@ -95,3 +95,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_backend_views.xml b/edi_oca/views/edi_backend_views.xml
index f2377bcb49..f456a80b4b 100644
--- a/edi_oca/views/edi_backend_views.xml
+++ b/edi_oca/views/edi_backend_views.xml
@@ -51,6 +51,8 @@
+
+
diff --git a/edi_oca/views/edi_exchange_record_views.xml b/edi_oca/views/edi_exchange_record_views.xml
index a913888c67..aa2d022fa0 100644
--- a/edi_oca/views/edi_exchange_record_views.xml
+++ b/edi_oca/views/edi_exchange_record_views.xml
@@ -48,6 +48,12 @@
string="Retry"
attrs="{'invisible': [('retryable', '=', False)]}"
/>
+
diff --git a/edi_oca/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml
index 75a4116dee..f8fee0843f 100644
--- a/edi_oca/views/edi_exchange_type_views.xml
+++ b/edi_oca/views/edi_exchange_type_views.xml
@@ -50,6 +50,10 @@
name="encoding_in_error_handler"
attrs="{'invisible': [('direction', '=', 'output')]}"
/>
+