Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions edi_storage_oca/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
29 changes: 29 additions & 0 deletions edi_storage_oca/data/edi_configuration.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<!--
IMPORTANT: these configurations are *inactive* by default. Before
activating them, set 'backend_id' to the specific storage backend
whose directories should be used, otherwise the snippet will not
know which input_dir_pending / input_dir_done / input_dir_error to
operate on.
-->
<record id="edi_conf_storage_move_on_done" model="edi.configuration">
<field name="name">Storage: move input file to done</field>
<field name="active" eval="False" />
<field name="is_global" eval="True" />
<field name="trigger_id" ref="edi_core_oca.edi_config_trigger_record_done" />
<field
name="snippet_do"
>record.backend_id._storage_on_edi_exchange_done(record)</field>
</record>

<record id="edi_conf_storage_move_on_error" model="edi.configuration">
<field name="name">Storage: move input file to error</field>
<field name="active" eval="False" />
<field name="is_global" eval="True" />
<field name="trigger_id" ref="edi_core_oca.edi_config_trigger_record_error" />
<field
name="snippet_do"
>record.backend_id._storage_on_edi_exchange_error(record)</field>
</record>
</odoo>
50 changes: 50 additions & 0 deletions edi_storage_oca/models/edi_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = storage._move_file(pending_dir, done_dir, file_name)
if not res and self.input_dir_error:
res = storage._move_file(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 storage._move_file(pending_dir, error_dir, file_name)
10 changes: 10 additions & 0 deletions edi_storage_oca/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions edi_storage_oca/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from . import test_edi_backend_storage
from . import test_event_listener
from . import test_exchange_type
88 changes: 88 additions & 0 deletions edi_storage_oca/tests/test_event_listener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright 2020 ACSONE
# @author: Simone Orsi <simahawk@gmail.com>
# 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.fs_storage.models.fs_storage.FSStorage._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):
Comment thread
ArnauCForgeFlow marked this conversation as resolved.
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)
Loading