diff --git a/README.md b/README.md index 844fecd95..0c94c22dc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ addon | version | maintainers | summary [edi_component_oca](edi_component_oca/) | 19.0.1.1.0 | simahawk etobella | Allow to use Connector as a source in EDI [edi_core_oca](edi_core_oca/) | 19.0.1.2.2 | simahawk etobella | Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. [edi_endpoint_oca](edi_endpoint_oca/) | 19.0.1.1.1 | | Base module allowing configuration of custom endpoints for EDI framework. +[edi_exchange_deduplicate_oca](edi_exchange_deduplicate_oca/) | 19.0.1.1.0 | simahawk etobella | Introduce a deduplication mechanism at the sending step [edi_product_oca](edi_product_oca/) | 19.0.1.0.0 | | EDI framework configuration and base logic for products and units of measure [edi_purchase_oca](edi_purchase_oca/) | 19.0.1.0.0 | | Define EDI Configuration for Purchase Orders [edi_queue_oca](edi_queue_oca/) | 19.0.1.0.0 | | Set Queue Jobs on EDI diff --git a/edi_exchange_deduplicate_oca/README.rst b/edi_exchange_deduplicate_oca/README.rst new file mode 100644 index 000000000..1152fb298 --- /dev/null +++ b/edi_exchange_deduplicate_oca/README.rst @@ -0,0 +1,126 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================ +Edi Exchange Deduplicate OCA +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f5a7501a3960f60fb0869000e315e9d6bdb2e254682e5038d8603111d141f1b7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/19.0/edi_exchange_deduplicate_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-19-0/edi-framework-19-0-edi_exchange_deduplicate_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=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +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** + +.. contents:: + :local: + +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. +- You can restrict deduplication to specific exchange states with the + technical field "deduplicate_on_exchange_record_status" + (comma-separated values from "edi_exchange_state"). + +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) +- Hadrien Huvelle + +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..728a9d37a --- /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": "19.0.1.1.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..bc0f7d94c --- /dev/null +++ b/edi_exchange_deduplicate_oca/i18n/edi_exchange_deduplicate_oca.pot @@ -0,0 +1,103 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_exchange_deduplicate_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.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_exchange_record_status +msgid "Deduplicate On Exchange Record Status" +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.fields,field_description:edi_exchange_deduplicate_oca.field_edi_backend__display_name +#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_record__display_name +#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_type__display_name +msgid "Display Name" +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,field_description:edi_exchange_deduplicate_oca.field_edi_backend__id +#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_record__id +#: model:ir.model.fields,field_description:edi_exchange_deduplicate_oca.field_edi_exchange_type__id +msgid "ID" +msgstr "" + +#. module: edi_exchange_deduplicate_oca +#. odoo-python +#: code:addons/edi_exchange_deduplicate_oca/models/edi_exchange_type.py:0 +msgid "" +"Invalid exchange state(s): %(invalid_states)s. Allowed values are: " +"%(allowed_states)s" +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..895a602bf --- /dev/null +++ b/edi_exchange_deduplicate_oca/models/edi_backend.py @@ -0,0 +1,49 @@ +# Copyright 2024 Camptocamp +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from odoo import models +from odoo.fields import Domain + +_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(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..fdc965fd1 --- /dev/null +++ b/edi_exchange_deduplicate_oca/models/edi_exchange_record.py @@ -0,0 +1,57 @@ +# Copyright 2024 Camptocamp +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models +from odoo.fields import Domain + + +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() + edi_exchange_state_to_check = list( + self.type_id._deduplicate_get_exchange_record_states() + ) + return (self.search_count if count else self.search)( + Domain( + [ + ("id", "<", self.id), + ("res_id", "=", self.res_id), + ("model", "=", self.model), + ("type_id", "=", self.type_id.id), + ("edi_exchange_state", "in", edi_exchange_state_to_check), + ("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..d5ae889c2 --- /dev/null +++ b/edi_exchange_deduplicate_oca/models/edi_exchange_type.py @@ -0,0 +1,53 @@ +# Copyright 2024 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +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.", + ) + + deduplicate_on_exchange_record_status = fields.Char( + default="new,output_pending", + groups="base.group_no_one", + ) + + def _deduplicate_get_exchange_record_states(self): + self.ensure_one() + return { + state.strip() + for state in (self.deduplicate_on_exchange_record_status or "").split(",") + if state.strip() + } + + @api.constrains("deduplicate_on_exchange_record_status") + def _check_deduplicate_on_exchange_record_status(self): + exchange_state_field = self.env["edi.exchange.record"]._fields[ + "edi_exchange_state" + ] + allowed_states = set(exchange_state_field.get_values(self.env)) + for rec in self: + configured_states = rec._deduplicate_get_exchange_record_states() + invalid_states = sorted(configured_states - allowed_states) + if invalid_states: + raise ValidationError( + self.env._( + "Invalid exchange state(s): %(invalid_states)s. " + "Allowed values are: %(allowed_states)s", + invalid_states=", ".join(invalid_states), + allowed_states=", ".join(sorted(allowed_states)), + ) + ) 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..599f66192 --- /dev/null +++ b/edi_exchange_deduplicate_oca/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Simone Orsi \ +- Duong (Tran Quoc) \ +- Hadrien Huvelle \ \ No newline at end of file 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..900ed5f76 --- /dev/null +++ b/edi_exchange_deduplicate_oca/readme/USAGE.md @@ -0,0 +1,8 @@ +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. +- You can restrict deduplication to specific exchange states with the technical field "deduplicate_on_exchange_record_status" (comma-separated values from "edi_exchange_state"). + +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..69b2e9024 --- /dev/null +++ b/edi_exchange_deduplicate_oca/static/description/index.html @@ -0,0 +1,467 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Edi Exchange Deduplicate OCA

+ +

Beta License: LGPL-3 OCA/edi-framework Translate me on Weblate Try me on Runboat

+

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

+

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.
  • +
  • You can restrict deduplication to specific exchange states with the +technical field “deduplicate_on_exchange_record_status” +(comma-separated values from “edi_exchange_state”).
  • +
+

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

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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:

+

simahawk 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/tests/__init__.py b/edi_exchange_deduplicate_oca/tests/__init__.py new file mode 100644 index 000000000..6c6e6a325 --- /dev/null +++ b/edi_exchange_deduplicate_oca/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_edi_backend_cron +from . import test_deduplicate_on_exchange_record_status +from . import test_edi_duplicate diff --git a/edi_exchange_deduplicate_oca/tests/test_deduplicate_on_exchange_record_status.py b/edi_exchange_deduplicate_oca/tests/test_deduplicate_on_exchange_record_status.py new file mode 100644 index 000000000..0d3d0971b --- /dev/null +++ b/edi_exchange_deduplicate_oca/tests/test_deduplicate_on_exchange_record_status.py @@ -0,0 +1,98 @@ +# Copyright 2024 Camptocamp +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.exceptions import ValidationError +from odoo.tools import mute_logger + +from .test_edi_duplicate import EDIDeduplicateTestCase + +LOGGERS = ( + "odoo.addons.edi_core_oca.models.edi_backend", + "odoo.addons.queue_job.delay", +) + + +class TestDeduplicateOnExchangeRecordStatus(EDIDeduplicateTestCase): + def test_configured_statuses_must_exist_in_selection(self): + with self.assertRaisesRegex(ValidationError, "Invalid exchange state"): + self.exchange_type_out.write( + { + "deduplicate_on_exchange_record_status": "new,not_a_state", + } + ) + + def test_configured_statuses_accept_valid_values(self): + # "obsolete" comes from this addon via selection_add on edi_exchange_state. + self.exchange_type_out.write( + { + "deduplicate_on_exchange_record_status": ( + "new, output_pending, obsolete" + ), + } + ) + self.assertEqual( + self.exchange_type_out.deduplicate_on_exchange_record_status, + "new, output_pending, obsolete", + ) + + @mute_logger(*LOGGERS) + def test_default_status_deduplicates_new_records(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, + }, + ) + self.backend.create_record( + "test_csv_output", + { + "model": self.partner._name, + "res_id": self.partner.id, + }, + ) + + self.assertEqual(record1.edi_exchange_state, "obsolete") + + @mute_logger(*LOGGERS) + def test_custom_status_filter_is_used_for_deduplication(self): + self.exchange_type_out.write( + { + "deduplicate_on_send": True, + "deduplicate_on_exchange_record_status": "output_pending", + } + ) + 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, + }, + ) + + # "new" is not part of the configured list, so no deduplication yet. + self.assertEqual(record1.edi_exchange_state, "new") + + record1.edi_exchange_state = "output_pending" + self.backend.create_record( + "test_csv_output", + { + "model": self.partner._name, + "res_id": self.partner.id, + }, + ) + + self.assertEqual(record1.edi_exchange_state, "obsolete") + self.assertEqual(record2.edi_exchange_state, "new") 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..4eba171a9 --- /dev/null +++ b/edi_exchange_deduplicate_oca/tests/test_edi_duplicate.py @@ -0,0 +1,146 @@ +# Copyright 2024 Camptocamp +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.orm.model_classes import add_to_registry +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): + @classmethod + def _setup_records(cls): # pylint:disable=missing-return + super()._setup_records() + # Load fake models + from odoo.addons.edi_core_oca.tests.fake_models import EdiTestExecution + + add_to_registry(cls.registry, EdiTestExecution) + cls.registry._setup_models__(cls.env.cr, ["edi.framework.test.execution"]) + cls.registry.init_models( + cls.env.cr, + ["edi.framework.test.execution"], + {"models_to_check": True}, + ) + cls.addClassCleanup(cls.registry.__delitem__, "edi.framework.test.execution") + cls.model = cls.env["ir.model"].search( + [("model", "=", "edi.framework.test.execution")] + ) + cls.exchange_type_out.write( + { + "exchange_file_auto_generate": True, + "generate_model_id": cls.model.id, + "send_model_id": cls.model.id, + "output_validate_model_id": cls.model.id, + } + ) + + @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 cbbcbd4bf..6971a1cc7 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,10 +1,11 @@ [project] name = "odoo-addons-oca-edi-framework" -version = "19.0.20260604.0" +version = "19.0.20260610.0" dependencies = [ "odoo-addon-edi_component_oca==19.0.*", "odoo-addon-edi_core_oca==19.0.*", "odoo-addon-edi_endpoint_oca==19.0.*", + "odoo-addon-edi_exchange_deduplicate_oca==19.0.*", "odoo-addon-edi_product_oca==19.0.*", "odoo-addon-edi_purchase_oca==19.0.*", "odoo-addon-edi_queue_oca==19.0.*",