Maintainers
+Maintainers
This module is maintained by the OCA.
diff --git a/edi_core_oca/tests/common.py b/edi_core_oca/tests/common.py
index 74f8cd3f9..cab6bd906 100644
--- a/edi_core_oca/tests/common.py
+++ b/edi_core_oca/tests/common.py
@@ -86,3 +86,26 @@ def setUpClass(cls):
super().setUpClass()
cls._setup_env()
cls._setup_records()
+
+ def _make_global_error_conf(self, exchange_type):
+ """Register a global ``edi.configuration`` bound to the
+ ``on_edi_exchange_error`` event.
+
+ Its snippet writes a marker on the configuration so a test can assert
+ the error event actually fired. This is the observable behaviour that
+ distinguishes an errored exchange (which must notify, e.g. create the
+ activities handled by ``edi_notification_oca``) from one that merely
+ posts a chatter message via ``notify_action_complete``.
+ """
+ trigger = self.env.ref("edi_core_oca.edi_config_trigger_record_error")
+ return self.env["edi.configuration"].create(
+ {
+ "name": "Test notify on error",
+ "active": True,
+ "backend_id": self.backend.id,
+ "type_id": exchange_type.id,
+ "trigger_id": trigger.id,
+ "is_global": True,
+ "snippet_do": "conf.write({'description': 'error-event-fired'})",
+ }
+ )
diff --git a/edi_core_oca/tests/test_backend_base.py b/edi_core_oca/tests/test_backend_base.py
index 0bda10c0a..ab30fb873 100644
--- a/edi_core_oca/tests/test_backend_base.py
+++ b/edi_core_oca/tests/test_backend_base.py
@@ -12,24 +12,29 @@
class EDIBackendTestCaseBase(EDIBackendCommonTestCase):
@classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.default_record = cls.backend.create_record(
+ "test_csv_input",
+ {"model": cls.partner._name, "res_id": cls.partner.id},
+ )
+
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiTestExecution
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"]._get("edi.framework.test.execution")
- cls.exchange_type_in.receive_model_id = cls.model
- cls.exchange_type_in.process_model_id = cls.model
- cls.exchange_type_in.input_validate_model_id = cls.model
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"]._get("edi.framework.test.execution")
+ self.exchange_type_in.receive_model_id = self.model
+ self.exchange_type_in.process_model_id = self.model
+ self.exchange_type_in.input_validate_model_id = self.model
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
@freeze_time("2020-10-21 10:00:00")
def test_create_record(self):
@@ -95,3 +100,61 @@ def test_get_handler_no_handler(self):
for action in ["receive", "input_validate", "process"]:
with self.assertRaises(EDINotImplementedError):
self.backend._get_exec_handler(record, action)
+
+ # ---- conf / env_ctx resolution ------------------------------------------
+
+ def test_get_conf_for_record(self):
+ """`_get_conf_for_record` returns the action conf, or {} when missing."""
+ self.exchange_type_in.advanced_settings_edit = (
+ "execution_model:\n"
+ " receive:\n"
+ " env_ctx:\n"
+ " foo: bar\n"
+ " other_key: 1\n"
+ )
+ record = self.default_record
+ # Action declared -> its conf
+ self.assertEqual(
+ self.backend._get_conf_for_record(record, "receive"),
+ {"env_ctx": {"foo": "bar"}, "other_key": 1},
+ )
+ # Action not declared -> empty
+ self.assertEqual(self.backend._get_conf_for_record(record, "process"), {})
+
+ def test_get_record_env_ctx(self):
+ """`_get_record_env_ctx` returns env_ctx for the action, else {}."""
+ self.exchange_type_in.advanced_settings_edit = (
+ "execution_model:\n"
+ " receive:\n"
+ " env_ctx:\n"
+ " foo: bar\n"
+ " flag: true\n"
+ " process:\n"
+ " other_key: 1\n"
+ )
+ record = self.default_record
+ # Action with env_ctx -> mapping
+ self.assertEqual(
+ self.backend._get_record_env_ctx(record, "receive"),
+ {"foo": "bar", "flag": True},
+ )
+ # Action present but no env_ctx -> empty
+ self.assertEqual(self.backend._get_record_env_ctx(record, "process"), {})
+
+ def test_get_exec_handler_propagates_env_ctx(self):
+ """The handler returned by `_get_exec_handler` carries env_ctx keys."""
+ self.exchange_type_in.advanced_settings_edit = (
+ "execution_model:\n"
+ " receive:\n"
+ " env_ctx:\n"
+ " edi_test_marker: hello\n"
+ " edi_test_flag: true\n"
+ )
+ record = self.default_record
+ handler = self.backend._get_exec_handler(record, "receive")
+ ctx = handler.__self__.env.context
+ self.assertEqual(ctx.get("edi_test_marker"), "hello")
+ self.assertEqual(ctx.get("edi_test_flag"), True)
+ # Action without env_ctx -> handler context not polluted
+ handler_proc = self.backend._get_exec_handler(record, "process")
+ self.assertNotIn("edi_test_marker", handler_proc.__self__.env.context)
diff --git a/edi_core_oca/tests/test_backend_input.py b/edi_core_oca/tests/test_backend_input.py
index 0c59f471a..8d3924232 100644
--- a/edi_core_oca/tests/test_backend_input.py
+++ b/edi_core_oca/tests/test_backend_input.py
@@ -3,42 +3,12 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo_test_helper import FakeModelLoader
+from psycopg2 import OperationalError
from .common import EDIBackendCommonTestCase
class EDIBackendTestInputCase(EDIBackendCommonTestCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- vals = {
- "model": cls.partner._name,
- "res_id": cls.partner.id,
- }
- cls.record = cls.backend.create_record("test_csv_input", vals)
-
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
- from .fake_models import EdiTestExecution
-
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
- [("model", "=", "edi.framework.test.execution")]
- )
- cls.exchange_type_in.receive_model_id = cls.model
- cls.exchange_type_in.process_model_id = cls.model
- cls.exchange_type_in.input_validate_model_id = cls.model
-
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
-
@classmethod
def _setup_context(cls):
return dict(
@@ -49,8 +19,29 @@ def _setup_context(cls):
def setUp(self):
super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
+ from .fake_models import EdiTestExecution
+
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
+ [("model", "=", "edi.framework.test.execution")]
+ )
+ self.exchange_type_in.receive_model_id = self.model
+ self.exchange_type_in.process_model_id = self.model
+ self.exchange_type_in.input_validate_model_id = self.model
+ vals = {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ }
+ self.record = self.backend.create_record("test_csv_input", vals)
self.ExecutionAbstractModel.reset_faked("receive")
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
def test_receive_record_nothing_todo(self):
self.backend.with_context(fake_output="yeah!").exchange_receive(self.record)
self.assertEqual(self.record._get_file_content(), "")
@@ -79,6 +70,16 @@ def test_receive_no_allow_empty_file_record(self):
self.record, [{"edi_exchange_state": "input_receive_error"}]
)
+ def test_receive_no_allow_empty_file_triggers_notify_error(self):
+ self.record.edi_exchange_state = "input_pending"
+ conf = self._make_global_error_conf(self.record.type_id)
+ self.backend.with_context(
+ fake_output="", _edi_receive_break_on_error=False
+ ).exchange_receive(self.record)
+ # The error event must fire so downstream notifications (e.g.
+ # edi_notification_oca activities) are triggered.
+ self.assertEqual(conf.description, "error-event-fired")
+
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
@@ -88,3 +89,12 @@ def test_receive_allow_empty_file_record(self):
# Check the record
self.assertEqual(self.record._get_file_content(), "")
self.assertRecordValues(self.record, [{"edi_exchange_state": "input_received"}])
+
+ def test_receive_record_with_operational_error(self):
+ self.record.edi_exchange_state = "input_pending"
+ with self.assertRaises(OperationalError):
+ self.backend.with_context(
+ test_break_receive=OperationalError("SQL error")
+ ).exchange_receive(self.record)
+ self.assertRecordValues(self.record, [{"edi_exchange_state": "input_pending"}])
+ self.assertFalse(self.record.exchange_error)
diff --git a/edi_core_oca/tests/test_backend_output.py b/edi_core_oca/tests/test_backend_output.py
index b9140b710..92d931a9f 100644
--- a/edi_core_oca/tests/test_backend_output.py
+++ b/edi_core_oca/tests/test_backend_output.py
@@ -7,6 +7,7 @@
from freezegun import freeze_time
from odoo_test_helper import FakeModelLoader
+from psycopg2 import OperationalError
from odoo import fields, tools
from odoo.exceptions import UserError
@@ -15,44 +16,33 @@
class EDIBackendTestOutputCase(EDIBackendCommonTestCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
-
- vals = {
- "model": cls.partner._name,
- "res_id": cls.partner.id,
- }
- cls.record = cls.backend.create_record("test_csv_output", vals)
-
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiTestExecution
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_out.generate_model_id = cls.model
- cls.exchange_type_out.send_model_id = cls.model
- cls.exchange_type_out.output_validate_model_id = cls.model
-
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
-
- def setUp(self):
- super().setUp()
+ self.exchange_type_out.generate_model_id = self.model
+ self.exchange_type_out.send_model_id = self.model
+ self.exchange_type_out.output_validate_model_id = self.model
+ vals = {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ }
+ self.record = self.backend.create_record("test_csv_output", vals)
self.ExecutionAbstractModel.reset_faked("generate")
self.ExecutionAbstractModel.reset_faked("send")
self.ExecutionAbstractModel.reset_faked("check")
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
def test_generate_record_output(self):
self.record.with_context(fake_output="yeah!").action_exchange_generate()
self.assertEqual(self.record._get_file_content(), "yeah!")
@@ -103,6 +93,17 @@ def test_send_record_with_error(self):
"OOPS! Something went wrong :(", self.record.exchange_error_traceback
)
+ def test_send_record_with_error_triggers_notify_error(self):
+ self.record.write({"edi_exchange_state": "output_pending"})
+ self.record._set_file_content(f"TEST {self.record.id}")
+ conf = self._make_global_error_conf(self.record.type_id)
+ self.record.with_context(
+ test_break_send="OOPS! Something went wrong :("
+ ).action_exchange_send()
+ # The error event must fire so downstream notifications (e.g.
+ # edi_notification_oca activities) are triggered.
+ self.assertEqual(conf.description, "error-event-fired")
+
def test_send_invalid_direction(self):
vals = {
"model": self.partner._name,
@@ -133,3 +134,13 @@ def test_send_not_generated_record(self):
err.exception.args[0], "Record ID=%d has no file to send!" % record.id
)
mocked.assert_not_called()
+
+ def test_send_record_with_operational_error(self):
+ self.record.write({"edi_exchange_state": "output_pending"})
+ self.record._set_file_content("TEST %d" % self.record.id)
+ with self.assertRaises(OperationalError):
+ self.backend.with_context(
+ test_break_send=OperationalError("SQL error")
+ ).exchange_send(self.record)
+ self.assertRecordValues(self.record, [{"edi_exchange_state": "output_pending"}])
+ self.assertFalse(self.record.exchange_error)
diff --git a/edi_core_oca/tests/test_backend_process.py b/edi_core_oca/tests/test_backend_process.py
index fefbf21e1..de259115e 100644
--- a/edi_core_oca/tests/test_backend_process.py
+++ b/edi_core_oca/tests/test_backend_process.py
@@ -6,6 +6,7 @@
from freezegun import freeze_time
from odoo_test_helper import FakeModelLoader
+from psycopg2 import IntegrityError
from odoo import fields
from odoo.exceptions import UserError
@@ -15,42 +16,32 @@
class EDIBackendTestProcessCase(EDIBackendCommonTestCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- vals = {
- "model": cls.partner._name,
- "res_id": cls.partner.id,
- "exchange_file": base64.b64encode(b"1234"),
- }
- cls.record = cls.backend.create_record("test_csv_input", vals)
-
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiTestExecution
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_in.generate_model_id = cls.model
- cls.exchange_type_in.process_model_id = cls.model
- cls.exchange_type_in.input_validate_model_id = cls.model
-
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
-
- def setUp(self):
- super().setUp()
+ self.exchange_type_in.generate_model_id = self.model
+ self.exchange_type_in.process_model_id = self.model
+ self.exchange_type_in.input_validate_model_id = self.model
+ vals = {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ "exchange_file": base64.b64encode(b"1234"),
+ }
+ self.record = self.backend.create_record("test_csv_input", vals)
self.ExecutionAbstractModel.reset_faked("process")
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
def test_process_record(self):
self.record.write({"edi_exchange_state": "input_received"})
with freeze_time("2020-10-22 10:00:00"):
@@ -113,4 +104,13 @@ def test_process_outbound_record(self):
with self.assertRaises(UserError):
record.action_exchange_process()
+ def test_process_record_with_integrity_error(self):
+ self.record.write({"edi_exchange_state": "input_received"})
+ with self.assertRaises(IntegrityError):
+ self.backend.with_context(
+ test_break_process=IntegrityError("SQL error")
+ ).exchange_process(self.record)
+ self.assertRecordValues(self.record, [{"edi_exchange_state": "input_received"}])
+ self.assertFalse(self.record.exchange_error)
+
# TODO: test ack file are processed
diff --git a/edi_core_oca/tests/test_backend_validate.py b/edi_core_oca/tests/test_backend_validate.py
index 990810506..a5fbbf766 100644
--- a/edi_core_oca/tests/test_backend_validate.py
+++ b/edi_core_oca/tests/test_backend_validate.py
@@ -11,50 +11,40 @@
class EDIBackendTestValidateCase(EDIBackendCommonTestCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- vals = {
- "model": cls.partner._name,
- "res_id": cls.partner.id,
- "exchange_file": base64.b64encode(b"1234"),
- }
- cls.record_in = cls.backend.create_record("test_csv_input", vals)
- vals.pop("exchange_file")
- cls.record_out = cls.backend.create_record("test_csv_output", vals)
-
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiTestExecution
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_out.generate_model_id = cls.model
- cls.exchange_type_out.send_model_id = cls.model
- cls.exchange_type_out.output_validate_model_id = cls.model
- cls.exchange_type_in.receive_model_id = cls.model
- cls.exchange_type_in.process_model_id = cls.model
- cls.exchange_type_in.input_validate_model_id = cls.model
-
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
-
- def setUp(self):
- super().setUp()
+ self.exchange_type_out.generate_model_id = self.model
+ self.exchange_type_out.send_model_id = self.model
+ self.exchange_type_out.output_validate_model_id = self.model
+ self.exchange_type_in.receive_model_id = self.model
+ self.exchange_type_in.process_model_id = self.model
+ self.exchange_type_in.input_validate_model_id = self.model
+ vals = {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ "exchange_file": base64.b64encode(b"1234"),
+ }
+ self.record_in = self.backend.create_record("test_csv_input", vals)
+ vals.pop("exchange_file")
+ self.record_out = self.backend.create_record("test_csv_output", vals)
self.ExecutionAbstractModel.reset_faked("input_validate")
self.ExecutionAbstractModel.reset_faked("receive")
self.ExecutionAbstractModel.reset_faked("generate")
self.ExecutionAbstractModel.reset_faked("output_validate")
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
def test_receive_validate_record(self):
self.record_in.write({"edi_exchange_state": "input_pending"})
self.backend.exchange_receive(self.record_in)
@@ -89,6 +79,17 @@ def test_receive_validate_record_error(self):
)
self.assertIn("Data seems wrong!", self.record_in.exchange_error_traceback)
+ def test_receive_validate_record_error_triggers_notify_error(self):
+ self.record_in.write({"edi_exchange_state": "input_pending"})
+ exc = EDIValidationError("Data seems wrong!")
+ conf = self._make_global_error_conf(self.record_in.type_id)
+ self.backend.with_context(test_break_input_validate=exc).exchange_receive(
+ self.record_in
+ )
+ # The error event must fire so downstream notifications (e.g.
+ # edi_notification_oca activities) are triggered.
+ self.assertEqual(conf.description, "error-event-fired")
+
def test_generate_validate_record(self):
self.record_out.write({"edi_exchange_state": "new"})
self.backend.exchange_generate(self.record_out)
@@ -123,6 +124,17 @@ def test_generate_validate_record_error(self):
)
self.assertIn("Data seems wrong!", self.record_out.exchange_error_traceback)
+ def test_generate_validate_record_error_triggers_notify_error(self):
+ self.record_out.write({"edi_exchange_state": "new"})
+ exc = EDIValidationError("Data seems wrong!")
+ conf = self._make_global_error_conf(self.record_out.type_id)
+ self.backend.with_context(test_break_output_validate=exc).exchange_generate(
+ self.record_out
+ )
+ # The error event must fire so downstream notifications (e.g.
+ # edi_notification_oca activities) are triggered.
+ self.assertEqual(conf.description, "error-event-fired")
+
def test_validate_record_error_regenerate(self):
self.record_out.write({"edi_exchange_state": "new"})
exc = EDIValidationError("Data seems wrong!")
diff --git a/edi_core_oca/tests/test_consumer_mixin.py b/edi_core_oca/tests/test_consumer_mixin.py
index 8629edeaa..b078aff4e 100644
--- a/edi_core_oca/tests/test_consumer_mixin.py
+++ b/edi_core_oca/tests/test_consumer_mixin.py
@@ -19,36 +19,28 @@
# If you still want to run `edi` tests w/ pytest when this happens, set this env var.
@skipIf(os.getenv("SKIP_EDI_CONSUMER_CASE"), "Consumer test case disabled.")
class TestConsumerMixinCase(EDIBackendCommonTestCase):
- @classmethod
- def _setup_env(cls):
- super()._setup_env()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiExchangeConsumerTest
- cls.loader.update_registry((EdiExchangeConsumerTest,))
- return super()._setup_env()
-
- # pylint: disable=W8110
- @classmethod
- def _setup_records(cls):
- super()._setup_records()
- cls.consumer_record = cls.env["edi.exchange.consumer.test"].create(
+ self.loader.update_registry((EdiExchangeConsumerTest,))
+ self.consumer_record = self.env["edi.exchange.consumer.test"].create(
{"name": "Test Consumer"}
)
- cls.exchange_type_out.exchange_filename_pattern = "{record.id}"
+ self.exchange_type_out.exchange_filename_pattern = "{record.id}"
rule_vals = {
"name": "Test",
- "model_id": cls.env["ir.model"]._get_id(cls.consumer_record._name),
+ "model_id": self.env["ir.model"]._get_id(self.consumer_record._name),
"kind": "custom",
"enable_domain": "[]",
"enable_snippet": """
result = not record._has_exchange_record(exchange_type)
""",
}
- cls.exchange_type_new = cls._create_exchange_type(
+ self.exchange_type_new = self._create_exchange_type(
name="Test CSV output",
code="test_csv_new_output",
direction="output",
@@ -59,20 +51,19 @@ def _setup_records(cls):
)
rule_vals = {
"name": "Test",
- "model_id": cls.env["ir.model"]._get_id(cls.consumer_record._name),
+ "model_id": self.env["ir.model"]._get_id(self.consumer_record._name),
"kind": "custom",
"enable_domain": "[]",
"enable_snippet": """
result = not record._has_exchange_record(exchange_type, exchange_type.backend_id)
""",
}
- cls.exchange_type_out.write({"rule_ids": [(0, 0, rule_vals)]})
- cls.backend_02 = cls.backend.copy()
+ self.exchange_type_out.write({"rule_ids": [(0, 0, rule_vals)]})
+ self.backend_02 = self.backend.copy()
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
def test_mixin(self):
self.assertEqual(self.consumer_record.exchange_record_count, 0)
diff --git a/edi_core_oca/tests/test_edi_backend_cron.py b/edi_core_oca/tests/test_edi_backend_cron.py
index db227b5ad..9c6aff0fb 100644
--- a/edi_core_oca/tests/test_edi_backend_cron.py
+++ b/edi_core_oca/tests/test_edi_backend_cron.py
@@ -16,50 +16,40 @@
class EDIBackendTestCronCase(EDIBackendCommonTestCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- cls.partner2 = cls.env.ref("base.res_partner_10")
- cls.partner3 = cls.env.ref("base.res_partner_12")
- cls.record1 = cls.backend.create_record(
- "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner.id}
- )
- cls.record2 = cls.backend.create_record(
- "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner2.id}
- )
- cls.record3 = cls.backend.create_record(
- "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner3.id}
- )
- cls.records = cls.record1 + cls.record1 + cls.record3
-
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiTestExecution
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_out.generate_model_id = cls.model
- cls.exchange_type_out.send_model_id = cls.model
- cls.exchange_type_out.output_validate_model_id = cls.model
-
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
-
- def setUp(self):
- super().setUp()
+ self.exchange_type_out.generate_model_id = self.model
+ self.exchange_type_out.send_model_id = self.model
+ self.exchange_type_out.output_validate_model_id = self.model
+ self.partner2 = self.env.ref("base.res_partner_10")
+ self.partner3 = self.env.ref("base.res_partner_12")
+ self.record1 = self.backend.create_record(
+ "test_csv_output", {"model": self.partner._name, "res_id": self.partner.id}
+ )
+ self.record2 = self.backend.create_record(
+ "test_csv_output", {"model": self.partner._name, "res_id": self.partner2.id}
+ )
+ self.record3 = self.backend.create_record(
+ "test_csv_output", {"model": self.partner._name, "res_id": self.partner3.id}
+ )
+ self.records = self.record1 + self.record1 + self.record3
self.ExecutionAbstractModel.reset_faked("generate")
self.ExecutionAbstractModel.reset_faked("send")
self.ExecutionAbstractModel.reset_faked("check")
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
@mute_logger(*LOGGERS)
def test_exchange_generate_new_no_auto(self):
# No content ready to be sent, no auto-generate, nothing happens
@@ -87,14 +77,8 @@ def test_exchange_generate_new_auto_skip_send(self):
# TODO: test better?
self.assertFalse(rec.ack_exchange_id)
- @mute_logger(*LOGGERS)
- def test_exchange_generate_new_auto_send(self):
- self.exchange_type_out.exchange_file_auto_generate = True
- # No content ready to be sent, will get the content and send it
- for rec in self.records:
- self.assertEqual(rec.edi_exchange_state, "new")
- self.backend._cron_check_output_exchange_sync()
- for rec in self.records:
+ def _test_generate_new_auto_send(self, records):
+ for rec in records:
self.assertEqual(rec.edi_exchange_state, "output_sent")
self.assertTrue(
self.ExecutionAbstractModel.check_called_for(rec, "generate")
@@ -104,6 +88,26 @@ def test_exchange_generate_new_auto_send(self):
)
self.assertTrue(self.ExecutionAbstractModel.check_called_for(rec, "send"))
+ @mute_logger(*LOGGERS)
+ def test_exchange_generate_new_auto_send(self):
+ self.exchange_type_out.exchange_file_auto_generate = True
+ # No content ready to be sent, will get the content and send it
+ for rec in self.records:
+ self.assertEqual(rec.edi_exchange_state, "new")
+ self.backend._cron_check_output_exchange_sync()
+ self._test_generate_new_auto_send(self.records)
+
+ @mute_logger(*LOGGERS)
+ def test_exchange_generate_new_quick_exec_skip_cron(self):
+ self.exchange_type_out.exchange_file_auto_generate = True
+ self.exchange_type_out.quick_exec = True
+ for rec in self.records:
+ self.assertEqual(rec.edi_exchange_state, "new")
+ # Records w/ quick exec should be skipped by the cron
+ self.backend._cron_check_output_exchange_sync()
+ for rec in self.records:
+ self.assertEqual(rec.edi_exchange_state, "new")
+
@mute_logger(*LOGGERS)
def test_exchange_generate_output_ready_auto_send(self):
# No content ready to be sent, will get the content and send it
diff --git a/edi_core_oca/tests/test_edi_configuration.py b/edi_core_oca/tests/test_edi_configuration.py
index 88f1a2918..d624b697e 100644
--- a/edi_core_oca/tests/test_edi_configuration.py
+++ b/edi_core_oca/tests/test_edi_configuration.py
@@ -24,65 +24,61 @@ def setUpClass(cls):
def setUp(self):
super().setUp()
- self.ExecutionAbstractModel.reset_faked("generate")
- self.ExecutionAbstractModel.reset_faked("send")
- self.ExecutionAbstractModel.reset_faked("check")
- self.consumer_record = self.env["edi.exchange.consumer.test"].create(
- {
- "name": "Test Consumer",
- "edi_config_ids": [
- (4, self.create_config.id),
- (4, self.write_config.id),
- ],
- }
- )
-
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiExchangeConsumerTest, EdiTestExecution
- cls.loader.update_registry((EdiExchangeConsumerTest, EdiTestExecution))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiExchangeConsumerTest, EdiTestExecution))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_out.generate_model_id = cls.model
- cls.exchange_type_out.send_model_id = cls.model
- cls.exchange_type_out.exchange_filename_pattern = "{record.id}"
- cls.edi_configuration = cls.env["edi.configuration"]
- cls.create_trigger = cls.env.ref("edi_core_oca.edi_conf_trigger_record_create")
- cls.write_trigger = cls.env.ref("edi_core_oca.edi_conf_trigger_record_write")
- cls.create_config = cls.edi_configuration.create(
+ self.exchange_type_out.generate_model_id = self.model
+ self.exchange_type_out.send_model_id = self.model
+ self.exchange_type_out.exchange_filename_pattern = "{record.id}"
+ self.edi_configuration = self.env["edi.configuration"]
+ self.create_trigger = self.env.ref(
+ "edi_core_oca.edi_conf_trigger_record_create"
+ )
+ self.write_trigger = self.env.ref("edi_core_oca.edi_conf_trigger_record_write")
+ self.create_config = self.edi_configuration.create(
{
"name": "Create Config",
"active": True,
- "backend_id": cls.backend.id,
- "type_id": cls.exchange_type_out.id,
- "trigger_id": cls.create_trigger.id,
- "model_id": cls.env["ir.model"]._get_id("edi.exchange.consumer.test"),
+ "backend_id": self.backend.id,
+ "type_id": self.exchange_type_out.id,
+ "trigger_id": self.create_trigger.id,
+ "model_id": self.env["ir.model"]._get_id("edi.exchange.consumer.test"),
"snippet_do": "record._edi_send_via_edi(conf.type_id)",
}
)
- cls.write_config = cls.edi_configuration.create(
+ self.write_config = self.edi_configuration.create(
{
"name": "Write Config 1",
"active": True,
- "backend_id": cls.backend.id,
- "type_id": cls.exchange_type_out.id,
- "trigger_id": cls.write_trigger.id,
- "model_id": cls.env["ir.model"]._get_id("edi.exchange.consumer.test"),
+ "backend_id": self.backend.id,
+ "type_id": self.exchange_type_out.id,
+ "trigger_id": self.write_trigger.id,
+ "model_id": self.env["ir.model"]._get_id("edi.exchange.consumer.test"),
"snippet_do": "record._edi_send_via_edi(conf.type_id)",
}
)
+ self.ExecutionAbstractModel.reset_faked("generate")
+ self.ExecutionAbstractModel.reset_faked("send")
+ self.ExecutionAbstractModel.reset_faked("check")
+ self.consumer_record = self.env["edi.exchange.consumer.test"].create(
+ {
+ "name": "Test Consumer",
+ "edi_config_ids": [
+ (4, self.create_config.id),
+ (4, self.write_config.id),
+ ],
+ }
+ )
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
def test_edi_send_via_edi_config(self):
# Check configuration on create
@@ -156,3 +152,256 @@ def test_edi_code_snippet(self):
)
# Check the new vals after execution
self.assertEqual(vals, expected_value)
+
+
+class TestEDIConfigurationGlobalEvents(EDIBackendCommonTestCase):
+ """Test the global event dispatch via edi.configuration.
+
+ `EDIExchangeRecord._trigger_edi_event` looks up all `edi.configuration`
+ records flagged as `is_global` and matching the event trigger code,
+ then executes their `snippet_do` against the target record.
+ These tests verify the dispatch happens for all `notify_*` events
+ and that the proper target (exchange record vs related record)
+ is passed to the snippet.
+ """
+
+ # Snippet appends a marker per call so we can verify multiple invocations
+ # against different targets within the same transaction.
+ _marker_snippet = (
+ "conf.write({'description': (conf.description or '') + '|' + record._name})"
+ )
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ vals = {
+ "model": cls.partner._name,
+ "res_id": cls.partner.id,
+ }
+ cls.record = cls.backend.create_record("test_csv_output", vals)
+ cls.trigger_model = cls.env["edi.configuration.trigger"]
+ cls.conf_model = cls.env["edi.configuration"]
+ # Reuse existing data triggers when available, create the missing ones.
+ cls.trigger_done = cls.env.ref("edi_core_oca.edi_config_trigger_record_done")
+ cls.trigger_error = cls.env.ref("edi_core_oca.edi_config_trigger_record_error")
+ cls.trigger_ack_received = cls._get_or_create_trigger(
+ "on_edi_exchange_done_ack_received", "On ACK received"
+ )
+ cls.trigger_ack_missing = cls._get_or_create_trigger(
+ "on_edi_exchange_done_ack_missing", "On ACK missing"
+ )
+ cls.trigger_ack_received_error = cls._get_or_create_trigger(
+ "on_edi_exchange_done_ack_received_error", "On ACK received error"
+ )
+ cls.trigger_generate_complete = cls._get_or_create_trigger(
+ "on_edi_exchange_generate_complete", "On generate complete"
+ )
+
+ @classmethod
+ def _get_or_create_trigger(cls, code, name):
+ trigger = cls.trigger_model.search([("code", "=", code)], limit=1)
+ if not trigger:
+ trigger = cls.trigger_model.create({"name": name, "code": code})
+ return trigger
+
+ def _make_conf(self, trigger, name, is_global=True, snippet=None, **overrides):
+ vals = {
+ "name": name,
+ "active": True,
+ "backend_id": self.backend.id,
+ "type_id": self.exchange_type_out.id,
+ "trigger_id": trigger.id,
+ "is_global": is_global,
+ "snippet_do": snippet or self._marker_snippet,
+ }
+ vals.update(overrides)
+ return self.conf_model.create(vals)
+
+ def test_notify_done_triggers_global_conf(self):
+ conf = self._make_conf(self.trigger_done, "Global Done")
+ self.record._notify_done()
+ self.assertEqual(conf.description, f"|{self.record._name}")
+
+ def test_notify_error_triggers_global_conf(self):
+ conf = self._make_conf(self.trigger_error, "Global Error")
+ self.record._notify_error("send_ko")
+ self.assertEqual(conf.description, f"|{self.record._name}")
+
+ def test_notify_ack_received_triggers_global_conf(self):
+ conf = self._make_conf(self.trigger_ack_received, "Global ACK received")
+ self.record._notify_ack_received()
+ self.assertEqual(conf.description, f"|{self.record._name}")
+
+ def test_notify_ack_missing_triggers_global_conf(self):
+ conf = self._make_conf(self.trigger_ack_missing, "Global ACK missing")
+ self.record._notify_ack_missing()
+ self.assertEqual(conf.description, f"|{self.record._name}")
+
+ def test_notify_ack_received_error_triggers_global_conf(self):
+ conf = self._make_conf(
+ self.trigger_ack_received_error, "Global ACK received error"
+ )
+ self.record._notify_ack_received_error()
+ self.assertEqual(conf.description, f"|{self.record._name}")
+
+ def test_non_global_conf_is_ignored(self):
+ conf = self._make_conf(self.trigger_done, "Non Global Done", is_global=False)
+ self.record._notify_done()
+ self.assertFalse(conf.description)
+
+ def test_inactive_global_conf_is_ignored(self):
+ conf = self._make_conf(self.trigger_done, "Inactive Global Done")
+ conf.active = False
+ self.record._notify_done()
+ self.assertFalse(conf.description)
+
+ def test_notify_action_complete_dispatches_to_both_targets(self):
+ """`notify_action_complete` fires the event twice when the related
+ record exists: once with the exchange record as target, once with the
+ related record (partner here)."""
+ conf = self._make_conf(
+ self.trigger_generate_complete, "Global generate complete"
+ )
+ # Sanity check: the exchange record has a related record.
+ self.assertTrue(self.record.related_record_exists)
+ self.record.notify_action_complete("generate")
+ # The snippet appended one marker per call: exchange record then partner.
+ self.assertEqual(
+ conf.description,
+ f"|{self.record._name}|{self.partner._name}",
+ )
+
+ def test_notify_action_complete_no_related_record(self):
+ """When no related record exists, the event fires only on the
+ exchange record itself."""
+ conf = self._make_conf(
+ self.trigger_generate_complete, "Global generate complete - no related"
+ )
+ # Create an exchange record with no related record.
+ orphan_record = self.backend.create_record(
+ "test_csv_output", {"model": False, "res_id": False}
+ )
+ orphan_record.notify_action_complete("generate")
+ self.assertEqual(conf.description, f"|{orphan_record._name}")
+
+ def test_snippet_receives_conf_and_record(self):
+ """The snippet eval context must expose both `conf` (the configuration)
+ and `record` (the target of the event)."""
+ snippet = (
+ "conf.write({'description': 'conf=%s|record=%s' % "
+ "(conf.name, record.display_name)})"
+ )
+ conf = self._make_conf(self.trigger_done, "Context check", snippet=snippet)
+ self.record._notify_done()
+ self.assertEqual(
+ conf.description,
+ f"conf={conf.name}|record={self.record.display_name}",
+ )
+
+ def test_multiple_global_confs_all_executed(self):
+ """All global confs matching the trigger are executed."""
+ conf1 = self._make_conf(self.trigger_done, "Global Done 1")
+ conf2 = self._make_conf(self.trigger_done, "Global Done 2")
+ self.record._notify_done()
+ self.assertEqual(conf1.description, f"|{self.record._name}")
+ self.assertEqual(conf2.description, f"|{self.record._name}")
+
+ # ------------------------------------------------------------------
+ # Filtering tests for `edi_get_conf_global`
+ # ------------------------------------------------------------------
+ def test_filter_by_type_mismatch(self):
+ """A conf bound to a different exchange type must not fire."""
+ conf = self._make_conf(
+ self.trigger_done,
+ "Wrong type",
+ type_id=self.exchange_type_in.id,
+ )
+ self.record._notify_done()
+ self.assertFalse(conf.description)
+
+ def test_filter_by_type_empty_matches(self):
+ """A conf without a type matches any exchange record's type."""
+ conf = self._make_conf(self.trigger_done, "No type", type_id=False)
+ self.record._notify_done()
+ self.assertEqual(conf.description, f"|{self.record._name}")
+
+ def test_filter_by_backend_mismatch(self):
+ """A conf bound to a different backend must not fire."""
+ other_backend = self.env["edi.backend"].create(
+ {
+ "name": "Other backend",
+ "backend_type_id": self.backend.backend_type_id.id,
+ }
+ )
+ # `_constrains_backend` requires backend to be compatible with the type's
+ # backend if the type has one set. Detach the type from the conf to test
+ # only the backend filter.
+ conf = self._make_conf(
+ self.trigger_done,
+ "Wrong backend",
+ backend_id=other_backend.id,
+ type_id=False,
+ )
+ self.record._notify_done()
+ self.assertFalse(conf.description)
+
+ def test_filter_by_backend_empty_matches(self):
+ """A conf without a backend matches any exchange record's backend."""
+ conf = self._make_conf(
+ self.trigger_done,
+ "No backend",
+ backend_id=False,
+ type_id=False,
+ )
+ self.record._notify_done()
+ self.assertEqual(conf.description, f"|{self.record._name}")
+
+ def test_filter_by_model_mismatch(self):
+ """A conf bound to a different model must not fire."""
+ other_model = self.env["ir.model"]._get("res.users")
+ conf = self._make_conf(
+ self.trigger_done,
+ "Wrong model",
+ model_id=other_model.id,
+ )
+ self.record._notify_done()
+ self.assertFalse(conf.description)
+
+ def test_filter_by_model_match(self):
+ """A conf bound to the related record model fires."""
+ partner_model = self.env["ir.model"]._get(self.partner._name)
+ conf = self._make_conf(
+ self.trigger_done,
+ "Matching model",
+ model_id=partner_model.id,
+ )
+ self.record._notify_done()
+ self.assertEqual(conf.description, f"|{self.record._name}")
+
+ def test_filter_by_model_orphan_record(self):
+ """A conf with a model is skipped on records with no related model."""
+ partner_model = self.env["ir.model"]._get(self.partner._name)
+ conf_with_model = self._make_conf(
+ self.trigger_done,
+ "Model bound",
+ model_id=partner_model.id,
+ )
+ conf_no_model = self._make_conf(self.trigger_done, "Model-less")
+ orphan_record = self.backend.create_record(
+ "test_csv_output", {"model": False, "res_id": False}
+ )
+ orphan_record._notify_done()
+ self.assertFalse(conf_with_model.description)
+ self.assertEqual(conf_no_model.description, f"|{orphan_record._name}")
+
+ def test_edi_get_conf_global_returns_only_matching(self):
+ """Direct check on the new helper method."""
+ matching = self._make_conf(self.trigger_done, "Matching")
+ wrong_trigger = self._make_conf(self.trigger_error, "Wrong trigger")
+ non_global = self._make_conf(self.trigger_done, "Non global", is_global=False)
+ result = self.env["edi.configuration"].edi_get_conf_global(
+ self.record, self.trigger_done.code
+ )
+ self.assertIn(matching, result)
+ self.assertNotIn(wrong_trigger, result)
+ self.assertNotIn(non_global, result)
diff --git a/edi_core_oca/tests/test_exchange_type.py b/edi_core_oca/tests/test_exchange_type.py
index 6edfd79c6..753c0b652 100644
--- a/edi_core_oca/tests/test_exchange_type.py
+++ b/edi_core_oca/tests/test_exchange_type.py
@@ -5,6 +5,7 @@
from freezegun import freeze_time
+from odoo.fields import Command
from odoo.tools import mute_logger
from .common import EDIBackendCommonTestCase
@@ -147,23 +148,105 @@ def test_filename_pattern_settings(self):
self._test_exchange_filename("Test-File-0000001.csv")
def test_archive_rules(self):
- exc_type = self.exchange_type_out
- rule1 = exc_type.rule_ids.create(
+ # Make sure to drop the ``active_test`` flag to be able to properly test
+ # whether archived rules can be found in the exchange type O2M field
+ ctx = dict(self.env.context)
+ ctx.pop("active_test", None)
+ exc_type = self.exchange_type_out.with_context(ctx) # pylint: disable=W8121
+ exc_type.write(
{
- "type_id": exc_type.id,
- "name": "Fake partner rule",
- "model_id": self.env["ir.model"]._get("res.partner").id,
+ "rule_ids": [
+ Command.clear(), # Drop preexisting rules to avoid pollution
+ Command.create(
+ {
+ "name": "Fake partner rule",
+ "model_id": self.env["ir.model"]._get("res.partner").id,
+ }
+ ),
+ Command.create(
+ {
+ "name": "Fake user rule",
+ "model_id": self.env["ir.model"]._get("res.users").id,
+ }
+ ),
+ ]
}
)
- rule2 = exc_type.rule_ids.create(
- {
- "type_id": exc_type.id,
- "name": "Fake user rule",
- "model_id": self.env["ir.model"]._get("res.users").id,
- }
+ rules = rule_1, rule_2 = exc_type.rule_ids
+
+ def _check_exc_type_rule_ids():
+ exc_type.invalidate_recordset(["rule_ids"])
+ self.assertEqual(exc_type.rule_ids, rules)
+
+ # Make sure both Exc Type and all its rules are active
+ self.assertTrue(exc_type.active)
+ self.assertTrue(rule_1.active)
+ self.assertTrue(rule_2.active)
+ _check_exc_type_rule_ids()
+
+ # Archive one of the rules, make sure the Exc Type and the other rule stay
+ # active, and the archived rule is still found in the Exc Type O2M field
+ rule_1.action_archive()
+ self.assertTrue(exc_type.active)
+ self.assertFalse(rule_1.active)
+ self.assertTrue(rule_2.active)
+ _check_exc_type_rule_ids()
+
+ # Archive the Exc Type, make sure both rules are archived, and they both are
+ # still found in the Exc Type O2M field
+ exc_type.action_archive()
+ self.assertFalse(exc_type.active)
+ self.assertFalse(rule_1.active)
+ self.assertFalse(rule_2.active)
+ _check_exc_type_rule_ids()
+
+ # Reactivate the Exc Type, make sure both rules are still archived, and they
+ # both are still found in the Exc Type O2M field
+ exc_type.action_unarchive()
+ self.assertTrue(exc_type.active)
+ self.assertFalse(rule_1.active)
+ self.assertFalse(rule_2.active)
+ _check_exc_type_rule_ids()
+
+ # Force ``active_test`` in record ctx => archived rules are found anyway
+ # (record context does not override field context)
+ for value in (True, False):
+ exc_type = exc_type.with_context(active_test=value)
+ _check_exc_type_rule_ids()
+
+ def _create_exchange_record(self, exc_type):
+ return self.backend.create_record(
+ exc_type.code,
+ {"model": self.partner._name, "res_id": self.partner.id},
)
- exc_type.active = False
- rule1.invalidate_recordset()
- rule2.invalidate_recordset()
- self.assertFalse(rule1.active)
- self.assertFalse(rule2.active)
+
+ def test_exchange_record_count(self):
+ exc_type = self.exchange_type_out
+ self.assertEqual(exc_type.exchange_record_count, 0)
+ rec1 = self._create_exchange_record(exc_type)
+ rec2 = self._create_exchange_record(exc_type)
+ # Record on a different type must not be counted
+ self._create_exchange_record(self.exchange_type_in)
+ self.assertEqual(exc_type.exchange_record_count, 2)
+ self.assertEqual(set(exc_type.exchange_record_ids.ids), {rec1.id, rec2.id})
+
+ def test_action_view_exchange_records(self):
+ exc_type = self.exchange_type_out
+ rec = self._create_exchange_record(exc_type)
+ action = exc_type.action_view_exchange_records()
+ self.assertEqual(action["type"], "ir.actions.act_window")
+ self.assertEqual(action["res_model"], "edi.exchange.record")
+ self.assertIn(("type_id", "=", exc_type.id), action["domain"])
+ ctx = action["context"]
+ self.assertEqual(ctx.get("default_type_id"), exc_type.id)
+ self.assertEqual(ctx.get("default_backend_id"), exc_type.backend_id.id)
+ self.assertEqual(ctx.get("search_default_type_id"), exc_type.id)
+ # The action's domain must actually match the created exchange record
+ records = self.env[action["res_model"]].search(action["domain"])
+ self.assertIn(rec, records)
+
+ def test_action_view_exchange_records_requires_singleton(self):
+ with self.assertRaises(ValueError):
+ (
+ self.exchange_type_out | self.exchange_type_in
+ ).action_view_exchange_records()
diff --git a/edi_core_oca/tests/test_exchange_type_configuration.py b/edi_core_oca/tests/test_exchange_type_configuration.py
index 2f93a4ef0..931dcbc71 100644
--- a/edi_core_oca/tests/test_exchange_type_configuration.py
+++ b/edi_core_oca/tests/test_exchange_type_configuration.py
@@ -16,33 +16,29 @@ def setUpClass(cls):
}
cls.record = cls.backend.create_record("test_csv_output", vals)
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiTestExecution, EdiTestExecutionExtra
- cls.loader.update_registry((EdiTestExecution, EdiTestExecutionExtra))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.ExecutionAbstractModelExtra = cls.env["edi.framework.test.execution.extra"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiTestExecution, EdiTestExecutionExtra))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.ExecutionAbstractModelExtra = self.env[
+ "edi.framework.test.execution.extra"
+ ]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_out.generate_model_id = cls.model
- cls.exchange_type_out.send_model_id = cls.model
-
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
-
- def setUp(self):
- super().setUp()
+ self.exchange_type_out.generate_model_id = self.model
+ self.exchange_type_out.send_model_id = self.model
self.ExecutionAbstractModel.reset_faked("generate")
self.ExecutionAbstractModelExtra.reset_faked("validate")
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
def test_multiple_configuration(self):
vals = {
"model": self.partner._name,
diff --git a/edi_core_oca/tests/test_exchange_type_encoding.py b/edi_core_oca/tests/test_exchange_type_encoding.py
index f83a522f1..295669ec9 100644
--- a/edi_core_oca/tests/test_exchange_type_encoding.py
+++ b/edi_core_oca/tests/test_exchange_type_encoding.py
@@ -9,40 +9,30 @@
class EDIBackendTestOutputCase(EDIBackendCommonTestCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- vals = {
- "model": cls.partner._name,
- "res_id": cls.partner.id,
- }
- cls.record = cls.backend.create_record("test_csv_output", vals)
-
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiTestExecution
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_out.generate_model_id = cls.model
- cls.exchange_type_out.send_model_id = cls.model
-
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
-
- def setUp(self):
- super().setUp()
+ self.exchange_type_out.generate_model_id = self.model
+ self.exchange_type_out.send_model_id = self.model
+ vals = {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ }
+ self.record = self.backend.create_record("test_csv_output", vals)
self.ExecutionAbstractModel.reset_faked("generate")
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
def test_encoding_default(self):
"""
Test default output/input encoding (UTF-8). Use string with special
diff --git a/edi_core_oca/tests/test_quick_exec.py b/edi_core_oca/tests/test_quick_exec.py
index b777eaeaa..16a114462 100644
--- a/edi_core_oca/tests/test_quick_exec.py
+++ b/edi_core_oca/tests/test_quick_exec.py
@@ -25,38 +25,32 @@ def setUpClass(cls):
cls.partner2 = cls.env.ref("base.res_partner_10")
cls.partner3 = cls.env.ref("base.res_partner_12")
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiTestExecution
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_out.generate_model_id = cls.model
- cls.exchange_type_out.send_model_id = cls.model
- cls.exchange_type_out.output_validate_model_id = cls.model
- cls.exchange_type_in.generate_model_id = cls.model
- cls.exchange_type_in.process_model_id = cls.model
- cls.exchange_type_in.input_validate_model_id = cls.model
-
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
-
- def setUp(self):
- super().setUp()
+ self.exchange_type_out.generate_model_id = self.model
+ self.exchange_type_out.send_model_id = self.model
+ self.exchange_type_out.output_validate_model_id = self.model
+ self.exchange_type_in.generate_model_id = self.model
+ self.exchange_type_in.process_model_id = self.model
+ self.exchange_type_in.input_validate_model_id = self.model
self.ExecutionAbstractModel.reset_faked("generate")
self.ExecutionAbstractModel.reset_faked("send")
self.ExecutionAbstractModel.reset_faked("check")
self.ExecutionAbstractModel.reset_faked("process")
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
@mute_logger(*LOGGERS)
def test_quick_exec_on_create_no_call(self):
vals = {
diff --git a/edi_core_oca/tests/test_security.py b/edi_core_oca/tests/test_security.py
index a8bca2f94..c26c62e4e 100644
--- a/edi_core_oca/tests/test_security.py
+++ b/edi_core_oca/tests/test_security.py
@@ -11,65 +11,57 @@
class TestEDIExchangeRecordSecurity(EDIBackendCommonTestCase):
- @classmethod
- def _setup_env(cls):
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EdiExchangeConsumerTest
- cls.loader.update_registry((EdiExchangeConsumerTest,))
- return super()._setup_env()
-
- # pylint: disable=W8110
- @classmethod
- def _setup_records(cls):
- super()._setup_records()
- cls.group = cls.env["res.groups"].create({"name": "Demo Group"})
- cls.ir_access = cls.env["ir.model.access"].create(
+ self.loader.update_registry((EdiExchangeConsumerTest,))
+ self.group = self.env["res.groups"].create({"name": "Demo Group"})
+ self.ir_access = self.env["ir.model.access"].create(
{
"name": "model access",
- "model_id": cls.env.ref(
+ "model_id": self.env.ref(
"edi_core_oca.model_edi_exchange_consumer_test"
).id,
- "group_id": cls.group.id,
+ "group_id": self.group.id,
"perm_read": True,
"perm_write": True,
"perm_create": True,
"perm_unlink": True,
}
)
- cls.rule = cls.env["ir.rule"].create(
+ self.rule = self.env["ir.rule"].create(
{
"name": "Exchange Record rule demo",
- "model_id": cls.env.ref(
+ "model_id": self.env.ref(
"edi_core_oca.model_edi_exchange_consumer_test"
).id,
"domain_force": "[('name', '=', 'test')]",
- "groups": [(4, cls.group.id)],
+ "groups": [(4, self.group.id)],
}
)
- cls.user = (
- cls.env["res.users"]
+ self.user = (
+ self.env["res.users"]
.with_context(no_reset_password=True, mail_notrack=True)
.create(
{
"name": "Poor Partner (not integrating one)",
"email": "poor.partner@ododo.com",
"login": "poorpartner",
- "groups_id": [(6, 0, [cls.env.ref("base_edi.group_edi_user").id])],
+ "groups_id": [(6, 0, [self.env.ref("base_edi.group_edi_user").id])],
}
)
)
- cls.consumer_record = cls.env["edi.exchange.consumer.test"].create(
+ self.consumer_record = self.env["edi.exchange.consumer.test"].create(
{"name": "test"}
)
- cls.exchange_type_out.exchange_filename_pattern = "{record.id}"
+ self.exchange_type_out.exchange_filename_pattern = "{record.id}"
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
def create_record(self, user=False):
vals = {
@@ -238,3 +230,108 @@ def test_no_group_no_read_child(self):
msg = rf"not allowed to access '{model._description}' \({model._name}\)"
with self.assertRaisesRegex(AccessError, msg):
child_exchange_record.with_user(self.user).read()
+
+ def test_search_pagination_with_inaccessible_middle_records(self):
+ """
+ Regression test:
+ If some records in the first page are filtered out due to access rules,
+ _search must fetch additional records from next pages without truncating them.
+ """
+
+ self.user.write({"groups_id": [(4, self.group.id)]})
+
+ # Two different companies are used to trigger multi-company access filtering
+ company_1 = self.env.ref("base.main_company")
+ company_2 = self.env["res.company"].create({"name": "Other Company"})
+
+ # Three target records:
+ # - consumer_c1 and consumer_c3 belong to the active company and are readable
+ # - consumer_c2 belongs to another company and will be filtered out
+ # by access rules
+ consumer_c1 = self.env["res.partner"].create(
+ {"name": "c1-a", "company_id": company_1.id}
+ )
+ consumer_c2 = self.env["res.partner"].create(
+ {"name": "c2", "company_id": company_2.id}
+ )
+ consumer_c3 = self.env["res.partner"].create(
+ {"name": "c1-b", "company_id": company_1.id}
+ )
+
+ # One EDI records pointing to readable target records
+ self.backend.create_record(
+ "test_csv_output",
+ {"model": consumer_c1._name, "res_id": consumer_c1.id},
+ )
+
+ # One EDI records pointing to records from another company
+ self.backend.create_record(
+ "test_csv_output",
+ {"model": consumer_c2._name, "res_id": consumer_c2.id},
+ )
+
+ # One EDI records pointing to readable target records
+ visible_id_2 = self.backend.create_record(
+ "test_csv_output",
+ {"model": consumer_c3._name, "res_id": consumer_c3.id},
+ ).id
+
+ # Restrict the environment to company_1 only, activating the multi-company rule
+ # that will hide records pointing to consumer_c2
+ env_company_1 = self.env(
+ context=dict(self.env.context, allowed_company_ids=[company_1.id])
+ )
+
+ # Execute the search as a non-superuser:
+ # - super()._search returns the first 2 IDs (1 visible + 1 hidden)
+ # - custom logic removes the 1 hidden
+ # - pagination logic fetches 1 more record from the next page
+ records = (
+ env_company_1["edi.exchange.record"]
+ .with_user(self.user)
+ .search([], limit=2, order="id asc")
+ )
+
+ # The result must NOT be truncated: the search should still return `
+ # limit` records
+ self.assertEqual(
+ len(records),
+ 2,
+ "Search results were truncated when inaccessible records were "
+ "present in the first page",
+ )
+
+ # The records fetched from the second page must be present in the final result
+ self.assertIn(visible_id_2, records.ids)
+
+ def test_search_no_res_id(self):
+ """Test Exc Rec visibility for internal users when ``res_id`` is False-ish
+
+ Exchange Record's ``res_id`` is a ``Many2onReference`` field, which internally
+ converts False-ish values to 0 before storing them to the cache and the DB.
+ The rule's domain old leaf ``('res_id', '=', False)`` was instead converted to a
+ SQL query clause ``WHERE "edi_exchange_record.res_id" IS NULL``.
+ Since all ``edi_exchange_record`` rows contain a non-negative integer in the
+ ``res_id`` column, the rule old domain leaf always failed to fetch any record.
+
+ Changing the leaf to ``('res_id', '=', 0)`` fixes the issue, making such
+ Exchange Records visible again for internal users.
+ """
+ # Add the test user to the internal users group
+ self.user.write({"groups_id": [(4, self.env.ref("base.group_user").id)]})
+
+ # Create Exchange Records with no model (condition ``('model', '!=', False)``
+ # will fail) and False-ish record ID (to test condition ``('res_id', '=', 0)``):
+ # such False-ish values are all converted to 0 by ``fields.Many2oneReference``
+ # methods (and methods of its superclasses) when updating the cache values and
+ # preparing SQL queries to flush to the DB
+ exc_recs = self.env["edi.exchange.record"]
+ type_code = "test_csv_output"
+ vals = {"model": False}
+ for res_id in (0, 0.00, False, None, "", self.env["base"]):
+ exc_recs += self.backend.create_record(type_code, vals | {"res_id": res_id})
+ self.assertEqual(exc_recs.mapped("res_id"), [0] * len(exc_recs))
+
+ # Check that the test user can actually fetch such records
+ exc_recs_model = self.env["edi.exchange.record"].with_user(self.user)
+ self.assertEqual(exc_recs_model.search([("id", "in", exc_recs.ids)]), exc_recs)
diff --git a/edi_core_oca/views/edi_backend_views.xml b/edi_core_oca/views/edi_backend_views.xml
index 2f557e0d3..5c4aea4f4 100644
--- a/edi_core_oca/views/edi_backend_views.xml
+++ b/edi_core_oca/views/edi_backend_views.xml
@@ -57,7 +57,14 @@
@@ -43,11 +57,17 @@
-
+
+
-
@@ -189,14 +209,7 @@
list,form
[]
-
- {'search_default_filter_all': 1, 'active_test': False}
+ {'search_default_filter_all': 1}
Config -> Exchange Type".
+
+Enable "Deduplicate on Send" option -> Enable "Delete obsolete records"
+option.
+
+Usage
+=====
+
+With all the types that have been enabled "Deduplicate on Send" option,
+this module will check their records if a fresher one does not exist for
+the same record. If so, mark the oldest one as obsolete (except
+"block_obsolescence" records)
+
+- "block_obsolescence" is an technical option on records to avoid
+ marking them as obsolete.
+
+With all the types that have been enabled "Delete obsolete records"
+option, the cron will remove their obsolete records.
+
+- If the records are obsolete, delete them even if their type's flag has
+ been disabled.
+
+Bug Tracker
+===========
+
+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
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* Camptocamp
+
+Contributors
+------------
+
+- Simone Orsi
+- Duong (Tran Quoc)
+
+Maintainers
+-----------
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+.. |maintainer-simahawk| image:: https://github.com/simahawk.png?size=40px
+ :target: https://github.com/simahawk
+ :alt: simahawk
+.. |maintainer-etobella| image:: https://github.com/etobella.png?size=40px
+ :target: https://github.com/etobella
+ :alt: etobella
+
+Current `maintainers `__:
+
+|maintainer-simahawk| |maintainer-etobella|
+
+This module is part of the `OCA/edi-framework `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/edi_exchange_deduplicate_oca/__init__.py b/edi_exchange_deduplicate_oca/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/edi_exchange_deduplicate_oca/__manifest__.py b/edi_exchange_deduplicate_oca/__manifest__.py
new file mode 100644
index 000000000..8576d5b5e
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/__manifest__.py
@@ -0,0 +1,19 @@
+# Copyright 2024 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+{
+ "name": "Edi Exchange Deduplicate OCA",
+ "summary": """
+ Introduce a deduplication mechanism at the sending step""",
+ "version": "18.0.1.0.0",
+ "license": "LGPL-3",
+ "author": "Camptocamp,Odoo Community Association (OCA)",
+ "maintainers": ["simahawk", "etobella"],
+ "website": "https://github.com/OCA/edi-framework",
+ "depends": ["edi_core_oca"],
+ "data": [
+ "data/cron.xml",
+ "views/edi_exchange_type_views.xml",
+ ],
+ "demo": [],
+}
diff --git a/edi_exchange_deduplicate_oca/data/cron.xml b/edi_exchange_deduplicate_oca/data/cron.xml
new file mode 100644
index 000000000..02e9eb208
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/data/cron.xml
@@ -0,0 +1,17 @@
+
+
+
+ EDI exchange delete obsolete records
+
+
+ 1
+ days
+
+ code
+ model.search([])._cron_delete_obsolete_records()
+
+
diff --git a/edi_exchange_deduplicate_oca/i18n/edi_exchange_deduplicate_oca.pot b/edi_exchange_deduplicate_oca/i18n/edi_exchange_deduplicate_oca.pot
new file mode 100644
index 000000000..1dabac0d3
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/i18n/edi_exchange_deduplicate_oca.pot
@@ -0,0 +1,76 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_exchange_deduplicate_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,help:edi_exchange_deduplicate_oca.field_edi_exchange_type__deduplicate_on_send
+msgid ""
+"Before sending an exchange record, check if a fresher one does not exist for"
+" same record; if so, mark oldest one as obsolete."
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_record__block_obsolescence
+msgid "Block Obsolescence"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_type__deduplicate_on_send
+msgid "Deduplicate on Send"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_type__delete_obsolete_records
+msgid "Delete obsolete records"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,help:edi_exchange_deduplicate_oca.field_edi_exchange_type__delete_obsolete_records
+msgid "Delete records marked as obsolete."
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model,name:edi_exchange_deduplicate_oca.model_edi_backend
+msgid "EDI Backend"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model,name:edi_exchange_deduplicate_oca.model_edi_exchange_type
+msgid "EDI Exchange Type"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model,name:edi_exchange_deduplicate_oca.model_edi_exchange_record
+msgid "EDI exchange Record"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.actions.server,name:edi_exchange_deduplicate_oca.cron_edi_backend_delete_obsolete_records_ir_actions_server
+msgid "EDI exchange delete obsolete records"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_record__edi_exchange_state
+msgid "Exchange state"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,help:edi_exchange_deduplicate_oca.field_edi_exchange_record__block_obsolescence
+msgid "Flag record that can never be marked as obsolete"
+msgstr ""
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields.selection,name:edi_exchange_deduplicate_oca.selection__edi_exchange_record__edi_exchange_state__obsolete
+msgid "Obsolete"
+msgstr ""
diff --git a/edi_exchange_deduplicate_oca/i18n/it.po b/edi_exchange_deduplicate_oca/i18n/it.po
new file mode 100644
index 000000000..efddbf985
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/i18n/it.po
@@ -0,0 +1,82 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_exchange_deduplicate_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2024-08-07 17:58+0000\n"
+"Last-Translator: mymage \n"
+"Language-Team: none\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.6.2\n"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,help:edi_exchange_deduplicate_oca.field_edi_exchange_type__deduplicate_on_send
+msgid ""
+"Before sending an exchange record, check if a fresher one does not exist for"
+" same record; if so, mark oldest one as obsolete."
+msgstr ""
+"Prima di inviare un record di scambio, controlla se ne esiste uno più "
+"recente per lo stesso record; se così, marca il vecchio come obsoleto."
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_record__block_obsolescence
+msgid "Block Obsolescence"
+msgstr "Obsolescenza blocco"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_type__deduplicate_on_send
+msgid "Deduplicate on Send"
+msgstr "Duplica alla spedizione"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_type__delete_obsolete_records
+msgid "Delete obsolete records"
+msgstr "Cancella record obsoleti"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,help:edi_exchange_deduplicate_oca.field_edi_exchange_type__delete_obsolete_records
+msgid "Delete records marked as obsolete."
+msgstr "Cancella record marcati come obsoleti."
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model,name:edi_exchange_deduplicate_oca.model_edi_backend
+msgid "EDI Backend"
+msgstr "Backend EDI"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model,name:edi_exchange_deduplicate_oca.model_edi_exchange_type
+msgid "EDI Exchange Type"
+msgstr "Tipo scambio EDI"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model,name:edi_exchange_deduplicate_oca.model_edi_exchange_record
+msgid "EDI exchange Record"
+msgstr "Record di scambio EDI"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.actions.server,name:edi_exchange_deduplicate_oca.cron_edi_backend_delete_obsolete_records_ir_actions_server
+#: model:ir.cron,cron_name:edi_exchange_deduplicate_oca.cron_edi_backend_delete_obsolete_records
+msgid "EDI exchange delete obsolete records"
+msgstr "Scambio EDI cancella record obsoleti"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_record__edi_exchange_state
+msgid "Exchange state"
+msgstr "Stato scambio"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields,help:edi_exchange_deduplicate_oca.field_edi_exchange_record__block_obsolescence
+msgid "Flag record that can never be marked as obsolete"
+msgstr "Segna record che non devono mai essere marcati obsoleti"
+
+#. module: edi_exchange_deduplicate_oca
+#: model:ir.model.fields.selection,name:edi_exchange_deduplicate_oca.selection__edi_exchange_record__edi_exchange_state__obsolete
+msgid "Obsolete"
+msgstr "Obsoleto"
diff --git a/edi_exchange_deduplicate_oca/models/__init__.py b/edi_exchange_deduplicate_oca/models/__init__.py
new file mode 100644
index 000000000..ddad285da
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/models/__init__.py
@@ -0,0 +1,3 @@
+from . import edi_backend
+from . import edi_exchange_type
+from . import edi_exchange_record
diff --git a/edi_exchange_deduplicate_oca/models/edi_backend.py b/edi_exchange_deduplicate_oca/models/edi_backend.py
new file mode 100644
index 000000000..12a75a380
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/models/edi_backend.py
@@ -0,0 +1,48 @@
+# Copyright 2024 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+import logging
+
+from odoo import models
+
+_logger = logging.getLogger(__name__)
+
+
+class EDIBackend(models.Model):
+ _inherit = "edi.backend"
+
+ def _failed_output_check_send_msg(self):
+ return "Nothing to do. Likely already sent or obsolete."
+
+ def _cron_delete_obsolete_records(self, **kw):
+ for backend in self:
+ backend._delete_obsolete_records(**kw)
+
+ def _delete_obsolete_records(self, record_ids=None, **kw):
+ """Cleanup obsolete records.
+
+ Go through types with `delete_obsolete_records` flag on
+ and delete their obsolete records if any.
+ """
+ obsolete_records = self.exchange_record_model.search(
+ self._obsolete_records_domain(record_ids=record_ids)
+ )
+ _logger.info(
+ "EDI Exchange delete records: found %d obsolete records to delete.",
+ len(obsolete_records),
+ )
+ if obsolete_records:
+ obsolete_records.unlink()
+
+ def _obsolete_records_domain(self, record_ids=None):
+ """
+ Domain for obsolete records need to delete.
+ If the record is obsolete, delete it even if the type's flag has been disabled.
+ """
+ domain = [
+ ("backend_id", "=", self.id),
+ ("edi_exchange_state", "=", "obsolete"),
+ ]
+ if record_ids:
+ domain.append(("id", "in", record_ids))
+ return domain
diff --git a/edi_exchange_deduplicate_oca/models/edi_exchange_record.py b/edi_exchange_deduplicate_oca/models/edi_exchange_record.py
new file mode 100644
index 000000000..dcbb3f096
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/models/edi_exchange_record.py
@@ -0,0 +1,51 @@
+# Copyright 2024 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo import api, fields, models
+
+
+class EDIExchangeRecord(models.Model):
+ _inherit = "edi.exchange.record"
+
+ edi_exchange_state = fields.Selection(
+ selection_add=[
+ ("obsolete", "Obsolete"),
+ ],
+ ondelete={"obsolete": "cascade"},
+ )
+ block_obsolescence = fields.Boolean(
+ default=False,
+ help="Flag record that can never be marked as obsolete",
+ )
+
+ @api.constrains("edi_exchange_state")
+ def _constrain_edi_exchange_state(self):
+ # Remove `obsolete` record for this check
+ self = self.filtered(lambda r: r.edi_exchange_state != "obsolete")
+ return super()._constrain_edi_exchange_state()
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = super().create(vals_list)
+ for rec in records:
+ check_obsoleted_record = (
+ rec.type_id.direction == "output" and rec.type_id.deduplicate_on_send
+ )
+ if check_obsoleted_record:
+ obsoleted_records = rec._edi_get_duplicates()
+ if obsoleted_records:
+ obsoleted_records.edi_exchange_state = "obsolete"
+ return records
+
+ def _edi_get_duplicates(self, count=False):
+ self.ensure_one()
+ return (self.search_count if count else self.search)(
+ [
+ ("id", "<", self.id),
+ ("res_id", "=", self.res_id),
+ ("model", "=", self.model),
+ ("type_id", "=", self.type_id.id),
+ ("edi_exchange_state", "in", ("new", "output_pending")),
+ ("block_obsolescence", "=", False),
+ ],
+ )
diff --git a/edi_exchange_deduplicate_oca/models/edi_exchange_type.py b/edi_exchange_deduplicate_oca/models/edi_exchange_type.py
new file mode 100644
index 000000000..80fcbd7d8
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/models/edi_exchange_type.py
@@ -0,0 +1,20 @@
+# Copyright 2024 Camptocamp
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class EDIExchangeType(models.Model):
+ _inherit = "edi.exchange.type"
+
+ deduplicate_on_send = fields.Boolean(
+ string="Deduplicate on Send",
+ default=False,
+ help="Before sending an exchange record, check if a fresher one does not "
+ "exist for same record; if so, mark oldest one as obsolete.",
+ )
+ delete_obsolete_records = fields.Boolean(
+ string="Delete obsolete records",
+ default=True,
+ help="Delete records marked as obsolete.",
+ )
diff --git a/edi_exchange_deduplicate_oca/pyproject.toml b/edi_exchange_deduplicate_oca/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/edi_exchange_deduplicate_oca/readme/CONFIGURE.md b/edi_exchange_deduplicate_oca/readme/CONFIGURE.md
new file mode 100644
index 000000000..fd2d3271d
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/readme/CONFIGURE.md
@@ -0,0 +1,4 @@
+Go to "EDI -\> Config -\> Exchange Type".
+
+Enable "Deduplicate on Send" option -\> Enable "Delete obsolete records"
+option.
diff --git a/edi_exchange_deduplicate_oca/readme/CONTRIBUTORS.md b/edi_exchange_deduplicate_oca/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..faee38dc0
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/readme/CONTRIBUTORS.md
@@ -0,0 +1,2 @@
+- Simone Orsi \
+- Duong (Tran Quoc) \
diff --git a/edi_exchange_deduplicate_oca/readme/DESCRIPTION.md b/edi_exchange_deduplicate_oca/readme/DESCRIPTION.md
new file mode 100644
index 000000000..434a47858
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/readme/DESCRIPTION.md
@@ -0,0 +1,4 @@
+This module adds options for deduplication records before sending step on type:
+- deduplicate_on_send: check if a fresher one does not exist for the
+ same record. If so, mark the oldest one as obsolete.
+- delete_obsolete_records: Delete records marked as obsolete.
diff --git a/edi_exchange_deduplicate_oca/readme/USAGE.md b/edi_exchange_deduplicate_oca/readme/USAGE.md
new file mode 100644
index 000000000..70fcdfdbd
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/readme/USAGE.md
@@ -0,0 +1,7 @@
+With all the types that have been enabled "Deduplicate on Send" option, this module will check their records if a fresher one does not exist for the same record. If so, mark the oldest one as obsolete (except "block_obsolescence" records)
+- "block_obsolescence" is an technical option on records to avoid
+ marking them as obsolete.
+
+With all the types that have been enabled "Delete obsolete records" option, the cron will remove their obsolete records.
+- If the records are obsolete, delete them even if their type's flag has
+ been disabled.
diff --git a/edi_exchange_deduplicate_oca/static/description/icon.png b/edi_exchange_deduplicate_oca/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/edi_exchange_deduplicate_oca/static/description/icon.png differ
diff --git a/edi_exchange_deduplicate_oca/static/description/index.html b/edi_exchange_deduplicate_oca/static/description/index.html
new file mode 100644
index 000000000..f8a806666
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/static/description/index.html
@@ -0,0 +1,463 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+Edi Exchange Deduplicate OCA
+
+
+This module adds options for deduplication records before sending step
+on type:
+
+- deduplicate_on_send: check if a fresher one does not exist for the
+same record. If so, mark the oldest one as obsolete.
+- delete_obsolete_records: Delete records marked as obsolete.
+
+Table of contents
+
+
+- Configuration
+- Usage
+- Bug Tracker
+- Credits
+
+
+
+
+Configuration
+Go to “EDI -> Config -> Exchange Type”.
+Enable “Deduplicate on Send” option -> Enable “Delete obsolete records”
+option.
+
+
+Usage
+With all the types that have been enabled “Deduplicate on Send” option,
+this module will check their records if a fresher one does not exist for
+the same record. If so, mark the oldest one as obsolete (except
+“block_obsolescence” records)
+
+- “block_obsolescence” is an technical option on records to avoid
+marking them as obsolete.
+
+With all the types that have been enabled “Delete obsolete records”
+option, the cron will remove their obsolete records.
+
+- If the records are obsolete, delete them even if their type’s flag has
+been disabled.
+
+
+
+Bug Tracker
+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
+feedback.
+Do not contact contributors directly about support or help with technical issues.
+
+
+Credits
+
+Authors
+
+- Camptocamp
+
+
+
+Contributors
+
+- Simone Orsi <simone.orsi@camptocamp.com>
+- Duong (Tran Quoc) <duongtq@trobz.com>
+
+
+
+Maintainers
+This module is maintained by the OCA.
+
+
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+Current maintainers:
+
+This module is part of the OCA/edi-framework project on GitHub.
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
+
diff --git a/edi_exchange_deduplicate_oca/tests/__init__.py b/edi_exchange_deduplicate_oca/tests/__init__.py
new file mode 100644
index 000000000..c4bb24c72
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/tests/__init__.py
@@ -0,0 +1,2 @@
+from . import test_edi_backend_cron
+from . import test_edi_duplicate
diff --git a/edi_exchange_deduplicate_oca/tests/test_edi_backend_cron.py b/edi_exchange_deduplicate_oca/tests/test_edi_backend_cron.py
new file mode 100644
index 000000000..3162001be
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/tests/test_edi_backend_cron.py
@@ -0,0 +1,39 @@
+# Copyright 2024 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo.tools import mute_logger
+
+from odoo.addons.edi_core_oca.tests.test_edi_backend_cron import EDIBackendTestCronCase
+
+LOGGERS = ("odoo.addons.edi_core_oca.models.edi_backend", "odoo.addons.queue_job.delay")
+
+
+class EDIBackendTestCronDeduplicationCase(EDIBackendTestCronCase):
+ @mute_logger(*LOGGERS)
+ def test_exchange_delete_obsolete_records(self):
+ self.exchange_type_out.write(
+ {
+ "exchange_file_auto_generate": True,
+ "deduplicate_on_send": True,
+ "delete_obsolete_records": True,
+ }
+ )
+ record1_1 = self.backend.create_record(
+ "test_csv_output", {"model": self.partner._name, "res_id": self.partner.id}
+ )
+ record1_2 = self.backend.create_record(
+ "test_csv_output", {"model": self.partner._name, "res_id": self.partner.id}
+ )
+ record1_3 = self.backend.create_record(
+ "test_csv_output", {"model": self.partner._name, "res_id": self.partner.id}
+ )
+ # all the older records should have been obsolete by record1_3
+ records = self.record1 + record1_1 + record1_2
+ self.backend._check_output_exchange_sync()
+ for record in records:
+ self.assertEqual(record.edi_exchange_state, "obsolete")
+ self.assertEqual(record1_3.edi_exchange_state, "output_sent")
+ self.backend._delete_obsolete_records()
+ for record in records:
+ self.assertFalse(record.exists())
+ self.assertTrue(record1_3.exists())
diff --git a/edi_exchange_deduplicate_oca/tests/test_edi_duplicate.py b/edi_exchange_deduplicate_oca/tests/test_edi_duplicate.py
new file mode 100644
index 000000000..2273e51d8
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/tests/test_edi_duplicate.py
@@ -0,0 +1,144 @@
+# Copyright 2024 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo_test_helper import FakeModelLoader
+
+from odoo.tools import mute_logger
+
+from odoo.addons.edi_core_oca.tests.common import EDIBackendCommonTestCase
+
+LOGGERS = (
+ "odoo.addons.edi_core_oca.models.edi_backend",
+ "odoo.addons.queue_job.delay",
+)
+
+
+class EDIDeduplicateTestCase(EDIBackendCommonTestCase):
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
+ from odoo.addons.edi_core_oca.tests.fake_models import EdiTestExecution
+
+ self.loader.update_registry((EdiTestExecution,))
+ self.model = self.env["ir.model"].search(
+ [("model", "=", "edi.framework.test.execution")]
+ )
+ self.exchange_type_out.write(
+ {
+ "exchange_file_auto_generate": True,
+ "generate_model_id": self.model.id,
+ "send_model_id": self.model.id,
+ "output_validate_model_id": self.model.id,
+ }
+ )
+
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
+
+ @mute_logger(*LOGGERS)
+ def test_deduplicate_on_send(self):
+ self.exchange_type_out.write(
+ {
+ "deduplicate_on_send": True,
+ }
+ )
+ record1 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ },
+ )
+ record2 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ },
+ )
+ record3 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ },
+ )
+ records = record1 + record2
+ self.backend._check_output_exchange_sync()
+ # Because we just sent the last record, so the others should be "obsolete"
+ for record in records:
+ self.assertEqual(record.edi_exchange_state, "obsolete")
+ self.assertEqual(record3.edi_exchange_state, "output_sent")
+
+ @mute_logger(*LOGGERS)
+ def test_no_deduplicate_on_send(self):
+ self.exchange_type_out.write(
+ {
+ "deduplicate_on_send": False,
+ }
+ )
+ record1 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ },
+ )
+ record2 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ },
+ )
+ record3 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ },
+ )
+ records = record1 + record2 + record3
+ self.backend._check_output_exchange_sync()
+ # All the records should be "output_sent"
+ for record in records:
+ self.assertEqual(record.edi_exchange_state, "output_sent")
+
+ @mute_logger(*LOGGERS)
+ def test_block_obsolescence(self):
+ self.exchange_type_out.write(
+ {
+ "deduplicate_on_send": True,
+ }
+ )
+ record1 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ },
+ )
+ record2 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ # Checking
+ "block_obsolescence": True,
+ },
+ )
+ record3 = self.backend.create_record(
+ "test_csv_output",
+ {
+ "model": self.partner._name,
+ "res_id": self.partner.id,
+ },
+ )
+ self.backend._check_output_exchange_sync()
+ # Normally, record2 has been "obsolete"
+ # But with block_obsolescence = True, it will be "output_sent" too
+ self.assertEqual(record1.edi_exchange_state, "obsolete")
+ self.assertEqual(record2.edi_exchange_state, "output_sent")
+ self.assertEqual(record3.edi_exchange_state, "output_sent")
diff --git a/edi_exchange_deduplicate_oca/views/edi_exchange_type_views.xml b/edi_exchange_deduplicate_oca/views/edi_exchange_type_views.xml
new file mode 100644
index 000000000..0470b5255
--- /dev/null
+++ b/edi_exchange_deduplicate_oca/views/edi_exchange_type_views.xml
@@ -0,0 +1,17 @@
+
+
+
+ edi.exchange.type.form.inherit
+ edi.exchange.type
+
+
+
+
+
+
+
+
+
diff --git a/edi_exchange_template_oca/README.rst b/edi_exchange_template_oca/README.rst
index b93ec5437..be1ecdf9a 100644
--- a/edi_exchange_template_oca/README.rst
+++ b/edi_exchange_template_oca/README.rst
@@ -11,7 +11,7 @@ EDI Exchange Template
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:ebed0a953bbe9571fc02c722ad850e26c3af87cf9ee52fd7f496048a102717ec
+ !! source digest: sha256:c6b98455272323462e208761ef6f68cff26fd2e6f561245ef60fe8af4fe329e8
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/edi_exchange_template_oca/__manifest__.py b/edi_exchange_template_oca/__manifest__.py
index e1d3d5fbc..11190f20a 100644
--- a/edi_exchange_template_oca/__manifest__.py
+++ b/edi_exchange_template_oca/__manifest__.py
@@ -5,7 +5,7 @@
{
"name": "EDI Exchange Template",
"summary": """Allows definition of exchanges via templates.""",
- "version": "18.0.1.3.2",
+ "version": "18.0.1.3.3",
"development_status": "Beta",
"license": "LGPL-3",
"author": "ACSONE,Camptocamp,Odoo Community Association (OCA)",
diff --git a/edi_exchange_template_oca/static/description/index.html b/edi_exchange_template_oca/static/description/index.html
index c072c6345..664d6c0b7 100644
--- a/edi_exchange_template_oca/static/description/index.html
+++ b/edi_exchange_template_oca/static/description/index.html
@@ -372,7 +372,7 @@ EDI Exchange Template
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:ebed0a953bbe9571fc02c722ad850e26c3af87cf9ee52fd7f496048a102717ec
+!! source digest: sha256:c6b98455272323462e208761ef6f68cff26fd2e6f561245ef60fe8af4fe329e8
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
Provide EDI exchange templates to control input/output records contents.
diff --git a/edi_exchange_template_oca/tests/test_edi_backend_output.py b/edi_exchange_template_oca/tests/test_edi_backend_output.py
index 89a91bb37..117f170e4 100644
--- a/edi_exchange_template_oca/tests/test_edi_backend_output.py
+++ b/edi_exchange_template_oca/tests/test_edi_backend_output.py
@@ -242,13 +242,21 @@ def test_generate_file(self):
self.assertEqual(file_content.strip(), expected)
def test_prettify(self):
- self.tmpl_out2.template_id.arch = (
+ tmpl_out2 = self.env["edi.exchange.template.output"].browse(self.tmpl_out2.id)
+ record2 = self.env["edi.exchange.record"].browse(self.record2.id)
+ self.assertTrue(
+ tmpl_out2.exists(), "Template output record vanished during test execution"
+ )
+ self.assertTrue(
+ record2.exists(), "Exchange record vanished during test execution"
+ )
+ tmpl_out2.template_id.arch = (
'1 '
)
- output = self.tmpl_out2.exchange_generate(self.record2)
+ output = tmpl_out2.exchange_generate(record2)
self.assertEqual(output, b"1 ")
- self.tmpl_out2.prettify = True
- output = self.tmpl_out2.exchange_generate(self.record2)
+ tmpl_out2.prettify = True
+ output = tmpl_out2.exchange_generate(record2)
self.assertEqual(output, b"\n 1\n \n")
def test_generate_file_report(self):
diff --git a/edi_notification_oca/README.rst b/edi_notification_oca/README.rst
new file mode 100644
index 000000000..3c08c4141
--- /dev/null
+++ b/edi_notification_oca/README.rst
@@ -0,0 +1,97 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
+================
+EDI Notification
+================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:d865a9ca8287a26c0e9185d73c56951401b600d75381622a3f3474f5ee2c0992
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Alpha
+.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
+ :alt: License: LGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi--framework-lightgray.png?logo=github
+ :target: https://github.com/OCA/edi-framework/tree/18.0/edi_notification_oca
+ :alt: OCA/edi-framework
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_notification_oca
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=18.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module creates activities for users when an exchange record's
+process fails.
+
+Exchange types must be configured properly to create such activities:
+
+- field "Notify On Process Error" must be checked to activate the
+ feature for the current exchange type
+- field "Activity Type Used When Notify On Process Error" is used to
+ define the type of the newly created activity
+- fields "Notify Groups On Process Error" and "Notify Users On Process
+ Error" are used to define the users that will be assigned to the newly
+ created activity
+
+.. IMPORTANT::
+ This is an alpha version, the data model and design can change at any time without warning.
+ Only for development or testing purpose, do not use in production.
+ `More details on development status `_
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Bug Tracker
+===========
+
+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
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* Camptocamp
+
+Contributors
+------------
+
+- Duong (Tran Quoc)
+- Simone Orsi
+
+Maintainers
+-----------
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/edi-framework `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/edi_notification_oca/__init__.py b/edi_notification_oca/__init__.py
new file mode 100644
index 000000000..f24d3e242
--- /dev/null
+++ b/edi_notification_oca/__init__.py
@@ -0,0 +1,2 @@
+from . import components
+from . import models
diff --git a/edi_notification_oca/__manifest__.py b/edi_notification_oca/__manifest__.py
new file mode 100644
index 000000000..5fc8183f9
--- /dev/null
+++ b/edi_notification_oca/__manifest__.py
@@ -0,0 +1,16 @@
+# Copyright 2024 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+{
+ "name": "EDI Notification",
+ "summary": """Define notification activities on exchange records.""",
+ "version": "18.0.1.0.0",
+ "development_status": "Alpha",
+ "license": "LGPL-3",
+ "website": "https://github.com/OCA/edi-framework",
+ "author": "Camptocamp,Odoo Community Association (OCA)",
+ # TODO v19: consider getting rid off `edi_component_oca` dep
+ "depends": ["edi_core_oca", "edi_component_oca"],
+ "data": ["data/mail_activity_type.xml", "views/edi_exchange_type.xml"],
+ "installable": True,
+}
diff --git a/edi_notification_oca/components/__init__.py b/edi_notification_oca/components/__init__.py
new file mode 100644
index 000000000..9430369c2
--- /dev/null
+++ b/edi_notification_oca/components/__init__.py
@@ -0,0 +1 @@
+from . import listener
diff --git a/edi_notification_oca/components/listener.py b/edi_notification_oca/components/listener.py
new file mode 100644
index 000000000..30945e128
--- /dev/null
+++ b/edi_notification_oca/components/listener.py
@@ -0,0 +1,44 @@
+# Copyright 2024 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo.addons.component.core import Component
+
+
+class EdiNotificationListener(Component):
+ _name = "edi.notification.component.listener"
+ _inherit = "base.event.listener"
+
+ def on_edi_exchange_error(self, record):
+ exc_type = record.type_id
+ notify_on_process_error = exc_type.notify_on_process_error
+ activity_type = exc_type.notify_on_process_error_activity_type_id
+ if (
+ not notify_on_process_error
+ or not activity_type
+ or not (
+ exc_type.notify_on_process_error_groups_ids
+ or exc_type.notify_on_process_error_users_ids
+ )
+ ):
+ return True
+ users = self._get_users_to_notify(exc_type)
+ # Send notification to defined users
+ for user in users:
+ record.activity_schedule(
+ activity_type_id=activity_type.id,
+ summary=self.env._(
+ "EDI: Process error on record '%(identifier)s'.",
+ identifier=record.identifier,
+ ),
+ note=record.exchange_error,
+ user_id=user.id,
+ automated=True,
+ )
+ return True
+
+ def _get_users_to_notify(self, exc_type):
+ exc_type.ensure_one()
+ return (
+ exc_type.notify_on_process_error_groups_ids.users
+ | exc_type.notify_on_process_error_users_ids
+ )
diff --git a/edi_notification_oca/data/mail_activity_type.xml b/edi_notification_oca/data/mail_activity_type.xml
new file mode 100644
index 000000000..7fa92c0d4
--- /dev/null
+++ b/edi_notification_oca/data/mail_activity_type.xml
@@ -0,0 +1,12 @@
+
+
+
+ EDI Exchange Record: Failed
+ fa-warning
+ edi.exchange.record
+ warning
+
+
diff --git a/edi_notification_oca/i18n/edi_notification_oca.pot b/edi_notification_oca/i18n/edi_notification_oca.pot
new file mode 100644
index 000000000..f31e62e4a
--- /dev/null
+++ b/edi_notification_oca/i18n/edi_notification_oca.pot
@@ -0,0 +1,149 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_notification_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_ids
+msgid "Activities"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_exception_decoration
+msgid "Activity Exception Decoration"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_state
+msgid "Activity State"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_type_icon
+msgid "Activity Type Icon"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_activity_type_id
+msgid "Activity Type Used When Notify On Process Error"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:mail.activity.type,name:edi_notification_oca.mail_activity_failed_exchange_record_warning
+msgid "EDI Exchange Record: Failed"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model,name:edi_notification_oca.model_edi_exchange_type
+msgid "EDI Exchange Type"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model,name:edi_notification_oca.model_edi_exchange_record
+msgid "EDI exchange Record"
+msgstr ""
+
+#. module: edi_notification_oca
+#. odoo-python
+#: code:addons/edi_notification_oca/components/listener.py:0
+msgid "EDI: Process error on record '%(identifier)s'."
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_exception_icon
+msgid "Icon"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_type__notify_on_process_error
+msgid ""
+"If an error happens on process, a notification will be sent to all selected "
+"users. If active, please select the specific groups and specific users in "
+"the 'Notifications' page."
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_summary
+msgid "Next Activity Summary"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_type_id
+msgid "Next Activity Type"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model_terms:ir.ui.view,arch_db:edi_notification_oca.edi_exchange_type_view_form
+msgid "Notification"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_groups_ids
+msgid "Notify Groups On Process Error"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error
+msgid "Notify On Process Error"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_users_ids
+msgid "Notify Users On Process Error"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_user_id
+msgid "Responsible User"
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_users_ids
+msgid ""
+"Select users to send notifications to. If 'Notification Groups' have been "
+"selected, notifications will also be sent to users selected in here."
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr ""
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr ""
diff --git a/edi_notification_oca/i18n/it.po b/edi_notification_oca/i18n/it.po
new file mode 100644
index 000000000..5a02a680f
--- /dev/null
+++ b/edi_notification_oca/i18n/it.po
@@ -0,0 +1,163 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_notification_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2024-09-10 14:06+0000\n"
+"Last-Translator: mymage \n"
+"Language-Team: none\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.6.2\n"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_ids
+msgid "Activities"
+msgstr "Attività"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_exception_decoration
+msgid "Activity Exception Decoration"
+msgstr "Decorazione eccezione attività"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_state
+msgid "Activity State"
+msgstr "Stato attività"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_type_icon
+msgid "Activity Type Icon"
+msgstr "Icona tipo attività"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_activity_type_id
+msgid "Activity Type Used When Notify On Process Error"
+msgstr "Tipo attività utilizzata nelle notifiche di errori di processo"
+
+#. module: edi_notification_oca
+#: model:mail.activity.type,name:edi_notification_oca.mail_activity_failed_exchange_record_warning
+msgid "EDI Exchange Record: Failed"
+msgstr "Record scambio EDI: fallito"
+
+#. module: edi_notification_oca
+#: model:ir.model,name:edi_notification_oca.model_edi_exchange_type
+msgid "EDI Exchange Type"
+msgstr "Tipo scambio EDI"
+
+#. module: edi_notification_oca
+#: model:ir.model,name:edi_notification_oca.model_edi_exchange_record
+msgid "EDI exchange Record"
+msgstr "Record di scambio EDI"
+
+#. module: edi_notification_oca
+#. odoo-python
+#: code:addons/edi_notification_oca/components/listener.py:0
+#, python-format
+msgid "EDI: Process error on record '%(identifier)s'."
+msgstr "EDI: errore di processo nel record '%(identifier)s'."
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr "Icona Font Awesome es. fa-tasks"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_exception_icon
+msgid "Icon"
+msgstr "Icona"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr "Icona per indicare un'attività eccezione."
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_type__notify_on_process_error
+msgid ""
+"If an error happens on process, a notification will be sent to all selected "
+"users. If active, please select the specific groups and specific users in "
+"the 'Notifications' page."
+msgstr ""
+"Se si verifica un errore durante il processo, verrà inviata una notifica a "
+"tutti gli utenti selezionati. Se attiva, selezionare i gruppi e gli utenti "
+"specifici nella pagina \"Notifiche\"."
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr "Scadenza mia attività"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr "Scadenza prossima attività"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_summary
+msgid "Next Activity Summary"
+msgstr "Riepilogo prossima attività"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_type_id
+msgid "Next Activity Type"
+msgstr "Tipo prossima attività"
+
+#. module: edi_notification_oca
+#: model_terms:ir.ui.view,arch_db:edi_notification_oca.edi_exchange_type_view_form
+msgid "Notification"
+msgstr "Notifica"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_groups_ids
+msgid "Notify Groups On Process Error"
+msgstr "Avvisa i gruppi all'errore di processo"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error
+msgid "Notify On Process Error"
+msgstr "Avvisa all'errore di processo"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_users_ids
+msgid "Notify Users On Process Error"
+msgstr "Avvisa gli utenti all'errore di processo"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_user_id
+msgid "Responsible User"
+msgstr "Utente responsabile"
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_users_ids
+msgid ""
+"Select users to send notifications to. If 'Notification Groups' have been "
+"selected, notifications will also be sent to users selected in here."
+msgstr ""
+"Seleziona gli utenti a cui inviare le notifiche. Se sono stati selezionati "
+"'Gruppi di notifica', le notifiche saranno inviate anche agli utenti "
+"selezionati qui."
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr ""
+"Stato in base alle attività\n"
+"Scaduto: la data richiesta è trascorsa\n"
+"Oggi: la data attività è oggi\n"
+"Pianificato: attività future."
+
+#. module: edi_notification_oca
+#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr "Tipo di attività eccezione sul record."
diff --git a/edi_notification_oca/models/__init__.py b/edi_notification_oca/models/__init__.py
new file mode 100644
index 000000000..6128f744d
--- /dev/null
+++ b/edi_notification_oca/models/__init__.py
@@ -0,0 +1,2 @@
+from . import edi_exchange_type
+from . import edi_exchange_record
diff --git a/edi_notification_oca/models/edi_exchange_record.py b/edi_notification_oca/models/edi_exchange_record.py
new file mode 100644
index 000000000..e6b48bc68
--- /dev/null
+++ b/edi_notification_oca/models/edi_exchange_record.py
@@ -0,0 +1,9 @@
+# Copyright 2024 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo import models
+
+
+class EDIExchangeRecord(models.Model):
+ _name = "edi.exchange.record"
+ _inherit = ["edi.exchange.record", "mail.activity.mixin"]
diff --git a/edi_notification_oca/models/edi_exchange_type.py b/edi_notification_oca/models/edi_exchange_type.py
new file mode 100644
index 000000000..3f29d606e
--- /dev/null
+++ b/edi_notification_oca/models/edi_exchange_type.py
@@ -0,0 +1,52 @@
+# Copyright 2024 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo import api, fields, models
+
+
+class EDIExchangeType(models.Model):
+ _inherit = "edi.exchange.type"
+
+ notify_on_process_error = fields.Boolean(
+ help="If an error happens on process, a notification will be sent to all"
+ " selected users. If active, please select the specific groups and"
+ " specific users in the 'Notifications' page.",
+ default=False,
+ )
+ notify_on_process_error_groups_ids = fields.Many2many(
+ comodel_name="res.groups",
+ string="Notify Groups On Process Error",
+ inverse="_inverse_notify_on_process_error_groups_users",
+ )
+ notify_on_process_error_users_ids = fields.Many2many(
+ comodel_name="res.users",
+ string="Notify Users On Process Error",
+ inverse="_inverse_notify_on_process_error_groups_users",
+ help="Select users to send notifications to."
+ " If 'Notification Groups' have been selected, notifications will also be sent"
+ " to users selected in here.",
+ )
+ notify_on_process_error_activity_type_id = fields.Many2one(
+ "mail.activity.type",
+ string="Activity Type Used When Notify On Process Error",
+ default=lambda self: self._default_notify_on_process_error_activity_type_id(),
+ )
+
+ def _default_notify_on_process_error_activity_type_id(self):
+ return self.env.ref(
+ "edi_notification_oca.mail_activity_failed_exchange_record_warning", False
+ )
+
+ @api.onchange("notify_on_process_error")
+ def _onchange_notify_on_process_error(self):
+ if not self.notify_on_process_error:
+ self.notify_on_process_error_groups_ids = None
+ self.notify_on_process_error_users_ids = None
+
+ def _inverse_notify_on_process_error_groups_users(self):
+ for rec in self:
+ if (
+ rec.notify_on_process_error_groups_ids
+ or rec.notify_on_process_error_users_ids
+ ):
+ rec.notify_on_process_error = True
diff --git a/edi_notification_oca/pyproject.toml b/edi_notification_oca/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/edi_notification_oca/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/edi_notification_oca/readme/CONTRIBUTORS.md b/edi_notification_oca/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..289708028
--- /dev/null
+++ b/edi_notification_oca/readme/CONTRIBUTORS.md
@@ -0,0 +1,2 @@
+- Duong (Tran Quoc) \<\>
+- Simone Orsi \<\>
diff --git a/edi_notification_oca/readme/DESCRIPTION.md b/edi_notification_oca/readme/DESCRIPTION.md
new file mode 100644
index 000000000..3f250b798
--- /dev/null
+++ b/edi_notification_oca/readme/DESCRIPTION.md
@@ -0,0 +1,10 @@
+This module creates activities for users when an exchange record's process fails.
+
+Exchange types must be configured properly to create such activities:
+
+- field "Notify On Process Error" must be checked to activate the feature
+ for the current exchange type
+- field "Activity Type Used When Notify On Process Error" is used to define
+ the type of the newly created activity
+- fields "Notify Groups On Process Error" and "Notify Users On Process Error" are used
+ to define the users that will be assigned to the newly created activity
diff --git a/edi_notification_oca/static/description/icon.png b/edi_notification_oca/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/edi_notification_oca/static/description/icon.png differ
diff --git a/edi_notification_oca/static/description/index.html b/edi_notification_oca/static/description/index.html
new file mode 100644
index 000000000..cbf2538dc
--- /dev/null
+++ b/edi_notification_oca/static/description/index.html
@@ -0,0 +1,447 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+EDI Notification
+
+
+This module creates activities for users when an exchange record’s
+process fails.
+Exchange types must be configured properly to create such activities:
+
+- field “Notify On Process Error” must be checked to activate the
+feature for the current exchange type
+- field “Activity Type Used When Notify On Process Error” is used to
+define the type of the newly created activity
+- fields “Notify Groups On Process Error” and “Notify Users On Process
+Error” are used to define the users that will be assigned to the newly
+created activity
+
+
+Important
+This is an alpha version, the data model and design can change at any time without warning.
+Only for development or testing purpose, do not use in production.
+More details on development status
+
+Table of contents
+
+
+- Bug Tracker
+- Credits
+
+
+
+
+Bug Tracker
+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
+feedback.
+Do not contact contributors directly about support or help with technical issues.
+
+
+Credits
+
+Authors
+
+- Camptocamp
+
+
+
+Contributors
+
+- Duong (Tran Quoc) <duontq@troz.com>
+- Simone Orsi <simone.orsi@camptocamp.com>
+
+
+
+Maintainers
+This module is maintained by the OCA.
+
+
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+This module is part of the OCA/edi-framework project on GitHub.
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
+
diff --git a/edi_notification_oca/tests/__init__.py b/edi_notification_oca/tests/__init__.py
new file mode 100644
index 000000000..ab8b6e8bd
--- /dev/null
+++ b/edi_notification_oca/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_edi_notification
diff --git a/edi_notification_oca/tests/test_edi_notification.py b/edi_notification_oca/tests/test_edi_notification.py
new file mode 100644
index 000000000..cd9ee4f88
--- /dev/null
+++ b/edi_notification_oca/tests/test_edi_notification.py
@@ -0,0 +1,177 @@
+# Copyright 2024 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+
+import base64
+
+from odoo.tests.common import RecordCapturer
+
+from odoo.addons.edi_oca.tests.common import EDIBackendCommonComponentRegistryTestCase
+from odoo.addons.edi_oca.tests.fake_components import FakeInputProcess
+
+
+class TestEDINotification(EDIBackendCommonComponentRegistryTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls._setup_env()
+ cls._build_components(
+ cls,
+ FakeInputProcess,
+ )
+ cls._load_module_components(cls, "edi_notification_oca")
+ vals = {
+ "model": cls.partner._name,
+ "res_id": cls.partner.id,
+ "exchange_file": base64.b64encode(b"1234"),
+ }
+ cls.record = cls.backend.create_record("test_csv_input", vals)
+ cls.group_portal = cls.env.ref("base.group_portal")
+ cls.user_a = cls._create_user("A")
+ cls.user_b = cls._create_user("B")
+ cls.user_c = cls._create_user("C")
+
+ def setUp(self):
+ super().setUp()
+ FakeInputProcess.reset_faked()
+
+ @classmethod
+ def _create_user(cls, letter: str):
+ return (
+ cls.env["res.users"]
+ .with_context(no_reset_password=True)
+ .create(
+ {
+ "name": f"User {letter}",
+ "login": f"user_{letter}",
+ "groups_id": [(6, 0, [cls.group_portal.id])],
+ }
+ )
+ )
+
+ def test_inverse_notify_on_process_error(self):
+ self.exchange_type_in.notify_on_process_error = False
+ # If we forgot to enable notify_on_process_error
+ self.exchange_type_in.write(
+ {
+ "notify_on_process_error_groups_ids": [(6, 0, [self.group_portal.id])],
+ "notify_on_process_error_users_ids": [(6, 0, [self.user_c.id])],
+ }
+ )
+ # Make sure notify_on_process_error should be enabled
+ self.assertTrue(self.exchange_type_in.notify_on_process_error)
+
+ def test_dont_notify_on_process_error(self):
+ self.exchange_type_in.notify_on_process_error = False
+ self.record.write({"edi_exchange_state": "input_received"})
+ self.record._set_file_content("TEST %d" % self.record.id)
+ with RecordCapturer(self.env["mail.activity"], []) as capture:
+ self.record.with_context(
+ test_break_process="OOPS! Something went wrong :("
+ ).action_exchange_process()
+ self.assertRecordValues(
+ self.record,
+ [
+ {
+ "edi_exchange_state": "input_processed_error",
+ }
+ ],
+ )
+ self.assertIn("OOPS! Something went wrong :(", self.record.exchange_error)
+ # We don't expect any notification
+ self.assertEqual(len(capture.records), 0)
+
+ def test_notify_on_process_error_to_group(self):
+ self.exchange_type_in.write(
+ {
+ "notify_on_process_error": True,
+ "notify_on_process_error_groups_ids": [(6, 0, [self.group_portal.id])],
+ }
+ )
+ # Remove group on user C to test
+ self.user_c.groups_id = None
+ self.record.write({"edi_exchange_state": "input_received"})
+ self.record._set_file_content("TEST %d" % self.record.id)
+ with RecordCapturer(self.env["mail.activity"], []) as capture:
+ self.record.with_context(
+ test_break_process="OOPS! Something went wrong :("
+ ).action_exchange_process()
+ # Send notification to all users in defined groups when error
+ a_noti = capture.records.filtered(lambda x: x.user_id == self.user_a)
+ self.assertEqual(len(a_noti), 1)
+ self.assertEqual(
+ a_noti.summary,
+ f"EDI: Process error on record '{self.record.identifier}'.",
+ )
+ self.assertIn(
+ "OOPS! Something went wrong :(",
+ a_noti.note,
+ )
+ b_noti = capture.records.filtered(lambda x: x.user_id == self.user_b)
+ self.assertEqual(len(a_noti), 1)
+ self.assertEqual(
+ b_noti.summary,
+ f"EDI: Process error on record '{self.record.identifier}'.",
+ )
+ self.assertIn(
+ "OOPS! Something went wrong :(",
+ b_noti.note,
+ )
+ # We don't send notification to user C
+ # because C is not belonging to the group_portal
+ c_noti = capture.records.filtered(lambda x: x.user_id == self.user_c)
+ self.assertEqual(len(c_noti), 0)
+
+ def test_notify_on_process_error_to_users(self):
+ self.exchange_type_in.write(
+ {
+ "notify_on_process_error": True,
+ "notify_on_process_error_users_ids": [(6, 0, [self.user_c.id])],
+ }
+ )
+ self.record.write({"edi_exchange_state": "input_received"})
+ self.record._set_file_content("TEST %d" % self.record.id)
+ with RecordCapturer(self.env["mail.activity"], []) as capture:
+ self.record.with_context(
+ test_break_process="OOPS! Something went wrong :("
+ ).action_exchange_process()
+ # Send notification to all users in defined users when error
+ a_b_noti = capture.records.filtered(
+ lambda x: x.user_id in (self.user_a | self.user_b)
+ )
+ self.assertEqual(len(a_b_noti), 0)
+ c_noti = capture.records.filtered(lambda x: x.user_id == self.user_c)
+ self.assertEqual(len(c_noti), 1)
+ self.assertEqual(
+ c_noti.summary,
+ f"EDI: Process error on record '{self.record.identifier}'.",
+ )
+ self.assertIn(
+ "OOPS! Something went wrong :(",
+ c_noti.note,
+ )
+
+ def test_notify_on_process_error_to_groups_and_users(self):
+ self.exchange_type_in.write(
+ {
+ "notify_on_process_error": True,
+ "notify_on_process_error_groups_ids": [(6, 0, [self.group_portal.id])],
+ "notify_on_process_error_users_ids": [(6, 0, [self.user_c.id])],
+ }
+ )
+ # Remove group on user C to test
+ self.user_c.groups_id = None
+ self.record.write({"edi_exchange_state": "input_received"})
+ self.record._set_file_content("TEST %d" % self.record.id)
+ with RecordCapturer(self.env["mail.activity"], []) as capture:
+ self.record.with_context(
+ test_break_process="OOPS! Something went wrong :("
+ ).action_exchange_process()
+ # Send notification to all users in defined users when error
+ a_b_noti = capture.records.filtered(
+ lambda x: x.user_id in (self.user_a | self.user_b)
+ )
+ self.assertEqual(len(a_b_noti), 2)
+ # also send notification to user C
+ c_noti = capture.records.filtered(lambda x: x.user_id == self.user_c)
+ self.assertEqual(len(c_noti), 1)
diff --git a/edi_notification_oca/views/edi_exchange_type.xml b/edi_notification_oca/views/edi_exchange_type.xml
new file mode 100644
index 000000000..e052b6eb5
--- /dev/null
+++ b/edi_notification_oca/views/edi_exchange_type.xml
@@ -0,0 +1,36 @@
+
+
+
+ edi.exchange.type
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_oca/i18n/edi_oca.pot b/edi_oca/i18n/edi_oca.pot
index bd4283165..c7a592901 100644
--- a/edi_oca/i18n/edi_oca.pot
+++ b/edi_oca/i18n/edi_oca.pot
@@ -89,6 +89,11 @@ msgid ""
"* Custom: let devs handle a custom behavior with specific developments\n"
msgstr ""
+#. module: edi_oca
+#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_type__exchange_record_count
+msgid "# Exchange Records"
+msgstr ""
+
#. module: edi_oca
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_consumer_mixin_buttons
msgid " EDI actions"
@@ -168,10 +173,12 @@ msgstr ""
#: model:ir.model.fields,field_description:edi_oca.field_edi_backend__active
#: model:ir.model.fields,field_description:edi_oca.field_edi_configuration__active
#: model:ir.model.fields,field_description:edi_oca.field_edi_configuration_trigger__active
+#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_record__active
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_type__active
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_type_rule__active
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_configuration_view_search
+#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_view_search
msgid "Active"
@@ -210,11 +217,17 @@ msgstr ""
msgid "Apply to this model"
msgstr ""
+#. module: edi_oca
+#: model:ir.actions.server,name:edi_oca.ir_cron_archive_old_edi_records_ir_actions_server
+msgid "Archive Old EDI Exchange Records"
+msgstr ""
+
#. module: edi_oca
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_configuration_trigger_view_form
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_configuration_view_form
+#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_form
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_view_form
@@ -234,6 +247,30 @@ msgid ""
"will take care of generating the output when not set yet. "
msgstr ""
+#. module: edi_oca
+#: model:ir.model.fields,field_description:edi_oca.field_edi_backend__auto_archive_records_after_days
+msgid "Auto-archive records after (days)"
+msgstr ""
+
+#. module: edi_oca
+#: model:ir.model.fields,field_description:edi_oca.field_edi_backend__auto_delete_records_after_days
+msgid "Auto-delete archived records after (days)"
+msgstr ""
+
+#. module: edi_oca
+#: model:ir.model.fields,help:edi_oca.field_edi_backend__auto_archive_records_after_days
+msgid ""
+"Automatically archive EDI exchange records after X days. Set to <= 0 to "
+"disable auto-archiving."
+msgstr ""
+
+#. module: edi_oca
+#: model:ir.model.fields,help:edi_oca.field_edi_backend__auto_delete_records_after_days
+msgid ""
+"Automatically delete archived EDI exchange records after X days. Set to <= 0"
+" to disable auto-deletion."
+msgstr ""
+
#. module: edi_oca
#: model:ir.model.fields,field_description:edi_oca.field_edi_configuration__backend_id
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_record__backend_id
@@ -366,6 +403,11 @@ msgstr ""
msgid "Decoding Error Handler"
msgstr ""
+#. module: edi_oca
+#: model:ir.actions.server,name:edi_oca.ir_cron_delete_old_archived_edi_records_ir_actions_server
+msgid "Delete Old Archived EDI Exchange Records"
+msgstr ""
+
#. module: edi_oca
#: model:ir.model.fields,help:edi_oca.field_edi_configuration__description
#: model:ir.model.fields,help:edi_oca.field_edi_configuration_trigger__description
@@ -673,6 +715,7 @@ msgstr ""
#. module: edi_oca
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_consumer_mixin__exchange_record_ids
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_related_record__exchange_record_id
+#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_type__exchange_record_ids
msgid "Exchange Record"
msgstr ""
@@ -748,6 +791,7 @@ msgstr ""
#: model:ir.actions.act_window,name:edi_oca.act_open_edi_exchange_record_view
#: model:ir.ui.menu,name:edi_oca.menu_edi_exchange_record_root
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form
+#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_view_form
msgid "Exchanges"
msgstr ""
@@ -845,6 +889,11 @@ msgstr ""
msgid "Generator"
msgstr ""
+#. module: edi_oca
+#: model:ir.model.fields,field_description:edi_oca.field_edi_configuration__is_global
+msgid "Global Configuration"
+msgstr ""
+
#. module: edi_oca
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_search
@@ -909,6 +958,13 @@ msgstr ""
msgid "If checked, some messages have a delivery error."
msgstr ""
+#. module: edi_oca
+#: model:ir.model.fields,help:edi_oca.field_edi_configuration__is_global
+msgid ""
+"If checked, this configuration will be executed for all records, regardless "
+"of the partner relation."
+msgstr ""
+
#. module: edi_oca
#: model:ir.model.fields,help:edi_oca.field_edi_exchange_type__exchange_filename_sequence_id
msgid ""
@@ -1209,6 +1265,11 @@ msgstr ""
msgid "Record"
msgstr ""
+#. module: edi_oca
+#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form
+msgid "Records retention"
+msgstr ""
+
#. module: edi_oca
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_form
msgid "Regenerate"
@@ -1453,7 +1514,8 @@ msgstr ""
#: model:ir.model.fields,help:edi_oca.field_edi_exchange_type__quick_exec
msgid ""
"When active, records of this type will be processed immediately without "
-"waiting for the cron to pass by."
+"waiting for the cron to pass by. Requires auto generate flag to be active as"
+" well. The cron will skip these records unless forced."
msgstr ""
#. module: edi_oca
diff --git a/edi_oca/i18n/it.po b/edi_oca/i18n/it.po
index 9f91ca259..eb1ee4d26 100644
--- a/edi_oca/i18n/it.po
+++ b/edi_oca/i18n/it.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2025-12-02 15:42+0000\n"
+"PO-Revision-Date: 2026-05-29 13:46+0000\n"
"Last-Translator: mymage \n"
"Language-Team: none\n"
"Language: it\n"
@@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 5.10.4\n"
+"X-Generator: Weblate 5.15.2\n"
#. module: edi_oca
#: model:ir.model.fields,help:edi_oca.field_edi_exchange_type__advanced_settings_edit
@@ -153,6 +153,11 @@ msgstr ""
"* Personalizzato: consente agli sviluppatori di gestire un comportamento "
"personalizzato con sviluppo personalizzato\n"
+#. module: edi_oca
+#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_type__exchange_record_count
+msgid "# Exchange Records"
+msgstr "N° record scambio"
+
#. module: edi_oca
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_consumer_mixin_buttons
msgid " EDI actions"
@@ -234,10 +239,12 @@ msgstr "Azione richiesta"
#: model:ir.model.fields,field_description:edi_oca.field_edi_backend__active
#: model:ir.model.fields,field_description:edi_oca.field_edi_configuration__active
#: model:ir.model.fields,field_description:edi_oca.field_edi_configuration_trigger__active
+#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_record__active
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_type__active
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_type_rule__active
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_configuration_view_search
+#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_view_search
msgid "Active"
@@ -276,11 +283,17 @@ msgstr "Consentire file vuoti"
msgid "Apply to this model"
msgstr "Applica a questo modello"
+#. module: edi_oca
+#: model:ir.actions.server,name:edi_oca.ir_cron_archive_old_edi_records_ir_actions_server
+msgid "Archive Old EDI Exchange Records"
+msgstr "Archivia i vecchi record EDI"
+
#. module: edi_oca
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_configuration_trigger_view_form
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_configuration_view_form
+#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_form
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_view_form
@@ -303,6 +316,34 @@ msgstr ""
"contenuto. Se attiva, un cron gestirà la generazione dell'output quando non "
"ancora impostato. "
+#. module: edi_oca
+#: model:ir.model.fields,field_description:edi_oca.field_edi_backend__auto_archive_records_after_days
+msgid "Auto-archive records after (days)"
+msgstr "Auto archivia i record dopo (giorni)"
+
+#. module: edi_oca
+#: model:ir.model.fields,field_description:edi_oca.field_edi_backend__auto_delete_records_after_days
+msgid "Auto-delete archived records after (days)"
+msgstr "Auto cancella i record archiviati dopo (giorni)"
+
+#. module: edi_oca
+#: model:ir.model.fields,help:edi_oca.field_edi_backend__auto_archive_records_after_days
+msgid ""
+"Automatically archive EDI exchange records after X days. Set to <= 0 to "
+"disable auto-archiving."
+msgstr ""
+"Archivia automaticamente i record di scambio EDI dopo X giorni. Impostare "
+"inferiore o uguale a 0 per disabilitare l'auto archiviazione."
+
+#. module: edi_oca
+#: model:ir.model.fields,help:edi_oca.field_edi_backend__auto_delete_records_after_days
+msgid ""
+"Automatically delete archived EDI exchange records after X days. Set to <= 0 "
+"to disable auto-deletion."
+msgstr ""
+"Cancella automaticamente i record di scambio EDI archiviati dopo X giorni. "
+"Impostare inferiore o uguale a 0 per disabilitare l'auto cancellazione."
+
#. module: edi_oca
#: model:ir.model.fields,field_description:edi_oca.field_edi_configuration__backend_id
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_record__backend_id
@@ -435,6 +476,11 @@ msgstr "Personalizzato"
msgid "Decoding Error Handler"
msgstr "Gestore errore decodifica"
+#. module: edi_oca
+#: model:ir.actions.server,name:edi_oca.ir_cron_delete_old_archived_edi_records_ir_actions_server
+msgid "Delete Old Archived EDI Exchange Records"
+msgstr "Cancella i vecchi record di scambio EDI archiviati"
+
#. module: edi_oca
#: model:ir.model.fields,help:edi_oca.field_edi_configuration__description
#: model:ir.model.fields,help:edi_oca.field_edi_configuration_trigger__description
@@ -745,6 +791,7 @@ msgstr "Sequenza nome file scambio"
#. module: edi_oca
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_consumer_mixin__exchange_record_ids
#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_related_record__exchange_record_id
+#: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_type__exchange_record_ids
msgid "Exchange Record"
msgstr "Record di scambio"
@@ -820,6 +867,7 @@ msgstr "Scambiato oggi"
#: model:ir.actions.act_window,name:edi_oca.act_open_edi_exchange_record_view
#: model:ir.ui.menu,name:edi_oca.menu_edi_exchange_record_root
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form
+#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_view_form
msgid "Exchanges"
msgstr "Scambi"
@@ -929,6 +977,11 @@ msgstr "Genera record di scambio"
msgid "Generator"
msgstr "Generatore"
+#. module: edi_oca
+#: model:ir.model.fields,field_description:edi_oca.field_edi_configuration__is_global
+msgid "Global Configuration"
+msgstr "Configurazione globale"
+
#. module: edi_oca
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_search
@@ -1000,6 +1053,15 @@ msgstr "Se selezionata, nuovi messaggi richiedono attenzione."
msgid "If checked, some messages have a delivery error."
msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna."
+#. module: edi_oca
+#: model:ir.model.fields,help:edi_oca.field_edi_configuration__is_global
+msgid ""
+"If checked, this configuration will be executed for all records, regardless "
+"of the partner relation."
+msgstr ""
+"Se selezionata, questa configurazione verrà eseguita per tutti i record, "
+"indipendentemente dalla relazione con il partner."
+
#. module: edi_oca
#: model:ir.model.fields,help:edi_oca.field_edi_exchange_type__exchange_filename_sequence_id
msgid ""
@@ -1304,6 +1366,11 @@ msgstr "Scambi recenti"
msgid "Record"
msgstr "Record"
+#. module: edi_oca
+#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form
+msgid "Records retention"
+msgstr "Cancellazione record"
+
#. module: edi_oca
#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_form
msgid "Regenerate"
@@ -1560,10 +1627,13 @@ msgstr "Cronologia comunicazioni sito web"
#: model:ir.model.fields,help:edi_oca.field_edi_exchange_type__quick_exec
msgid ""
"When active, records of this type will be processed immediately without "
-"waiting for the cron to pass by."
+"waiting for the cron to pass by. Requires auto generate flag to be active as "
+"well. The cron will skip these records unless forced."
msgstr ""
-"Quando attiva, i record di questo tipo verranno elaborati immediatamente "
-"senza attendere il cron."
+"Quando attivi, i record di questo tipo verranno elaborati immediatamente, "
+"senza attendere il completamento del cron. Richiede che anche il flag di "
+"generazione automatica sia attivo. Il cron ignorerà questi record, a meno "
+"che non venga forzato."
#. module: edi_oca
#: model:ir.model.fields,help:edi_oca.field_edi_exchange_consumer_mixin__edi_disable_auto
@@ -1585,6 +1655,13 @@ msgstr ""
msgid "error on send"
msgstr "errore nell'invio"
+#~ msgid ""
+#~ "When active, records of this type will be processed immediately without "
+#~ "waiting for the cron to pass by."
+#~ msgstr ""
+#~ "Quando attiva, i record di questo tipo verranno elaborati immediatamente "
+#~ "senza attendere il cron."
+
#~ msgid ""
#~ "For output exchange types this should be a formatting string with the "
#~ "following variables available (to be used between brackets, `{}`): "
diff --git a/edi_product_oca/README.rst b/edi_product_oca/README.rst
new file mode 100644
index 000000000..2d11a2c73
--- /dev/null
+++ b/edi_product_oca/README.rst
@@ -0,0 +1,82 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
+===========
+EDI Product
+===========
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:a5d2a242159e8137d9d6b6f8e2a694c0ffe6721d91b3759ddf20bdef54ad0eb6
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi--framework-lightgray.png?logo=github
+ :target: https://github.com/OCA/edi-framework/tree/18.0/edi_product_oca
+ :alt: OCA/edi-framework
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_product_oca
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=18.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+Provide basic configuration for products and product packaging with EDI
+framework.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Bug Tracker
+===========
+
+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
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* ForgeFlow
+
+Contributors
+------------
+
+- Oriol Miranda
+- Duong (Tran Quoc)
+
+Maintainers
+-----------
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/edi-framework `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/edi_product_oca/__init__.py b/edi_product_oca/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/edi_product_oca/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/edi_product_oca/__manifest__.py b/edi_product_oca/__manifest__.py
new file mode 100644
index 000000000..50fbc6291
--- /dev/null
+++ b/edi_product_oca/__manifest__.py
@@ -0,0 +1,21 @@
+# Copyright 2023 ForgeFlow S.L. (http://www.forgeflow.com)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "EDI Product",
+ "summary": """
+ EDI framework configuration and base logic
+ for products and products packaging""",
+ "version": "18.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "ForgeFlow, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/edi-framework",
+ "depends": [
+ "product",
+ "edi_endpoint_oca",
+ ],
+ "data": [
+ "views/product_views.xml",
+ "views/product_packaging_views.xml",
+ ],
+}
diff --git a/edi_product_oca/i18n/de.po b/edi_product_oca/i18n/de.po
new file mode 100644
index 000000000..35746c207
--- /dev/null
+++ b/edi_product_oca/i18n/de.po
@@ -0,0 +1,115 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_product_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2025-09-26 15:44+0000\n"
+"Last-Translator: davidbeckercbl \n"
+"Language-Team: none\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.10.4\n"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_disable_auto
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_disable_auto
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_disable_auto
+msgid "Disable auto"
+msgstr "Automatik deaktivieren"
+
+#. module: edi_product_oca
+#: model_terms:ir.ui.view,arch_db:edi_product_oca.product_packaging_form_view
+#: model_terms:ir.ui.view,arch_db:edi_product_oca.product_template_form_view
+msgid "EDI"
+msgstr "EDI"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_edi_endpoint_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_edi_endpoint_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_edi_endpoint_id
+msgid "EDI origin endpoint"
+msgstr "EDI-Ursprungs-Endpunkt"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_exchange_type_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_exchange_type_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_exchange_type_id
+msgid "EDI origin exchange type"
+msgstr "EDI-Ursprungs-Austauschtyp"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_exchange_record_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_exchange_record_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_exchange_record_id
+msgid "EDI origin record"
+msgstr "EDI-Ursprungsdatensatz"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,help:edi_product_oca.field_product_packaging__origin_exchange_record_id
+#: model:ir.model.fields,help:edi_product_oca.field_product_product__origin_exchange_record_id
+#: model:ir.model.fields,help:edi_product_oca.field_product_template__origin_exchange_record_id
+msgid "EDI record that originated this document."
+msgstr "EDI Datensatz, aus dem dieses Dokument erstellt wurde."
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_config
+msgid "Edi Config"
+msgstr "EDI Konfiguration"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_has_form_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_has_form_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_has_form_config
+msgid "Edi Has Form Config"
+msgstr "EDI hat Formular-Konfiguration"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_record_ids
+msgid "Exchange Record"
+msgstr "Austauschdatensatz"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_record_count
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_record_count
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_record_count
+msgid "Exchange Record Count"
+msgstr "Anzahl Austauschdatensätze"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_related_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_related_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_related_record_ids
+msgid "Exchange Related Record"
+msgstr "Zugehöriger Austauschdatensatz"
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_template
+msgid "Product"
+msgstr "Produkt"
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_packaging
+msgid "Product Packaging"
+msgstr "Produktverpackung"
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_product
+msgid "Product Variant"
+msgstr "Produktvariante"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,help:edi_product_oca.field_product_packaging__edi_disable_auto
+#: model:ir.model.fields,help:edi_product_oca.field_product_product__edi_disable_auto
+#: model:ir.model.fields,help:edi_product_oca.field_product_template__edi_disable_auto
+msgid "When marked, EDI automatic processing will be avoided"
+msgstr "Wenn markiert, wird die automatische EDI-Verarbeitung vermieden"
diff --git a/edi_product_oca/i18n/edi_product_oca.pot b/edi_product_oca/i18n/edi_product_oca.pot
new file mode 100644
index 000000000..d1e8fd111
--- /dev/null
+++ b/edi_product_oca/i18n/edi_product_oca.pot
@@ -0,0 +1,119 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_product_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_disable_auto
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_disable_auto
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_disable_auto
+msgid "Disable auto"
+msgstr ""
+
+#. module: edi_product_oca
+#: model_terms:ir.ui.view,arch_db:edi_product_oca.product_packaging_form_view
+#: model_terms:ir.ui.view,arch_db:edi_product_oca.product_template_form_view
+msgid "EDI"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_edi_endpoint_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_edi_endpoint_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_edi_endpoint_id
+msgid "EDI origin endpoint"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_exchange_type_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_exchange_type_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_exchange_type_id
+msgid "EDI origin exchange type"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_exchange_record_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_exchange_record_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_exchange_record_id
+msgid "EDI origin record"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,help:edi_product_oca.field_product_packaging__origin_exchange_record_id
+#: model:ir.model.fields,help:edi_product_oca.field_product_product__origin_exchange_record_id
+#: model:ir.model.fields,help:edi_product_oca.field_product_template__origin_exchange_record_id
+msgid "EDI record that originated this document."
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_config
+msgid "Edi Config"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_has_form_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_has_form_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_has_form_config
+msgid "Edi Has Form Config"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_record_ids
+msgid "Exchange Record"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_record_count
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_record_count
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_record_count
+msgid "Exchange Record Count"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_related_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_related_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_related_record_ids
+msgid "Exchange Related Record"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_template
+msgid "Product"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_packaging
+msgid "Product Packaging"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_product
+msgid "Product Variant"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,help:edi_product_oca.field_product_packaging__origin_edi_endpoint_id
+#: model:ir.model.fields,help:edi_product_oca.field_product_product__origin_edi_endpoint_id
+#: model:ir.model.fields,help:edi_product_oca.field_product_template__origin_edi_endpoint_id
+msgid "Record generated via this endpoint"
+msgstr ""
+
+#. module: edi_product_oca
+#: model:ir.model.fields,help:edi_product_oca.field_product_packaging__edi_disable_auto
+#: model:ir.model.fields,help:edi_product_oca.field_product_product__edi_disable_auto
+#: model:ir.model.fields,help:edi_product_oca.field_product_template__edi_disable_auto
+msgid "When marked, EDI automatic processing will be avoided"
+msgstr ""
diff --git a/edi_product_oca/i18n/it.po b/edi_product_oca/i18n/it.po
new file mode 100644
index 000000000..8ef58843a
--- /dev/null
+++ b/edi_product_oca/i18n/it.po
@@ -0,0 +1,118 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_product_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2025-08-04 14:25+0000\n"
+"Last-Translator: mymage \n"
+"Language-Team: none\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.10.4\n"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_disable_auto
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_disable_auto
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_disable_auto
+msgid "Disable auto"
+msgstr "Disabilita automatico"
+
+#. module: edi_product_oca
+#: model_terms:ir.ui.view,arch_db:edi_product_oca.product_packaging_form_view
+#: model_terms:ir.ui.view,arch_db:edi_product_oca.product_template_form_view
+msgid "EDI"
+msgstr "EDI"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_edi_endpoint_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_edi_endpoint_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_edi_endpoint_id
+msgid "EDI origin endpoint"
+msgstr "Endpoint origine EDI"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_exchange_type_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_exchange_type_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_exchange_type_id
+msgid "EDI origin exchange type"
+msgstr "Tipo scambio origine EDI"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__origin_exchange_record_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__origin_exchange_record_id
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__origin_exchange_record_id
+msgid "EDI origin record"
+msgstr "Record origine EDI"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,help:edi_product_oca.field_product_packaging__origin_exchange_record_id
+#: model:ir.model.fields,help:edi_product_oca.field_product_product__origin_exchange_record_id
+#: model:ir.model.fields,help:edi_product_oca.field_product_template__origin_exchange_record_id
+msgid "EDI record that originated this document."
+msgstr "Record EDI che ha generato questo documento."
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_config
+msgid "Edi Config"
+msgstr "Configurazione EDI"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__edi_has_form_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__edi_has_form_config
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__edi_has_form_config
+msgid "Edi Has Form Config"
+msgstr "EDI ha una maschera di configurazione"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_record_ids
+msgid "Exchange Record"
+msgstr "Record di scambio"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_record_count
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_record_count
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_record_count
+msgid "Exchange Record Count"
+msgstr "Conteggio record di scambio"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_packaging__exchange_related_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_product__exchange_related_record_ids
+#: model:ir.model.fields,field_description:edi_product_oca.field_product_template__exchange_related_record_ids
+msgid "Exchange Related Record"
+msgstr "Record relativo allo scambio"
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_template
+msgid "Product"
+msgstr "Prodotto"
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_packaging
+msgid "Product Packaging"
+msgstr "Imballaggio prodotto"
+
+#. module: edi_product_oca
+#: model:ir.model,name:edi_product_oca.model_product_product
+msgid "Product Variant"
+msgstr "Variante prodotto"
+
+#. module: edi_product_oca
+#: model:ir.model.fields,help:edi_product_oca.field_product_packaging__edi_disable_auto
+#: model:ir.model.fields,help:edi_product_oca.field_product_product__edi_disable_auto
+#: model:ir.model.fields,help:edi_product_oca.field_product_template__edi_disable_auto
+msgid "When marked, EDI automatic processing will be avoided"
+msgstr "Quando selezionata, l'elaborazione EDI automatica verrà evitata"
+
+#~ msgid "Record generated via this endpoint"
+#~ msgstr "Record generato attraverso questo endpoint"
diff --git a/edi_product_oca/models/__init__.py b/edi_product_oca/models/__init__.py
new file mode 100644
index 000000000..e9bc8fca4
--- /dev/null
+++ b/edi_product_oca/models/__init__.py
@@ -0,0 +1,3 @@
+from . import product_product
+from . import product_template
+from . import product_packaging
diff --git a/edi_product_oca/models/product_packaging.py b/edi_product_oca/models/product_packaging.py
new file mode 100644
index 000000000..8dbccec84
--- /dev/null
+++ b/edi_product_oca/models/product_packaging.py
@@ -0,0 +1,9 @@
+# Copyright 2024 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import models
+
+
+class ProductPackaging(models.Model):
+ _name = "product.packaging"
+ _inherit = ["product.packaging", "edi.exchange.consumer.mixin"]
diff --git a/edi_product_oca/models/product_product.py b/edi_product_oca/models/product_product.py
new file mode 100644
index 000000000..a411f920b
--- /dev/null
+++ b/edi_product_oca/models/product_product.py
@@ -0,0 +1,9 @@
+# Copyright 2023 ForgeFlow S.L. (http://www.forgeflow.com)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import models
+
+
+class ProductProduct(models.Model):
+ _name = "product.product"
+ _inherit = ["product.product", "edi.exchange.consumer.mixin"]
diff --git a/edi_product_oca/models/product_template.py b/edi_product_oca/models/product_template.py
new file mode 100644
index 000000000..ade61d91f
--- /dev/null
+++ b/edi_product_oca/models/product_template.py
@@ -0,0 +1,9 @@
+# Copyright 2023 ForgeFlow S.L. (http://www.forgeflow.com)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import models
+
+
+class ProductTemplate(models.Model):
+ _name = "product.template"
+ _inherit = ["product.template", "edi.exchange.consumer.mixin"]
diff --git a/edi_product_oca/pyproject.toml b/edi_product_oca/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/edi_product_oca/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/edi_product_oca/readme/CONTRIBUTORS.md b/edi_product_oca/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..76cf72b67
--- /dev/null
+++ b/edi_product_oca/readme/CONTRIBUTORS.md
@@ -0,0 +1,2 @@
+- Oriol Miranda \
+- Duong (Tran Quoc) \
diff --git a/edi_product_oca/readme/DESCRIPTION.md b/edi_product_oca/readme/DESCRIPTION.md
new file mode 100644
index 000000000..06918307c
--- /dev/null
+++ b/edi_product_oca/readme/DESCRIPTION.md
@@ -0,0 +1,2 @@
+Provide basic configuration for products and product packaging with EDI
+framework.
diff --git a/edi_product_oca/static/description/icon.png b/edi_product_oca/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/edi_product_oca/static/description/icon.png differ
diff --git a/edi_product_oca/static/description/index.html b/edi_product_oca/static/description/index.html
new file mode 100644
index 000000000..280bab5c4
--- /dev/null
+++ b/edi_product_oca/static/description/index.html
@@ -0,0 +1,431 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+EDI Product
+
+
+Provide basic configuration for products and product packaging with EDI
+framework.
+Table of contents
+
+
+- Bug Tracker
+- Credits
+
+
+
+
+Bug Tracker
+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
+feedback.
+Do not contact contributors directly about support or help with technical issues.
+
+
+Credits
+
+Authors
+
+- ForgeFlow
+
+
+
+Contributors
+
+- Oriol Miranda <oriol.miranda@forgeflow.com>
+- Duong (Tran Quoc) <duongtq@trobz.com>
+
+
+
+Maintainers
+This module is maintained by the OCA.
+
+
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+This module is part of the OCA/edi-framework project on GitHub.
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
+
diff --git a/edi_product_oca/views/product_packaging_views.xml b/edi_product_oca/views/product_packaging_views.xml
new file mode 100644
index 000000000..c6f4c4bc5
--- /dev/null
+++ b/edi_product_oca/views/product_packaging_views.xml
@@ -0,0 +1,34 @@
+
+
+
+ product.packaging.form.view - edi_product_oca
+ product.packaging
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_product_oca/views/product_views.xml b/edi_product_oca/views/product_views.xml
new file mode 100644
index 000000000..8047201c7
--- /dev/null
+++ b/edi_product_oca/views/product_views.xml
@@ -0,0 +1,38 @@
+
+
+
+ product.template.form.view - edi_product_oca
+ product.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_diapar_oca/README.rst b/edi_purchase_diapar_oca/README.rst
new file mode 100644
index 000000000..7dd81cab7
--- /dev/null
+++ b/edi_purchase_diapar_oca/README.rst
@@ -0,0 +1,26 @@
+.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+
+======================
+EDI Purchase DIAPAR
+======================
+
+Features
+--------
+
+This module contains all specific and necessary conditions and treatments to DIAPAR relationship.
+
+
+Credits
+=======
+
+Yassine TEIMI
+Simon Mas
+
+Funders
+-------
+
+The development of this module has been financially supported by:
+
+* La Louve (https://cooplalouve.fr/)
diff --git a/edi_purchase_diapar_oca/__init__.py b/edi_purchase_diapar_oca/__init__.py
new file mode 100644
index 000000000..3d5be67ea
--- /dev/null
+++ b/edi_purchase_diapar_oca/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (C) 2016-Today: Druidoo ( )
+# @author: Druidoo
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html
+
+from . import models
+from . import components
diff --git a/edi_purchase_diapar_oca/__manifest__.py b/edi_purchase_diapar_oca/__manifest__.py
new file mode 100644
index 000000000..b1651e333
--- /dev/null
+++ b/edi_purchase_diapar_oca/__manifest__.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2016-Today: Druidoo ( )
+# @author: Druidoo
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html
+
+{
+ "name": "EDI Purchase DIAPAR",
+ "version": "18.0.1.0.0",
+ "category": "Custom",
+ "author": "Druidoo, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/edi-framework",
+ "license": "AGPL-3",
+ "depends": [
+ "product",
+ "purchase_stock",
+ "edi_storage_oca",
+ "edi_purchase_oca",
+ "edi_exchange_template_oca",
+ ],
+ "data": [
+ "security/ir.model.access.csv",
+ "views/supplier_price_list_views.xml",
+ "views/picking_update_views.xml",
+ "views/product_template_views.xml",
+ "views/res_partner_views.xml",
+ "views/exchange_type_views.xml",
+ "views/edi_exchange_template_output.xml",
+ "views/res_config_settings_views.xml",
+ "views/menus.xml",
+ "templates/exchange_template_output_diapar.xml",
+ ],
+ "demo": [
+ "demo/fs_storage_demo.xml",
+ "demo/edi_backend_type_demo.xml",
+ "demo/edi_exchange_template_output_demo.xml",
+ "demo/edi_exchange_type_demo.xml",
+ "demo/edi_field_mapping_demo.xml",
+ "demo/edi_configuration_demo.xml",
+ ],
+}
diff --git a/edi_purchase_diapar_oca/components/__init__.py b/edi_purchase_diapar_oca/components/__init__.py
new file mode 100644
index 000000000..b871c10f8
--- /dev/null
+++ b/edi_purchase_diapar_oca/components/__init__.py
@@ -0,0 +1,3 @@
+from . import input_process_ble
+from . import input_process_ch
+from . import generate_diapar_output
diff --git a/edi_purchase_diapar_oca/components/generate_diapar_output.py b/edi_purchase_diapar_oca/components/generate_diapar_output.py
new file mode 100644
index 000000000..6256a3489
--- /dev/null
+++ b/edi_purchase_diapar_oca/components/generate_diapar_output.py
@@ -0,0 +1,30 @@
+from odoo import models
+
+from odoo.addons.edi_core_oca.exceptions import EDINotImplementedError
+
+
+class EdiOcaHandlerGenerate(models.AbstractModel):
+ _name = "edi.output.diapar.handler"
+ _inherit = [
+ "edi.oca.handler.generate",
+ ]
+ _description = "EDI Handler Generate Output For Diapar"
+
+ def generate(self, exchange_record):
+ exchange_record = self.env["edi.exchange.record"].browse(exchange_record.id)
+ tmpl = exchange_record.backend_id._get_output_template(exchange_record)
+ if tmpl:
+ exchange_record = exchange_record.with_context(
+ edi_framework_action="generate"
+ )
+ tmpl = tmpl.with_context(edi_framework_action="generate")
+ if exchange_record.model == "purchase.order" and exchange_record.res_id:
+ order = self.env["purchase.order"].browse(exchange_record.res_id)
+ if order:
+ return order.generate_template_output_diapar(
+ tmpl,
+ exchange_record,
+ )
+ raise EDINotImplementedError(
+ self.env._("Only purchase order with Diapar output template are supported.")
+ )
diff --git a/edi_purchase_diapar_oca/components/input_process_ble.py b/edi_purchase_diapar_oca/components/input_process_ble.py
new file mode 100644
index 000000000..ec51a3a99
--- /dev/null
+++ b/edi_purchase_diapar_oca/components/input_process_ble.py
@@ -0,0 +1,192 @@
+import logging
+
+from odoo import Command, models
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
+
+
+class EDIInputProcessDiaparDespatchAdvice(models.AbstractModel):
+ """
+ INPUT PROCESSOR BLE
+ """
+
+ _name = "input.process.diapar.despatch.advice"
+ _inherit = ["edi.oca.handler.process"]
+ _description = "Input process despatch advice from Diapar"
+
+ def process(self, exchange_record):
+ _logger.info(">>>>>>>>>>>>>>>>>> Reading BLE file >>>>>>>>>>>>>>>>>>>>>")
+ picking_update, purchase_order = self._handle_create_picking_update(
+ exchange_record
+ )
+ if picking_update:
+ picking_update.button_update_picking_order()
+ _related = self.env["edi.exchange.related.record"].create(
+ {
+ "exchange_record_id": exchange_record.id,
+ "res_id": purchase_order.id,
+ "model": "purchase.order",
+ }
+ )
+ return self.env._(
+ "Stock Picking %s updated. Related Order: %s",
+ picking_update.name.name,
+ purchase_order.name,
+ )
+ return self.env._("No picking update created for this file.")
+
+ def _parse_file_content_ble(self, exchange_record):
+ datas = exchange_record._get_file_content()
+ return datas.split("\n")
+
+ def _get_mapping_field_position(self, exchange_type, field_name):
+ mapping = exchange_type.field_mapping_ids.filtered(
+ lambda rec: rec.mapping_field_id.name == field_name
+ )
+ pos_from = mapping.sequence_start
+ pos_to = mapping.sequence_end
+ return pos_from, pos_to
+
+ def _get_picking_order_and_supplier_info(
+ self, line, edi_exchange_type, picking_order, supplier_info, delivery_date
+ ):
+ pos_from, pos_to = self._get_mapping_field_position(
+ edi_exchange_type, "product_code"
+ )
+ product_code = line[pos_from:pos_to]
+ purchase_order = self.env["purchase.order"]
+ supplier_info = self.env["product.supplierinfo"].search(
+ [("product_code", "=", product_code)],
+ limit=1,
+ )
+ supplier_ids = supplier_info.mapped("partner_id").ids
+ # Look for corresponding purchase order, normally it
+ # should be just one even if there is more than one
+ # supplier associated to product
+ if supplier_ids:
+ sdate = f"{delivery_date} 00:00:00"
+ edate = f"{delivery_date} 23:59:59"
+ purchase_order = self.env["purchase.order"].search(
+ [
+ ("partner_id", "in", supplier_ids),
+ ("state", "in", ["purchase", "done"]),
+ ("date_planned", ">=", sdate),
+ ("date_planned", "<=", edate),
+ ],
+ limit=1,
+ )
+ # Assuming purchase order has only one picking order associated.
+ if purchase_order.exists():
+ picking_order = purchase_order.picking_ids[0]
+ return picking_order, supplier_info, purchase_order
+
+ def _get_updated_quantity_values(
+ self, line, edi_exchange_type, picking_order, supplier_info
+ ):
+ # Look for updated quantity
+ pos_from, pos_to = self._get_mapping_field_position(
+ edi_exchange_type, "product_qty"
+ )
+ new_quantity = line[pos_from:pos_to]
+ # Look for ordered quantity
+ product_tmpl = self.env["product.template"]
+ if supplier_info:
+ product_tmpl = supplier_info[0].product_tmpl_id
+ ordered_product = self.env["product.product"].search(
+ [("product_tmpl_id", "=", product_tmpl.id)],
+ limit=1,
+ )
+ ordered_operation = self.env["stock.move"].search(
+ [
+ ("picking_id", "=", picking_order.id),
+ ("product_id", "=", ordered_product.id),
+ ]
+ )
+ ordered_quantity = ordered_operation.quantity
+ # Construct one2many values
+ vals = dict()
+ if ordered_quantity != float(new_quantity):
+ vals.update(
+ {
+ "line_to_update_id": ordered_operation.id,
+ "product_id": ordered_product.id,
+ "ordered_quantity": ordered_quantity,
+ "product_qty": float(new_quantity),
+ }
+ )
+ return vals
+
+ def _reprepare_delivery_date(self, line, edi_exchange_type):
+ pos_from, pos_to = self._get_mapping_field_position(
+ edi_exchange_type, "date_planned"
+ )
+ data = line[pos_from:pos_to]
+ return self.env["edi.configuration"].get_date_format_ble_yyyymmdd(data)
+
+ def _prepare_picking_update_values(self, exchange_record):
+ lines = self._parse_file_content_ble(exchange_record)
+ edi_exchange_type = exchange_record.type_id
+
+ if not lines:
+ raise ValidationError(
+ self.env._(
+ "Please configure fields mapping for BLE interface on your"
+ " EDI system!"
+ )
+ )
+
+ delivery_date = delivery_sign = ""
+ picking_order = self.env["stock.picking"]
+ supplier_info = self.env["product.supplierinfo"]
+ purchase_order = self.env["purchase.order"]
+ proposition_vals = dict()
+ values_list = []
+ for line in lines:
+ if not line or not isinstance(line, str):
+ continue
+ # This condition ensures that this job
+ # consider only one picking per EDI File
+ line = line.lstrip()
+ if line.startswith(edi_exchange_type.header_code) and not delivery_date:
+ # Header processing
+ delivery_date = self._reprepare_delivery_date(line, edi_exchange_type)
+ elif line.startswith(edi_exchange_type.lines_code):
+ # Look if it's a first delivery
+ delivery_sign = edi_exchange_type.delivery_sign
+ if delivery_sign == "-":
+ break
+
+ # Look for picking_order, supplier_info
+ picking_order, supplier_info, purchase_order = (
+ self._get_picking_order_and_supplier_info(
+ line,
+ edi_exchange_type,
+ picking_order,
+ supplier_info,
+ delivery_date,
+ )
+ )
+ # Look for updated quantity
+ updated_values = self._get_updated_quantity_values(
+ line, edi_exchange_type, picking_order, supplier_info
+ )
+ if updated_values:
+ values_list += [Command.create(updated_values)]
+ if delivery_sign == "+":
+ proposition_vals.update(
+ {
+ "name": picking_order.id,
+ "values_proposed_ids": values_list,
+ }
+ )
+ return proposition_vals, purchase_order
+
+ def _handle_create_picking_update(self, exchange_record):
+ picking_update = self.env["picking.update"]
+ proposition_vals, purchase_order = self._prepare_picking_update_values(
+ exchange_record
+ )
+ if proposition_vals:
+ picking_update = picking_update.create(proposition_vals)
+ return picking_update, purchase_order
diff --git a/edi_purchase_diapar_oca/components/input_process_ch.py b/edi_purchase_diapar_oca/components/input_process_ch.py
new file mode 100644
index 000000000..0593fc4a4
--- /dev/null
+++ b/edi_purchase_diapar_oca/components/input_process_ch.py
@@ -0,0 +1,126 @@
+import datetime
+import logging
+
+from odoo import models
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
+
+
+class EDIInputProcessDiaparPurchasePrice(models.AbstractModel):
+ """
+ INPUT PROCESSOR CH
+ """
+
+ _name = "input.process.diapar.purchase.price"
+ _inherit = ["edi.oca.handler.process"]
+ _description = "Input process purchase price from Diapar"
+
+ def process(self, exchange_record):
+ _logger.info(">>>>>>>>>>>>>>>>>> Reading CH file >>>>>>>>>>>>>>>>>>>>>")
+ SupplierPriceList = self.env["supplier.price.list"]
+
+ self.create_supplier_price_list(exchange_record)
+ args = [
+ ("product_tmpl_id", "!=", False),
+ ("price_updated", "=", False),
+ ("supplier_id", "!=", False),
+ ]
+ recs = self.env["supplier.price.list"].search(args)
+ products = recs.mapped("product_tmpl_id")
+ for product in products:
+ splists = recs.filtered(
+ lambda rec, product=product: rec.product_tmpl_id == product
+ )
+ try:
+ SupplierPriceList.update_product_price_list(product, splists)
+ except Exception as e:
+ _logger.error(
+ "Could not update price list for the product %s: %s",
+ product,
+ str(e),
+ )
+
+ return self.env._("Process update purchase price from Diapar done!")
+
+ def _parse_file_content_ch(self, exchange_record):
+ datas = exchange_record._get_file_content()
+ return datas.split("\n")
+
+ def _get_mapping_field_position(self, exchange_type, field_name):
+ mapping = exchange_type.field_mapping_ids.filtered(
+ lambda rec: rec.mapping_field_id.name == field_name
+ )
+ pos_from = mapping.sequence_start
+ pos_to = mapping.sequence_end
+ return pos_from, pos_to
+
+ def _prepare_supplier_price_list_values(self, exchange_record):
+ lines = self._parse_file_content_ch(exchange_record)
+ if not lines:
+ raise ValidationError(
+ self.env._(
+ "Please configure fields mapping for prices interface on your \
+ EDI Exchange Type!"
+ )
+ )
+
+ edi_exchange_type = exchange_record.type_id
+
+ EDIConfig = self.env["edi.configuration"]
+ ProductSupplierInfo = self.env["product.supplierinfo"]
+
+ prices = []
+ product_codes = []
+ today = datetime.date.today()
+ for line in lines:
+ if not line or not isinstance(line, str):
+ continue
+ line = line.lstrip()
+ # check if this line is already imported.
+ pos_from, pos_to = self._get_mapping_field_position(
+ edi_exchange_type, "supplier_code"
+ )
+ product_code = line[pos_from:pos_to]
+ if product_code in product_codes:
+ continue
+ else:
+ product_codes.append(product_code)
+ key = ["supplier_id", "import_date"]
+ value = [edi_exchange_type.partner_ids.ids[0], today]
+ for mapping in edi_exchange_type.field_mapping_ids:
+ slice_from = mapping.sequence_start
+ slice_to = mapping.sequence_end
+ # construct dictionary
+ key.append(mapping.mapping_field_id.name)
+ data = line[slice_from:slice_to]
+ # Product test
+ if mapping.mapping_field_id.name == "supplier_code":
+ # appending supplier_code data
+ value.append(data)
+ # appending product_id data
+ supp_info = ProductSupplierInfo.search(
+ [("product_code", "=", data)], limit=1
+ )
+ product_id = supp_info.product_tmpl_id.id
+ key.append("product_tmpl_id")
+ value.append(product_id)
+ # Date test
+ elif mapping.is_date:
+ # slice dates
+ apply_date = EDIConfig.get_date_format_yyyymmdd(data)
+ value.append(apply_date)
+ # numeric test
+ elif mapping.is_numeric:
+ decimal_precision = mapping.decimal_precision
+ price = EDIConfig.insert_separator(data, -decimal_precision, ".")
+ value.append(float(price))
+ elif mapping.mapping_field_id.name == "product_name":
+ value.append(data)
+ prices_dict = {k: v for k, v in zip(key, value, strict=False)}
+ prices.append(prices_dict)
+ return prices
+
+ def create_supplier_price_list(self, exchange_record):
+ prices = self._prepare_supplier_price_list_values(exchange_record)
+ return self.env["supplier.price.list"].create(prices)
diff --git a/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml b/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml
new file mode 100644
index 000000000..dcecad37a
--- /dev/null
+++ b/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml
@@ -0,0 +1,22 @@
+
+
+
+ DIAPAR
+ diapar
+
+
+
+ Diapar
+
+
+ IN/PENDING
+ IN/DONE
+ IN/ERROR
+ OUT/PENDING
+ OUT/DONE
+ OUT/ERROR
+
+
diff --git a/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml b/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml
new file mode 100644
index 000000000..64ac55f9c
--- /dev/null
+++ b/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml
@@ -0,0 +1,23 @@
+
+
+
+ Diapar Purchase Order
+
+
+
+
+
+if record.state == 'purchase':
+ record._edi_send_via_edi(conf.type_id)
+
+
+
diff --git a/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml b/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml
new file mode 100644
index 000000000..ebb3dc919
--- /dev/null
+++ b/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml
@@ -0,0 +1,28 @@
+
+
+
+ Diapar Output Exchange Template
+
+
+ diapar.output.exchange.template
+ text
+ qweb
+
+ 33513
+ 03
+ HDIAPAR
+ *DIAPAR*DIAPAR
+
+
diff --git a/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml b/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml
new file mode 100644
index 000000000..d9bef0bc6
--- /dev/null
+++ b/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml
@@ -0,0 +1,82 @@
+
+
+
+ diapar_out_order
+ diapar_out_order
+
+
+ output
+ LD{dt}H{time}
+ C99
+ iso-8859-1
+
+
+
+ filename_pattern:
+ date_pattern: "%Y%m%d"
+ time_pattern: "%H%M%S"
+
+
+
+ diapar_in_despatch_advice
+ diapar_in_despatch_advice
+
+
+ input
+ BLE.*
+ txt
+ iso-8859-1
+ 1
+ 2
+ +
+
+
+
+
+
+ diapar_in_purchase_price
+ diapar_in_purchase_price
+
+
+ input
+ CH*
+ txt
+ iso-8859-1
+
+
+
+
+
diff --git a/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml b/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml
new file mode 100644
index 000000000..335ca3ee7
--- /dev/null
+++ b/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml
@@ -0,0 +1,107 @@
+
+
+
+
+ Date Planned
+ 1
+ 24
+ 32
+ True
+
+
+
+
+ Product Code
+ 2
+ 16
+ 22
+
+
+
+
+ Product Quantity
+ 3
+ 52
+ 57
+
+
+
+
+
+ Product Code
+ 1
+ 2
+ 8
+
+
+
+
+ Product Name
+ 2
+ 13
+ 38
+
+
+
+
+ Price HT
+ 3
+ 38
+ 47
+ True
+
+
+ 4
+
+
+ Apply Date
+ 4
+ 96
+ 102
+ True
+
+
+
+
diff --git a/edi_purchase_diapar_oca/demo/fs_storage_demo.xml b/edi_purchase_diapar_oca/demo/fs_storage_demo.xml
new file mode 100644
index 000000000..e1397c28c
--- /dev/null
+++ b/edi_purchase_diapar_oca/demo/fs_storage_demo.xml
@@ -0,0 +1,14 @@
+
+
+
+ Diapar FTP Storage
+ diapar_ftp
+ ftp
+ {
+ "host": "127.0.0.1",
+ "username": "admin",
+ "password": "admin",
+ "port": 5000
+ }
+
+
diff --git a/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot b/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot
new file mode 100644
index 000000000..8c7aa2156
--- /dev/null
+++ b/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot
@@ -0,0 +1,205 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_purchase_diapar
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: edi_purchase_diapar
+#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update
+msgid "Cancel"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/purchase.py:33
+#, python-format
+msgid "Check price for lines with product %s!"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/purchase.py:28
+#, python-format
+msgid "Check taxes for lines with product %s!"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__constant_file_end
+msgid "Constant File End"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__constant_file_start
+msgid "Constant File Start"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__customer_code
+msgid "Customer Code"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model,name:edi_purchase_diapar.model_edi_config_system
+msgid "EDI Config System"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__edi_line_ids
+msgid "Edi Line"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_login
+msgid "FTP Login"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_password
+msgid "FTP Password"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_host
+msgid "FTP Server Host"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__id
+msgid "ID"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_supplier_info_update_line__inv_supplier_price_id
+msgid "Inv Supplier Price"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model,name:edi_purchase_diapar.model_account_invoice
+msgid "Invoice"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model,name:edi_purchase_diapar.model_invoice_supplier_price_update
+msgid "Invoice Supplier Price Update"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update
+msgid "Lines"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/purchase.py:22
+#, python-format
+msgid "No lines in this order %s!"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__partner_id
+msgid "Partner"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_account_invoice__partner_is_edi
+msgid "Partner (Is Edit)"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/product.py:21
+#, python-format
+msgid "Product code must be 6 digits for %s!"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/product.py:17
+#, python-format
+msgid "Product code must be numeric for %s!"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model,name:edi_purchase_diapar.model_purchase_order
+msgid "Purchase Order"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__po_text_file_pattern
+msgid "Purchase order File pattern"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__csv_relative_in_path
+msgid "Relative path for IN interfaces"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__csv_relative_out_path
+msgid "Relative path for OUT interfaces"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__show_discount
+msgid "Show Discount"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model,name:edi_purchase_diapar.model_supplier_info_update_line
+msgid "Supplier Information Update Line"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.actions.act_window,name:edi_purchase_diapar.invoice_supplier_price_update_act
+msgid "Update Prices From EDI"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.invoice_supplier_form_inherit_diapar
+msgid "Update Prices from EDI"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__vrp_code
+msgid "VRP Code"
+msgstr ""
+
+#. module: edi_purchase_diapar
+#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update
+msgid "Validate"
+msgstr ""
+
diff --git a/edi_purchase_diapar_oca/i18n/fr.po b/edi_purchase_diapar_oca/i18n/fr.po
new file mode 100644
index 000000000..15fa78b85
--- /dev/null
+++ b/edi_purchase_diapar_oca/i18n/fr.po
@@ -0,0 +1,84 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_purchase_diapar
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 9.0c\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-12-26 10:56+0000\n"
+"PO-Revision-Date: 2020-07-01 10:27+0000\n"
+"Last-Translator: Simon Mas \n"
+"Language-Team: French \n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 3.8\n"
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/purchase.py:31
+#, python-format
+msgid "Check price for lines with product %s!"
+msgstr "Check price for lines with product %s!"
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/purchase.py:29
+#, python-format
+msgid "Check taxes for lines with product %s!"
+msgstr "Merci de vérifier la taxe pour les lignes au produit: %s!"
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:23
+#, python-format
+msgid "No Config FTP for this supplier %s!"
+msgstr "Aucune configuration trouvée pour le fournisseur: %s!"
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/purchase.py:24
+#, python-format
+msgid "No lines in this order %s!"
+msgstr "Aucune ligne pour cette commande: %s!"
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:72
+#, python-format
+msgid ""
+"No supplier code given for product: %s for supplier: %s!, please give a "
+"supplier code to continue prices operation update"
+msgstr ""
+"Aucun code fournisseur renseigné pour ce produit : %s avec le fournisseur: "
+"%s!, merci de mettre à jour le codde fournisseur pour continuer la mise à "
+"jour des prix."
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/product.py:20
+#, python-format
+msgid "Product code must be 6 digits for %s!"
+msgstr "Le code produit doit être de 6 numériques pour : %s!"
+
+#. module: edi_purchase_diapar
+#: code:addons/edi_purchase_diapar/models/product.py:18
+#, python-format
+msgid "Product code must be numeric for %s!"
+msgstr "Le code produit doit être numérique pour le produit : %s!"
+
+#. module: edi_purchase_diapar
+#: model:ir.model,name:edi_purchase_diapar.model_purchase_order
+msgid "Purchase Order"
+msgstr "Bon de commande"
+
+#. module: edi_purchase_diapar
+#: model:ir.model,name:edi_purchase_diapar.model_edi_config_system
+msgid "edi.config.system"
+msgstr "edi.config.system"
+
+#. module: edi_purchase_diapar
+#: model:ir.model,name:edi_purchase_diapar.model_supplier_info_update
+msgid "supplier.info.update"
+msgstr ""
+
+#~ msgid "Information about a product vendor"
+#~ msgstr "Information sur le vendeur de l'article"
diff --git a/edi_purchase_diapar_oca/models/__init__.py b/edi_purchase_diapar_oca/models/__init__.py
new file mode 100644
index 000000000..9c3912f3b
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2016-Today: Druidoo ( )
+# @author: Druidoo
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html
+
+from . import account_move
+from . import res_partner
+from . import edi_configuration
+from . import edi_field_mapping
+from . import edi_exchange_type
+from . import edi_exchange_template_output
+from . import picking_update
+from . import purchase_order
+from . import product_product
+from . import product_supplierinfo
+from . import supplier_price_list
+from . import res_company
+from . import res_config_settings
+from . import stock_move
diff --git a/edi_purchase_diapar_oca/models/account_move.py b/edi_purchase_diapar_oca/models/account_move.py
new file mode 100644
index 000000000..23188f3d0
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/account_move.py
@@ -0,0 +1,15 @@
+# Copyright (C) 2016-Today: Druidoo ( )
+# @author: Druidoo
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html
+
+from odoo import fields, models
+
+
+class AccountMove(models.Model):
+ _inherit = "account.move"
+
+ partner_is_edi = fields.Boolean(
+ related="partner_id.is_edi",
+ string="Partner (Is Edi)",
+ store=True,
+ )
diff --git a/edi_purchase_diapar_oca/models/edi_configuration.py b/edi_purchase_diapar_oca/models/edi_configuration.py
new file mode 100644
index 000000000..a7fe93981
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/edi_configuration.py
@@ -0,0 +1,59 @@
+import datetime
+import time
+
+from odoo import api, models
+
+
+class EDIConfiguration(models.Model):
+ _inherit = "edi.configuration"
+
+ @api.model
+ def get_datenow_format_for_file(self):
+ now = time.strftime("%Y-%m-%d %H:%M:%S")
+ date = now.split(" ")[0].replace("-", "")
+ hour = now.split(" ")[1].replace(":", "")
+ return date, hour
+
+ @api.model
+ def get_datetime_format_ddmmyyyy(self, do_date):
+ if not isinstance(do_date, datetime.datetime):
+ do_date = datetime.datetime.strptime(do_date, "%Y-%m-%d %H:%M:%S")
+ return "%02d%02d%s" % (
+ do_date.day,
+ do_date.month,
+ str(do_date.year)[2:],
+ )
+
+ @api.model
+ def get_date_format_yyyymmdd(self, date):
+ """
+ Transform a string date to datetime and format it to standard odoo
+ date format
+ """
+ return datetime.datetime.strptime(date, "%y%m%d").strftime("%Y-%m-%d")
+
+ @api.model
+ def get_date_format_ble_yyyymmdd(self, date):
+ """
+ Transform a string date (specific to delivery order interface format)
+ to datetime object and format it to standard odoo date format
+ """
+ return datetime.datetime.strptime(date, "%Y%m%d").strftime("%Y-%m-%d")
+
+ @api.model
+ def insert_separator(self, string, index, separator):
+ """
+ This method is to insert a separator inside string on a certain
+ position
+ """
+ return string[:index] + separator + string[index:]
+
+ def _fix_lenght(self, value, lenght, mode="float", replace="", position="before"):
+ value = str(value)
+ if mode == "float":
+ value = value.split(".")[0]
+ if position == "before":
+ value = "".join([replace for i in range(lenght - len(value))]) + value
+ else:
+ value += "".join([replace for i in range(lenght - len(value))])
+ return value[0:lenght]
diff --git a/edi_purchase_diapar_oca/models/edi_exchange_template_output.py b/edi_purchase_diapar_oca/models/edi_exchange_template_output.py
new file mode 100644
index 000000000..511a5b515
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/edi_exchange_template_output.py
@@ -0,0 +1,10 @@
+from odoo import fields, models
+
+
+class EDIExchangeTemplateOutput(models.Model):
+ _inherit = "edi.exchange.template.output"
+
+ customer_code = fields.Char()
+ constant_file_start = fields.Char()
+ constant_file_end = fields.Char()
+ vrp_code = fields.Char()
diff --git a/edi_purchase_diapar_oca/models/edi_exchange_type.py b/edi_purchase_diapar_oca/models/edi_exchange_type.py
new file mode 100644
index 000000000..4bfad0ad2
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/edi_exchange_type.py
@@ -0,0 +1,54 @@
+from datetime import datetime
+
+from pytz import timezone, utc
+
+from odoo import fields, models
+
+DEFAULT_DATE_FMT = "%Y%m%d"
+DEFAULT_TIME_FMT = "%H%M%S"
+
+
+class EDIExchangeType(models.Model):
+ _inherit = "edi.exchange.type"
+
+ field_mapping_ids = fields.One2many(
+ comodel_name="edi.field.mapping",
+ inverse_name="exchange_type_id",
+ string="Field Mappings",
+ )
+ header_code = fields.Char()
+ lines_code = fields.Char()
+ delivery_sign = fields.Char()
+
+ def _make_exchange_filename_time(self):
+ self.ensure_one()
+ pattern_settings = self.advanced_settings.get("filename_pattern", {})
+ force_tz = pattern_settings.get("force_tz", self.env.user.tz)
+ time_pattern = pattern_settings.get("time_pattern", DEFAULT_TIME_FMT)
+ tz = timezone(force_tz) if force_tz else None
+ now = datetime.now(utc).astimezone(tz)
+ return self.env["ir.http"]._slugify(now.strftime(time_pattern))
+
+ def _make_exchange_filename(self, exchange_record):
+ pattern = self.exchange_filename_pattern
+ if "{time}" in pattern:
+ ext = self.exchange_file_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:
+ record = exchange_record.record
+ return pattern.format(
+ exchange_record=exchange_record,
+ record=record,
+ record_name=record_name,
+ type=self,
+ dt=dt,
+ seq=seq,
+ ext=ext,
+ time=self._make_exchange_filename_time(),
+ )
+ return super()._make_exchange_filename(exchange_record)
diff --git a/edi_purchase_diapar_oca/models/edi_field_mapping.py b/edi_purchase_diapar_oca/models/edi_field_mapping.py
new file mode 100644
index 000000000..0ddd2ee06
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/edi_field_mapping.py
@@ -0,0 +1,25 @@
+from odoo import fields, models
+
+
+class EDIPriceMapping(models.Model):
+ _name = "edi.field.mapping"
+ _order = "position"
+ _description = "EDI Price Mapping"
+
+ sequence = fields.Integer(default=1)
+ position = fields.Integer(required=True)
+ exchange_type_id = fields.Many2one(
+ comodel_name="edi.exchange.type",
+ string="Exchange type",
+ required=True,
+ )
+ mapping_field_id = fields.Many2one(
+ comodel_name="ir.model.fields",
+ string="Prices mapping field",
+ )
+ name = fields.Char(string="Zone description")
+ sequence_start = fields.Integer()
+ sequence_end = fields.Integer()
+ is_numeric = fields.Boolean(string="Is numeric ?")
+ is_date = fields.Boolean(string="Is a date?")
+ decimal_precision = fields.Integer()
diff --git a/edi_purchase_diapar_oca/models/picking_update.py b/edi_purchase_diapar_oca/models/picking_update.py
new file mode 100644
index 000000000..6f725c127
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/picking_update.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2016-Today: Druidoo ( )
+# @author: Druidoo
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html
+from odoo import Command, fields, models
+
+
+class PickingEdi(models.Model):
+ _name = "picking.edi"
+ _description = "Picking EDI"
+
+ product_id = fields.Many2one("product.product")
+ ordered_quantity = fields.Float()
+ product_qty = fields.Float(string="EDI Quantity")
+ package_qty = fields.Float(string="Product package")
+ line_to_update_id = fields.Many2one("stock.move")
+ picking_update_id = fields.Many2one("picking.update")
+
+
+class PickingUpdate(models.Model):
+ _name = "picking.update"
+ _inherit = ["mail.thread", "mail.activity.mixin"]
+ _description = "Picking Update"
+
+ done = fields.Boolean(readonly=True)
+ name = fields.Many2one("stock.picking", string="Order picking", readonly=True)
+ values_proposed_ids = fields.One2many(
+ "picking.edi",
+ inverse_name="picking_update_id",
+ string="Quantities to update",
+ readonly=True,
+ )
+
+ def _prepare_values_proposed(self, proposition):
+ self.ensure_one()
+ return {
+ "quantity": proposition.product_qty,
+ }
+
+ def button_update_picking_order(self):
+ self.ensure_one()
+ commands = []
+ for proposition in self.values_proposed_ids:
+ commands += [
+ Command.update(
+ proposition.line_to_update_id.id,
+ self._prepare_values_proposed(proposition),
+ )
+ ]
+ self.done = True
+ self.name.write({"move_ids": commands})
+ return True
diff --git a/edi_purchase_diapar_oca/models/product_product.py b/edi_purchase_diapar_oca/models/product_product.py
new file mode 100644
index 000000000..c7e393ade
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/product_product.py
@@ -0,0 +1,22 @@
+from odoo import models
+from odoo.exceptions import ValidationError
+
+
+class ProductProduct(models.Model):
+ _inherit = "product.product"
+
+ def _get_supplier_code_or_ean(self, seller_id):
+ self.ensure_one()
+ code, origin_code = "", ""
+ seller_line = self.seller_ids.filtered(
+ lambda seller: seller.partner_id.id == seller_id and seller.product_code
+ )
+ if seller_line and seller_line[0].product_code:
+ code = seller_line[0].product_code
+ origin_code = "supplier"
+ elif self.barcode:
+ code = self.barcode
+ origin_code = "barcode"
+ if not code:
+ raise ValidationError(self.env._("No code for this product %s!", self.name))
+ return code, origin_code
diff --git a/edi_purchase_diapar_oca/models/product_supplierinfo.py b/edi_purchase_diapar_oca/models/product_supplierinfo.py
new file mode 100644
index 000000000..52ca22637
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/product_supplierinfo.py
@@ -0,0 +1,80 @@
+from odoo import api, models
+from odoo.exceptions import ValidationError
+
+
+class SupplierInfo(models.Model):
+ _inherit = "product.supplierinfo"
+
+ @api.model
+ def compute_edi_partner(self, partner):
+ """
+ :param partner: purchase order/invoice supplier
+ :return: EDI supplier used
+ """
+ edi_exchange_type_obj = self.env["edi.exchange.type"]
+ exchange_type = edi_exchange_type_obj.search(
+ [("partner_ids", "=", partner.id)], limit=1
+ )
+ if not exchange_type:
+ raise ValidationError(
+ self.env._("No EDI Exchange Type for this supplier %s!") % partner.name
+ )
+ if partner.edi_purchase_supplier_id:
+ return partner.edi_purchase_supplier_id
+ else:
+ return partner
+
+ def _get_price_field(self):
+ return "price"
+
+ @api.model
+ def update_purchase_price(self, vals):
+ """
+ Looks for most recent price on purchase table of prices, only for
+ EDI suppliers
+ :param vals:
+ :return: updated values with product price
+ """
+ supplier_id = vals.get("partner_id", False)
+ supplier = self.env["res.partner"].browse(supplier_id)
+ if supplier.is_edi:
+ edi_supplier = self.compute_edi_partner(supplier)
+ supplier_code = vals.get("product_code", False)
+ if not supplier_code:
+ raise ValidationError(
+ self.env._("Please give a supplier code to create the product!")
+ )
+ price = self.env["supplier.price.list"].search(
+ [
+ ("supplier_id", "=", edi_supplier.id),
+ ("supplier_code", "=", supplier_code),
+ ],
+ order="apply_date DESC",
+ )
+ if price:
+ price_field = self._get_price_field()
+ vals.update({price_field: price[0].price})
+ price.sudo().write({"price_updated": True})
+ return vals
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ vals = self.update_purchase_price(vals)
+ return super().create(vals_list)
+
+ @api.constrains("product_code", "partner_id")
+ def _check_product_code(self):
+ if self.product_code and self.partner_id.is_edi:
+ if not self.product_code.isdigit():
+ raise ValidationError(
+ self.env._(
+ "Product code must be numeric for %s!", self.partner_id.name
+ )
+ ) from None
+ if len(self.product_code) != 6:
+ raise ValidationError(
+ self.env._(
+ "Product code must be 6 digits for %s!", self.partner_id.name
+ )
+ ) from None
diff --git a/edi_purchase_diapar_oca/models/purchase_order.py b/edi_purchase_diapar_oca/models/purchase_order.py
new file mode 100644
index 000000000..86e27ffd1
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/purchase_order.py
@@ -0,0 +1,83 @@
+from odoo import models
+from odoo.exceptions import ValidationError
+
+
+class PurchaseOrder(models.Model):
+ _inherit = "purchase.order"
+
+ def _should_apply_receipt_qty_policy(self):
+ """Condition to apply the policy of using the quantity from
+ the EDI file instead of the one in the order line."""
+ self.ensure_one()
+ exchange_types = self.env["edi.exchange.type"].search(
+ [
+ ("partner_ids", "in", self.partner_id.ids),
+ ("direction", "=", "input"),
+ ]
+ )
+ return (
+ not self.edi_disable_auto
+ and self.partner_id.is_edi
+ and self.partner_id.edi_purchase_conf_ids
+ and exchange_types
+ )
+
+ def _consolidate_product_qty(self, order_line):
+ return order_line.product_qty
+
+ def _consolidate_products(self):
+ self.ensure_one()
+ if not self.order_line:
+ raise ValidationError(
+ self.env._("No lines in this order %s!", self.name)
+ ) from None
+ lines = {}
+ for line in self.order_line:
+ quantity = self._consolidate_product_qty(line)
+ if line.product_id in lines:
+ if line.taxes_id != lines[line.product_id]["taxes_id"]:
+ raise ValidationError(
+ self.env._(
+ "Check taxes for lines with product %s!",
+ line.product_id.name,
+ )
+ )
+ if line.price_unit != lines[line.product_id]["price_unit"]:
+ raise ValidationError(
+ self.env._(
+ "Check price for lines with product %s!",
+ line.product_id.name,
+ )
+ )
+ lines[line.product_id]["quantity"] += quantity
+ else:
+ code, origin_code = line.product_id._get_supplier_code_or_ean(
+ line.partner_id.id
+ )
+ values = {
+ "code": code,
+ "origin_code": origin_code,
+ "quantity": quantity,
+ "price_unit": line.price_unit,
+ "taxes_id": line.taxes_id,
+ }
+ lines.update({line.product_id: values})
+ return lines
+
+ def generate_template_output_diapar(self, output_template, exchange_record):
+ self.ensure_one()
+ return output_template.exchange_generate(
+ exchange_record,
+ order_lines=self._consolidate_products(),
+ env=self.env,
+ )
+
+
+class PurchaseOrderLine(models.Model):
+ _inherit = "purchase.order.line"
+
+ def _create_stock_moves(self, picking):
+ if self.order_id._should_apply_receipt_qty_policy():
+ if self.company_id.edi_receipt_qty_policy == "received_file":
+ self = self.with_context(qty_from_file_policy=True)
+ return super()._create_stock_moves(picking)
diff --git a/edi_purchase_diapar_oca/models/res_company.py b/edi_purchase_diapar_oca/models/res_company.py
new file mode 100644
index 000000000..6cc529364
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/res_company.py
@@ -0,0 +1,16 @@
+from odoo import fields, models
+
+
+class ResCompany(models.Model):
+ _inherit = "res.company"
+
+ edi_receipt_qty_policy = fields.Selection(
+ selection=[
+ ("ordered", "From PO"),
+ ("received_file", "From EDI Receipt File"),
+ ],
+ string="Receipt Quantity Policy",
+ default="ordered",
+ help="Policy to determine the quantity to be used in "
+ "receipt when processing EDI file for purchase order.",
+ )
diff --git a/edi_purchase_diapar_oca/models/res_config_settings.py b/edi_purchase_diapar_oca/models/res_config_settings.py
new file mode 100644
index 000000000..cd67ab420
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/res_config_settings.py
@@ -0,0 +1,9 @@
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = "res.config.settings"
+
+ edi_receipt_qty_policy = fields.Selection(
+ related="company_id.edi_receipt_qty_policy", readonly=False
+ )
diff --git a/edi_purchase_diapar_oca/models/res_partner.py b/edi_purchase_diapar_oca/models/res_partner.py
new file mode 100644
index 000000000..baea45a0a
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/res_partner.py
@@ -0,0 +1,12 @@
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ edi_purchase_supplier_id = fields.Many2one(
+ "res.partner",
+ domain=[("supplier_rank", ">", 0), ("is_edi", "=", True)],
+ )
+ is_edi = fields.Boolean(string="Is an EDI supplier")
+ show_discount = fields.Boolean()
diff --git a/edi_purchase_diapar_oca/models/stock_move.py b/edi_purchase_diapar_oca/models/stock_move.py
new file mode 100644
index 000000000..8ab945ae3
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/stock_move.py
@@ -0,0 +1,12 @@
+from odoo import models
+
+
+class StockMove(models.Model):
+ _inherit = "stock.move"
+
+ def _action_assign(self, force_qty=False):
+ res = super()._action_assign(force_qty=force_qty)
+ if self.env.context.get("qty_from_file_policy"):
+ # Set the quantity to 0 and it will be updated later in the process.
+ self.quantity = 0
+ return res
diff --git a/edi_purchase_diapar_oca/models/supplier_price_list.py b/edi_purchase_diapar_oca/models/supplier_price_list.py
new file mode 100644
index 000000000..a6b60b1fa
--- /dev/null
+++ b/edi_purchase_diapar_oca/models/supplier_price_list.py
@@ -0,0 +1,114 @@
+# Copyright (C) 2016-Today: Druidoo ( )
+# @author: Druidoo
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html
+
+from odoo import api, fields, models
+
+
+class SupplierPriceList(models.Model):
+ _name = "supplier.price.list"
+ _description = "Supplier Price List"
+
+ import_date = fields.Date(readonly=True)
+ supplier_id = fields.Many2one(
+ comodel_name="res.partner",
+ string="EDI Supplier",
+ domain=[("is_edi", "=", True), ("supplier_rank", ">", 0)],
+ readonly=True,
+ required=True,
+ )
+ product_tmpl_id = fields.Many2one(
+ comodel_name="product.template", string="Product", ondelete="set null"
+ )
+ product_name = fields.Char(readonly=True, required=True)
+ supplier_code = fields.Char(readonly=True, required=True)
+ price = fields.Float(
+ digits="Product Price",
+ readonly=True,
+ required=True,
+ help="The price HT to purchase a product",
+ )
+ apply_date = fields.Date(readonly=True, required=True)
+ barcode = fields.Char(string="Ean")
+ price_updated = fields.Boolean()
+
+ def _get_price_field(self):
+ return "price"
+
+ def _create_supplierinfo_with_price(self, vals):
+ price_field = self._get_price_field()
+ vals.update({price_field: self.price})
+ return self.env["product.supplierinfo"].create(vals)
+
+ def button_create_product(self):
+ self.ensure_one()
+ # create new product
+ product_tmpl_id = self.env["product.template"].create(
+ {
+ "name": self.product_name,
+ "sale_ok": True,
+ "purchase_ok": True,
+ "type": "consu",
+ "default_code": self.supplier_code,
+ "barcode": self.barcode,
+ }
+ )
+ # link product with current supplier price list
+ self.sudo().product_tmpl_id = product_tmpl_id.id
+ # create product supplier info
+ self._create_supplierinfo_with_price(
+ {
+ "partner_id": self.supplier_id.id,
+ "product_code": self.supplier_code,
+ "product_tmpl_id": product_tmpl_id.id,
+ }
+ )
+ self.sudo().price_updated = True
+ # find similar supplier price list
+ supplier_price_list_ids = self.search(
+ [
+ ("supplier_id", "=", self.supplier_id.id),
+ ("product_name", "=", self.product_name),
+ ("supplier_code", "=", self.supplier_code),
+ ("product_tmpl_id", "=", False),
+ ]
+ )
+ # link product to similar supplier price list
+ supplier_price_list_ids.sudo().write({"product_tmpl_id": product_tmpl_id.id})
+ # create action to open newly created product form view
+ action = {
+ "name": self.env._("Product Form"),
+ "res_model": "product.template",
+ "type": "ir.actions.act_window",
+ "target": "current",
+ "res_id": product_tmpl_id.id,
+ }
+ return action
+
+ @api.model
+ def update_product_price_list(self, product, splists):
+ splists = splists.sorted("apply_date")
+ splist = splists[-1]
+ price = splist.price
+ args = [
+ ("product_code", "=", splist.supplier_code),
+ ("product_tmpl_id", "=", product.id),
+ ]
+ supplierinfos = self.env["product.supplierinfo"].search(args, limit=1)
+ if supplierinfos:
+ price_field = self._get_price_field()
+ if getattr(supplierinfos, price_field) != price:
+ supplierinfos.write(
+ {
+ price_field: price,
+ }
+ )
+ else:
+ self._create_supplierinfo_with_price(
+ {
+ "partner_id": splist.supplier_id.id,
+ "product_code": splist.supplier_code,
+ "product_tmpl_id": product.id,
+ }
+ )
+ splists.sudo().write({"price_updated": True})
diff --git a/edi_purchase_diapar_oca/pyproject.toml b/edi_purchase_diapar_oca/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/edi_purchase_diapar_oca/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md b/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..68accf590
--- /dev/null
+++ b/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md
@@ -0,0 +1,3 @@
+- Druidoo
+- [Trobz](https://www.trobz.com)
+ - Phan Hong Phuc \<\>
diff --git a/edi_purchase_diapar_oca/security/ir.model.access.csv b/edi_purchase_diapar_oca/security/ir.model.access.csv
new file mode 100644
index 000000000..9138190da
--- /dev/null
+++ b/edi_purchase_diapar_oca/security/ir.model.access.csv
@@ -0,0 +1,8 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_model_edi_field_mapping_all,access_model_edi_field_mapping_all,model_edi_field_mapping,base.group_user,1,0,0,0
+access_model_edi_field_mapping_manager,access_model_edi_field_mapping_manager,model_edi_field_mapping,base_edi.group_edi_manager,1,1,1,1
+access_model_picking_edi_all,access_model_picking_edi_all,model_picking_edi,base.group_user,1,0,0,0
+access_model_picking_edi_manager,access_model_picking_edi_manager,model_picking_edi,base_edi.group_edi_manager,1,1,0,0
+access_model_picking_update_all,access_model_picking_update_all,model_picking_update,base.group_user,1,0,0,0
+access_model_picking_update_manager,access_model_picking_update_manager,model_picking_update,base_edi.group_edi_manager,1,1,0,0
+access_model_supplier_price_list_all,access_model_supplier_price_list_all,model_supplier_price_list,base.group_user,1,0,0,0
diff --git a/edi_purchase_diapar_oca/static/description/icon.png b/edi_purchase_diapar_oca/static/description/icon.png
new file mode 100644
index 000000000..fd60220fb
Binary files /dev/null and b/edi_purchase_diapar_oca/static/description/icon.png differ
diff --git a/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml b/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml
new file mode 100644
index 000000000..b2a5b5827
--- /dev/null
+++ b/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml
@@ -0,0 +1,10 @@
+
+
+ A B
+
diff --git a/edi_purchase_diapar_oca/tests/__init__.py b/edi_purchase_diapar_oca/tests/__init__.py
new file mode 100644
index 000000000..bd5bea96f
--- /dev/null
+++ b/edi_purchase_diapar_oca/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_edi_purchase_diapar_oca
diff --git a/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py b/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py
new file mode 100644
index 000000000..ad0366d01
--- /dev/null
+++ b/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py
@@ -0,0 +1,238 @@
+from odoo import Command
+from odoo.exceptions import ValidationError
+from odoo.tests.common import TransactionCase, tagged
+
+
+@tagged("post_install", "-at_install")
+class TestEdiPurchaseDiaparOCA(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.edi_config_diapar = cls.env.ref(
+ "edi_purchase_diapar_oca.demo_edi_config_diapar"
+ )
+ cls.backend = cls.env.ref("edi_purchase_diapar_oca.demo_edi_backend_diapar")
+ cls.edi_template_output_diapar = cls.env.ref(
+ "edi_purchase_diapar_oca.demo_edi_exchange_template_output_diapar"
+ )
+ cls.exchange_type_ble = cls.env.ref(
+ "edi_purchase_diapar_oca.demo_exchange_diapar_in_despatch_advice"
+ )
+ cls.exchange_type_ch = cls.env.ref(
+ "edi_purchase_diapar_oca.demo_exchange_diapar_in_purchase_price"
+ )
+ cls.edi_supplier = cls.env.ref("base.res_partner_12")
+ cls.edi_supplier.write(
+ {
+ "is_edi": True,
+ "edi_purchase_conf_ids": [Command.set(cls.edi_config_diapar.ids)],
+ }
+ )
+ cls.exchange_type_ble.partner_ids = [Command.set(cls.edi_supplier.ids)]
+ cls.exchange_type_ch.partner_ids = [Command.set(cls.edi_supplier.ids)]
+ cls.normal_supplier = cls.env.ref("base.res_partner_4")
+ cls.product_4 = cls.env.ref("product.product_product_4")
+ cls.product_5 = cls.env.ref("product.product_product_5")
+ cls.supplier_info_5 = cls.env["product.supplierinfo"].create(
+ {
+ "product_tmpl_id": cls.product_5.product_tmpl_id.id,
+ "partner_id": cls.edi_supplier.id,
+ "product_code": "987654",
+ "min_qty": 1,
+ "price": 10,
+ }
+ )
+
+ def _create_purchase_order(self, partner, product):
+ return self.env["purchase.order"].create(
+ {
+ "partner_id": partner.id,
+ "order_line": [
+ Command.create(
+ {
+ "product_id": product.id,
+ "product_qty": 10,
+ "price_unit": 10,
+ }
+ )
+ ],
+ }
+ )
+
+ def test_purchase_order_confirmation_without_product_code(self):
+ order = self._create_purchase_order(self.edi_supplier, self.product_4)
+ with self.assertRaises(ValidationError, msg="Please give a supplier code to"):
+ order.button_confirm()
+
+ def test_purchase_order_confirmation_with_edi_supplier(self):
+ order = self._create_purchase_order(self.edi_supplier, self.product_5)
+ order.button_confirm()
+ self.assertEqual(order.state, "purchase")
+ self.assertEqual(order.exchange_record_count, 1)
+
+ def test_purchase_order_confirmation_with_normal_supplier(self):
+ order = self._create_purchase_order(self.normal_supplier, self.product_5)
+ order.button_confirm()
+ self.assertEqual(order.state, "purchase")
+ self.assertEqual(order.exchange_record_count, 0)
+
+ def test_edi_ouput_process_diapar_purchase_order(self):
+ order = self._create_purchase_order(self.edi_supplier, self.product_5)
+ order.button_confirm()
+ exchange_record = order.exchange_record_ids[0]
+ exchange_record.backend_id.exchange_generate(exchange_record)
+ file_content = exchange_record._get_file_content()
+ self.assertTrue(file_content, "The generated file content should not be empty.")
+ # Test template output diapar, this shoule be removed?
+ constant_file_start = self.edi_template_output_diapar.constant_file_start
+ constant_file_end = self.edi_template_output_diapar.constant_file_end
+ customer_code = self.edi_template_output_diapar.customer_code
+ vrp_code = self.edi_template_output_diapar.vrp_code
+ mapping_line_1 = "C" + self.edi_config_diapar.get_datetime_format_ddmmyyyy(
+ order.date_planned
+ )
+ mapping_line_2 = "".join(
+ [
+ ("D" if vals["origin_code"] == "supplier" else "E")
+ + self.edi_config_diapar._fix_lenght(
+ vals["code"], 6, mode="string", replace="0", position="after"
+ )
+ + self.edi_config_diapar._fix_lenght(
+ vals["quantity"], 3, mode="float", replace="0", position="before"
+ )
+ for _product, vals in order._consolidate_products().items()
+ ]
+ )
+ self.assertEqual(
+ str(file_content).strip(),
+ f"{constant_file_start} A{vrp_code}B{customer_code}"
+ f"{mapping_line_1}{mapping_line_2}{constant_file_end}",
+ )
+
+ def _generate_fake_input_ble_content(self, order, new_quantity):
+ """
+ Example:
+ 1AAAAABBBBBCCDDD2026030420260305EEEE
+ 2AAAAABBBBBCCDDD987654UUUUUUUUUUUUUUUUUUUUUUUUUVVVVV00007-GG
+ (where:
+ - 1 is the header code, 2 is the lines code
+ - 20260305 is the planned date
+ - 987654 is the product code
+ - 000007 is the new quantity)
+ """
+ partner_id = order.partner_id.id
+ date_planned = order.date_planned.strftime("%Y%m%d")
+ header = f"1AAAAABBBBBCCDDD20260304{date_planned}EEEEEE"
+ lines = []
+ for order_line in order.order_line:
+ product = order_line.product_id
+ code, _origin_code = product._get_supplier_code_or_ean(partner_id)
+ line = (
+ f"2AAAAABBBBBCCDDD{code}UUUUUUUUUUUUUUUUUUUUUUUUU"
+ + f"VVVVV{str(new_quantity).zfill(5)}-GG"
+ )
+ lines.append(line)
+ return "\n".join([header] + lines)
+
+ def test_edi_input_process_ble(self):
+ new_quantity = 7
+ order = self._create_purchase_order(self.edi_supplier, self.product_5)
+ order.write({"date_planned": "2026-03-05"})
+ order.button_confirm()
+ exchange_record_out = order.exchange_record_ids[0]
+ exchange_record_out.backend_id.exchange_generate(exchange_record_out)
+
+ exchange_record_ble = self.env["edi.exchange.record"].create(
+ {
+ "backend_id": self.backend.id,
+ "type_id": self.exchange_type_ble.id,
+ "model": "purchase.order",
+ "res_id": order.id,
+ }
+ )
+ self.assertEqual(order.exchange_record_count, 2)
+
+ fake_content = self._generate_fake_input_ble_content(order, new_quantity)
+ exchange_record_ble._set_file_content(fake_content)
+ exchange_record_ble.write({"edi_exchange_state": "input_received"})
+ exchange_record_ble.backend_id.exchange_process(exchange_record_ble)
+ self.assertEqual(
+ exchange_record_ble.edi_exchange_state,
+ "input_processed",
+ "The EDI exchange record should be processed successfully.",
+ )
+ order_picking = order.picking_ids[0]
+ picking_update = self.env["picking.update"].search(
+ [("name", "=", order_picking.id)]
+ )
+ self.assertTrue(picking_update, "A picking update record should be created.")
+ self.assertTrue(
+ picking_update.done, "The picking update should be marked as done."
+ )
+ moves = order_picking.move_ids
+ self.assertEqual(
+ len(moves), 1, "There should be one stock move in the picking."
+ )
+ self.assertEqual(
+ moves[0].quantity,
+ new_quantity,
+ "The stock move quantity should be updated to the new quantity.",
+ )
+
+ def _generate_fake_input_ch_content(self):
+ product_code = self.supplier_info_5.product_code
+ product_name = "BISCOTTE 6CEREALE.300HEUD"
+ price_ht = "000015500"
+ apply_date = "20060305"
+ return (
+ f"07{product_code}00015{product_name}{price_ht}"
+ + f"0000214100024000033924604808270011013700000030000{apply_date}0001403"
+ )
+
+ def test_edi_input_process_ch(self):
+ order = self._create_purchase_order(self.edi_supplier, self.product_5)
+ order.button_confirm()
+ exchange_record_ch = self.env["edi.exchange.record"].create(
+ {
+ "backend_id": self.backend.id,
+ "type_id": self.exchange_type_ch.id,
+ "model": "purchase.order",
+ "res_id": order.id,
+ }
+ )
+ fake_content = self._generate_fake_input_ch_content().strip()
+ exchange_record_ch._set_file_content(fake_content)
+ exchange_record_ch.write({"edi_exchange_state": "input_received"})
+ exchange_record_ch.backend_id.exchange_process(exchange_record_ch)
+ self.assertEqual(
+ exchange_record_ch.edi_exchange_state,
+ "input_processed",
+ "The EDI exchange record should be processed successfully.",
+ )
+
+ supplier_price_list = self.env["supplier.price.list"].search(
+ [
+ ("supplier_id", "=", self.edi_supplier.id),
+ ("product_tmpl_id", "=", self.product_5.product_tmpl_id.id),
+ ]
+ )
+ self.assertTrue(
+ supplier_price_list, "A supplier price list record should be created."
+ )
+ self.assertEqual(
+ supplier_price_list.price,
+ 1.55,
+ "The price in the supplier price list should be updated to the new price.",
+ )
+ supllier_info = self.env["product.supplierinfo"].search(
+ [
+ ("partner_id", "=", self.edi_supplier.id),
+ ("product_code", "=", self.supplier_info_5.product_code),
+ ]
+ )
+ self.assertTrue(supllier_info, "The supplier info record should exist.")
+ self.assertEqual(
+ supllier_info.price,
+ 1.55,
+ "The base price in the supplier info should be updated to the new price.",
+ )
diff --git a/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml b/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml
new file mode 100644
index 000000000..2dd1b03ae
--- /dev/null
+++ b/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml
@@ -0,0 +1,18 @@
+
+
+
+ edi.exchange.template.output
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_diapar_oca/views/exchange_type_views.xml b/edi_purchase_diapar_oca/views/exchange_type_views.xml
new file mode 100644
index 000000000..280da8e30
--- /dev/null
+++ b/edi_purchase_diapar_oca/views/exchange_type_views.xml
@@ -0,0 +1,35 @@
+
+
+
+ edi.exchange.type.form.inherit
+ edi.exchange.type
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_diapar_oca/views/menus.xml b/edi_purchase_diapar_oca/views/menus.xml
new file mode 100644
index 000000000..ca0a422a9
--- /dev/null
+++ b/edi_purchase_diapar_oca/views/menus.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_diapar_oca/views/picking_update_views.xml b/edi_purchase_diapar_oca/views/picking_update_views.xml
new file mode 100644
index 000000000..43837f8cc
--- /dev/null
+++ b/edi_purchase_diapar_oca/views/picking_update_views.xml
@@ -0,0 +1,57 @@
+
+
+
+ picking.update.tree
+ picking.update
+
+
+
+
+
+
+
+
+
+
+ picking.update.form
+ picking.update
+
+
+
+
+
+
+ Delivery Order Update
+ ir.actions.act_window
+ picking.update
+ list,form
+
+
diff --git a/edi_purchase_diapar_oca/views/product_template_views.xml b/edi_purchase_diapar_oca/views/product_template_views.xml
new file mode 100644
index 000000000..f38dd4289
--- /dev/null
+++ b/edi_purchase_diapar_oca/views/product_template_views.xml
@@ -0,0 +1,19 @@
+
+
+
+ product.template
+
+ 20
+
+
+
+
+
+
+
diff --git a/edi_purchase_diapar_oca/views/res_config_settings_views.xml b/edi_purchase_diapar_oca/views/res_config_settings_views.xml
new file mode 100644
index 000000000..c1e4399a1
--- /dev/null
+++ b/edi_purchase_diapar_oca/views/res_config_settings_views.xml
@@ -0,0 +1,23 @@
+
+
+
+ res.config.settings.view.form.inherit.purchase
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_diapar_oca/views/res_partner_views.xml b/edi_purchase_diapar_oca/views/res_partner_views.xml
new file mode 100644
index 000000000..01df2b7fd
--- /dev/null
+++ b/edi_purchase_diapar_oca/views/res_partner_views.xml
@@ -0,0 +1,17 @@
+
+
+
+ view.partner.form.inherit
+ res.partner
+
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_diapar_oca/views/supplier_price_list_views.xml b/edi_purchase_diapar_oca/views/supplier_price_list_views.xml
new file mode 100644
index 000000000..3feb0856b
--- /dev/null
+++ b/edi_purchase_diapar_oca/views/supplier_price_list_views.xml
@@ -0,0 +1,81 @@
+
+
+
+ product.price.history.search
+ supplier.price.list
+
+
+
+
+
+
+
+
+
+
+ product.price.history.tree
+ supplier.price.list
+
+
+
+
+
+
+
+
+
+
+
+
+
+ product.price.history.form
+ supplier.price.list
+
+
+
+
+
+
+ Price History
+ ir.actions.act_window
+ supplier.price.list
+ list,form
+
+
+
+ Product Price History
+ ir.actions.act_window
+ supplier.price.list
+ {
+ 'search_default_product_tmpl_id': [active_id],
+ 'default_product_tmpl_id': active_id
+ }
+
+
+
diff --git a/edi_purchase_oca/README.rst b/edi_purchase_oca/README.rst
new file mode 100644
index 000000000..e35506069
--- /dev/null
+++ b/edi_purchase_oca/README.rst
@@ -0,0 +1,89 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
+============
+EDI Purchase
+============
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:35895d0baca724ca82a2652ae75fb72231316d24ea998ba0cfeeb49648bcfa83
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
+ :alt: License: LGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi--framework-lightgray.png?logo=github
+ :target: https://github.com/OCA/edi-framework/tree/18.0/edi_purchase_oca
+ :alt: OCA/edi-framework
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_purchase_oca
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=18.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+Handle purchase orders via EDI.
+
+This is a base module to plug purchase processes with the EDI framework.
+
+To handle inbound/outbound purchase orders, you need to create your own
+integration modules on top of this base module.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Bug Tracker
+===========
+
+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
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* ForgeFlow
+* Camptocamp
+
+Contributors
+------------
+
+- Lois Rilo lois.rilo@forgeflow.com
+- Simone Orsi simone.orsi@camptocamp.com
+- Phan Hong Phuc
+- Maksym Yankin maksym.yankin@camptocamp.com
+
+Maintainers
+-----------
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/edi-framework `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/edi_purchase_oca/__init__.py b/edi_purchase_oca/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/edi_purchase_oca/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/edi_purchase_oca/__manifest__.py b/edi_purchase_oca/__manifest__.py
new file mode 100644
index 000000000..211961ea0
--- /dev/null
+++ b/edi_purchase_oca/__manifest__.py
@@ -0,0 +1,30 @@
+# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+{
+ "name": "EDI Purchase",
+ "summary": """
+ Define EDI Configuration for Purchase Orders""",
+ "version": "18.0.1.0.0",
+ "license": "LGPL-3",
+ "author": "ForgeFlow, Camptocamp, Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/edi-framework",
+ "depends": [
+ "purchase",
+ "edi_core_oca",
+ "edi_record_metadata_oca",
+ ],
+ "data": [
+ # Data
+ "data/edi_configuration.xml",
+ # Views
+ "views/edi_exchange_record_views.xml",
+ "views/purchase_order_views.xml",
+ "views/res_partner_view.xml",
+ ],
+ "demo": [
+ "demo/edi_backend.xml",
+ "demo/edi_exchange_type.xml",
+ "demo/edi_configuration.xml",
+ ],
+}
diff --git a/edi_purchase_oca/data/edi_configuration.xml b/edi_purchase_oca/data/edi_configuration.xml
new file mode 100644
index 000000000..41417f92c
--- /dev/null
+++ b/edi_purchase_oca/data/edi_configuration.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ On PO state change
+ on_edi_purchase_order_state_change
+ Trigger when a purchase order state changes
+
+
+
diff --git a/edi_purchase_oca/demo/edi_backend.xml b/edi_purchase_oca/demo/edi_backend.xml
new file mode 100644
index 000000000..3410efd87
--- /dev/null
+++ b/edi_purchase_oca/demo/edi_backend.xml
@@ -0,0 +1,11 @@
+
+
+
+ Purchase DEMO
+ purchase_demo
+
+
+ purchase DEMO
+
+
+
diff --git a/edi_purchase_oca/demo/edi_configuration.xml b/edi_purchase_oca/demo/edi_configuration.xml
new file mode 100644
index 000000000..6f614a31f
--- /dev/null
+++ b/edi_purchase_oca/demo/edi_configuration.xml
@@ -0,0 +1,36 @@
+
+
+
+ Demo Purchase Order - order confirmed
+ Show case how you can send out an order automatically
+
+
+
+
+
+# ('draft', 'RFQ'),
+# ('sent', 'RFQ Sent'),
+# ('to approve', 'To Approve'),
+# ('purchase', 'Purchase Order'),
+# ('cancel', 'Cancelled')
+if record.state == 'purchase':
+ record._edi_send_via_edi(conf.type_id)
+
+
+
+ Demo Purchase Order - order cancelled
+ Show case how you can send out an order automatically
+
+
+
+
+
+if record.state == 'cancel':
+ record._edi_send_via_edi(conf.type_id)
+
+
+
diff --git a/edi_purchase_oca/demo/edi_exchange_type.xml b/edi_purchase_oca/demo/edi_exchange_type.xml
new file mode 100644
index 000000000..161349aae
--- /dev/null
+++ b/edi_purchase_oca/demo/edi_exchange_type.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Demo Purchase Order out
+ demo_PurchaseOrder_out
+ output
+ {record_name}-{type.code}-{dt}
+ xml
+
+
diff --git a/edi_purchase_oca/i18n/edi_purchase_oca.pot b/edi_purchase_oca/i18n/edi_purchase_oca.pot
new file mode 100644
index 000000000..249b4e6aa
--- /dev/null
+++ b/edi_purchase_oca/i18n/edi_purchase_oca.pot
@@ -0,0 +1,153 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_purchase_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: edi_purchase_oca
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form
+msgid "EDI"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model,name:edi_purchase_oca.model_res_partner
+msgid "Contact"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_disable_auto
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_disable_auto
+msgid "Disable auto"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form
+msgid "Disable automated actions"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_root
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form
+msgid "EDI"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_id
+msgid "EDI ID"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_edi_endpoint_id
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__origin_edi_endpoint_id
+msgid "EDI origin endpoint"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_type_id
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__origin_exchange_type_id
+msgid "EDI origin exchange type"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_record_id
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__origin_exchange_record_id
+msgid "EDI origin record"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_res_partner__edi_purchase_conf_ids
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_res_users__edi_purchase_conf_ids
+msgid "EDI purchase configuration"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__origin_exchange_record_id
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order_line__origin_exchange_record_id
+msgid "EDI record that originated this document."
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_config
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_config
+msgid "Edi Config"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_exchange_ready
+msgid "Edi Exchange Ready"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_has_form_config
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_has_form_config
+msgid "Edi Has Form Config"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_ids
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__exchange_record_ids
+msgid "Exchange Record"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_count
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__exchange_record_count
+msgid "Exchange Record Count"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_related_record_ids
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__exchange_related_record_ids
+msgid "Exchange Related Record"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_exchange_record
+msgid "Exchanges"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order_line__edi_id
+msgid "Internal or external identifier for records."
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.view_partner_form
+msgid "Purchase"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model,name:edi_purchase_oca.model_purchase_order
+msgid "Purchase Order"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.actions.act_window,name:edi_purchase_oca.act_open_edi_exchange_record_purchase_order_view
+msgid "Purchase Order Exchange Records"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model,name:edi_purchase_oca.model_purchase_order_line
+msgid "Purchase Order Line"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__origin_edi_endpoint_id
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order_line__origin_edi_endpoint_id
+msgid "Record generated via this endpoint"
+msgstr ""
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__edi_disable_auto
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order_line__edi_disable_auto
+msgid "When marked, EDI automatic processing will be avoided"
+msgstr ""
diff --git a/edi_purchase_oca/i18n/es.po b/edi_purchase_oca/i18n/es.po
new file mode 100644
index 000000000..b10ade041
--- /dev/null
+++ b/edi_purchase_oca/i18n/es.po
@@ -0,0 +1,112 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_purchase_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 14.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2023-11-25 11:34+0000\n"
+"Last-Translator: Ivorra78 \n"
+"Language-Team: none\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.17\n"
+
+#. module: edi_purchase_oca
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form
+msgid "EDI"
+msgstr "EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__disable_edi_auto
+msgid "Disable auto"
+msgstr "Deshabilitar auto"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__display_name
+msgid "Display Name"
+msgstr "Mostrar Nombre"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_edi_endpoint_id
+msgid "EDI origin endpoint"
+msgstr "Punto final de origen EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_type_id
+msgid "EDI origin exchange type"
+msgstr "Tipo de intercambio de origen EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_record_id
+msgid "EDI origin record"
+msgstr "Registro de origen EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__origin_exchange_record_id
+msgid "EDI record that originated this document."
+msgstr "Registro EDI que originó este documento."
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_config
+msgid "Edi Config"
+msgstr "Configuración Edi"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_has_form_config
+msgid "Edi Has Form Config"
+msgstr "Edi Tiene Formulario Config"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_ids
+msgid "Exchange Record"
+msgstr "Registro de Intercambio"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_count
+msgid "Exchange Record Count"
+msgstr "Recuento de Registros de Intercambio"
+
+#. module: edi_purchase_oca
+#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_root
+msgid "Exchange records"
+msgstr "Registros de intercambio"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__id
+msgid "ID"
+msgstr "ID"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order____last_update
+msgid "Last Modified on"
+msgstr "Última actualización el"
+
+#. module: edi_purchase_oca
+#: model:ir.model,name:edi_purchase_oca.model_purchase_order
+msgid "Purchase Order"
+msgstr "Orden de Compra"
+
+#. module: edi_purchase_oca
+#: model:ir.actions.act_window,name:edi_purchase_oca.act_open_edi_exchange_record_purchase_order_view
+msgid "Purchase Order Exchange Record"
+msgstr "Registro de Intercambio de Órdenes de Compra"
+
+#. module: edi_purchase_oca
+#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_exchange_record
+msgid "Purchase Orders"
+msgstr "Órdenes de Compra"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__origin_edi_endpoint_id
+msgid "Record generated via this endpoint"
+msgstr "Registro generado a través de este punto final"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__disable_edi_auto
+msgid "When marked, EDI automatic processing will be avoided"
+msgstr "Si se marca, se evitará el procesamiento automático EDI"
diff --git a/edi_purchase_oca/i18n/it.po b/edi_purchase_oca/i18n/it.po
new file mode 100644
index 000000000..173176104
--- /dev/null
+++ b/edi_purchase_oca/i18n/it.po
@@ -0,0 +1,156 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * edi_purchase_oca
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2026-05-29 13:46+0000\n"
+"Last-Translator: mymage \n"
+"Language-Team: none\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.15.2\n"
+
+#. module: edi_purchase_oca
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form
+msgid "EDI"
+msgstr "EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model,name:edi_purchase_oca.model_res_partner
+msgid "Contact"
+msgstr "Contatto"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_disable_auto
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_disable_auto
+msgid "Disable auto"
+msgstr "Disabilita automatico"
+
+#. module: edi_purchase_oca
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form
+msgid "Disable automated actions"
+msgstr "Disabilita azioni automatiche"
+
+#. module: edi_purchase_oca
+#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_root
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form
+msgid "EDI"
+msgstr "EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_id
+msgid "EDI ID"
+msgstr "ID EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_edi_endpoint_id
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__origin_edi_endpoint_id
+msgid "EDI origin endpoint"
+msgstr "Endpoint origine EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_type_id
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__origin_exchange_type_id
+msgid "EDI origin exchange type"
+msgstr "Tipo scambio origine EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_record_id
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__origin_exchange_record_id
+msgid "EDI origin record"
+msgstr "Record origine EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_res_partner__edi_purchase_conf_ids
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_res_users__edi_purchase_conf_ids
+msgid "EDI purchase configuration"
+msgstr "Configurazione acquisto EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__origin_exchange_record_id
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order_line__origin_exchange_record_id
+msgid "EDI record that originated this document."
+msgstr "Record EDI che ha generato questo documento."
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_config
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_config
+msgid "Edi Config"
+msgstr "Configurazione EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_exchange_ready
+msgid "Edi Exchange Ready"
+msgstr "Pronto allo scambio EDI"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_has_form_config
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__edi_has_form_config
+msgid "Edi Has Form Config"
+msgstr "EDI ha una maschera di configurazione"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_ids
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__exchange_record_ids
+msgid "Exchange Record"
+msgstr "Record scambio"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_count
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__exchange_record_count
+msgid "Exchange Record Count"
+msgstr "Conteggio record scambio"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_related_record_ids
+#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order_line__exchange_related_record_ids
+msgid "Exchange Related Record"
+msgstr "Record relativo allo scambio"
+
+#. module: edi_purchase_oca
+#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_exchange_record
+msgid "Exchanges"
+msgstr "Scambi"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order_line__edi_id
+msgid "Internal or external identifier for records."
+msgstr "Identificatore interno o esterno per i record."
+
+#. module: edi_purchase_oca
+#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.view_partner_form
+msgid "Purchase"
+msgstr "Acquisto"
+
+#. module: edi_purchase_oca
+#: model:ir.model,name:edi_purchase_oca.model_purchase_order
+msgid "Purchase Order"
+msgstr "Ordine di acquisto"
+
+#. module: edi_purchase_oca
+#: model:ir.actions.act_window,name:edi_purchase_oca.act_open_edi_exchange_record_purchase_order_view
+msgid "Purchase Order Exchange Records"
+msgstr "Record scambio ordine acquisto"
+
+#. module: edi_purchase_oca
+#: model:ir.model,name:edi_purchase_oca.model_purchase_order_line
+msgid "Purchase Order Line"
+msgstr "Riga ordine di acquisto"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__origin_edi_endpoint_id
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order_line__origin_edi_endpoint_id
+msgid "Record generated via this endpoint"
+msgstr "Record generato attraverso questo endpoint"
+
+#. module: edi_purchase_oca
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__edi_disable_auto
+#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order_line__edi_disable_auto
+msgid "When marked, EDI automatic processing will be avoided"
+msgstr "Quando selezionata, l'elaborazione EDI automatica verrà evitata"
diff --git a/edi_purchase_oca/models/__init__.py b/edi_purchase_oca/models/__init__.py
new file mode 100644
index 000000000..7b66e4fca
--- /dev/null
+++ b/edi_purchase_oca/models/__init__.py
@@ -0,0 +1,3 @@
+from . import purchase_order_line
+from . import purchase_order
+from . import res_partner
diff --git a/edi_purchase_oca/models/purchase_order.py b/edi_purchase_oca/models/purchase_order.py
new file mode 100644
index 000000000..f066ce6c7
--- /dev/null
+++ b/edi_purchase_oca/models/purchase_order.py
@@ -0,0 +1,27 @@
+# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo import models
+
+
+class PurchaseOrder(models.Model):
+ _name = "purchase.order"
+ _inherit = [
+ "purchase.order",
+ "edi.exchange.consumer.mixin",
+ ]
+
+ def _edi_config_field_relation(self):
+ return self.partner_id.edi_purchase_conf_ids
+
+ # edi_record_metadata api
+ def _edi_get_metadata_to_store(self, orig_vals):
+ data = super()._edi_get_metadata_to_store(orig_vals)
+ line_vals_by_edi_id = {}
+ for line_vals in orig_vals.get("order_line", []):
+ vals = line_vals[-1]
+ edi_id = vals.get("edi_id")
+ if edi_id:
+ line_vals_by_edi_id[edi_id] = vals
+ data.update({"orig_values": {"lines": line_vals_by_edi_id}})
+ return data
diff --git a/edi_purchase_oca/models/purchase_order_line.py b/edi_purchase_oca/models/purchase_order_line.py
new file mode 100644
index 000000000..9cec4f939
--- /dev/null
+++ b/edi_purchase_oca/models/purchase_order_line.py
@@ -0,0 +1,36 @@
+# Copyright 2026 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import api, fields, models
+
+
+class PurchaseOrderLine(models.Model):
+ _name = "purchase.order.line"
+ _inherit = [
+ "purchase.order.line",
+ "edi.exchange.consumer.mixin",
+ "edi.id.mixin",
+ ]
+
+ edi_disable_auto = fields.Boolean(related="order_id.edi_disable_auto")
+ edi_exchange_ready = fields.Boolean(compute="_compute_edi_exchange_ready")
+
+ @api.depends()
+ def _compute_edi_exchange_ready(self):
+ for rec in self:
+ rec.edi_exchange_ready = rec._edi_exchange_ready()
+
+ def _edi_exchange_ready(self):
+ # Only product lines are eligible for EDI processing
+ # sections/notes and downpayment lines should be ignored
+ return not self.display_type and not self.is_downpayment
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ # Set default origin if not passed
+ for vals in vals_list:
+ orig_id = vals.get("origin_exchange_record_id")
+ if not orig_id and "order_id" in vals:
+ order = self.env["purchase.order"].browse(vals["order_id"])
+ vals["origin_exchange_record_id"] = order.origin_exchange_record_id.id
+ return super().create(vals_list)
diff --git a/edi_purchase_oca/models/res_partner.py b/edi_purchase_oca/models/res_partner.py
new file mode 100644
index 000000000..7c38d54b4
--- /dev/null
+++ b/edi_purchase_oca/models/res_partner.py
@@ -0,0 +1,18 @@
+# Copyright 2024 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ edi_purchase_conf_ids = fields.Many2many(
+ string="EDI purchase configuration",
+ comodel_name="edi.configuration",
+ relation="res_partner_edi_purchase_configuration_rel",
+ column1="partner_id",
+ column2="conf_id",
+ domain=[("model_name", "=", "purchase.order")],
+ )
diff --git a/edi_purchase_oca/pyproject.toml b/edi_purchase_oca/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/edi_purchase_oca/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/edi_purchase_oca/readme/CONTRIBUTORS.md b/edi_purchase_oca/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..1ea50d1f7
--- /dev/null
+++ b/edi_purchase_oca/readme/CONTRIBUTORS.md
@@ -0,0 +1,4 @@
+* Lois Rilo
+* Simone Orsi
+* Phan Hong Phuc \<\>
+* Maksym Yankin
\ No newline at end of file
diff --git a/edi_purchase_oca/readme/DESCRIPTION.md b/edi_purchase_oca/readme/DESCRIPTION.md
new file mode 100644
index 000000000..0ea465878
--- /dev/null
+++ b/edi_purchase_oca/readme/DESCRIPTION.md
@@ -0,0 +1,6 @@
+Handle purchase orders via EDI.
+
+This is a base module to plug purchase processes with the EDI framework.
+
+To handle inbound/outbound purchase orders, you need to create your own
+integration modules on top of this base module.
diff --git a/edi_purchase_oca/static/description/icon.png b/edi_purchase_oca/static/description/icon.png
new file mode 100644
index 000000000..a79752645
Binary files /dev/null and b/edi_purchase_oca/static/description/icon.png differ
diff --git a/edi_purchase_oca/static/description/index.html b/edi_purchase_oca/static/description/index.html
new file mode 100644
index 000000000..228082825
--- /dev/null
+++ b/edi_purchase_oca/static/description/index.html
@@ -0,0 +1,436 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+EDI Purchase
+
+
+Handle purchase orders via EDI.
+This is a base module to plug purchase processes with the EDI framework.
+To handle inbound/outbound purchase orders, you need to create your own
+integration modules on top of this base module.
+Table of contents
+
+
+- Bug Tracker
+- Credits
+
+
+
+
+Bug Tracker
+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
+feedback.
+Do not contact contributors directly about support or help with technical issues.
+
+
+Credits
+
+Authors
+
+- ForgeFlow
+- Camptocamp
+
+
+
+Contributors
+
+- Lois Rilo lois.rilo@forgeflow.com
+- Simone Orsi simone.orsi@camptocamp.com
+- Phan Hong Phuc <phucph@trobz.com>
+- Maksym Yankin maksym.yankin@camptocamp.com
+
+
+
+Maintainers
+This module is maintained by the OCA.
+
+
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+This module is part of the OCA/edi-framework project on GitHub.
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
+
diff --git a/edi_purchase_oca/tests/__init__.py b/edi_purchase_oca/tests/__init__.py
new file mode 100644
index 000000000..6bdd2b970
--- /dev/null
+++ b/edi_purchase_oca/tests/__init__.py
@@ -0,0 +1,2 @@
+from . import test_generate
+from . import test_order
diff --git a/edi_purchase_oca/tests/common.py b/edi_purchase_oca/tests/common.py
new file mode 100644
index 000000000..80d99c031
--- /dev/null
+++ b/edi_purchase_oca/tests/common.py
@@ -0,0 +1,82 @@
+# Copyright 2026 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields
+from odoo.fields import Command
+
+from odoo.addons.edi_core_oca.tests.common import EDIBackendTestMixin
+
+
+class PurchaseEDIBackendTestMixin(EDIBackendTestMixin):
+ @classmethod
+ def _get_backend_type(cls):
+ backend_type = cls.env["edi.backend.type"].search(
+ [("code", "=", "purchase_demo")], limit=1
+ )
+ if backend_type:
+ return backend_type
+ return cls.env["edi.backend.type"].create(
+ {
+ "name": "Purchase DEMO",
+ "code": "purchase_demo",
+ }
+ )
+
+ @classmethod
+ def _get_backend(cls):
+ backend_type = cls._get_backend_type()
+ backend = cls.env["edi.backend"].search(
+ [("backend_type_id", "=", backend_type.id)], limit=1
+ )
+ if backend:
+ return backend
+ return cls.env["edi.backend"].create(
+ {
+ "name": "purchase DEMO",
+ "backend_type_id": backend_type.id,
+ }
+ )
+
+ @classmethod
+ def _create_exchange_type(cls, **kw):
+ model = cls.env["edi.exchange.type"]
+ code = kw.get("code")
+ if code:
+ exchange_type = model.search(
+ [("code", "=", code), ("backend_id", "=", cls.backend.id)], limit=1
+ )
+ if exchange_type:
+ return exchange_type
+ return super()._create_exchange_type(**kw)
+
+
+class OrderMixin:
+ @classmethod
+ def _create_purchase_order(cls, **kw):
+ model = cls.env["purchase.order"]
+ vals = {
+ "partner_id": cls.vendor.id,
+ "user_id": cls.env.ref("base.user_admin").id,
+ "date_planned": fields.Datetime.now(),
+ }
+ vals.update(kw)
+ if hasattr(model, "play_onchanges"):
+ po_vals = model.play_onchanges(vals, [])
+ else:
+ po_vals = vals.copy()
+ if "order_line" in vals:
+ po_vals["order_line"] = [Command.create(x) for x in vals["order_line"]]
+ return model.create(po_vals)
+
+ @classmethod
+ def _setup_order_records(cls):
+ cls.vendor = cls.env["res.partner"].create(
+ {"name": "ACME inc", "country_id": cls.env.company.country_id.id}
+ )
+ cls.product = cls.env["product.product"].create(
+ {
+ "name": "Product 1",
+ "default_code": "1234567",
+ "purchase_ok": True,
+ }
+ )
diff --git a/edi_purchase_oca/tests/test_generate.py b/edi_purchase_oca/tests/test_generate.py
new file mode 100644
index 000000000..0f814b911
--- /dev/null
+++ b/edi_purchase_oca/tests/test_generate.py
@@ -0,0 +1,96 @@
+# Copyright 2026 Camptocamp SA
+# @author Simone Orsi
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.tests.common import TransactionCase
+
+from .common import OrderMixin, PurchaseEDIBackendTestMixin
+
+
+class TestGenerateViaConf(TransactionCase, PurchaseEDIBackendTestMixin, OrderMixin):
+ """Verify that purchase EDI generation is driven by ``edi.configuration``.
+
+ No component / no fake handler: we simply assert that the snippets bound
+ to the partner via ``partner_id.edi_purchase_conf_ids`` are executed by
+ the state-change event dispatched by ``edi.exchange.consumer.mixin``.
+
+ Each snippet writes a marker on ``conf.description`` so we can verify
+ which configurations actually ran.
+ """
+
+ # Snippet writes the order's state on the conf description if it matches
+ # the expected target state.
+ _snippet_tpl = (
+ "if record.state == '{state}':\n"
+ " conf.write({{'description': "
+ "(conf.description or '') + '|' + record.state}})"
+ )
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls._setup_env()
+ cls._setup_records()
+
+ cls.exc_type = cls._create_exchange_type(
+ name="Demo Purchase Order out",
+ code="demo_PurchaseOrder_out",
+ direction="output",
+ exchange_filename_pattern="{record_name}-{type.code}-{dt}",
+ exchange_file_ext="xml",
+ )
+ cls.state_change_trigger = cls.env.ref(
+ "edi_purchase_oca.edi_conf_trigger_purchase_order_state_change"
+ )
+ purchase_model_id = cls.env["ir.model"]._get_id("purchase.order")
+ cls.edi_conf_confirmed = cls.env["edi.configuration"].create(
+ {
+ "name": "Demo Purchase Order - order confirmed",
+ "type_id": cls.exc_type.id,
+ "backend_id": cls.backend.id,
+ "model_id": purchase_model_id,
+ "trigger_id": cls.state_change_trigger.id,
+ "snippet_do": cls._snippet_tpl.format(state="purchase"),
+ }
+ )
+ cls.edi_conf_cancelled = cls.env["edi.configuration"].create(
+ {
+ "name": "Demo Purchase Order - order cancelled",
+ "type_id": cls.exc_type.id,
+ "backend_id": cls.backend.id,
+ "model_id": purchase_model_id,
+ "trigger_id": cls.state_change_trigger.id,
+ "snippet_do": cls._snippet_tpl.format(state="cancel"),
+ }
+ )
+ cls._setup_order_records()
+
+ def test_new_order_no_conf_no_output(self):
+ # No conf linked to the vendor -> no snippet executed.
+ order = self._create_purchase_order()
+ order.button_confirm()
+ self.assertFalse(self.edi_conf_confirmed.description)
+ self.assertFalse(self.edi_conf_cancelled.description)
+
+ def test_new_order_1conf_output(self):
+ self.vendor.edi_purchase_conf_ids = self.edi_conf_confirmed
+ order = self._create_purchase_order()
+ self.assertFalse(self.edi_conf_confirmed.description)
+ order.button_confirm()
+ self.assertEqual(self.edi_conf_confirmed.description, "|purchase")
+ # The cancelled conf is not even attached to the vendor.
+ self.assertFalse(self.edi_conf_cancelled.description)
+
+ def test_new_order_2conf_output(self):
+ self.vendor.edi_purchase_conf_ids = (
+ self.edi_conf_confirmed | self.edi_conf_cancelled
+ )
+ order = self._create_purchase_order()
+ # Confirm -> only the "confirmed" snippet matches
+ order.button_confirm()
+ self.assertEqual(self.edi_conf_confirmed.description, "|purchase")
+ self.assertFalse(self.edi_conf_cancelled.description)
+ # Cancel -> the "cancelled" snippet matches
+ order.button_cancel()
+ self.assertEqual(self.edi_conf_confirmed.description, "|purchase")
+ self.assertEqual(self.edi_conf_cancelled.description, "|cancel")
diff --git a/edi_purchase_oca/tests/test_order.py b/edi_purchase_oca/tests/test_order.py
new file mode 100644
index 000000000..4fc5ad212
--- /dev/null
+++ b/edi_purchase_oca/tests/test_order.py
@@ -0,0 +1,70 @@
+# Copyright 2026 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.tests.common import TransactionCase
+
+from .common import OrderMixin, PurchaseEDIBackendTestMixin
+
+
+class TestOrder(TransactionCase, PurchaseEDIBackendTestMixin, OrderMixin):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(cls.env.context, edi_framework_action=True))
+ cls._setup_records()
+ cls.exchange_type_in.exchange_filename_pattern = "{record.id}-{type.code}-{dt}"
+ cls.exc_record_in = cls.backend.create_record(
+ cls.exchange_type_in.code, {"edi_exchange_state": "input_received"}
+ )
+ cls._setup_order_records()
+ order_vals = {
+ "order_line": [
+ {
+ "product_id": cls.product.id,
+ "product_qty": 10,
+ "price_unit": 100.0,
+ }
+ ],
+ }
+ cls.order = cls._create_purchase_order(
+ origin_exchange_record_id=cls.exc_record_in.id,
+ **order_vals,
+ )
+
+ def test_line_origin(self):
+ order = self.order
+ self.assertEqual(order.origin_exchange_record_id, self.exc_record_in)
+ lines = order.order_line
+ self.env["purchase.order.line"].create(
+ [
+ {
+ "order_id": order.id,
+ "product_id": self.product.id,
+ "product_qty": 20,
+ "price_unit": 100.0,
+ "edi_id": 2000,
+ },
+ {
+ "order_id": order.id,
+ "product_id": self.product.id,
+ "product_qty": 30,
+ "price_unit": 100.0,
+ "edi_id": 3000,
+ },
+ ]
+ )
+ order.invalidate_recordset()
+ new_line1, new_line2 = order.order_line - lines
+ self.assertEqual(new_line1.origin_exchange_record_id, self.exc_record_in)
+ self.assertEqual(new_line2.origin_exchange_record_id, self.exc_record_in)
+
+ def test_line_exchange_ready(self):
+ line_model = self.env["purchase.order.line"]
+
+ regular_line = line_model.new({"product_id": self.product.id})
+ section_line = line_model.new({"display_type": "line_section"})
+ downpayment_line = line_model.new({"is_downpayment": True})
+
+ self.assertTrue(regular_line.edi_exchange_ready)
+ self.assertFalse(section_line.edi_exchange_ready)
+ self.assertFalse(downpayment_line.edi_exchange_ready)
diff --git a/edi_purchase_oca/views/edi_exchange_record_views.xml b/edi_purchase_oca/views/edi_exchange_record_views.xml
new file mode 100644
index 000000000..5b3dbeb92
--- /dev/null
+++ b/edi_purchase_oca/views/edi_exchange_record_views.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ Purchase Order Exchange Records
+ ir.actions.act_window
+ edi.exchange.record
+ list,form
+ [('model', '=', 'purchase.order')]
+ {}
+
+
+
+
diff --git a/edi_purchase_oca/views/purchase_order_views.xml b/edi_purchase_oca/views/purchase_order_views.xml
new file mode 100644
index 000000000..4c97b26e7
--- /dev/null
+++ b/edi_purchase_oca/views/purchase_order_views.xml
@@ -0,0 +1,52 @@
+
+
+
+
+ purchase.order
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_oca/views/res_partner_view.xml b/edi_purchase_oca/views/res_partner_view.xml
new file mode 100644
index 000000000..4985d326e
--- /dev/null
+++ b/edi_purchase_oca/views/res_partner_view.xml
@@ -0,0 +1,21 @@
+
+
+
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_queue_oca/README.rst b/edi_queue_oca/README.rst
index 4f0005b07..c9b6f4945 100644
--- a/edi_queue_oca/README.rst
+++ b/edi_queue_oca/README.rst
@@ -11,7 +11,7 @@ Edi Queue Oca
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:e3afeef7814c93bbb02342bbea9a025fdac06fcbf906dcc4eb2b221aa4757a1e
+ !! source digest: sha256:273ae337ce810a27ea20810513190a340066e443fdb5d1b3d00065665bf8c6aa
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/edi_queue_oca/__manifest__.py b/edi_queue_oca/__manifest__.py
index 28121311c..dec27cca5 100644
--- a/edi_queue_oca/__manifest__.py
+++ b/edi_queue_oca/__manifest__.py
@@ -4,7 +4,7 @@
{
"name": "Edi Queue Oca",
"summary": """Set Queue Jobs on EDI""",
- "version": "18.0.1.0.1",
+ "version": "18.0.1.0.2",
"license": "LGPL-3",
"author": "Dixmit,Camptocamp,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/edi-framework",
diff --git a/edi_queue_oca/static/description/index.html b/edi_queue_oca/static/description/index.html
index 94c3d5675..c65aff333 100644
--- a/edi_queue_oca/static/description/index.html
+++ b/edi_queue_oca/static/description/index.html
@@ -372,7 +372,7 @@ Edi Queue Oca
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:e3afeef7814c93bbb02342bbea9a025fdac06fcbf906dcc4eb2b221aa4757a1e
+!! source digest: sha256:273ae337ce810a27ea20810513190a340066e443fdb5d1b3d00065665bf8c6aa
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
This module integrates EDI with Queue Job and now the edi exchange
diff --git a/edi_queue_oca/tests/test_backend_jobs.py b/edi_queue_oca/tests/test_backend_jobs.py
index 6998d08eb..44c90eac7 100644
--- a/edi_queue_oca/tests/test_backend_jobs.py
+++ b/edi_queue_oca/tests/test_backend_jobs.py
@@ -20,25 +20,27 @@ class EDIBackendTestJobsCase(EDIBackendCommonTestCase, JobMixin):
def _setup_context(cls):
return dict(super()._setup_context(), queue_job__no_delay=None)
- @classmethod
- def _setup_records(cls): # pylint:disable=missing-return
- super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from odoo.addons.edi_core_oca.tests.fake_models import EdiTestExecution
- cls.loader.update_registry((EdiTestExecution,))
- cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"]
- cls.model = cls.env["ir.model"].search(
+ self.loader.update_registry((EdiTestExecution,))
+ self.ExecutionAbstractModel = self.env["edi.framework.test.execution"]
+ self.model = self.env["ir.model"].search(
[("model", "=", "edi.framework.test.execution")]
)
- cls.exchange_type_out.generate_model_id = cls.model
- cls.exchange_type_out.send_model_id = cls.model
- cls.exchange_type_out.output_validate_model_id = cls.model
- cls.exchange_type_in.receive_model_id = cls.model
- cls.exchange_type_in.process_model_id = cls.model
- cls.exchange_type_in.input_validate_model_id = cls.model
+ self.exchange_type_out.generate_model_id = self.model
+ self.exchange_type_out.send_model_id = self.model
+ self.exchange_type_out.output_validate_model_id = self.model
+ self.exchange_type_in.receive_model_id = self.model
+ self.exchange_type_in.process_model_id = self.model
+ self.exchange_type_in.input_validate_model_id = self.model
+
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
def _get_related_jobs(self, record):
# Use domain in action to find all related jobs
diff --git a/edi_record_metadata_oca/__manifest__.py b/edi_record_metadata_oca/__manifest__.py
index 9b162ace3..4dec99089 100644
--- a/edi_record_metadata_oca/__manifest__.py
+++ b/edi_record_metadata_oca/__manifest__.py
@@ -7,8 +7,8 @@
"summary": """
Allow to store metadata for related records.
""",
- "version": "18.0.1.0.2",
- "development_status": "Alpha",
+ "version": "18.0.1.0.5",
+ "development_status": "Beta",
"license": "LGPL-3",
"website": "https://github.com/OCA/edi-framework",
"author": "Camptocamp, Odoo Community Association (OCA)",
diff --git a/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py b/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py
index ba9080a6e..87b6af99c 100644
--- a/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py
+++ b/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py
@@ -36,11 +36,11 @@ def _edi_get_metadata_to_store(self, orig_vals):
def _edi_store_metadata(self, metadata):
if self.origin_exchange_record_id:
- self.origin_exchange_record_id.set_metadata(metadata)
+ self.origin_exchange_record_id.edi_set_metadata(metadata)
@api.model
def _edi_store_metadata_before_create(self, origin_id, metadata):
- self.env["edi.exchange.record"].browse(origin_id).set_metadata(metadata)
+ self.env["edi.exchange.record"].browse(origin_id).edi_set_metadata(metadata)
def _edi_get_metadata(self):
- return self.origin_exchange_record_id.get_metadata()
+ return self.origin_exchange_record_id.edi_get_metadata()
diff --git a/edi_record_metadata_oca/models/edi_exchange_record.py b/edi_record_metadata_oca/models/edi_exchange_record.py
index 960fafef7..cd9be86cb 100644
--- a/edi_record_metadata_oca/models/edi_exchange_record.py
+++ b/edi_record_metadata_oca/models/edi_exchange_record.py
@@ -25,8 +25,8 @@ def _compute_metadata_display(self):
for rec in self:
rec.metadata_display = json.dumps(rec.metadata, sort_keys=True, indent=4)
- def set_metadata(self, data):
+ def edi_set_metadata(self, data):
self.metadata = data
- def get_metadata(self):
+ def edi_get_metadata(self):
return self.metadata
diff --git a/edi_record_metadata_oca/tests/test_metadata.py b/edi_record_metadata_oca/tests/test_metadata.py
index 62144fe94..28daf71a6 100644
--- a/edi_record_metadata_oca/tests/test_metadata.py
+++ b/edi_record_metadata_oca/tests/test_metadata.py
@@ -10,32 +10,28 @@
class TestEDIMetadata(EDIBackendCommonTestCase):
- @classmethod
- def _setup_records(cls):
- res = super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EDIMetadataConsumerFake
- cls.loader.update_registry((EDIMetadataConsumerFake,))
- cls.consumer_model = cls.env[EDIMetadataConsumerFake._name]
+ self.loader.update_registry((EDIMetadataConsumerFake,))
+ self.consumer_model = self.env[EDIMetadataConsumerFake._name]
- cls.exc_type = cls._create_exchange_type(
+ self.exc_type = self._create_exchange_type(
name="Metadata test",
code="metadata_test",
direction="output",
)
- cls.exc_record = cls.backend.create_record(cls.exc_type.code, {})
- return res
+ self.exc_record = self.backend.create_record(self.exc_type.code, {})
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
def test_fields(self):
- self.exc_record.set_metadata({"foo": "baz", "bar": "waa"})
+ self.exc_record.edi_set_metadata({"foo": "baz", "bar": "waa"})
self.assertTrue(self.exc_record.metadata)
self.assertTrue(self.exc_record.metadata_display)
@@ -48,7 +44,7 @@ def test_no_store(self):
}
)
self.assertFalse(consumer_record._edi_get_metadata())
- self.assertFalse(self.exc_record.get_metadata())
+ self.assertFalse(self.exc_record.edi_get_metadata())
def test_store(self):
vals = {
@@ -70,4 +66,4 @@ def test_store(self):
"additional": True,
}
self.assertEqual(consumer_record._edi_get_metadata(), expected)
- self.assertEqual(self.exc_record.get_metadata(), expected)
+ self.assertEqual(self.exc_record.edi_get_metadata(), expected)
diff --git a/edi_sale_input_oca/README.rst b/edi_sale_input_oca/README.rst
index 1b89a27a9..99ac47dd6 100644
--- a/edi_sale_input_oca/README.rst
+++ b/edi_sale_input_oca/README.rst
@@ -11,7 +11,7 @@ EDI Sales input
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:1458f5d439bfde955a3c1dc00e8c8ab99b674d8bdb518b9bfe6e475df80742a6
+ !! source digest: sha256:031a6af4aecc4f3f856b85d4d793f21481e7f45e181875c59435bf59e17b3239
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
diff --git a/edi_sale_input_oca/__manifest__.py b/edi_sale_input_oca/__manifest__.py
index b182ce1fc..a523ea605 100644
--- a/edi_sale_input_oca/__manifest__.py
+++ b/edi_sale_input_oca/__manifest__.py
@@ -6,7 +6,7 @@
"summary": """
Process incoming sale orders with the EDI framework.
""",
- "version": "18.0.1.0.1",
+ "version": "18.0.1.0.2",
"development_status": "Alpha",
"license": "AGPL-3",
"author": "Camptocamp,Odoo Community Association (OCA)",
diff --git a/edi_sale_input_oca/static/description/index.html b/edi_sale_input_oca/static/description/index.html
index 1ce64058d..2187525cd 100644
--- a/edi_sale_input_oca/static/description/index.html
+++ b/edi_sale_input_oca/static/description/index.html
@@ -372,7 +372,7 @@
EDI Sales input
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:1458f5d439bfde955a3c1dc00e8c8ab99b674d8bdb518b9bfe6e475df80742a6
+!! source digest: sha256:031a6af4aecc4f3f856b85d4d793f21481e7f45e181875c59435bf59e17b3239
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
diff --git a/edi_sale_input_oca/tests/test_process.py b/edi_sale_input_oca/tests/test_process.py
index 2fdea5f5b..bb5ab5c23 100644
--- a/edi_sale_input_oca/tests/test_process.py
+++ b/edi_sale_input_oca/tests/test_process.py
@@ -130,7 +130,7 @@ def test_metadata(self):
order=dict(origin_exchange_record_id=self.record.id)
),
).create_order(parsed_order, "pricelist")
- metadata = self.record.get_metadata()
+ metadata = self.record.edi_get_metadata()
# Lines are mapped via `edi_id` (coming from `order_line_ref` by default)
line_metadata = metadata["orig_values"]["lines"]["1111"]
for k in (
diff --git a/edi_sale_ubl_output_oca/i18n/edi_sale_ubl_output_oca.pot b/edi_sale_ubl_output_oca/i18n/edi_sale_ubl_output_oca.pot
index 197b4f2d4..28651d779 100644
--- a/edi_sale_ubl_output_oca/i18n/edi_sale_ubl_output_oca.pot
+++ b/edi_sale_ubl_output_oca/i18n/edi_sale_ubl_output_oca.pot
@@ -13,46 +13,6 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "1.0"
-msgstr ""
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "12:30:00"
-msgstr ""
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "2.2"
-msgstr ""
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "2010-01-21"
-msgstr ""
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "2010-02-10"
-msgstr ""
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "2010-02-25"
-msgstr ""
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qweb_tmpl_ubl_party
-msgid "7300070011115"
-msgstr ""
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qweb_tmpl_ubl_party
-msgid "7302347231111"
-msgstr ""
-
#. module: edi_sale_ubl_output_oca
#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
msgid "EUR"
diff --git a/edi_sale_ubl_output_oca/i18n/it.po b/edi_sale_ubl_output_oca/i18n/it.po
index 09016fe82..6342ce979 100644
--- a/edi_sale_ubl_output_oca/i18n/it.po
+++ b/edi_sale_ubl_output_oca/i18n/it.po
@@ -16,46 +16,6 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.4\n"
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "1.0"
-msgstr "1.0"
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "12:30:00"
-msgstr "12:30:00"
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "2.2"
-msgstr "2.2"
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "2010-01-21"
-msgstr "2010-01-21"
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "2010-02-10"
-msgstr "2010-02-10"
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
-msgid "2010-02-25"
-msgstr "2010-02-25"
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qweb_tmpl_ubl_party
-msgid "7300070011115"
-msgstr "7300070011115"
-
-#. module: edi_sale_ubl_output_oca
-#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qweb_tmpl_ubl_party
-msgid "7302347231111"
-msgstr "7302347231111"
-
#. module: edi_sale_ubl_output_oca
#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
msgid "EUR"
@@ -85,3 +45,27 @@ msgstr "urn:www.cenbii.eu:profile:BIIXYZ:ver1.0"
#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out
msgid "urn:www.cenbii.eu:transaction:biicoretrdmXYZ:ver1.0"
msgstr "urn:www.cenbii.eu:transaction:biicoretrdmXYZ:ver1.0"
+
+#~ msgid "1.0"
+#~ msgstr "1.0"
+
+#~ msgid "12:30:00"
+#~ msgstr "12:30:00"
+
+#~ msgid "2.2"
+#~ msgstr "2.2"
+
+#~ msgid "2010-01-21"
+#~ msgstr "2010-01-21"
+
+#~ msgid "2010-02-10"
+#~ msgstr "2010-02-10"
+
+#~ msgid "2010-02-25"
+#~ msgstr "2010-02-25"
+
+#~ msgid "7300070011115"
+#~ msgstr "7300070011115"
+
+#~ msgid "7302347231111"
+#~ msgstr "7302347231111"
diff --git a/edi_state_oca/README.rst b/edi_state_oca/README.rst
index 21e8056ad..62956dee6 100644
--- a/edi_state_oca/README.rst
+++ b/edi_state_oca/README.rst
@@ -11,7 +11,7 @@ EDI state
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:b94605243de3a33cc1d60443e16739b5e3c1a135859aab0b21154ff1173e1f0a
+ !! source digest: sha256:e8a144d5155e42c0155d565988d9fecd87ed66b45db2090a072b8829eab6ab34
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
diff --git a/edi_state_oca/__manifest__.py b/edi_state_oca/__manifest__.py
index 51e33f736..2fc7bf39e 100644
--- a/edi_state_oca/__manifest__.py
+++ b/edi_state_oca/__manifest__.py
@@ -7,7 +7,7 @@
"summary": """
Allow to assign specific EDI states to related records.
""",
- "version": "18.0.1.0.2",
+ "version": "18.0.1.0.3",
"development_status": "Alpha",
"license": "LGPL-3",
"website": "https://github.com/OCA/edi-framework",
diff --git a/edi_state_oca/static/description/index.html b/edi_state_oca/static/description/index.html
index efa1a7a75..20ea1443b 100644
--- a/edi_state_oca/static/description/index.html
+++ b/edi_state_oca/static/description/index.html
@@ -372,7 +372,7 @@ EDI state
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:b94605243de3a33cc1d60443e16739b5e3c1a135859aab0b21154ff1173e1f0a
+!! source digest: sha256:e8a144d5155e42c0155d565988d9fecd87ed66b45db2090a072b8829eab6ab34
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
Technical module for the EDI suite to provide additional states for any
diff --git a/edi_state_oca/tests/test_edi_state.py b/edi_state_oca/tests/test_edi_state.py
index 08ba3ec56..55f719643 100644
--- a/edi_state_oca/tests/test_edi_state.py
+++ b/edi_state_oca/tests/test_edi_state.py
@@ -10,63 +10,59 @@
class TestEDIState(EDIBackendCommonTestCase):
- @classmethod
- def _setup_records(cls):
- res = super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EDIStateConsumerFake
- cls.loader.update_registry((EDIStateConsumerFake,))
- cls.consumer_model = cls.env[EDIStateConsumerFake._name]
- cls.consumer_record = cls.consumer_model.create(
+ self.loader.update_registry((EDIStateConsumerFake,))
+ self.consumer_model = self.env[EDIStateConsumerFake._name]
+ self.consumer_record = self.consumer_model.create(
{
"name": "State Test Consumer",
}
)
# Suitable workflow
- cls.wf1_ok = cls.env["edi.state.workflow"].create(
+ self.wf1_ok = self.env["edi.state.workflow"].create(
{
"name": "WF1",
- "backend_type_id": cls.backend.backend_type_id.id,
- "model_id": cls.env["ir.model"]._get(cls.consumer_record._name).id,
+ "backend_type_id": self.backend.backend_type_id.id,
+ "model_id": self.env["ir.model"]._get(self.consumer_record._name).id,
}
)
for i in range(1, 4):
- cls.env["edi.state"].create(
- {"name": f"OK {i}", "code": f"OK_{i}", "workflow_id": cls.wf1_ok.id}
+ self.env["edi.state"].create(
+ {"name": f"OK {i}", "code": f"OK_{i}", "workflow_id": self.wf1_ok.id}
)
# Non suitable workflow
- cls.wf2_ko = cls.env["edi.state.workflow"].create(
+ self.wf2_ko = self.env["edi.state.workflow"].create(
{
"name": "WF2",
- "backend_type_id": cls.backend.backend_type_id.id,
- "model_id": cls.env["ir.model"]._get("res.partner").id,
+ "backend_type_id": self.backend.backend_type_id.id,
+ "model_id": self.env["ir.model"]._get("res.partner").id,
}
)
for i in range(1, 4):
- cls.env["edi.state"].create(
- {"name": f"KO {i}", "code": f"KO_{i}", "workflow_id": cls.wf2_ko.id}
+ self.env["edi.state"].create(
+ {"name": f"KO {i}", "code": f"KO_{i}", "workflow_id": self.wf2_ko.id}
)
- cls.exc_type = cls._create_exchange_type(
+ self.exc_type = self._create_exchange_type(
name="State test",
code="state_test",
direction="output",
- state_workflow_ids=[(6, 0, cls.wf1_ok.ids)],
+ state_workflow_ids=[(6, 0, self.wf1_ok.ids)],
)
vals = {
- "model": cls.consumer_record._name,
- "res_id": cls.consumer_record.id,
+ "model": self.consumer_record._name,
+ "res_id": self.consumer_record.id,
}
- record = cls.backend.create_record("state_test", vals)
- cls.consumer_record._edi_set_origin(record)
- return res
+ record = self.backend.create_record("state_test", vals)
+ self.consumer_record._edi_set_origin(record)
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
def test_is_state_valid(self):
self.assertTrue(self.wf1_ok.is_valid_for_model(self.consumer_model._name))
diff --git a/edi_storage_oca/README.rst b/edi_storage_oca/README.rst
index 52b9f7ece..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:ae52c0238c8bab278e4e2d6a48d86d4222672b893df1f171be046f3b7a3c58ae
+ !! 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 39f47e247..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.2",
+ "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/i18n/edi_storage_oca.pot b/edi_storage_oca/i18n/edi_storage_oca.pot
index cba90f769..7b3703040 100644
--- a/edi_storage_oca/i18n/edi_storage_oca.pot
+++ b/edi_storage_oca/i18n/edi_storage_oca.pot
@@ -114,6 +114,12 @@ msgstr ""
msgid "Storage Handler for EDI"
msgstr ""
+#. module: edi_storage_oca
+#. odoo-python
+#: code:addons/edi_storage_oca/models/edi_oca_storage_handler.py:0
+msgid "Storage error while reading %(path)s: %(error)s"
+msgstr ""
+
#. module: edi_storage_oca
#: model:ir.model.fields,help:edi_storage_oca.field_edi_backend__storage_id
msgid "Storage for in-out files"
diff --git a/edi_storage_oca/i18n/it.po b/edi_storage_oca/i18n/it.po
index e35011ab4..64925eccc 100644
--- a/edi_storage_oca/i18n/it.po
+++ b/edi_storage_oca/i18n/it.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2024-11-15 17:06+0000\n"
+"PO-Revision-Date: 2026-05-29 13:46+0000\n"
"Last-Translator: mymage \n"
"Language-Team: none\n"
"Language: it\n"
@@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 5.6.2\n"
+"X-Generator: Weblate 5.15.2\n"
#. module: edi_storage_oca
#: model:ir.model,name:edi_storage_oca.model_edi_backend
@@ -28,7 +28,6 @@ msgstr "Tipo scambio EDI"
#. module: edi_storage_oca
#: model:ir.actions.server,name:edi_storage_oca.cron_check_storage_pending_input_ir_actions_server
-#: model:ir.cron,cron_name:edi_storage_oca.cron_check_storage_pending_input
msgid "EDI backend storage check pending input"
msgstr "Controllo ingressi in attesa deposito backend EDI"
@@ -51,8 +50,13 @@ msgstr "Deposito FS"
#. module: edi_storage_oca
#: model:ir.model.fields,help:edi_storage_oca.field_edi_exchange_type__exchange_filename_pattern
msgid ""
-"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` and `dt`. For instance, a valid string would be {record_name}-{type.code}-{dt}\n"
-"For input exchange types related to storage backends it should be a regex expression to filter the files to be fetched from the pending directory in the related storage. E.g: `.*my-type-[0-9]*.\\.csv`"
+"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` and `dt`. For instance, a valid "
+"string would be {record_name}-{type.code}-{dt}\n"
+"For input exchange types related to storage backends it should be a regex "
+"expression to filter the files to be fetched from the pending directory in "
+"the related storage. E.g: `.*my-type-[0-9]*.\\.csv`"
msgstr ""
"Per i tipi di scambio in uscita questo dovrebbe essere una stringa di "
"formattazione con le seguenti variabili disponibili (da utilizzare tra "
@@ -120,6 +124,17 @@ msgstr "Record creato da un file trovato nel deposito FS"
msgid "Storage"
msgstr "Deposito"
+#. module: edi_storage_oca
+#: model:ir.model,name:edi_storage_oca.model_edi_oca_storage_handler
+msgid "Storage Handler for EDI"
+msgstr "Gestore archivio per EDI"
+
+#. module: edi_storage_oca
+#. odoo-python
+#: code:addons/edi_storage_oca/models/edi_oca_storage_handler.py:0
+msgid "Storage error while reading %(path)s: %(error)s"
+msgstr "Errore archivio nella lettura di %(path)s: %(error)s"
+
#. module: edi_storage_oca
#: model:ir.model.fields,help:edi_storage_oca.field_edi_backend__storage_id
msgid "Storage for in-out files"
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/models/edi_oca_storage_handler.py b/edi_storage_oca/models/edi_oca_storage_handler.py
index ce0f7682f..3e2a3d938 100644
--- a/edi_storage_oca/models/edi_oca_storage_handler.py
+++ b/edi_storage_oca/models/edi_oca_storage_handler.py
@@ -9,6 +9,7 @@
from pathlib import PurePath
from odoo import models
+from odoo.exceptions import UserError
from .. import utils
@@ -34,7 +35,9 @@ def send(self, exchange_record):
utils.add_file(exchange_record.backend_id.storage_id, path.as_posix(), filedata)
def receive(self, exchange_record):
- return self._get_remote_file(exchange_record, "pending", binary=True)
+ return self._get_remote_file(
+ exchange_record, "pending", binary=True, raise_if_missing=True
+ )
def _dir_by_state(self, backend, direction, state):
"""Return remote directory path by direction and state.
@@ -61,11 +64,21 @@ def _get_remote_file_path(self, exchange_record, state, filename=None):
)
return path
- def _get_remote_file(self, exchange_record, state, filename=None, binary=False):
+ def _get_remote_file(
+ self,
+ exchange_record,
+ state,
+ filename=None,
+ binary=False,
+ raise_if_missing=False,
+ ):
"""Get file for current exchange_record in the given destination state.
:param state: string ("pending", "done", "error")
:param filename: custom file name, exchange_record filename used by default
+ :param raise_if_missing: when True, storage errors are re-raised as
+ swallable exceptions so the caller transitions the record to an
+ error state instead of treating "no content" as a successful read.
:return: remote file content as string
"""
path = self._get_remote_file_path(exchange_record, state, filename=filename)
@@ -84,14 +97,25 @@ def _get_remote_file(self, exchange_record, state, filename=None, binary=False):
path,
state,
)
+ if raise_if_missing:
+ raise
return None
- except OSError:
- _logger.info(
- "Ignored OSError when trying to get file %s into path %s for state %s",
+ except OSError as err:
+ _logger.warning(
+ "OSError when trying to get file %s into path %s for state %s: %s",
filename,
path,
state,
+ err,
)
+ if raise_if_missing:
+ raise UserError(
+ self.env._(
+ "Storage error while reading %(path)s: %(error)s",
+ path=path.as_posix(),
+ error=str(err),
+ )
+ ) from err
return None
def check(self, exchange_record):
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 87b66f49d..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:ae52c0238c8bab278e4e2d6a48d86d4222672b893df1f171be046f3b7a3c58ae
+!! source digest: sha256:f0ff3ee30416f5a8090d6c738df2c392381b61bc0d104c7679a147ce3b503018
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
Allow exchange files using storage backends from OCA/storage.
@@ -398,31 +398,45 @@ EDI Storage backend support
Table of contents
+
+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.
+
-Bug Tracker
+Bug Tracker
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 @@
Bug Tracker
Do not contact contributors directly about support or help with technical issues.
-Credits
+Credits
-Contributors
+Contributors
- Simone Orsi <simahawk@gmail.com>
- Foram Shah <foram.shah@initos.com>
@@ -451,12 +465,12 @@ Contributors
-Other credits
+Other credits
The migration of this module from 15.0 to 16.0 was financially supported
by Camptocamp.
-Maintainers
+Maintainers
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 0b93e5da0..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):
@@ -48,6 +50,8 @@ def list_files(storage, relative_path="", pattern=False):
if pattern:
relative_path = fs.sep.join([relative_path, pattern])
return fs.glob(relative_path)
+ if fs.protocol == "ftp" and fs.ftp:
+ return fs.ftp.nlst(relative_path)
return fs.ls(relative_path, detail=False)
@@ -58,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
diff --git a/eslint.config.cjs b/eslint.config.cjs
index 0d5731f89..dd0cbe0ae 100644
--- a/eslint.config.cjs
+++ b/eslint.config.cjs
@@ -1,3 +1,4 @@
+var globals = require('globals');
jsdoc = require("eslint-plugin-jsdoc");
const config = [{
@@ -16,6 +17,8 @@ const config = [{
openerp: "readonly",
owl: "readonly",
luxon: "readonly",
+ QUnit: "readonly",
+ ...globals.browser,
},
ecmaVersion: 2024,
@@ -191,7 +194,7 @@ const config = [{
},
}, {
- files: ["**/*.esm.js"],
+ files: ["**/*.esm.js", "**/*test.js"],
languageOptions: {
ecmaVersion: 2024,
diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml
index ef3979096..7c59f4867 100644
--- a/setup/_metapackage/pyproject.toml
+++ b/setup/_metapackage/pyproject.toml
@@ -1,16 +1,20 @@
[project]
name = "odoo-addons-oca-edi-framework"
-version = "18.0.20251201.0"
+version = "18.0.20260609.0"
dependencies = [
"odoo-addon-edi_account_core_oca==18.0.*",
"odoo-addon-edi_account_oca==18.0.*",
"odoo-addon-edi_component_oca==18.0.*",
"odoo-addon-edi_core_oca==18.0.*",
"odoo-addon-edi_endpoint_oca==18.0.*",
+ "odoo-addon-edi_exchange_deduplicate_oca==18.0.*",
"odoo-addon-edi_exchange_template_oca==18.0.*",
"odoo-addon-edi_exchange_template_party_data==18.0.*",
+ "odoo-addon-edi_notification_oca==18.0.*",
"odoo-addon-edi_oca==18.0.*",
"odoo-addon-edi_party_data_oca==18.0.*",
+ "odoo-addon-edi_product_oca==18.0.*",
+ "odoo-addon-edi_purchase_oca==18.0.*",
"odoo-addon-edi_queue_oca==18.0.*",
"odoo-addon-edi_record_metadata_oca==18.0.*",
"odoo-addon-edi_sale_endpoint==18.0.*",
diff --git a/test-requirements.txt b/test-requirements.txt
index a8133e4b5..662b87ade 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,2 +1,3 @@
odoo-test-helper
xmlunittest
+odoo-addon-edi_purchase_oca @ git+https://github.com/OCA/edi-framework.git@refs/pull/180/head#subdirectory=edi_purchase_oca
Edi Exchange Deduplicate OCA
+ + +This module adds options for deduplication records before sending step +on type:
+-
+
- deduplicate_on_send: check if a fresher one does not exist for the +same record. If so, mark the oldest one as obsolete. +
- delete_obsolete_records: Delete records marked as obsolete. +
Table of contents
+-
+
- Configuration +
- Usage +
- Bug Tracker +
- Credits + +
Configuration
+Go to “EDI -> Config -> Exchange Type”.
+Enable “Deduplicate on Send” option -> Enable “Delete obsolete records” +option.
+Usage
+With all the types that have been enabled “Deduplicate on Send” option, +this module will check their records if a fresher one does not exist for +the same record. If so, mark the oldest one as obsolete (except +“block_obsolescence” records)
+-
+
- “block_obsolescence” is an technical option on records to avoid +marking them as obsolete. +
With all the types that have been enabled “Delete obsolete records” +option, the cron will remove their obsolete records.
+-
+
- If the records are obsolete, delete them even if their type’s flag has +been disabled. +
Bug Tracker
+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 +feedback.
+Do not contact contributors directly about support or help with technical issues.
+Credits
+Authors
+-
+
- Camptocamp +
Contributors
+-
+
- Simone Orsi <simone.orsi@camptocamp.com> +
- Duong (Tran Quoc) <duongtq@trobz.com> +
Maintainers
+This module is maintained by the OCA.
+ +
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.
+Current maintainers:
+ +This module is part of the OCA/edi-framework project on GitHub.
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+EDI Exchange Template
!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:ebed0a953bbe9571fc02c722ad850e26c3af87cf9ee52fd7f496048a102717ec +!! source digest: sha256:c6b98455272323462e208761ef6f68cff26fd2e6f561245ef60fe8af4fe329e8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->Provide EDI exchange templates to control input/output records contents.
diff --git a/edi_exchange_template_oca/tests/test_edi_backend_output.py b/edi_exchange_template_oca/tests/test_edi_backend_output.py index 89a91bb37..117f170e4 100644 --- a/edi_exchange_template_oca/tests/test_edi_backend_output.py +++ b/edi_exchange_template_oca/tests/test_edi_backend_output.py @@ -242,13 +242,21 @@ def test_generate_file(self): self.assertEqual(file_content.strip(), expected) def test_prettify(self): - self.tmpl_out2.template_id.arch = ( + tmpl_out2 = self.env["edi.exchange.template.output"].browse(self.tmpl_out2.id) + record2 = self.env["edi.exchange.record"].browse(self.record2.id) + self.assertTrue( + tmpl_out2.exists(), "Template output record vanished during test execution" + ) + self.assertTrue( + record2.exists(), "Exchange record vanished during test execution" + ) + tmpl_out2.template_id.arch = ( 'EDI Notification
+ + +This module creates activities for users when an exchange record’s +process fails.
+Exchange types must be configured properly to create such activities:
+-
+
- field “Notify On Process Error” must be checked to activate the +feature for the current exchange type +
- field “Activity Type Used When Notify On Process Error” is used to +define the type of the newly created activity +
- fields “Notify Groups On Process Error” and “Notify Users On Process +Error” are used to define the users that will be assigned to the newly +created activity +
Important
+This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status
+Table of contents
+-
+
- Bug Tracker +
- Credits + +
Bug Tracker
+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 +feedback.
+Do not contact contributors directly about support or help with technical issues.
+Credits
+Authors
+-
+
- Camptocamp +
Contributors
+-
+
- Duong (Tran Quoc) <duontq@troz.com> +
- Simone Orsi <simone.orsi@camptocamp.com> +
Maintainers
+This module is maintained by the OCA.
+ +
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.
+This module is part of the OCA/edi-framework project on GitHub.
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+EDI Product
+ + +Provide basic configuration for products and product packaging with EDI +framework.
+Table of contents
+-
+
- Bug Tracker +
- Credits + +
Bug Tracker
+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 +feedback.
+Do not contact contributors directly about support or help with technical issues.
+Credits
+Authors
+-
+
- ForgeFlow +
Contributors
+-
+
- Oriol Miranda <oriol.miranda@forgeflow.com> +
- Duong (Tran Quoc) <duongtq@trobz.com> +
Maintainers
+This module is maintained by the OCA.
+ +
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.
+This module is part of the OCA/edi-framework project on GitHub.
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+-
+
-
+
-
+
EDI Purchase
+ + +Handle purchase orders via EDI.
+This is a base module to plug purchase processes with the EDI framework.
+To handle inbound/outbound purchase orders, you need to create your own +integration modules on top of this base module.
+Table of contents
+-
+
- Bug Tracker +
- Credits + +
Bug Tracker
+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 +feedback.
+Do not contact contributors directly about support or help with technical issues.
+Credits
+Authors
+-
+
- ForgeFlow +
- Camptocamp +
Contributors
+-
+
- Lois Rilo lois.rilo@forgeflow.com +
- Simone Orsi simone.orsi@camptocamp.com +
- Phan Hong Phuc <phucph@trobz.com> +
- Maksym Yankin maksym.yankin@camptocamp.com +
Maintainers
+This module is maintained by the OCA.
+ +
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.
+This module is part of the OCA/edi-framework project on GitHub.
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+-
+
Edi Queue Oca
!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:e3afeef7814c93bbb02342bbea9a025fdac06fcbf906dcc4eb2b221aa4757a1e +!! source digest: sha256:273ae337ce810a27ea20810513190a340066e443fdb5d1b3d00065665bf8c6aa !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->This module integrates EDI with Queue Job and now the edi exchange diff --git a/edi_queue_oca/tests/test_backend_jobs.py b/edi_queue_oca/tests/test_backend_jobs.py index 6998d08eb..44c90eac7 100644 --- a/edi_queue_oca/tests/test_backend_jobs.py +++ b/edi_queue_oca/tests/test_backend_jobs.py @@ -20,25 +20,27 @@ class EDIBackendTestJobsCase(EDIBackendCommonTestCase, JobMixin): def _setup_context(cls): return dict(super()._setup_context(), queue_job__no_delay=None) - @classmethod - def _setup_records(cls): # pylint:disable=missing-return - super()._setup_records() - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() from odoo.addons.edi_core_oca.tests.fake_models import EdiTestExecution - cls.loader.update_registry((EdiTestExecution,)) - cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"] - cls.model = cls.env["ir.model"].search( + self.loader.update_registry((EdiTestExecution,)) + self.ExecutionAbstractModel = self.env["edi.framework.test.execution"] + self.model = self.env["ir.model"].search( [("model", "=", "edi.framework.test.execution")] ) - cls.exchange_type_out.generate_model_id = cls.model - cls.exchange_type_out.send_model_id = cls.model - cls.exchange_type_out.output_validate_model_id = cls.model - cls.exchange_type_in.receive_model_id = cls.model - cls.exchange_type_in.process_model_id = cls.model - cls.exchange_type_in.input_validate_model_id = cls.model + self.exchange_type_out.generate_model_id = self.model + self.exchange_type_out.send_model_id = self.model + self.exchange_type_out.output_validate_model_id = self.model + self.exchange_type_in.receive_model_id = self.model + self.exchange_type_in.process_model_id = self.model + self.exchange_type_in.input_validate_model_id = self.model + + def tearDown(self): + self.loader.restore_registry() + super().tearDown() def _get_related_jobs(self, record): # Use domain in action to find all related jobs diff --git a/edi_record_metadata_oca/__manifest__.py b/edi_record_metadata_oca/__manifest__.py index 9b162ace3..4dec99089 100644 --- a/edi_record_metadata_oca/__manifest__.py +++ b/edi_record_metadata_oca/__manifest__.py @@ -7,8 +7,8 @@ "summary": """ Allow to store metadata for related records. """, - "version": "18.0.1.0.2", - "development_status": "Alpha", + "version": "18.0.1.0.5", + "development_status": "Beta", "license": "LGPL-3", "website": "https://github.com/OCA/edi-framework", "author": "Camptocamp, Odoo Community Association (OCA)", diff --git a/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py b/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py index ba9080a6e..87b6af99c 100644 --- a/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py @@ -36,11 +36,11 @@ def _edi_get_metadata_to_store(self, orig_vals): def _edi_store_metadata(self, metadata): if self.origin_exchange_record_id: - self.origin_exchange_record_id.set_metadata(metadata) + self.origin_exchange_record_id.edi_set_metadata(metadata) @api.model def _edi_store_metadata_before_create(self, origin_id, metadata): - self.env["edi.exchange.record"].browse(origin_id).set_metadata(metadata) + self.env["edi.exchange.record"].browse(origin_id).edi_set_metadata(metadata) def _edi_get_metadata(self): - return self.origin_exchange_record_id.get_metadata() + return self.origin_exchange_record_id.edi_get_metadata() diff --git a/edi_record_metadata_oca/models/edi_exchange_record.py b/edi_record_metadata_oca/models/edi_exchange_record.py index 960fafef7..cd9be86cb 100644 --- a/edi_record_metadata_oca/models/edi_exchange_record.py +++ b/edi_record_metadata_oca/models/edi_exchange_record.py @@ -25,8 +25,8 @@ def _compute_metadata_display(self): for rec in self: rec.metadata_display = json.dumps(rec.metadata, sort_keys=True, indent=4) - def set_metadata(self, data): + def edi_set_metadata(self, data): self.metadata = data - def get_metadata(self): + def edi_get_metadata(self): return self.metadata diff --git a/edi_record_metadata_oca/tests/test_metadata.py b/edi_record_metadata_oca/tests/test_metadata.py index 62144fe94..28daf71a6 100644 --- a/edi_record_metadata_oca/tests/test_metadata.py +++ b/edi_record_metadata_oca/tests/test_metadata.py @@ -10,32 +10,28 @@ class TestEDIMetadata(EDIBackendCommonTestCase): - @classmethod - def _setup_records(cls): - res = super()._setup_records() - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() from .fake_models import EDIMetadataConsumerFake - cls.loader.update_registry((EDIMetadataConsumerFake,)) - cls.consumer_model = cls.env[EDIMetadataConsumerFake._name] + self.loader.update_registry((EDIMetadataConsumerFake,)) + self.consumer_model = self.env[EDIMetadataConsumerFake._name] - cls.exc_type = cls._create_exchange_type( + self.exc_type = self._create_exchange_type( name="Metadata test", code="metadata_test", direction="output", ) - cls.exc_record = cls.backend.create_record(cls.exc_type.code, {}) - return res + self.exc_record = self.backend.create_record(self.exc_type.code, {}) - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() + def tearDown(self): + self.loader.restore_registry() + super().tearDown() def test_fields(self): - self.exc_record.set_metadata({"foo": "baz", "bar": "waa"}) + self.exc_record.edi_set_metadata({"foo": "baz", "bar": "waa"}) self.assertTrue(self.exc_record.metadata) self.assertTrue(self.exc_record.metadata_display) @@ -48,7 +44,7 @@ def test_no_store(self): } ) self.assertFalse(consumer_record._edi_get_metadata()) - self.assertFalse(self.exc_record.get_metadata()) + self.assertFalse(self.exc_record.edi_get_metadata()) def test_store(self): vals = { @@ -70,4 +66,4 @@ def test_store(self): "additional": True, } self.assertEqual(consumer_record._edi_get_metadata(), expected) - self.assertEqual(self.exc_record.get_metadata(), expected) + self.assertEqual(self.exc_record.edi_get_metadata(), expected) diff --git a/edi_sale_input_oca/README.rst b/edi_sale_input_oca/README.rst index 1b89a27a9..99ac47dd6 100644 --- a/edi_sale_input_oca/README.rst +++ b/edi_sale_input_oca/README.rst @@ -11,7 +11,7 @@ EDI Sales input !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:1458f5d439bfde955a3c1dc00e8c8ab99b674d8bdb518b9bfe6e475df80742a6 + !! source digest: sha256:031a6af4aecc4f3f856b85d4d793f21481e7f45e181875c59435bf59e17b3239 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png diff --git a/edi_sale_input_oca/__manifest__.py b/edi_sale_input_oca/__manifest__.py index b182ce1fc..a523ea605 100644 --- a/edi_sale_input_oca/__manifest__.py +++ b/edi_sale_input_oca/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ Process incoming sale orders with the EDI framework. """, - "version": "18.0.1.0.1", + "version": "18.0.1.0.2", "development_status": "Alpha", "license": "AGPL-3", "author": "Camptocamp,Odoo Community Association (OCA)", diff --git a/edi_sale_input_oca/static/description/index.html b/edi_sale_input_oca/static/description/index.html index 1ce64058d..2187525cd 100644 --- a/edi_sale_input_oca/static/description/index.html +++ b/edi_sale_input_oca/static/description/index.html @@ -372,7 +372,7 @@
EDI Sales input
!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:1458f5d439bfde955a3c1dc00e8c8ab99b674d8bdb518b9bfe6e475df80742a6 +!! source digest: sha256:031a6af4aecc4f3f856b85d4d793f21481e7f45e181875c59435bf59e17b3239 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->EDI state
!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:b94605243de3a33cc1d60443e16739b5e3c1a135859aab0b21154ff1173e1f0a +!! source digest: sha256:e8a144d5155e42c0155d565988d9fecd87ed66b45db2090a072b8829eab6ab34 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->Technical module for the EDI suite to provide additional states for any
diff --git a/edi_state_oca/tests/test_edi_state.py b/edi_state_oca/tests/test_edi_state.py
index 08ba3ec56..55f719643 100644
--- a/edi_state_oca/tests/test_edi_state.py
+++ b/edi_state_oca/tests/test_edi_state.py
@@ -10,63 +10,59 @@
class TestEDIState(EDIBackendCommonTestCase):
- @classmethod
- def _setup_records(cls):
- res = super()._setup_records()
- # Load fake models ->/
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
+ def setUp(self):
+ super().setUp()
+ self.loader = FakeModelLoader(self.env, self.__module__)
+ self.loader.backup_registry()
from .fake_models import EDIStateConsumerFake
- cls.loader.update_registry((EDIStateConsumerFake,))
- cls.consumer_model = cls.env[EDIStateConsumerFake._name]
- cls.consumer_record = cls.consumer_model.create(
+ self.loader.update_registry((EDIStateConsumerFake,))
+ self.consumer_model = self.env[EDIStateConsumerFake._name]
+ self.consumer_record = self.consumer_model.create(
{
"name": "State Test Consumer",
}
)
# Suitable workflow
- cls.wf1_ok = cls.env["edi.state.workflow"].create(
+ self.wf1_ok = self.env["edi.state.workflow"].create(
{
"name": "WF1",
- "backend_type_id": cls.backend.backend_type_id.id,
- "model_id": cls.env["ir.model"]._get(cls.consumer_record._name).id,
+ "backend_type_id": self.backend.backend_type_id.id,
+ "model_id": self.env["ir.model"]._get(self.consumer_record._name).id,
}
)
for i in range(1, 4):
- cls.env["edi.state"].create(
- {"name": f"OK {i}", "code": f"OK_{i}", "workflow_id": cls.wf1_ok.id}
+ self.env["edi.state"].create(
+ {"name": f"OK {i}", "code": f"OK_{i}", "workflow_id": self.wf1_ok.id}
)
# Non suitable workflow
- cls.wf2_ko = cls.env["edi.state.workflow"].create(
+ self.wf2_ko = self.env["edi.state.workflow"].create(
{
"name": "WF2",
- "backend_type_id": cls.backend.backend_type_id.id,
- "model_id": cls.env["ir.model"]._get("res.partner").id,
+ "backend_type_id": self.backend.backend_type_id.id,
+ "model_id": self.env["ir.model"]._get("res.partner").id,
}
)
for i in range(1, 4):
- cls.env["edi.state"].create(
- {"name": f"KO {i}", "code": f"KO_{i}", "workflow_id": cls.wf2_ko.id}
+ self.env["edi.state"].create(
+ {"name": f"KO {i}", "code": f"KO_{i}", "workflow_id": self.wf2_ko.id}
)
- cls.exc_type = cls._create_exchange_type(
+ self.exc_type = self._create_exchange_type(
name="State test",
code="state_test",
direction="output",
- state_workflow_ids=[(6, 0, cls.wf1_ok.ids)],
+ state_workflow_ids=[(6, 0, self.wf1_ok.ids)],
)
vals = {
- "model": cls.consumer_record._name,
- "res_id": cls.consumer_record.id,
+ "model": self.consumer_record._name,
+ "res_id": self.consumer_record.id,
}
- record = cls.backend.create_record("state_test", vals)
- cls.consumer_record._edi_set_origin(record)
- return res
+ record = self.backend.create_record("state_test", vals)
+ self.consumer_record._edi_set_origin(record)
- @classmethod
- def tearDownClass(cls):
- cls.loader.restore_registry()
- super().tearDownClass()
+ def tearDown(self):
+ self.loader.restore_registry()
+ super().tearDown()
def test_is_state_valid(self):
self.assertTrue(self.wf1_ok.is_valid_for_model(self.consumer_model._name))
diff --git a/edi_storage_oca/README.rst b/edi_storage_oca/README.rst
index 52b9f7ece..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:ae52c0238c8bab278e4e2d6a48d86d4222672b893df1f171be046f3b7a3c58ae
+ !! 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 39f47e247..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.2",
+ "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 @@
+
+ Allow exchange files using storage backends from OCA/storage. 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: 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. 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.EDI Storage backend support
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:ae52c0238c8bab278e4e2d6a48d86d4222672b893df1f171be046f3b7a3c58ae
+!! source digest: sha256:f0ff3ee30416f5a8090d6c738df2c392381b61bc0d104c7679a147ce3b503018
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
EDI Storage backend support
Configuration
+
+
+Bug Tracker
+Bug Tracker
Bug Tracker
Credits
+Credits
Contributors
+Contributors
Contributors
Other credits
+Other credits
Maintainers
+Maintainers
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








