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)]}" /> +