diff --git a/README.md b/README.md
index 3694fd50b..0fccb7e26 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ addon | version | maintainers | summary
[edi_sale_ubl_output_oca](edi_sale_ubl_output_oca/) | 18.0.1.0.1 | | Configuration and special behaviors for EDI on sales.
[edi_state_oca](edi_state_oca/) | 18.0.1.0.3 |
| Allow to assign specific EDI states to related records.
[edi_stock_oca](edi_stock_oca/) | 18.0.1.0.1 | | Define EDI Configuration for Stock
-[edi_storage_oca](edi_storage_oca/) | 18.0.1.0.4 | | Base module to allow exchanging files via storage backend (eg: SFTP).
+[edi_storage_oca](edi_storage_oca/) | 18.0.1.1.0 | | Base module to allow exchanging files via storage backend (eg: SFTP).
[edi_storage_queue_oca](edi_storage_queue_oca/) | 18.0.1.0.0 | | Integrates EDI Storage with Queue
[edi_ubl_oca](edi_ubl_oca/) | 18.0.1.0.1 |
| Define EDI backend type for UBL.
[edi_webservice_oca](edi_webservice_oca/) | 18.0.1.0.2 |
| Defines webservice integration from EDI Exchange records
diff --git a/edi_storage_oca/README.rst b/edi_storage_oca/README.rst
index f880503b3..1194f6dae 100644
--- a/edi_storage_oca/README.rst
+++ b/edi_storage_oca/README.rst
@@ -11,7 +11,7 @@ EDI Storage backend support
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:ca9c4e0d1ecd9744b5cfa517900a5dace2a206007360ce2c1299dc1da5910bfa
+ !! source digest: sha256:f0ff3ee30416f5a8090d6c738df2c392381b61bc0d104c7679a147ce3b503018
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@@ -62,6 +62,20 @@ in/from the right place and update exchange records data accordingly.
.. contents::
:local:
+Configuration
+=============
+
+This module has two **inactive** global ``edi.configuration`` records
+that move the input file across the storage directories on
+``on_edi_exchange_done`` / ``on_edi_exchange_error``:
+
+- \*Storage: move input file, pending → done (fallback error → done)
+- \*Storage: move input file, pending → error
+
+Before enabling them you **must** set ``backend_id`` on the record:
+otherwise the global match runs against every backend in the database,
+including non-storage ones.
+
Usage
=====
diff --git a/edi_storage_oca/__manifest__.py b/edi_storage_oca/__manifest__.py
index 87b4c5f92..ea681386e 100644
--- a/edi_storage_oca/__manifest__.py
+++ b/edi_storage_oca/__manifest__.py
@@ -7,7 +7,7 @@
"summary": """
Base module to allow exchanging files via storage backend (eg: SFTP).
""",
- "version": "18.0.1.0.4",
+ "version": "18.0.1.1.0",
"development_status": "Beta",
"license": "LGPL-3",
"website": "https://github.com/OCA/edi-framework",
@@ -15,6 +15,7 @@
"depends": ["edi_core_oca", "fs_storage"],
"data": [
"data/cron.xml",
+ "data/edi_configuration.xml",
"security/ir_model_access.xml",
"views/edi_backend_views.xml",
],
diff --git a/edi_storage_oca/data/edi_configuration.xml b/edi_storage_oca/data/edi_configuration.xml
new file mode 100644
index 000000000..53956f2eb
--- /dev/null
+++ b/edi_storage_oca/data/edi_configuration.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ Storage: move input file to done
+
+
+
+ record.backend_id._storage_on_edi_exchange_done(record)
+
+
+
+ Storage: move input file to error
+
+
+
+ record.backend_id._storage_on_edi_exchange_error(record)
+
+
diff --git a/edi_storage_oca/models/edi_backend.py b/edi_storage_oca/models/edi_backend.py
index e6767a789..a6d5b40fc 100644
--- a/edi_storage_oca/models/edi_backend.py
+++ b/edi_storage_oca/models/edi_backend.py
@@ -148,3 +148,53 @@ def _storage_new_exchange_record_vals(self, file_name):
"edi_exchange_state": "input_pending",
"storage_id": self.storage_id.id,
}
+
+ def _storage_on_edi_exchange_done(self, exchange_record):
+ """
+ Move an input file from the pending dir to the done dir.
+
+ Intended to be invoked from a global 'edi.configuration' snippet
+ bound to the 'on_edi_exchange_done' trigger.
+ """
+ self.ensure_one()
+ storage = exchange_record.storage_id
+ if exchange_record.direction != "input" or not storage:
+ return False
+ if not self.input_dir_done:
+ return False
+ file_name = exchange_record.exchange_filename
+ pending_dir = exchange_record.type_id._storage_fullpath(
+ self.input_dir_pending
+ ).as_posix()
+ done_dir = exchange_record.type_id._storage_fullpath(
+ self.input_dir_done
+ ).as_posix()
+ error_dir = exchange_record.type_id._storage_fullpath(
+ self.input_dir_error
+ ).as_posix()
+ res = utils.move_file(storage, pending_dir, done_dir, file_name)
+ if not res and self.input_dir_error:
+ res = utils.move_file(storage, error_dir, done_dir, file_name)
+ return res
+
+ def _storage_on_edi_exchange_error(self, exchange_record):
+ """
+ Move an input file from the pending dir to the error dir.
+
+ Intended to be invoked from a global 'edi.configuration' snippet
+ bound to the 'on_edi_exchange_error' trigger.
+ """
+ self.ensure_one()
+ storage = exchange_record.storage_id
+ if exchange_record.direction != "input" or not storage:
+ return False
+ if not self.input_dir_error:
+ return False
+ file_name = exchange_record.exchange_filename
+ pending_dir = exchange_record.type_id._storage_fullpath(
+ self.input_dir_pending
+ ).as_posix()
+ error_dir = exchange_record.type_id._storage_fullpath(
+ self.input_dir_error
+ ).as_posix()
+ return utils.move_file(storage, pending_dir, error_dir, file_name)
diff --git a/edi_storage_oca/readme/CONFIGURE.md b/edi_storage_oca/readme/CONFIGURE.md
new file mode 100644
index 000000000..4050a7424
--- /dev/null
+++ b/edi_storage_oca/readme/CONFIGURE.md
@@ -0,0 +1,10 @@
+This module has two **inactive** global `edi.configuration` records
+that move the input file across the storage directories on
+`on_edi_exchange_done` / `on_edi_exchange_error`:
+
+- *Storage: move input file, pending → done (fallback error → done)
+- *Storage: move input file, pending → error
+
+Before enabling them you **must** set `backend_id` on the record:
+otherwise the global match runs against every backend in the database,
+including non-storage ones.
diff --git a/edi_storage_oca/static/description/index.html b/edi_storage_oca/static/description/index.html
index fe56fe211..77611d9c7 100644
--- a/edi_storage_oca/static/description/index.html
+++ b/edi_storage_oca/static/description/index.html
@@ -372,7 +372,7 @@
EDI Storage backend support
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:ca9c4e0d1ecd9744b5cfa517900a5dace2a206007360ce2c1299dc1da5910bfa
+!! source digest: sha256:f0ff3ee30416f5a8090d6c738df2c392381b61bc0d104c7679a147ce3b503018
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Allow exchange files using storage backends from OCA/storage.
@@ -398,31 +398,45 @@ EDI Storage backend support
Table of contents
+
+
+
This module has two inactive global edi.configuration records
+that move the input file across the storage directories on
+on_edi_exchange_done / on_edi_exchange_error:
+
+- *Storage: move input file, pending → done (fallback error → done)
+- *Storage: move input file, pending → error
+
+
Before enabling them you must set backend_id on the record:
+otherwise the global match runs against every backend in the database,
+including non-storage ones.
+
-
+
Go to “EDI -> EDI backend” then configure your backend to use a storage
backend.
-
+
- clean deprecated methods in the storage
-
+
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
@@ -430,15 +444,15 @@
Do not contact contributors directly about support or help with technical issues.
-
+
-
+
The migration of this module from 15.0 to 16.0 was financially supported
by Camptocamp.
-
+
This module is maintained by the OCA.
diff --git a/edi_storage_oca/tests/__init__.py b/edi_storage_oca/tests/__init__.py
index 0a73fb8a1..54510bc1a 100644
--- a/edi_storage_oca/tests/__init__.py
+++ b/edi_storage_oca/tests/__init__.py
@@ -1,2 +1,3 @@
from . import test_edi_backend_storage
+from . import test_event_listener
from . import test_exchange_type
diff --git a/edi_storage_oca/tests/test_event_listener.py b/edi_storage_oca/tests/test_event_listener.py
new file mode 100644
index 000000000..af9be8741
--- /dev/null
+++ b/edi_storage_oca/tests/test_event_listener.py
@@ -0,0 +1,88 @@
+# Copyright 2020 ACSONE
+# @author: Simone Orsi
+# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com)
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+import base64
+from unittest import mock
+
+from odoo_test_helper import FakeModelLoader
+
+from odoo.addons.edi_core_oca.tests.common import EDIBackendCommonTestCase
+from odoo.addons.edi_core_oca.tests.fake_models import EdiTestExecution
+
+STORAGE_MOVE_FILE_PATH = "odoo.addons.edi_storage_oca.utils.move_file"
+
+
+class TestStorageEventListener(EDIBackendCommonTestCase):
+ @classmethod
+ def _get_backend(cls):
+ return cls.env.ref("edi_storage_oca.demo_edi_backend_storage")
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.conf_done = cls.env.ref("edi_storage_oca.edi_conf_storage_move_on_done")
+ cls.conf_error = cls.env.ref("edi_storage_oca.edi_conf_storage_move_on_error")
+ (cls.conf_done | cls.conf_error).write(
+ {"active": True, "backend_id": cls.backend.id}
+ )
+
+ vals = {
+ "model": cls.partner._name,
+ "res_id": cls.partner.id,
+ "exchange_file": base64.b64encode(b"1234"),
+ "storage_id": cls.backend.storage_id.id,
+ }
+ cls.record = cls.backend.create_record("test_csv_input", vals)
+
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
+
+ self.loader.update_registry((EdiTestExecution,))
+ fake_model = self.env["ir.model"].search(
+ [("model", "=", "edi.framework.test.execution")]
+ )
+ self.exchange_type_in.process_model_id = fake_model
+ self.exchange_type_in.input_validate_model_id = fake_model
+
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
+ def _patch_move_file(self):
+ return mock.patch(STORAGE_MOVE_FILE_PATH, autospec=True, return_value=True)
+
+ def _expected_dir(self, raw_dir):
+ return self.exchange_type_in._storage_fullpath(raw_dir).as_posix()
+
+ def test_01_process_record_success(self):
+ self.record.write({"edi_exchange_state": "input_received"})
+ with self._patch_move_file() as mocked:
+ self.record.action_exchange_process()
+ mocked.assert_called_once()
+ storage, from_dir_str, to_dir_str, filename = mocked.call_args[0]
+ self.assertEqual(storage, self.backend.storage_id)
+ self.assertEqual(
+ from_dir_str, self._expected_dir(self.backend.input_dir_pending)
+ )
+ self.assertEqual(to_dir_str, self._expected_dir(self.backend.input_dir_done))
+ self.assertEqual(filename, self.record.exchange_filename)
+
+ def test_02_process_record_with_error(self):
+ self.record.write({"edi_exchange_state": "input_received"})
+ self.record._set_file_content("TEST %d" % self.record.id)
+ with self._patch_move_file() as mocked:
+ self.record.with_context(
+ test_break_process="OOPS! Something went wrong :("
+ ).action_exchange_process()
+ mocked.assert_called_once()
+ storage, from_dir_str, to_dir_str, filename = mocked.call_args[0]
+ self.assertEqual(storage, self.backend.storage_id)
+ self.assertEqual(
+ from_dir_str, self._expected_dir(self.backend.input_dir_pending)
+ )
+ self.assertEqual(to_dir_str, self._expected_dir(self.backend.input_dir_error))
+ self.assertEqual(filename, self.record.exchange_filename)
diff --git a/edi_storage_oca/utils.py b/edi_storage_oca/utils.py
index 0b791c09b..179c25904 100644
--- a/edi_storage_oca/utils.py
+++ b/edi_storage_oca/utils.py
@@ -2,8 +2,10 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl)
import base64
+import functools
import os
import re
+from pathlib import PurePath
def add_file(storage, path, filedata, binary=False):
@@ -60,3 +62,13 @@ def move_files(storage, files, destination_path, **kw):
storage.fs.sep.join([destination_path, os.path.basename(file_path)]),
**kw,
)
+
+
+# TODO: drop this helper once https://github.com/OCA/storage/pull/606 is merged.
+def move_file(storage, from_dir_str, to_dir_str, filename):
+ src = (PurePath(from_dir_str) / filename).as_posix()
+ if not storage.fs.exists(src):
+ return False
+ dst = (PurePath(to_dir_str) / filename).as_posix()
+ storage.env.cr.postcommit.add(functools.partial(storage.fs.move, src, dst))
+ return True