diff --git a/README.md b/README.md
index 0fccb7e26..ee496644b 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,9 @@ addon | version | maintainers | summary
[edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 |
| Define EDI Configuration for Account Moves
[edi_account_oca](edi_account_oca/) | 18.0.1.1.1 |
| Define some component listeners for Account Moves
[edi_component_oca](edi_component_oca/) | 18.0.1.1.0 |
| Allow to use Connector as a source in EDI
-[edi_core_oca](edi_core_oca/) | 18.0.1.7.2 |
| Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges.
+[edi_core_oca](edi_core_oca/) | 18.0.1.7.3 |
| Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges.
[edi_endpoint_oca](edi_endpoint_oca/) | 18.0.1.0.3 | | Base module allowing configuration of custom endpoints for EDI framework.
+[edi_exchange_deduplicate_oca](edi_exchange_deduplicate_oca/) | 18.0.1.0.0 |
| Introduce a deduplication mechanism at the sending step
[edi_exchange_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 |
| Allows definition of exchanges via templates.
[edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 |
| Glue module between edi_exchange_template and edi_party_data
[edi_notification_oca](edi_notification_oca/) | 18.0.1.0.0 | | Define notification activities on exchange records.
diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst
index a73b1e222..1746c1441 100644
--- a/edi_core_oca/README.rst
+++ b/edi_core_oca/README.rst
@@ -11,7 +11,7 @@ EDI
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:5e54bc58f7c88ff2eaac2207226e25488a4aa47b800e1cdc606f485cf0523830
+ !! source digest: sha256:a2ec0c8c9a701363efa1965a3296fbc0972859cf5c22504865d391c74d13ab86
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py
index 31955b191..ddd8abbcd 100644
--- a/edi_core_oca/__manifest__.py
+++ b/edi_core_oca/__manifest__.py
@@ -9,7 +9,7 @@
Define backends, exchange types, exchange records,
basic automation and views for handling EDI exchanges.
""",
- "version": "18.0.1.7.2",
+ "version": "18.0.1.7.3",
"website": "https://github.com/OCA/edi-framework",
"development_status": "Beta",
"license": "LGPL-3",
diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py
index 3346e487f..4eaa92824 100644
--- a/edi_core_oca/models/edi_exchange_type.py
+++ b/edi_core_oca/models/edi_exchange_type.py
@@ -158,6 +158,7 @@ class EDIExchangeType(models.Model):
rule_ids = fields.One2many(
comodel_name="edi.exchange.type.rule",
inverse_name="type_id",
+ context={"active_test": False},
help="Rules to handle exchanges and UI automatically",
)
quick_exec = fields.Boolean(
diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html
index 7c70c56f1..014b14ba2 100644
--- a/edi_core_oca/static/description/index.html
+++ b/edi_core_oca/static/description/index.html
@@ -372,7 +372,7 @@
EDI
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:5e54bc58f7c88ff2eaac2207226e25488a4aa47b800e1cdc606f485cf0523830
+!! source digest: sha256:a2ec0c8c9a701363efa1965a3296fbc0972859cf5c22504865d391c74d13ab86
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Base EDI backend.
diff --git a/edi_core_oca/tests/test_exchange_type.py b/edi_core_oca/tests/test_exchange_type.py
index 5d3ae33bb..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,26 +148,71 @@ 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(
- {
- "type_id": exc_type.id,
- "name": "Fake partner rule",
- "model_id": self.env["ir.model"]._get("res.partner").id,
- }
- )
- rule2 = 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 user rule",
- "model_id": self.env["ir.model"]._get("res.users").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,
+ }
+ ),
+ ]
}
)
- exc_type.active = False
- rule1.invalidate_recordset()
- rule2.invalidate_recordset()
- self.assertFalse(rule1.active)
- self.assertFalse(rule2.active)
+ 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(
diff --git a/edi_core_oca/views/edi_exchange_type_views.xml b/edi_core_oca/views/edi_exchange_type_views.xml
index cddc4a36b..67c2e97c1 100644
--- a/edi_core_oca/views/edi_exchange_type_views.xml
+++ b/edi_core_oca/views/edi_exchange_type_views.xml
@@ -209,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
+
+
+
+
Go to “EDI -> Config -> Exchange Type”.
+
Enable “Deduplicate on Send” option -> Enable “Delete obsolete records”
+option.
+
+
+
+
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.
+
+
+
+
+
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.
+
+
+
+
+
+
+
+
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/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml
index abcb4d0a4..7c59f4867 100644
--- a/setup/_metapackage/pyproject.toml
+++ b/setup/_metapackage/pyproject.toml
@@ -1,12 +1,13 @@
[project]
name = "odoo-addons-oca-edi-framework"
-version = "18.0.20260524.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.*",