From adbd73abe83c4712dae2f7a34129f63dbaf580d8 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 20 Jan 2026 17:35:55 +0100 Subject: [PATCH 01/58] edi_core: clarify quick exec usage Ignore quick_exec types when running automated actions via cron. We assume that quick exec records will be executed right away. You still have the possibility to enforce this using edi__force_generate ctx key. At the same time, make the usage more clear in the UI: quick exec is available only if you want to generate the output automatically. --- edi_core_oca/models/edi_backend.py | 9 ++++++ edi_core_oca/models/edi_exchange_record.py | 5 ++-- edi_core_oca/models/edi_exchange_type.py | 4 ++- edi_core_oca/tests/test_edi_backend_cron.py | 30 ++++++++++++++----- .../views/edi_exchange_type_views.xml | 10 +++++-- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index bdea2e24f..47d9c3db7 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -373,6 +373,15 @@ def _output_new_records_domain(self, record_ids=None): ] if record_ids: domain.append(("id", "in", record_ids)) + # By default, it's pointless to consider records with quick_exec + # because they will be executed right away when created. + domain.append( + ( + "type_id.quick_exec", + "=", + self.env.context.get("edi__quick_exec", False), + ) + ) return domain def _output_pending_records_domain(self, skip_sent=True, record_ids=None): diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 8e4f697ed..8e24a5e9e 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -379,10 +379,11 @@ def _execute_next_action(self): # The backend already knows how to handle records # according to their direction and status. # Let it decide. + backend = self.backend_id.with_context(edi__quick_exec=True) if self.type_id.direction == "output": - self.backend_id._check_output_exchange_sync(record_ids=self.ids) + backend._check_output_exchange_sync(record_ids=self.ids) else: - self.backend_id._check_input_exchange_sync(record_ids=self.ids) + backend._check_input_exchange_sync(record_ids=self.ids) @api.constrains("backend_id", "type_id") def _constrain_backend(self): diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py index 3abe5848f..5850cf356 100644 --- a/edi_core_oca/models/edi_exchange_type.py +++ b/edi_core_oca/models/edi_exchange_type.py @@ -155,7 +155,9 @@ class EDIExchangeType(models.Model): quick_exec = fields.Boolean( string="Quick execution", help="When active, records of this type will be processed immediately " - "without waiting for the cron to pass by.", + "without waiting for the cron to pass by. " + "Requires auto generate flag to be active as well. " + "The cron will skip these records unless forced.", ) partner_ids = fields.Many2many( string="Enabled for partners", diff --git a/edi_core_oca/tests/test_edi_backend_cron.py b/edi_core_oca/tests/test_edi_backend_cron.py index db227b5ad..37d3911bd 100644 --- a/edi_core_oca/tests/test_edi_backend_cron.py +++ b/edi_core_oca/tests/test_edi_backend_cron.py @@ -87,14 +87,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 +98,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/views/edi_exchange_type_views.xml b/edi_core_oca/views/edi_exchange_type_views.xml index 49453e842..244019da9 100644 --- a/edi_core_oca/views/edi_exchange_type_views.xml +++ b/edi_core_oca/views/edi_exchange_type_views.xml @@ -43,11 +43,17 @@ - + + - From 4d7cd6bbe378ee4257c67f13838b833a979369e9 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 6 Feb 2026 10:31:26 +0000 Subject: [PATCH 02/58] [UPD] Update edi_core_oca.pot --- edi_core_oca/i18n/edi_core_oca.pot | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/edi_core_oca/i18n/edi_core_oca.pot b/edi_core_oca/i18n/edi_core_oca.pot index c77ee2f77..d599fe05b 100644 --- a/edi_core_oca/i18n/edi_core_oca.pot +++ b/edi_core_oca/i18n/edi_core_oca.pot @@ -1608,7 +1608,8 @@ msgstr "" #: model:ir.model.fields,help:edi_core_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_core_oca From 33c85ef6834c506535cbb4c8a124677369b605e9 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 6 Feb 2026 10:31:28 +0000 Subject: [PATCH 03/58] [UPD] Update edi_oca.pot --- edi_oca/i18n/edi_oca.pot | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/edi_oca/i18n/edi_oca.pot b/edi_oca/i18n/edi_oca.pot index bd4283165..4db84723b 100644 --- a/edi_oca/i18n/edi_oca.pot +++ b/edi_oca/i18n/edi_oca.pot @@ -1453,7 +1453,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 From 281050b27b0caa069c854eda45a21a0f37ad30e1 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 6 Feb 2026 10:35:24 +0000 Subject: [PATCH 04/58] [BOT] post-merge updates --- README.md | 2 +- edi_core_oca/README.rst | 2 +- edi_core_oca/__manifest__.py | 2 +- edi_core_oca/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 35b61245d..7ba24b384 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ addon | version | maintainers | summary [edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 | etobella | Define EDI Configuration for Account Moves [edi_account_oca](edi_account_oca/) | 18.0.1.1.1 | etobella | Define some component listeners for Account Moves [edi_component_oca](edi_component_oca/) | 18.0.1.0.2 | simahawk etobella | Allow to use Connector as a source in EDI -[edi_core_oca](edi_core_oca/) | 18.0.1.5.6 | simahawk etobella | Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. +[edi_core_oca](edi_core_oca/) | 18.0.1.6.0 | simahawk etobella | 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_template_oca](edi_exchange_template_oca/) | 18.0.1.3.2 | simahawk | Allows definition of exchanges via templates. [edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 | simahawk | Glue module between edi_exchange_template and edi_party_data diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index 7b34951d6..4e3f221be 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:05f11d491e9ac910591ac4fbb7dc7372d0efb74d2b81b38dd84b58a526739cfc + !! source digest: sha256:1949f87b78e227c0b7d68a4b8967de8fc28a273125c228aad2d3a906b10923d8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 83a9d7452..a4e8ff8b9 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.5.6", + "version": "18.0.1.6.0", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html index fa279ca4c..a946bb227 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:05f11d491e9ac910591ac4fbb7dc7372d0efb74d2b81b38dd84b58a526739cfc +!! source digest: sha256:1949f87b78e227c0b7d68a4b8967de8fc28a273125c228aad2d3a906b10923d8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Base EDI backend.

From be9a7632a176f2d4a5715d1a9044b812cb578f37 Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 6 Feb 2026 10:35:33 +0000 Subject: [PATCH 05/58] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: edi-framework-18.0/edi-framework-18.0-edi_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_oca/ --- edi_oca/i18n/it.po | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/edi_oca/i18n/it.po b/edi_oca/i18n/it.po index 9f91ca259..4c524e0cb 100644 --- a/edi_oca/i18n/it.po +++ b/edi_oca/i18n/it.po @@ -1560,10 +1560,9 @@ 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." #. module: edi_oca #: model:ir.model.fields,help:edi_oca.field_edi_exchange_consumer_mixin__edi_disable_auto @@ -1585,6 +1584,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, `{}`): " From c3e3de7c28381d37715247718c3e9ae06d0c45f5 Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 6 Feb 2026 10:35:33 +0000 Subject: [PATCH 06/58] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: edi-framework-18.0/edi-framework-18.0-edi_core_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_core_oca/ --- edi_core_oca/i18n/es.po | 3 ++- edi_core_oca/i18n/fr.po | 3 ++- edi_core_oca/i18n/it.po | 12 +++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/edi_core_oca/i18n/es.po b/edi_core_oca/i18n/es.po index 36f9a0c0d..3fecfe8e6 100644 --- a/edi_core_oca/i18n/es.po +++ b/edi_core_oca/i18n/es.po @@ -1618,7 +1618,8 @@ msgstr "" #: model:ir.model.fields,help:edi_core_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_core_oca diff --git a/edi_core_oca/i18n/fr.po b/edi_core_oca/i18n/fr.po index ee73e8c5f..dc7d2a607 100644 --- a/edi_core_oca/i18n/fr.po +++ b/edi_core_oca/i18n/fr.po @@ -1636,7 +1636,8 @@ msgstr "" #: model:ir.model.fields,help:edi_core_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_core_oca diff --git a/edi_core_oca/i18n/it.po b/edi_core_oca/i18n/it.po index 0ee1522e0..311feb1bd 100644 --- a/edi_core_oca/i18n/it.po +++ b/edi_core_oca/i18n/it.po @@ -1721,10 +1721,9 @@ msgstr "Cronologia comunicazioni sito web" #: model:ir.model.fields,help:edi_core_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." #. module: edi_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_exchange_consumer_mixin__edi_disable_auto @@ -1746,6 +1745,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, `{}`): " From 39369727a198543c82108d426cb976d4f5595850 Mon Sep 17 00:00:00 2001 From: ThiagoMForgeFlow Date: Tue, 28 Oct 2025 15:17:00 +0100 Subject: [PATCH 07/58] [FIX] edi_core_oca: fix search logic Maintaining this part of code can occur a visual issue. If the result variable has 80 records (default by Odoo) and the orig_ids is less than result, Odoo only shows the orig_ids, and we lost the previous work finding next records. --- edi_core_oca/models/edi_exchange_record.py | 2 - edi_core_oca/tests/test_security.py | 73 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 824fe01a7..d1ee76ca5 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -616,8 +616,6 @@ def _search(self, domain, offset=0, limit=None, order=None): extend_ids = list(extend_query) result.extend(extend_ids[: limit - len(result)]) - # Restore original ordering - result = [x for x in orig_ids if x in result] if set(orig_ids) != set(result): # Create a virgin query query = self.browse(result)._as_query() diff --git a/edi_core_oca/tests/test_security.py b/edi_core_oca/tests/test_security.py index a8bca2f94..13dd1ee1d 100644 --- a/edi_core_oca/tests/test_security.py +++ b/edi_core_oca/tests/test_security.py @@ -238,3 +238,76 @@ 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) From 2278b830a54408438a6a01b59910024057db3bd0 Mon Sep 17 00:00:00 2001 From: mymage Date: Tue, 10 Feb 2026 12:57:55 +0000 Subject: [PATCH 08/58] Translated using Weblate (Italian) Currently translated at 100.0% (276 of 276 strings) Translation: edi-framework-18.0/edi-framework-18.0-edi_core_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_core_oca/it/ --- edi_core_oca/i18n/it.po | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/edi_core_oca/i18n/it.po b/edi_core_oca/i18n/it.po index 311feb1bd..68ed9f741 100644 --- a/edi_core_oca/i18n/it.po +++ b/edi_core_oca/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 17.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-12-02 15:42+0000\n" +"PO-Revision-Date: 2026-02-10 15:09+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_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_exchange_type__advanced_settings_edit @@ -1724,6 +1724,10 @@ msgid "" "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 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_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_exchange_consumer_mixin__edi_disable_auto From 62eba93641bcdd3fbc2d4fc1361342cc1199f327 Mon Sep 17 00:00:00 2001 From: mymage Date: Tue, 10 Feb 2026 12:57:41 +0000 Subject: [PATCH 09/58] Translated using Weblate (Italian) Currently translated at 100.0% (251 of 251 strings) Translation: edi-framework-18.0/edi-framework-18.0-edi_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_oca/it/ --- edi_oca/i18n/it.po | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/edi_oca/i18n/it.po b/edi_oca/i18n/it.po index 4c524e0cb..cbff8fe7c 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-02-10 15:09+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 @@ -1563,6 +1563,10 @@ msgid "" "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 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 From 28da724d7c8de558e6d2c81c40b6b0b5b7766d16 Mon Sep 17 00:00:00 2001 From: Ricardoalso Date: Fri, 13 Feb 2026 14:05:34 +0100 Subject: [PATCH 10/58] [PERF] edi_core_oca: add index to parent_id field Used to display related exchanges --- edi_core_oca/models/edi_exchange_record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 8e24a5e9e..4286e2c92 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -113,6 +113,7 @@ class EDIExchangeRecord(models.Model): parent_id = fields.Many2one( comodel_name="edi.exchange.record", help="Original exchange which originated this record", + index=True, ) related_exchange_ids = fields.One2many( string="Related exchanges", From 4570099e36f6524393576993266ead644ac6864b Mon Sep 17 00:00:00 2001 From: Ricardoalso Date: Fri, 13 Feb 2026 14:11:22 +0100 Subject: [PATCH 11/58] [PERF] edi_core_oca: add index to ack_exchange_id field --- edi_core_oca/models/edi_exchange_record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 4286e2c92..27d417137 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -134,6 +134,7 @@ class EDIExchangeRecord(models.Model): help="ACK generated for current exchange.", compute="_compute_ack_exchange_id", store=True, + index=True, ) ack_received_on = fields.Datetime( string="ACK received on", related="ack_exchange_id.exchanged_on" From 8f9b882a7afa6ea0fcd1a35c7d0de7c839376ee6 Mon Sep 17 00:00:00 2001 From: Ricardoalso Date: Fri, 20 Feb 2026 15:24:21 +0100 Subject: [PATCH 12/58] [FIX] edi_*_oca: New way for FakeModelLoader --- .../tests/test_edi_configuration.py | 70 ++++++++-------- edi_core_oca/tests/test_backend_base.py | 29 +++---- edi_core_oca/tests/test_backend_input.py | 52 +++++------- edi_core_oca/tests/test_backend_output.py | 49 +++++------- edi_core_oca/tests/test_backend_process.py | 50 +++++------- edi_core_oca/tests/test_backend_validate.py | 60 ++++++-------- edi_core_oca/tests/test_consumer_mixin.py | 39 ++++----- edi_core_oca/tests/test_edi_backend_cron.py | 62 ++++++-------- edi_core_oca/tests/test_edi_configuration.py | 80 +++++++++---------- .../tests/test_exchange_type_configuration.py | 36 ++++----- .../tests/test_exchange_type_encoding.py | 46 +++++------ edi_core_oca/tests/test_quick_exec.py | 40 ++++------ edi_core_oca/tests/test_security.py | 48 +++++------ .../tests/test_edi_backend_output.py | 16 +++- edi_queue_oca/tests/test_backend_jobs.py | 32 ++++---- .../tests/test_metadata.py | 26 +++--- edi_state_oca/tests/test_edi_state.py | 56 ++++++------- 17 files changed, 348 insertions(+), 443 deletions(-) diff --git a/edi_component_oca/tests/test_edi_configuration.py b/edi_component_oca/tests/test_edi_configuration.py index 509b3e2ab..acb9267d9 100644 --- a/edi_component_oca/tests/test_edi_configuration.py +++ b/edi_component_oca/tests/test_edi_configuration.py @@ -40,66 +40,64 @@ def setUpClass(cls): def setUp(self): super().setUp() - FakeOutputGenerator.reset_faked() - FakeOutputSender.reset_faked() - FakeOutputChecker.reset_faked() - 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 odoo.addons.edi_core_oca.tests.fake_models import EdiExchangeConsumerTest EdiExchangeConsumerTest._edi_config_field_relation = lambda self: self.env[ "edi.configuration" ] - # We need to override it, as we want to test the usage with components + self.loader.update_registry((EdiExchangeConsumerTest,)) - cls.loader.update_registry((EdiExchangeConsumerTest,)) - cls.exchange_type_out.exchange_filename_pattern = "{record.id}" - cls.edi_configuration = cls.env["edi.configuration"] - cls.create_config = cls.edi_configuration.create( + FakeOutputGenerator.reset_faked() + FakeOutputSender.reset_faked() + FakeOutputChecker.reset_faked() + + self.create_config = self.env["edi.configuration"].create( { "name": "Create Config", "active": True, - "backend_id": cls.backend.id, - "type_id": cls.exchange_type_out.id, - "trigger_id": cls.env.ref( + "backend_id": self.backend.id, + "type_id": self.exchange_type_out.id, + "trigger_id": self.env.ref( "edi_core_oca.edi_conf_trigger_record_create" ).id, - "model_id": cls.env["ir.model"]._get_id("edi.exchange.consumer.test"), + "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.env["edi.configuration"].create( { "name": "Write Config 1", "active": True, - "backend_id": cls.backend.id, - "type_id": cls.exchange_type_out.id, - "trigger_id": cls.env.ref( + "backend_id": self.backend.id, + "type_id": self.exchange_type_out.id, + "trigger_id": self.env.ref( "edi_core_oca.edi_conf_trigger_record_write" ).id, - "model_id": cls.env["ir.model"]._get_id("edi.exchange.consumer.test"), + "model_id": self.env["ir.model"]._get_id("edi.exchange.consumer.test"), "snippet_do": "record._edi_send_via_edi(conf.type_id)", } ) + 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 _setup_records(cls): # pylint:disable=missing-return + super()._setup_records() + cls.exchange_type_out.exchange_filename_pattern = "{record.id}" + + def tearDown(self): + self.loader.restore_registry() + super().tearDown() def test_edi_send_via_edi_config(self): # Check configuration on create diff --git a/edi_core_oca/tests/test_backend_base.py b/edi_core_oca/tests/test_backend_base.py index 0bda10c0a..bb4bb322a 100644 --- a/edi_core_oca/tests/test_backend_base.py +++ b/edi_core_oca/tests/test_backend_base.py @@ -11,25 +11,22 @@ 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 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): diff --git a/edi_core_oca/tests/test_backend_input.py b/edi_core_oca/tests/test_backend_input.py index 0c59f471a..89d7fe13f 100644 --- a/edi_core_oca/tests/test_backend_input.py +++ b/edi_core_oca/tests/test_backend_input.py @@ -8,37 +8,6 @@ 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 +18,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(), "") diff --git a/edi_core_oca/tests/test_backend_output.py b/edi_core_oca/tests/test_backend_output.py index b9140b710..69bd07fe8 100644 --- a/edi_core_oca/tests/test_backend_output.py +++ b/edi_core_oca/tests/test_backend_output.py @@ -15,44 +15,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!") diff --git a/edi_core_oca/tests/test_backend_process.py b/edi_core_oca/tests/test_backend_process.py index fefbf21e1..913abfb6d 100644 --- a/edi_core_oca/tests/test_backend_process.py +++ b/edi_core_oca/tests/test_backend_process.py @@ -15,42 +15,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"): diff --git a/edi_core_oca/tests/test_backend_validate.py b/edi_core_oca/tests/test_backend_validate.py index 990810506..cdac2899d 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) 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 37d3911bd..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 diff --git a/edi_core_oca/tests/test_edi_configuration.py b/edi_core_oca/tests/test_edi_configuration.py index 88f1a2918..689a8f6c1 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 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..e2e241619 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 = { 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_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/tests/test_metadata.py b/edi_record_metadata_oca/tests/test_metadata.py index 62144fe94..a29916863 100644 --- a/edi_record_metadata_oca/tests/test_metadata.py +++ b/edi_record_metadata_oca/tests/test_metadata.py @@ -10,29 +10,25 @@ 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"}) 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)) From b7ca2bc3e2744b1fe30ba811bfd15d609d748fe8 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 24 Feb 2026 09:05:06 +0000 Subject: [PATCH 13/58] [BOT] post-merge updates --- README.md | 12 ++++++------ edi_component_oca/README.rst | 2 +- edi_component_oca/__manifest__.py | 2 +- edi_component_oca/static/description/index.html | 2 +- edi_core_oca/README.rst | 2 +- edi_core_oca/__manifest__.py | 2 +- edi_core_oca/static/description/index.html | 2 +- edi_exchange_template_oca/README.rst | 2 +- edi_exchange_template_oca/__manifest__.py | 2 +- .../static/description/index.html | 2 +- edi_queue_oca/README.rst | 2 +- edi_queue_oca/__manifest__.py | 2 +- edi_queue_oca/static/description/index.html | 2 +- edi_record_metadata_oca/__manifest__.py | 2 +- edi_state_oca/README.rst | 2 +- edi_state_oca/__manifest__.py | 2 +- edi_state_oca/static/description/index.html | 2 +- 17 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7ba24b384..4215364a4 100644 --- a/README.md +++ b/README.md @@ -23,22 +23,22 @@ addon | version | maintainers | summary --- | --- | --- | --- [edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 | etobella | Define EDI Configuration for Account Moves [edi_account_oca](edi_account_oca/) | 18.0.1.1.1 | etobella | Define some component listeners for Account Moves -[edi_component_oca](edi_component_oca/) | 18.0.1.0.2 | simahawk etobella | Allow to use Connector as a source in EDI -[edi_core_oca](edi_core_oca/) | 18.0.1.6.0 | simahawk etobella | Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. +[edi_component_oca](edi_component_oca/) | 18.0.1.0.3 | simahawk etobella | Allow to use Connector as a source in EDI +[edi_core_oca](edi_core_oca/) | 18.0.1.6.1 | simahawk etobella | 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_template_oca](edi_exchange_template_oca/) | 18.0.1.3.2 | simahawk | Allows definition of exchanges via templates. +[edi_exchange_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 | simahawk | Allows definition of exchanges via templates. [edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 | simahawk | Glue module between edi_exchange_template and edi_party_data [edi_oca](edi_oca/) | 18.0.1.5.2 | simahawk etobella | Integrate all EDI modules together [edi_party_data_oca](edi_party_data_oca/) | 18.0.1.0.1 | simahawk | Allow to configure and retrieve party information for EDI exchanges. -[edi_queue_oca](edi_queue_oca/) | 18.0.1.0.1 | | Set Queue Jobs on EDI -[edi_record_metadata_oca](edi_record_metadata_oca/) | 18.0.1.0.2 | simahawk | Allow to store metadata for related records. +[edi_queue_oca](edi_queue_oca/) | 18.0.1.0.2 | | Set Queue Jobs on EDI +[edi_record_metadata_oca](edi_record_metadata_oca/) | 18.0.1.0.3 | simahawk | Allow to store metadata for related records. [edi_sale_endpoint](edi_sale_endpoint/) | 18.0.1.0.0 | simahawk | Glue module between edi_sale_oca and edi_endpoint_oca. [edi_sale_input_oca](edi_sale_input_oca/) | 18.0.1.0.1 | simahawk | Process incoming sale orders with the EDI framework. [edi_sale_oca](edi_sale_oca/) | 18.0.1.0.1 | simahawk | Configuration and special behaviors for EDI on sales. [edi_sale_stock_oca](edi_sale_stock_oca/) | 18.0.1.0.0 | ivantodorovich | Configuration and special behaviors for EDI on sales & stock. [edi_sale_ubl_oca](edi_sale_ubl_oca/) | 18.0.1.0.2 | | Configuration and special behaviors for EDI UBL exchanges related to sales. [edi_sale_ubl_output_oca](edi_sale_ubl_output_oca/) | 18.0.1.0.1 | | Configuration and special behaviors for EDI on sales. -[edi_state_oca](edi_state_oca/) | 18.0.1.0.2 | simahawk | Allow to assign specific EDI states to related records. +[edi_state_oca](edi_state_oca/) | 18.0.1.0.3 | simahawk | Allow to assign specific EDI states to related records. [edi_stock_oca](edi_stock_oca/) | 18.0.1.0.1 | | Define EDI Configuration for Stock [edi_storage_oca](edi_storage_oca/) | 18.0.1.0.2 | | Base module to allow exchanging files via storage backend (eg: SFTP). [edi_storage_queue_oca](edi_storage_queue_oca/) | 18.0.1.0.0 | | Integrates EDI Storage with Queue diff --git a/edi_component_oca/README.rst b/edi_component_oca/README.rst index 8cd40f4f6..db2cffdd7 100644 --- a/edi_component_oca/README.rst +++ b/edi_component_oca/README.rst @@ -11,7 +11,7 @@ Edi Connector Oca !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:3bf081843ed5a121210f466d0a354fe495798e66f75d76f6958176548d647356 + !! source digest: sha256:81c11c0d670f363513d25e5d2d6cb038f1fc56580f20c837c2d2a7665798018d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/edi_component_oca/__manifest__.py b/edi_component_oca/__manifest__.py index 245d468d5..fa8111718 100644 --- a/edi_component_oca/__manifest__.py +++ b/edi_component_oca/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Edi Connector Oca", "summary": """Allow to use Connector as a source in EDI""", - "version": "18.0.1.0.2", + "version": "18.0.1.0.3", "license": "LGPL-3", "author": "ACSONE,Dixmit,Camptocamp,Odoo Community Association (OCA)", "maintainers": ["simahawk", "etobella"], diff --git a/edi_component_oca/static/description/index.html b/edi_component_oca/static/description/index.html index b82405afa..e6128b6fa 100644 --- a/edi_component_oca/static/description/index.html +++ b/edi_component_oca/static/description/index.html @@ -372,7 +372,7 @@

Edi Connector Oca

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:3bf081843ed5a121210f466d0a354fe495798e66f75d76f6958176548d647356 +!! source digest: sha256:81c11c0d670f363513d25e5d2d6cb038f1fc56580f20c837c2d2a7665798018d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module allows to use components to handle code to execute on EDI diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index 4e3f221be..c9d20457e 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:1949f87b78e227c0b7d68a4b8967de8fc28a273125c228aad2d3a906b10923d8 + !! source digest: sha256:95302609a43b47d26df26c7943e10d1f31b7d4ca39df3de4bb8d7a5e345890fc !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 a4e8ff8b9..9598d843d 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.6.0", + "version": "18.0.1.6.1", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html index a946bb227..fdf943e75 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:1949f87b78e227c0b7d68a4b8967de8fc28a273125c228aad2d3a906b10923d8 +!! source digest: sha256:95302609a43b47d26df26c7943e10d1f31b7d4ca39df3de4bb8d7a5e345890fc !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Base EDI backend.

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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Provide EDI exchange templates to control input/output records contents.

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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module integrates EDI with Queue Job and now the edi exchange diff --git a/edi_record_metadata_oca/__manifest__.py b/edi_record_metadata_oca/__manifest__.py index 9b162ace3..68e7b8631 100644 --- a/edi_record_metadata_oca/__manifest__.py +++ b/edi_record_metadata_oca/__manifest__.py @@ -7,7 +7,7 @@ "summary": """ Allow to store metadata for 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/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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Technical module for the EDI suite to provide additional states for any From d205a94f09e007fff06936b79e6eaec4fe0de4a0 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 6 Mar 2026 14:08:35 +0000 Subject: [PATCH 14/58] [BOT] post-merge updates --- README.md | 2 +- edi_core_oca/README.rst | 2 +- edi_core_oca/__manifest__.py | 2 +- edi_core_oca/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4215364a4..582b145a7 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ addon | version | maintainers | summary [edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 | etobella | Define EDI Configuration for Account Moves [edi_account_oca](edi_account_oca/) | 18.0.1.1.1 | etobella | Define some component listeners for Account Moves [edi_component_oca](edi_component_oca/) | 18.0.1.0.3 | simahawk etobella | Allow to use Connector as a source in EDI -[edi_core_oca](edi_core_oca/) | 18.0.1.6.1 | simahawk etobella | Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. +[edi_core_oca](edi_core_oca/) | 18.0.1.6.2 | simahawk etobella | 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_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 | simahawk | Allows definition of exchanges via templates. [edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 | simahawk | Glue module between edi_exchange_template and edi_party_data diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index c9d20457e..3a825331c 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:95302609a43b47d26df26c7943e10d1f31b7d4ca39df3de4bb8d7a5e345890fc + !! source digest: sha256:dc74a21567e5367c2732791c6f09154378de1d36f2e182554cbc42776da1b386 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 9598d843d..e6978a9c0 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.6.1", + "version": "18.0.1.6.2", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html index fdf943e75..d37fc6fe8 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:95302609a43b47d26df26c7943e10d1f31b7d4ca39df3de4bb8d7a5e345890fc +!! source digest: sha256:dc74a21567e5367c2732791c6f09154378de1d36f2e182554cbc42776da1b386 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Base EDI backend.

From fe331cf54a5694a82d3cdf0b5d5f4a2518cf0414 Mon Sep 17 00:00:00 2001 From: Hadrien Huvelle Date: Thu, 12 Mar 2026 10:02:37 +0100 Subject: [PATCH 15/58] [FIX] edi_core_oca: fix UnboundLocalError --- edi_core_oca/models/edi_backend.py | 1 + 1 file changed, 1 insertion(+) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 47d9c3db7..a8c6ce8bd 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -436,6 +436,7 @@ def exchange_process(self, exchange_record): old_state = state = exchange_record.edi_exchange_state error = traceback = False message = None + res = None try: res = self._exchange_process(exchange_record) except self._swallable_exceptions() as err: From c400231d93ee42f8f8fb0b8f06eff6ede5c60e30 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 12 Mar 2026 10:39:47 +0000 Subject: [PATCH 16/58] [BOT] post-merge updates --- README.md | 2 +- edi_core_oca/README.rst | 2 +- edi_core_oca/__manifest__.py | 2 +- edi_core_oca/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 582b145a7..c47671a72 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ addon | version | maintainers | summary [edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 | etobella | Define EDI Configuration for Account Moves [edi_account_oca](edi_account_oca/) | 18.0.1.1.1 | etobella | Define some component listeners for Account Moves [edi_component_oca](edi_component_oca/) | 18.0.1.0.3 | simahawk etobella | Allow to use Connector as a source in EDI -[edi_core_oca](edi_core_oca/) | 18.0.1.6.2 | simahawk etobella | Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. +[edi_core_oca](edi_core_oca/) | 18.0.1.6.3 | simahawk etobella | 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_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 | simahawk | Allows definition of exchanges via templates. [edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 | simahawk | Glue module between edi_exchange_template and edi_party_data diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index 3a825331c..e2e7a0291 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:dc74a21567e5367c2732791c6f09154378de1d36f2e182554cbc42776da1b386 + !! source digest: sha256:b6fae2fa03965f4601739b334b3c948154db0f7b82d9b8cc4ae5e945d278c013 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 e6978a9c0..2d91d84a0 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.6.2", + "version": "18.0.1.6.3", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html index d37fc6fe8..9948c300b 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:dc74a21567e5367c2732791c6f09154378de1d36f2e182554cbc42776da1b386 +!! source digest: sha256:b6fae2fa03965f4601739b334b3c948154db0f7b82d9b8cc4ae5e945d278c013 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Base EDI backend.

From 4e98519006d2b890d152ec3852e5a4afed662252 Mon Sep 17 00:00:00 2001 From: SilvioC2C Date: Thu, 26 Mar 2026 16:21:27 +0100 Subject: [PATCH 17/58] [FIX] edi_core_oca: fix record rule 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. --- edi_core_oca/__manifest__.py | 2 +- .../migrations/18.0.1.6.4/post-mig.py | 24 ++++++++++++++ edi_core_oca/security/ir_model_access.xml | 2 +- edi_core_oca/tests/test_security.py | 32 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 edi_core_oca/migrations/18.0.1.6.4/post-mig.py diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 2d91d84a0..6274a6c3c 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.6.3", + "version": "18.0.1.6.4", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", diff --git a/edi_core_oca/migrations/18.0.1.6.4/post-mig.py b/edi_core_oca/migrations/18.0.1.6.4/post-mig.py new file mode 100644 index 000000000..6b52c0399 --- /dev/null +++ b/edi_core_oca/migrations/18.0.1.6.4/post-mig.py @@ -0,0 +1,24 @@ +# Copyright 2026 Camptocamp SA (http://www.camptocamp.com) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from logging import getLogger + +from openupgradelib import openupgrade + +_logger = getLogger(__name__) + + +@openupgrade.migrate() +def migrate(env, version): + xmlid = "edi_core_oca.rule_edi_exchange_record_user" + if rule := env.ref(xmlid, False): + old_domain = (rule.domain_force or "").strip() + new_domain = ["|", ("model", "!=", False), ("res_id", "=", 0)] + _logger.info( + f"Updating {rule} ({xmlid=}) domain:\n" + f" - old: {old_domain}\n" + f" - new: {new_domain}" + ) + rule.domain_force = new_domain + else: + _logger.warning(f"No rule found with XMLID '{xmlid}', skipping...") diff --git a/edi_core_oca/security/ir_model_access.xml b/edi_core_oca/security/ir_model_access.xml index 8c85f038b..31c9c73ae 100644 --- a/edi_core_oca/security/ir_model_access.xml +++ b/edi_core_oca/security/ir_model_access.xml @@ -122,7 +122,7 @@ ['|', ('model','!=', False), ('res_id', '=', False)] + >['|', ('model', '!=', False), ('res_id', '=', 0)] diff --git a/edi_core_oca/tests/test_security.py b/edi_core_oca/tests/test_security.py index c0f7b1b9b..c26c62e4e 100644 --- a/edi_core_oca/tests/test_security.py +++ b/edi_core_oca/tests/test_security.py @@ -303,3 +303,35 @@ def test_search_pagination_with_inaccessible_middle_records(self): # 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) From 42a520270f7c9fbc1c6f2dd650cf3505cfd8a78e Mon Sep 17 00:00:00 2001 From: Vincent Van Rossem Date: Wed, 1 Apr 2026 12:10:35 +0200 Subject: [PATCH 18/58] [FIX] edi_record_metadata_oca: rename get_metadata (ORM conflict) `get_metadata()` is a built-in Odoo ORM method called by the "Debug > View Metadata" action. Defining it on `edi.exchange.record` overrode that behavior, causing a JS crash when opening the debug dialog: "Cannot read properties of undefined (reading 'id')" Renamed to `edi_get_metadata()` / `edi_set_metadata()` (for consistency) and updated all callers. --- .../models/edi_exchange_consumer_mixin.py | 6 +++--- edi_record_metadata_oca/models/edi_exchange_record.py | 4 ++-- edi_record_metadata_oca/tests/test_metadata.py | 6 +++--- edi_sale_input_oca/tests/test_process.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) 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 a29916863..28daf71a6 100644 --- a/edi_record_metadata_oca/tests/test_metadata.py +++ b/edi_record_metadata_oca/tests/test_metadata.py @@ -31,7 +31,7 @@ def tearDown(self): 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) @@ -44,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 = { @@ -66,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/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 ( From ed73890cb1cb9968bc9614222f080f3764c982f3 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 2 Apr 2026 08:35:11 +0000 Subject: [PATCH 19/58] [BOT] post-merge updates --- README.md | 4 ++-- edi_record_metadata_oca/__manifest__.py | 2 +- edi_sale_input_oca/README.rst | 2 +- edi_sale_input_oca/__manifest__.py | 2 +- edi_sale_input_oca/static/description/index.html | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c47671a72..769b64da9 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ addon | version | maintainers | summary [edi_oca](edi_oca/) | 18.0.1.5.2 | simahawk etobella | Integrate all EDI modules together [edi_party_data_oca](edi_party_data_oca/) | 18.0.1.0.1 | simahawk | Allow to configure and retrieve party information for EDI exchanges. [edi_queue_oca](edi_queue_oca/) | 18.0.1.0.2 | | Set Queue Jobs on EDI -[edi_record_metadata_oca](edi_record_metadata_oca/) | 18.0.1.0.3 | simahawk | Allow to store metadata for related records. +[edi_record_metadata_oca](edi_record_metadata_oca/) | 18.0.1.0.4 | simahawk | Allow to store metadata for related records. [edi_sale_endpoint](edi_sale_endpoint/) | 18.0.1.0.0 | simahawk | Glue module between edi_sale_oca and edi_endpoint_oca. -[edi_sale_input_oca](edi_sale_input_oca/) | 18.0.1.0.1 | simahawk | Process incoming sale orders with the EDI framework. +[edi_sale_input_oca](edi_sale_input_oca/) | 18.0.1.0.2 | simahawk | Process incoming sale orders with the EDI framework. [edi_sale_oca](edi_sale_oca/) | 18.0.1.0.1 | simahawk | Configuration and special behaviors for EDI on sales. [edi_sale_stock_oca](edi_sale_stock_oca/) | 18.0.1.0.0 | ivantodorovich | Configuration and special behaviors for EDI on sales & stock. [edi_sale_ubl_oca](edi_sale_ubl_oca/) | 18.0.1.0.2 | | Configuration and special behaviors for EDI UBL exchanges related to sales. diff --git a/edi_record_metadata_oca/__manifest__.py b/edi_record_metadata_oca/__manifest__.py index 68e7b8631..050f49fac 100644 --- a/edi_record_metadata_oca/__manifest__.py +++ b/edi_record_metadata_oca/__manifest__.py @@ -7,7 +7,7 @@ "summary": """ Allow to store metadata for related records. """, - "version": "18.0.1.0.3", + "version": "18.0.1.0.4", "development_status": "Alpha", "license": "LGPL-3", "website": "https://github.com/OCA/edi-framework", 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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Alpha License: AGPL-3 OCA/edi-framework Translate me on Weblate Try me on Runboat

From 22712bd73e33393ae04edfc21ff83024a68fb36c Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 14 Apr 2026 04:55:40 +0000 Subject: [PATCH 20/58] [UPD] Update edi_sale_ubl_output_oca.pot --- .../i18n/edi_sale_ubl_output_oca.pot | 40 ------------------- 1 file changed, 40 deletions(-) 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" From 558897ff6fc6dfe1ffcc3ff565da229f2977f4c4 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 14 Apr 2026 05:01:58 +0000 Subject: [PATCH 21/58] [BOT] post-merge updates --- README.md | 2 +- edi_core_oca/README.rst | 2 +- edi_core_oca/__manifest__.py | 2 +- edi_core_oca/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 769b64da9..7764503f7 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ addon | version | maintainers | summary [edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 | etobella | Define EDI Configuration for Account Moves [edi_account_oca](edi_account_oca/) | 18.0.1.1.1 | etobella | Define some component listeners for Account Moves [edi_component_oca](edi_component_oca/) | 18.0.1.0.3 | simahawk etobella | Allow to use Connector as a source in EDI -[edi_core_oca](edi_core_oca/) | 18.0.1.6.3 | simahawk etobella | Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. +[edi_core_oca](edi_core_oca/) | 18.0.1.6.4 | simahawk etobella | 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_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 | simahawk | Allows definition of exchanges via templates. [edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 | simahawk | Glue module between edi_exchange_template and edi_party_data diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index e2e7a0291..509f600b6 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:b6fae2fa03965f4601739b334b3c948154db0f7b82d9b8cc4ae5e945d278c013 + !! source digest: sha256:f88985c95bf0be6a23386723430462ce467c85c53c5ca6dc0d76766191d7ab86 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 2d91d84a0..6274a6c3c 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.6.3", + "version": "18.0.1.6.4", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html index 9948c300b..ba02a122c 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:b6fae2fa03965f4601739b334b3c948154db0f7b82d9b8cc4ae5e945d278c013 +!! source digest: sha256:f88985c95bf0be6a23386723430462ce467c85c53c5ca6dc0d76766191d7ab86 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Base EDI backend.

From 8b9cdeb1a7c5ef8447c77d542a2716fbc573ed66 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 14 Apr 2026 05:02:07 +0000 Subject: [PATCH 22/58] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: edi-framework-18.0/edi-framework-18.0-edi_sale_ubl_output_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_sale_ubl_output_oca/ --- edi_sale_ubl_output_oca/i18n/it.po | 64 +++++++++++------------------- 1 file changed, 24 insertions(+), 40 deletions(-) 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" From 8365818d798fb025fb4365a3e7a9af334cc8a103 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 16 Apr 2026 14:32:29 +0000 Subject: [PATCH 23/58] [BOT] post-merge updates --- README.md | 2 +- edi_core_oca/README.rst | 2 +- edi_core_oca/__manifest__.py | 2 +- edi_core_oca/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7764503f7..5b92fb3a4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ addon | version | maintainers | summary [edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 | etobella | Define EDI Configuration for Account Moves [edi_account_oca](edi_account_oca/) | 18.0.1.1.1 | etobella | Define some component listeners for Account Moves [edi_component_oca](edi_component_oca/) | 18.0.1.0.3 | simahawk etobella | Allow to use Connector as a source in EDI -[edi_core_oca](edi_core_oca/) | 18.0.1.6.4 | simahawk etobella | Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. +[edi_core_oca](edi_core_oca/) | 18.0.1.6.5 | simahawk etobella | 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_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 | simahawk | Allows definition of exchanges via templates. [edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 | simahawk | Glue module between edi_exchange_template and edi_party_data diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index 509f600b6..f6f7f6077 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:f88985c95bf0be6a23386723430462ce467c85c53c5ca6dc0d76766191d7ab86 + !! source digest: sha256:97e3e3a909c79dced8fe9e6165a0c440f95f672e4a184a9b014cde518a5848c2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 6274a6c3c..2f2f6a9f6 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.6.4", + "version": "18.0.1.6.5", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html index ba02a122c..e7a67132d 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:f88985c95bf0be6a23386723430462ce467c85c53c5ca6dc0d76766191d7ab86 +!! source digest: sha256:97e3e3a909c79dced8fe9e6165a0c440f95f672e4a184a9b014cde518a5848c2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Base EDI backend.

From 743d6a6b10dcb9980153b971e1b24fbf32f864a1 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 20 Apr 2026 16:32:47 +0200 Subject: [PATCH 24/58] Sync from odoo repo template --- .copier-answers.yml | 3 +- .gitattributes | 1 + .github/workflows/pre-commit.yml | 2 + .github/workflows/test.yml | 9 ++++- .pre-commit-config.yaml | 10 ++++- .pylintrc | 69 ++++++++++++++++---------------- .pylintrc-mandatory | 50 +++++++++++------------ README.md | 5 ++- checklog-odoo.cfg | 2 + eslint.config.cjs | 5 ++- 10 files changed, 89 insertions(+), 67 deletions(-) create mode 100644 .gitattributes diff --git a/.copier-answers.yml b/.copier-answers.yml index a20b1cc13..19ac99534 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,8 +1,7 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: v1.29 +_commit: v1.40 _src_path: git+https://github.com/OCA/oca-addons-repo-template additional_ruff_rules: [] -ci: GitHub convert_readme_fragments_to_markdown: true enable_checklog_odoo: true generate_requirements_txt: true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..e0d56685a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +test-requirements.txt merge=union diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 5eb021ef1..1291da527 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -17,6 +17,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" + cache: 'pip' + cache-dependency-path: '.pre-commit-config.yaml' - name: Get python version run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - uses: actions/cache@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a06488079..97ed5dfd4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: makepot: "true" services: postgres: - image: postgres:12.0 + image: postgres:12 env: POSTGRES_USER: odoo POSTGRES_PASSWORD: odoo @@ -65,6 +65,13 @@ jobs: run: oca_init_test_database - name: Run tests run: oca_run_tests + - name: Upload screenshots from JS tests + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: Screenshots of failed JS tests - ${{ matrix.name }}${{ join(matrix.include) }} + path: /tmp/odoo_tests/${{ env.PGDATABASE }} + if-no-files-found: ignore - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d69703b5..32504883c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,12 +38,17 @@ repos: entry: found a en.po file language: fail files: '[a-zA-Z0-9_]*/i18n/en\.po$' + - id: obsolete dotfiles + name: obsolete dotfiles + entry: found obsolete files; remove them + files: '^(\.travis\.yml|\.t2d\.yml|CONTRIBUTING\.md|\.prettierrc\.yml|\.eslintrc\.yml)$' + language: fail - repo: https://github.com/sbidoul/whool - rev: v1.2 + rev: v1.3 hooks: - id: whool-init - repo: https://github.com/oca/maintainer-tools - rev: bf9ecb9938b6a5deca0ff3d870fbd3f33341fded + rev: b89f767503be6ab2b11e4f50a7557cb20066e667 hooks: # update the NOT INSTALLABLE ADDONS section above - id: oca-update-pre-commit-excluded-addons @@ -95,6 +100,7 @@ repos: additional_dependencies: - "eslint@9.12.0" - "eslint-plugin-jsdoc@50.3.1" + - "globals@16.0.0" - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: diff --git a/.pylintrc b/.pylintrc index 7c62b6d2e..197cb6737 100644 --- a/.pylintrc +++ b/.pylintrc @@ -25,19 +25,25 @@ disable=all enable=anomalous-backslash-in-string, api-one-deprecated, api-one-multi-together, - assignment-from-none, - attribute-deprecated, class-camelcase, - dangerous-default-value, dangerous-view-replace-wo-priority, - development-status-allowed, duplicate-id-csv, - duplicate-key, duplicate-xml-fields, duplicate-xml-record-id, eval-referenced, - eval-used, incoherent-interpreter-exec-perm, + openerp-exception-warning, + redundant-modulename-xml, + relative-import, + rst-syntax-error, + wrong-tabs-instead-of-spaces, + xml-syntax-error, + assignment-from-none, + attribute-deprecated, + dangerous-default-value, + development-status-allowed, + duplicate-key, + eval-used, license-allowed, manifest-author-string, manifest-deprecated-key, @@ -48,73 +54,68 @@ enable=anomalous-backslash-in-string, method-inverse, method-required-super, method-search, - openerp-exception-warning, pointless-statement, pointless-string-statement, print-used, redundant-keyword-arg, - redundant-modulename-xml, reimported, - relative-import, return-in-init, - rst-syntax-error, sql-injection, too-few-format-args, translation-field, translation-required, unreachable, use-vim-comment, - wrong-tabs-instead-of-spaces, - xml-syntax-error, - attribute-string-redundant, character-not-valid-in-resource-link, - consider-merging-classes-inherited, - context-overridden, create-user-wo-reset-password, dangerous-filter-wo-user, dangerous-qweb-replace-wo-priority, deprecated-data-xml-node, deprecated-openerp-xml-node, duplicate-po-message-definition, - except-pass, file-not-used, + missing-newline-extrafiles, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + str-format-used, + unnecessary-utf8-coding-comment, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute, + attribute-string-redundant, + consider-merging-classes-inherited, + context-overridden, + except-pass, invalid-commit, manifest-maintainers-list, - missing-newline-extrafiles, missing-readme, missing-return, odoo-addons-relative-import, - old-api7-method-defined, - po-msgstr-variables, - po-syntax-error, renamed-field-parameter, resource-not-exist, - str-format-used, test-folder-imported, translation-contains-variable, translation-positional-used, - unnecessary-utf8-coding-comment, website-manifest-key-not-valid-uri, - xml-attribute-translatable, - xml-deprecated-qweb-directive, - xml-deprecated-tree-attribute, external-request-timeout, - # messages that do not cause the lint step to fail - consider-merging-classes-inherited, + missing-manifest-dependency, + too-complex,, create-user-wo-reset-password, dangerous-filter-wo-user, - deprecated-module, file-not-used, - invalid-commit, - missing-manifest-dependency, missing-newline-extrafiles, - missing-readme, no-utf8-coding-comment, - odoo-addons-relative-import, old-api7-method-defined, + unnecessary-utf8-coding-comment, + # messages that do not cause the lint step to fail + consider-merging-classes-inherited, + deprecated-module, + invalid-commit, + missing-readme, + odoo-addons-relative-import, redefined-builtin, - too-complex, - unnecessary-utf8-coding-comment + manifest-external-assets [REPORTS] diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory index 018fd61cd..73674c04d 100644 --- a/.pylintrc-mandatory +++ b/.pylintrc-mandatory @@ -17,19 +17,25 @@ disable=all enable=anomalous-backslash-in-string, api-one-deprecated, api-one-multi-together, - assignment-from-none, - attribute-deprecated, class-camelcase, - dangerous-default-value, dangerous-view-replace-wo-priority, - development-status-allowed, duplicate-id-csv, - duplicate-key, duplicate-xml-fields, duplicate-xml-record-id, eval-referenced, - eval-used, incoherent-interpreter-exec-perm, + openerp-exception-warning, + redundant-modulename-xml, + relative-import, + rst-syntax-error, + wrong-tabs-instead-of-spaces, + xml-syntax-error, + assignment-from-none, + attribute-deprecated, + dangerous-default-value, + development-status-allowed, + duplicate-key, + eval-used, license-allowed, manifest-author-string, manifest-deprecated-key, @@ -40,56 +46,50 @@ enable=anomalous-backslash-in-string, method-inverse, method-required-super, method-search, - openerp-exception-warning, pointless-statement, pointless-string-statement, print-used, redundant-keyword-arg, - redundant-modulename-xml, reimported, - relative-import, return-in-init, - rst-syntax-error, sql-injection, too-few-format-args, translation-field, translation-required, unreachable, use-vim-comment, - wrong-tabs-instead-of-spaces, - xml-syntax-error, - attribute-string-redundant, character-not-valid-in-resource-link, - consider-merging-classes-inherited, - context-overridden, create-user-wo-reset-password, dangerous-filter-wo-user, dangerous-qweb-replace-wo-priority, deprecated-data-xml-node, deprecated-openerp-xml-node, duplicate-po-message-definition, - except-pass, file-not-used, + missing-newline-extrafiles, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + str-format-used, + unnecessary-utf8-coding-comment, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute, + attribute-string-redundant, + consider-merging-classes-inherited, + context-overridden, + except-pass, invalid-commit, manifest-maintainers-list, - missing-newline-extrafiles, missing-readme, missing-return, odoo-addons-relative-import, - old-api7-method-defined, - po-msgstr-variables, - po-syntax-error, renamed-field-parameter, resource-not-exist, - str-format-used, test-folder-imported, translation-contains-variable, translation-positional-used, - unnecessary-utf8-coding-comment, website-manifest-key-not-valid-uri, - xml-attribute-translatable, - xml-deprecated-qweb-directive, - xml-deprecated-tree-attribute, external-request-timeout [REPORTS] diff --git a/README.md b/README.md index 5b92fb3a4..35889a0be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ +[![Support the OCA](https://odoo-community.org/readme-banner-image)](https://odoo-community.org/get-involved?utm_source=repo-readme) + +# edi-framework [![Runboat](https://img.shields.io/badge/runboat-Try%20me-875A7B.png)](https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=18.0) [![Pre-commit Status](https://github.com/OCA/edi-framework/actions/workflows/pre-commit.yml/badge.svg?branch=18.0)](https://github.com/OCA/edi-framework/actions/workflows/pre-commit.yml?query=branch%3A18.0) [![Build Status](https://github.com/OCA/edi-framework/actions/workflows/test.yml/badge.svg?branch=18.0)](https://github.com/OCA/edi-framework/actions/workflows/test.yml?query=branch%3A18.0) @@ -7,8 +10,6 @@ -# edi-framework - edi-framework diff --git a/checklog-odoo.cfg b/checklog-odoo.cfg index 0b55b7bf6..58d43aa66 100644 --- a/checklog-odoo.cfg +++ b/checklog-odoo.cfg @@ -1,3 +1,5 @@ [checklog-odoo] ignore= WARNING.* 0 failed, 0 error\(s\).* + WARNING .* Killing chrome descendants-or-self .* + WARNING.* Missing widget: res_partner_many2one for field of type many2one.* 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, From d69012fd60dc300c2fa1ebaa7ca40e8c12c219d9 Mon Sep 17 00:00:00 2001 From: duongtq Date: Mon, 25 Mar 2024 12:19:40 +0700 Subject: [PATCH 25/58] [ADD] edi_notification_oca --- edi_notification_oca/README.rst | 87 ++++ edi_notification_oca/__init__.py | 2 + edi_notification_oca/__manifest__.py | 17 + edi_notification_oca/components/__init__.py | 1 + edi_notification_oca/components/listener.py | 46 ++ .../data/mail_activity_data.xml | 12 + edi_notification_oca/models/__init__.py | 2 + .../models/edi_exchange_record.py | 11 + .../models/edi_exchange_type.py | 52 +++ edi_notification_oca/readme/CONTRIBUTORS.rst | 2 + edi_notification_oca/readme/CREDITS.rst | 1 + edi_notification_oca/readme/DESCRIPTION.rst | 1 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 435 ++++++++++++++++++ edi_notification_oca/tests/__init__.py | 1 + .../tests/test_edi_notification.py | 176 +++++++ .../views/edi_exchange_type_views.xml | 36 ++ 17 files changed, 882 insertions(+) create mode 100644 edi_notification_oca/README.rst create mode 100644 edi_notification_oca/__init__.py create mode 100644 edi_notification_oca/__manifest__.py create mode 100644 edi_notification_oca/components/__init__.py create mode 100644 edi_notification_oca/components/listener.py create mode 100644 edi_notification_oca/data/mail_activity_data.xml create mode 100644 edi_notification_oca/models/__init__.py create mode 100644 edi_notification_oca/models/edi_exchange_record.py create mode 100644 edi_notification_oca/models/edi_exchange_type.py create mode 100644 edi_notification_oca/readme/CONTRIBUTORS.rst create mode 100644 edi_notification_oca/readme/CREDITS.rst create mode 100644 edi_notification_oca/readme/DESCRIPTION.rst create mode 100644 edi_notification_oca/static/description/icon.png create mode 100644 edi_notification_oca/static/description/index.html create mode 100644 edi_notification_oca/tests/__init__.py create mode 100644 edi_notification_oca/tests/test_edi_notification.py create mode 100644 edi_notification_oca/views/edi_exchange_type_views.xml diff --git a/edi_notification_oca/README.rst b/edi_notification_oca/README.rst new file mode 100644 index 000000000..61d2bf64f --- /dev/null +++ b/edi_notification_oca/README.rst @@ -0,0 +1,87 @@ +================ +EDI Notification +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:430afb16d5a7fa86b62589b77b3caa51698cd52619a5a384aff592cb0892a3d0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-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/16.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-16-0/edi-framework-16-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=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides sending notification feature when exchange record. + +.. 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 + +Other credits +~~~~~~~~~~~~~ + +The creation of this module was financially supported by Camptocamp. + +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..f3696bc1d --- /dev/null +++ b/edi_notification_oca/__manifest__.py @@ -0,0 +1,17 @@ +# 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": "16.0.1.0.0", + "development_status": "Alpha", + "license": "LGPL-3", + "website": "https://github.com/OCA/edi-framework", + "author": "Camptocamp,Odoo Community Association (OCA)", + "depends": [ + "edi_oca", + ], + "data": ["data/mail_activity_data.xml", "views/edi_exchange_type_views.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..4dcb9b138 --- /dev/null +++ b/edi_notification_oca/components/listener.py @@ -0,0 +1,46 @@ +# Copyright 2024 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import _ + +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=_( + "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_data.xml b/edi_notification_oca/data/mail_activity_data.xml new file mode 100644 index 000000000..1bf2c1c88 --- /dev/null +++ b/edi_notification_oca/data/mail_activity_data.xml @@ -0,0 +1,12 @@ + + + + EDI Exchange Record: Failed + fa-warning + edi.exchange.record + warning + + 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..a1279f4aa --- /dev/null +++ b/edi_notification_oca/models/edi_exchange_record.py @@ -0,0 +1,11 @@ +# 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..037f430ee --- /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/readme/CONTRIBUTORS.rst b/edi_notification_oca/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..7b3f8625e --- /dev/null +++ b/edi_notification_oca/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Duong (Tran Quoc) +* Simone Orsi diff --git a/edi_notification_oca/readme/CREDITS.rst b/edi_notification_oca/readme/CREDITS.rst new file mode 100644 index 000000000..ac19123b0 --- /dev/null +++ b/edi_notification_oca/readme/CREDITS.rst @@ -0,0 +1 @@ +The creation of this module was financially supported by Camptocamp. diff --git a/edi_notification_oca/readme/DESCRIPTION.rst b/edi_notification_oca/readme/DESCRIPTION.rst new file mode 100644 index 000000000..46a845420 --- /dev/null +++ b/edi_notification_oca/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module provides sending notification feature when exchange record. diff --git a/edi_notification_oca/static/description/icon.png b/edi_notification_oca/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/edi_notification_oca/static/description/index.html b/edi_notification_oca/static/description/index.html new file mode 100644 index 000000000..0f7ec18e4 --- /dev/null +++ b/edi_notification_oca/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +EDI Notification + + + +
+

EDI Notification

+ + +

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

+

This module provides sending notification feature when exchange record.

+
+

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

+

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

+ +
+
+

Other credits

+

The creation of this module was financially supported by Camptocamp.

+
+
+

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.

+

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..3e2e958a3 --- /dev/null +++ b/edi_notification_oca/tests/test_edi_notification.py @@ -0,0 +1,176 @@ +# 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(cls, "A") + cls.user_b = cls._create_user(cls, "B") + cls.user_c = cls._create_user(cls, "C") + + def setUp(self): + super().setUp() + FakeInputProcess.reset_faked() + + def _create_user(self, letter): + return ( + self.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "name": "User %s" % letter, + "login": "user_%s" % letter, + "groups_id": [(6, 0, [self.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_views.xml b/edi_notification_oca/views/edi_exchange_type_views.xml new file mode 100644 index 000000000..58da40517 --- /dev/null +++ b/edi_notification_oca/views/edi_exchange_type_views.xml @@ -0,0 +1,36 @@ + + + + edi.exchange.type + + + + + + + + + + + + + + + + + From 52c6cca1bb1772b2b125ef07a10fe406fe4ef19c Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 25 Jul 2024 06:39:53 +0000 Subject: [PATCH 26/58] [UPD] Update edi_notification_oca.pot --- .../i18n/edi_notification_oca.pot | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 edi_notification_oca/i18n/edi_notification_oca.pot 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..43162d14e --- /dev/null +++ b/edi_notification_oca/i18n/edi_notification_oca.pot @@ -0,0 +1,150 @@ +# 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" +"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 +#, python-format +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 "" From 4a3cb5d04a29bd410b9b72282fe530f3fd91d3cf Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 25 Jul 2024 06:42:54 +0000 Subject: [PATCH 27/58] [BOT] post-merge updates --- edi_notification_oca/README.rst | 2 +- edi_notification_oca/static/description/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/edi_notification_oca/README.rst b/edi_notification_oca/README.rst index 61d2bf64f..0953238f1 100644 --- a/edi_notification_oca/README.rst +++ b/edi_notification_oca/README.rst @@ -7,7 +7,7 @@ EDI Notification !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:430afb16d5a7fa86b62589b77b3caa51698cd52619a5a384aff592cb0892a3d0 + !! source digest: sha256:d8651cf2a0e90d542e65ce8083a2bccffd0b1a3549867bb7a8e91843bd62a770 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png diff --git a/edi_notification_oca/static/description/index.html b/edi_notification_oca/static/description/index.html index 0f7ec18e4..4e9ce77a0 100644 --- a/edi_notification_oca/static/description/index.html +++ b/edi_notification_oca/static/description/index.html @@ -367,7 +367,7 @@

EDI Notification

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:430afb16d5a7fa86b62589b77b3caa51698cd52619a5a384aff592cb0892a3d0 +!! source digest: sha256:d8651cf2a0e90d542e65ce8083a2bccffd0b1a3549867bb7a8e91843bd62a770 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module provides sending notification feature when exchange record.

From 2fd0bed2a68a82aa85006a57fdd7a11fb291586a Mon Sep 17 00:00:00 2001 From: mymage Date: Tue, 30 Jul 2024 12:13:25 +0000 Subject: [PATCH 28/58] Added translation using Weblate (Italian) --- edi_notification_oca/i18n/it.po | 151 ++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 edi_notification_oca/i18n/it.po diff --git a/edi_notification_oca/i18n/it.po b/edi_notification_oca/i18n/it.po new file mode 100644 index 000000000..442cbfe1b --- /dev/null +++ b/edi_notification_oca/i18n/it.po @@ -0,0 +1,151 @@ +# 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" +"Last-Translator: Automatically generated\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" + +#. 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 +#, python-format +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 "" From b358172c3af34b13fc132ab36004765911e95e4c Mon Sep 17 00:00:00 2001 From: mymage Date: Tue, 10 Sep 2024 11:15:28 +0000 Subject: [PATCH 29/58] Translated using Weblate (Italian) Currently translated at 100.0% (25 of 25 strings) Translation: edi-framework-16.0/edi-framework-16.0-edi_notification_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-16-0/edi-framework-16-0-edi_notification_oca/it/ --- edi_notification_oca/i18n/it.po | 58 ++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/edi_notification_oca/i18n/it.po b/edi_notification_oca/i18n/it.po index 442cbfe1b..5a02a680f 100644 --- a/edi_notification_oca/i18n/it.po +++ b/edi_notification_oca/i18n/it.po @@ -6,75 +6,77 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Tipo scambio EDI" #. module: edi_notification_oca #: model:ir.model,name:edi_notification_oca.model_edi_exchange_record msgid "EDI exchange Record" -msgstr "" +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 "" +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 "" +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 "" +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 "" +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 @@ -83,51 +85,54 @@ msgid "" "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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 @@ -135,6 +140,9 @@ 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 @@ -144,8 +152,12 @@ msgid "" "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 "" +msgstr "Tipo di attività eccezione sul record." From fa0985aa0b4d949484194a3ff189217618ed43a9 Mon Sep 17 00:00:00 2001 From: SilvioC2C Date: Wed, 18 Mar 2026 15:46:37 +0100 Subject: [PATCH 30/58] [IMP] edi_notification_oca: pre-commit auto fixes --- edi_notification_oca/README.rst | 22 +++++++++---------- .../data/mail_activity_data.xml | 12 +++++----- .../models/edi_exchange_record.py | 1 - edi_notification_oca/pyproject.toml | 3 +++ edi_notification_oca/readme/CONTRIBUTORS.md | 2 ++ edi_notification_oca/readme/CONTRIBUTORS.rst | 2 -- .../readme/{CREDITS.rst => CREDITS.md} | 0 .../{DESCRIPTION.rst => DESCRIPTION.md} | 0 .../static/description/index.html | 6 ++--- 9 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 edi_notification_oca/pyproject.toml create mode 100644 edi_notification_oca/readme/CONTRIBUTORS.md delete mode 100644 edi_notification_oca/readme/CONTRIBUTORS.rst rename edi_notification_oca/readme/{CREDITS.rst => CREDITS.md} (100%) rename edi_notification_oca/readme/{DESCRIPTION.rst => DESCRIPTION.md} (100%) diff --git a/edi_notification_oca/README.rst b/edi_notification_oca/README.rst index 0953238f1..3ea1c4c32 100644 --- a/edi_notification_oca/README.rst +++ b/edi_notification_oca/README.rst @@ -17,13 +17,13 @@ EDI Notification :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/16.0/edi_notification_oca + :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-16-0/edi-framework-16-0-edi_notification_oca + :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=16.0 + :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| @@ -46,7 +46,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -54,23 +54,23 @@ Credits ======= Authors -~~~~~~~ +------- * Camptocamp Contributors -~~~~~~~~~~~~ +------------ -* Duong (Tran Quoc) -* Simone Orsi +- Duong (Tran Quoc) +- Simone Orsi Other credits -~~~~~~~~~~~~~ +------------- The creation of this module was financially supported by Camptocamp. Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -82,6 +82,6 @@ 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. +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/data/mail_activity_data.xml b/edi_notification_oca/data/mail_activity_data.xml index 1bf2c1c88..7fa92c0d4 100644 --- a/edi_notification_oca/data/mail_activity_data.xml +++ b/edi_notification_oca/data/mail_activity_data.xml @@ -1,12 +1,12 @@ - - EDI Exchange Record: Failed - fa-warning - edi.exchange.record - warning - + EDI Exchange Record: Failed + fa-warning + edi.exchange.record + warning + diff --git a/edi_notification_oca/models/edi_exchange_record.py b/edi_notification_oca/models/edi_exchange_record.py index a1279f4aa..629c7c4df 100644 --- a/edi_notification_oca/models/edi_exchange_record.py +++ b/edi_notification_oca/models/edi_exchange_record.py @@ -6,6 +6,5 @@ class EDIExchangeRecord(models.Model): - _name = "edi.exchange.record" _inherit = ["edi.exchange.record", "mail.activity.mixin"] 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/CONTRIBUTORS.rst b/edi_notification_oca/readme/CONTRIBUTORS.rst deleted file mode 100644 index 7b3f8625e..000000000 --- a/edi_notification_oca/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,2 +0,0 @@ -* Duong (Tran Quoc) -* Simone Orsi diff --git a/edi_notification_oca/readme/CREDITS.rst b/edi_notification_oca/readme/CREDITS.md similarity index 100% rename from edi_notification_oca/readme/CREDITS.rst rename to edi_notification_oca/readme/CREDITS.md diff --git a/edi_notification_oca/readme/DESCRIPTION.rst b/edi_notification_oca/readme/DESCRIPTION.md similarity index 100% rename from edi_notification_oca/readme/DESCRIPTION.rst rename to edi_notification_oca/readme/DESCRIPTION.md diff --git a/edi_notification_oca/static/description/index.html b/edi_notification_oca/static/description/index.html index 4e9ce77a0..e1cd25ca1 100644 --- a/edi_notification_oca/static/description/index.html +++ b/edi_notification_oca/static/description/index.html @@ -369,7 +369,7 @@

EDI Notification

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:d8651cf2a0e90d542e65ce8083a2bccffd0b1a3549867bb7a8e91843bd62a770 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

+

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

This module provides sending notification feature when exchange record.

Important

@@ -395,7 +395,7 @@

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.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -426,7 +426,7 @@

Maintainers

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.

+

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.

From 358ea61c983fa8cb88b8864f5b3b22073727ec97 Mon Sep 17 00:00:00 2001 From: SilvioC2C Date: Wed, 18 Mar 2026 16:02:55 +0100 Subject: [PATCH 31/58] [MIG] edi_notification_oca: Migration to 18.0 --- edi_notification_oca/README.rst | 18 ++++++++++----- edi_notification_oca/__manifest__.py | 9 ++++---- edi_notification_oca/components/listener.py | 4 +--- ...tivity_data.xml => mail_activity_type.xml} | 0 .../models/edi_exchange_record.py | 1 - .../models/edi_exchange_type.py | 6 ++--- edi_notification_oca/readme/CREDITS.md | 1 - edi_notification_oca/readme/DESCRIPTION.md | 11 +++++++++- .../static/description/index.html | 22 ++++++++++++------- .../tests/test_edi_notification.py | 17 +++++++------- ...e_type_views.xml => edi_exchange_type.xml} | 8 +++---- 11 files changed, 57 insertions(+), 40 deletions(-) rename edi_notification_oca/data/{mail_activity_data.xml => mail_activity_type.xml} (100%) delete mode 100644 edi_notification_oca/readme/CREDITS.md rename edi_notification_oca/views/{edi_exchange_type_views.xml => edi_exchange_type.xml} (75%) diff --git a/edi_notification_oca/README.rst b/edi_notification_oca/README.rst index 3ea1c4c32..79b243d20 100644 --- a/edi_notification_oca/README.rst +++ b/edi_notification_oca/README.rst @@ -28,7 +28,18 @@ EDI Notification |badge1| |badge2| |badge3| |badge4| |badge5| -This module provides sending notification feature when exchange record. +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. @@ -64,11 +75,6 @@ Contributors - Duong (Tran Quoc) - Simone Orsi -Other credits -------------- - -The creation of this module was financially supported by Camptocamp. - Maintainers ----------- diff --git a/edi_notification_oca/__manifest__.py b/edi_notification_oca/__manifest__.py index f3696bc1d..5fc8183f9 100644 --- a/edi_notification_oca/__manifest__.py +++ b/edi_notification_oca/__manifest__.py @@ -4,14 +4,13 @@ { "name": "EDI Notification", "summary": """Define notification activities on exchange records.""", - "version": "16.0.1.0.0", + "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)", - "depends": [ - "edi_oca", - ], - "data": ["data/mail_activity_data.xml", "views/edi_exchange_type_views.xml"], + # 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/listener.py b/edi_notification_oca/components/listener.py index 4dcb9b138..30945e128 100644 --- a/edi_notification_oca/components/listener.py +++ b/edi_notification_oca/components/listener.py @@ -1,8 +1,6 @@ # Copyright 2024 Camptocamp SA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo import _ - from odoo.addons.component.core import Component @@ -28,7 +26,7 @@ def on_edi_exchange_error(self, record): for user in users: record.activity_schedule( activity_type_id=activity_type.id, - summary=_( + summary=self.env._( "EDI: Process error on record '%(identifier)s'.", identifier=record.identifier, ), diff --git a/edi_notification_oca/data/mail_activity_data.xml b/edi_notification_oca/data/mail_activity_type.xml similarity index 100% rename from edi_notification_oca/data/mail_activity_data.xml rename to edi_notification_oca/data/mail_activity_type.xml diff --git a/edi_notification_oca/models/edi_exchange_record.py b/edi_notification_oca/models/edi_exchange_record.py index 629c7c4df..e6b48bc68 100644 --- a/edi_notification_oca/models/edi_exchange_record.py +++ b/edi_notification_oca/models/edi_exchange_record.py @@ -1,7 +1,6 @@ # Copyright 2024 Camptocamp SA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - from odoo import models diff --git a/edi_notification_oca/models/edi_exchange_type.py b/edi_notification_oca/models/edi_exchange_type.py index 037f430ee..3f29d606e 100644 --- a/edi_notification_oca/models/edi_exchange_type.py +++ b/edi_notification_oca/models/edi_exchange_type.py @@ -1,7 +1,6 @@ # Copyright 2024 Camptocamp SA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - from odoo import api, fields, models @@ -23,8 +22,9 @@ class EDIExchangeType(models.Model): 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.", + 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", diff --git a/edi_notification_oca/readme/CREDITS.md b/edi_notification_oca/readme/CREDITS.md deleted file mode 100644 index ac19123b0..000000000 --- a/edi_notification_oca/readme/CREDITS.md +++ /dev/null @@ -1 +0,0 @@ -The creation of this module was financially supported by Camptocamp. diff --git a/edi_notification_oca/readme/DESCRIPTION.md b/edi_notification_oca/readme/DESCRIPTION.md index 46a845420..3f250b798 100644 --- a/edi_notification_oca/readme/DESCRIPTION.md +++ b/edi_notification_oca/readme/DESCRIPTION.md @@ -1 +1,10 @@ -This module provides sending notification feature when exchange record. +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/index.html b/edi_notification_oca/static/description/index.html index e1cd25ca1..0120c593b 100644 --- a/edi_notification_oca/static/description/index.html +++ b/edi_notification_oca/static/description/index.html @@ -370,7 +370,18 @@

EDI Notification

!! source digest: sha256:d8651cf2a0e90d542e65ce8083a2bccffd0b1a3549867bb7a8e91843bd62a770 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

-

This module provides sending notification feature when exchange record.

+

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. @@ -384,8 +395,7 @@

EDI Notification

  • Credits
  • @@ -413,12 +423,8 @@

    Contributors

  • Simone Orsi <simone.orsi@camptocamp.com>
  • -
    -

    Other credits

    -

    The creation of this module was financially supported by Camptocamp.

    -
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association diff --git a/edi_notification_oca/tests/test_edi_notification.py b/edi_notification_oca/tests/test_edi_notification.py index 3e2e958a3..cd9ee4f88 100644 --- a/edi_notification_oca/tests/test_edi_notification.py +++ b/edi_notification_oca/tests/test_edi_notification.py @@ -27,23 +27,24 @@ def setUpClass(cls): } 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(cls, "A") - cls.user_b = cls._create_user(cls, "B") - cls.user_c = cls._create_user(cls, "C") + 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() - def _create_user(self, letter): + @classmethod + def _create_user(cls, letter: str): return ( - self.env["res.users"] + cls.env["res.users"] .with_context(no_reset_password=True) .create( { - "name": "User %s" % letter, - "login": "user_%s" % letter, - "groups_id": [(6, 0, [self.group_portal.id])], + "name": f"User {letter}", + "login": f"user_{letter}", + "groups_id": [(6, 0, [cls.group_portal.id])], } ) ) diff --git a/edi_notification_oca/views/edi_exchange_type_views.xml b/edi_notification_oca/views/edi_exchange_type.xml similarity index 75% rename from edi_notification_oca/views/edi_exchange_type_views.xml rename to edi_notification_oca/views/edi_exchange_type.xml index 58da40517..e052b6eb5 100644 --- a/edi_notification_oca/views/edi_exchange_type_views.xml +++ b/edi_notification_oca/views/edi_exchange_type.xml @@ -11,22 +11,22 @@ From 0d91f71638914557d4b1ecdb0dd79ee666816fe6 Mon Sep 17 00:00:00 2001 From: Ricardoalso Date: Fri, 13 Feb 2026 10:12:47 +0100 Subject: [PATCH 32/58] [IMP] edi_core_oca: add backend-controlled auto-cleanup for exchange records Add configurable auto-archiving and auto-deletion of EDI exchange records per backend. This backend-specific settings that can be managed individually. Changes: - Add "Auto Cleanup" tab in backend form view for easy configuration - Add archive cron job that respect backend-specific retention policies - Add delete cron job that respect backend-specific retention policies - Add "Archived" filter to exchange record search view Benefits: - Each backend can have different retention policies - Setting fields to 0 (default value) disables the respective cleanup behavior - More flexible data retention management - Better control over storage and performance optimization The cron jobs iterate through backends with configured retention settings, applying appropriate cutoff values. chore(edi_core_oca): update documentation --- edi_core_oca/README.rst | 28 +++++++++++++++++-- edi_core_oca/__manifest__.py | 2 ++ .../data/ir_cron_archive_old_edi_records.xml | 27 ++++++++++++++++++ ...r_cron_delete_old_archived_edi_records.xml | 27 ++++++++++++++++++ edi_core_oca/models/edi_backend.py | 12 ++++++++ edi_core_oca/models/edi_exchange_record.py | 1 + edi_core_oca/views/edi_backend_views.xml | 9 +++++- .../views/edi_exchange_record_views.xml | 11 ++++++++ 8 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 edi_core_oca/data/ir_cron_archive_old_edi_records.xml create mode 100644 edi_core_oca/data/ir_cron_delete_old_archived_edi_records.xml diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index f6f7f6077..d8ededb9b 100644 --- a/edi_core_oca/README.rst +++ b/edi_core_oca/README.rst @@ -6,7 +6,7 @@ EDI === -.. +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! @@ -64,6 +64,30 @@ In order to define a new Exchange Record, we need to configure: - Backend - Components +Exchange Records Retention +--------------------------- + +Configure automatic archiving and deletion of old exchange records per backend +to manage data retention and optimize performance. + +Go to an EDI Backend form and open the **Auto Cleanup** tab. Two fields are +available: + +- **Auto-archive records after (days)**: Archives exchange records older than + the specified number of days. Set to 0 to disable. + +- **Auto-delete archived records after (days)**: Permanently deletes archived + exchange records older than the specified number of days. Set to 0 to disable. + +Each backend can have different retention policies. Scheduled actions run +periodically to apply these settings: + +- Archive job runs daily +- Delete job runs every 3 hours + +To view archived records, use the "Archived" filter in the exchange records +search view. + Jobs ---- @@ -233,7 +257,7 @@ promote its widespread use. Current `maintainers `__: -|maintainer-simahawk| |maintainer-etobella| +|maintainer-simahawk| |maintainer-etobella| This module is part of the `OCA/edi-framework `_ project on GitHub. diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 2f2f6a9f6..fe3f5400a 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -29,6 +29,8 @@ "data/ir_actions_server.xml", "data/sequence.xml", "data/edi_configuration.xml", + "data/ir_cron_archive_old_edi_records.xml", + "data/ir_cron_delete_old_archived_edi_records.xml", "security/res_groups.xml", "security/ir_model_access.xml", "views/edi_backend_views.xml", diff --git a/edi_core_oca/data/ir_cron_archive_old_edi_records.xml b/edi_core_oca/data/ir_cron_archive_old_edi_records.xml new file mode 100644 index 000000000..b1e8dc78d --- /dev/null +++ b/edi_core_oca/data/ir_cron_archive_old_edi_records.xml @@ -0,0 +1,27 @@ + + + + Archive Old EDI Exchange Records + + code + +# Archive old EDI exchange records based on backend configuration +backends = env['edi.backend'].search([ + ('auto_archive_records_after_days', '>', 0) +]) +for backend in backends: + cutoff_date = datetime.datetime.now() - datetime.timedelta(days=backend.auto_archive_records_after_days) + records = model.search([ + ('backend_id', '=', backend.id), + ('create_date', '<', cutoff_date), + ('active', '=', True) + ], limit=10000, order="create_date asc") + if records: + records.action_archive() + + 1 + days + + + + diff --git a/edi_core_oca/data/ir_cron_delete_old_archived_edi_records.xml b/edi_core_oca/data/ir_cron_delete_old_archived_edi_records.xml new file mode 100644 index 000000000..05d05a111 --- /dev/null +++ b/edi_core_oca/data/ir_cron_delete_old_archived_edi_records.xml @@ -0,0 +1,27 @@ + + + + Delete Old Archived EDI Exchange Records + + code + +# Delete old archived EDI exchange records based on backend configuration +backends = env['edi.backend'].search([ + ('auto_delete_records_after_days', '>', 0) +]) +for backend in backends: + cutoff_date = datetime.datetime.now() - datetime.timedelta(days=backend.auto_delete_records_after_days) + records = model.search([ + ('backend_id', '=', backend.id), + ('create_date', '<', cutoff_date), + ('active', '=', False) + ], limit=100, order="create_date asc") + if records: + records.unlink() + + 3 + hours + + + + diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index a8c6ce8bd..54c845480 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -62,6 +62,18 @@ class EDIBackend(models.Model): ) active = fields.Boolean(default=True) company_id = fields.Many2one("res.company", string="Company") + auto_archive_records_after_days = fields.Integer( + string="Auto-archive records after (days)", + default=0, + help="Automatically archive EDI exchange records after X days. " + "Set to <= 0 to disable auto-archiving.", + ) + auto_delete_records_after_days = fields.Integer( + string="Auto-delete archived records after (days)", + default=0, + help="Automatically delete archived EDI exchange records after X days. " + "Set to <= 0 to disable auto-deletion.", + ) @property def exchange_record_model(self): diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index c1fab6bc7..a87b4506e 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -144,6 +144,7 @@ class EDIExchangeRecord(models.Model): help="The record state can be rolled back manually in case of failure.", ) company_id = fields.Many2one("res.company", string="Company") + active = fields.Boolean(default=True) _sql_constraints = [ ("identifier_uniq", "unique(identifier)", "The identifier must be unique."), 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 @@ - + + + + + + + + diff --git a/edi_core_oca/views/edi_exchange_record_views.xml b/edi_core_oca/views/edi_exchange_record_views.xml index f3538ea6e..326461558 100644 --- a/edi_core_oca/views/edi_exchange_record_views.xml +++ b/edi_core_oca/views/edi_exchange_record_views.xml @@ -265,6 +265,17 @@ help="Show all records created in the last 7 days" /> + + + From 7c32e062f70caec85367632235a83c6583036ea1 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Mon, 27 Apr 2026 14:36:06 +0000 Subject: [PATCH 33/58] [UPD] Update edi_core_oca.pot --- edi_core_oca/i18n/edi_core_oca.pot | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/edi_core_oca/i18n/edi_core_oca.pot b/edi_core_oca/i18n/edi_core_oca.pot index d599fe05b..436bf77b7 100644 --- a/edi_core_oca/i18n/edi_core_oca.pot +++ b/edi_core_oca/i18n/edi_core_oca.pot @@ -192,10 +192,12 @@ msgstr "" #: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration_trigger__active +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_type__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_type_rule__active #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_view_search +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_view_search msgid "Active" @@ -240,11 +242,17 @@ msgstr "" msgid "Apply to this model" msgstr "" +#. module: edi_core_oca +#: model:ir.actions.server,name:edi_core_oca.ir_cron_archive_old_edi_records_ir_actions_server +msgid "Archive Old EDI Exchange Records" +msgstr "" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_trigger_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_view_form +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_view_form @@ -264,6 +272,30 @@ msgid "" "will take care of generating the output when not set yet. " msgstr "" +#. module: edi_core_oca +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_archive_records_after_days +msgid "Auto-archive records after (days)" +msgstr "" + +#. module: edi_core_oca +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_delete_records_after_days +msgid "Auto-delete archived records after (days)" +msgstr "" + +#. module: edi_core_oca +#: model:ir.model.fields,help:edi_core_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_core_oca +#: model:ir.model.fields,help:edi_core_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_core_oca #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__backend_id #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__backend_id @@ -416,6 +448,11 @@ msgstr "" msgid "Decoding Error Handler" msgstr "" +#. module: edi_core_oca +#: model:ir.actions.server,name:edi_core_oca.ir_cron_delete_old_archived_edi_records_ir_actions_server +msgid "Delete Old Archived EDI Exchange Records" +msgstr "" + #. module: edi_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__description #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration_trigger__description @@ -1364,6 +1401,11 @@ msgstr "" msgid "Record ID=%d is not meant to be sent!" msgstr "" +#. module: edi_core_oca +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form +msgid "Records retention" +msgstr "" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_form msgid "Regenerate" From 732972baa22ec0946eacaaa0a1949f25ad831121 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Mon, 27 Apr 2026 14:36:07 +0000 Subject: [PATCH 34/58] [UPD] Update edi_oca.pot --- edi_oca/i18n/edi_oca.pot | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/edi_oca/i18n/edi_oca.pot b/edi_oca/i18n/edi_oca.pot index 4db84723b..f3babc523 100644 --- a/edi_oca/i18n/edi_oca.pot +++ b/edi_oca/i18n/edi_oca.pot @@ -168,10 +168,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 +212,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 +242,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 +398,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 @@ -1209,6 +1246,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" From a82ba97316b22abe1c267ceb52e8e8c5ff3ad2ca Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 27 Apr 2026 14:42:43 +0000 Subject: [PATCH 35/58] [BOT] post-merge updates --- README.md | 2 +- edi_core_oca/README.rst | 30 +++------------------- edi_core_oca/__manifest__.py | 2 +- edi_core_oca/static/description/index.html | 2 +- 4 files changed, 6 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 35889a0be..4b9cfae86 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ addon | version | maintainers | summary [edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 | etobella | Define EDI Configuration for Account Moves [edi_account_oca](edi_account_oca/) | 18.0.1.1.1 | etobella | Define some component listeners for Account Moves [edi_component_oca](edi_component_oca/) | 18.0.1.0.3 | simahawk etobella | Allow to use Connector as a source in EDI -[edi_core_oca](edi_core_oca/) | 18.0.1.6.5 | simahawk etobella | Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. +[edi_core_oca](edi_core_oca/) | 18.0.1.6.6 | simahawk etobella | 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_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 | simahawk | Allows definition of exchanges via templates. [edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 | simahawk | Glue module between edi_exchange_template and edi_party_data diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index d8ededb9b..bb985bd0d 100644 --- a/edi_core_oca/README.rst +++ b/edi_core_oca/README.rst @@ -6,12 +6,12 @@ EDI === -.. +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:97e3e3a909c79dced8fe9e6165a0c440f95f672e4a184a9b014cde518a5848c2 + !! source digest: sha256:c609033733302fa71a3c01c11e2729fd2b47ccde0b9a1d0619bed03cc26db4fe !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -64,30 +64,6 @@ In order to define a new Exchange Record, we need to configure: - Backend - Components -Exchange Records Retention ---------------------------- - -Configure automatic archiving and deletion of old exchange records per backend -to manage data retention and optimize performance. - -Go to an EDI Backend form and open the **Auto Cleanup** tab. Two fields are -available: - -- **Auto-archive records after (days)**: Archives exchange records older than - the specified number of days. Set to 0 to disable. - -- **Auto-delete archived records after (days)**: Permanently deletes archived - exchange records older than the specified number of days. Set to 0 to disable. - -Each backend can have different retention policies. Scheduled actions run -periodically to apply these settings: - -- Archive job runs daily -- Delete job runs every 3 hours - -To view archived records, use the "Archived" filter in the exchange records -search view. - Jobs ---- @@ -257,7 +233,7 @@ promote its widespread use. Current `maintainers `__: -|maintainer-simahawk| |maintainer-etobella| +|maintainer-simahawk| |maintainer-etobella| This module is part of the `OCA/edi-framework `_ project on GitHub. diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index fe3f5400a..a2929a7ab 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.6.5", + "version": "18.0.1.6.6", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html index e7a67132d..0273741fe 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:97e3e3a909c79dced8fe9e6165a0c440f95f672e4a184a9b014cde518a5848c2 +!! source digest: sha256:c609033733302fa71a3c01c11e2729fd2b47ccde0b9a1d0619bed03cc26db4fe !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

    Base EDI backend.

    From eaf47116934d5301ec0baa1e7796c645762933bf Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 27 Apr 2026 14:42:52 +0000 Subject: [PATCH 36/58] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: edi-framework-18.0/edi-framework-18.0-edi_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_oca/ --- edi_oca/i18n/it.po | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/edi_oca/i18n/it.po b/edi_oca/i18n/it.po index cbff8fe7c..b7c6df9cf 100644 --- a/edi_oca/i18n/it.po +++ b/edi_oca/i18n/it.po @@ -234,10 +234,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 +278,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 "" + #. 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 +311,30 @@ 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 "" + +#. 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 @@ -435,6 +467,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 "" + #. 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 @@ -1304,6 +1341,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 "" + #. module: edi_oca #: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_form msgid "Regenerate" From 6d946900ed1d226cb463e4b2417e67f48b32cc70 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 27 Apr 2026 14:42:52 +0000 Subject: [PATCH 37/58] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: edi-framework-18.0/edi-framework-18.0-edi_core_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_core_oca/ --- edi_core_oca/i18n/es.po | 42 +++++++++++++++++++++++++++++++++++++++++ edi_core_oca/i18n/fr.po | 42 +++++++++++++++++++++++++++++++++++++++++ edi_core_oca/i18n/it.po | 42 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/edi_core_oca/i18n/es.po b/edi_core_oca/i18n/es.po index 3fecfe8e6..6aa91ccbc 100644 --- a/edi_core_oca/i18n/es.po +++ b/edi_core_oca/i18n/es.po @@ -196,10 +196,12 @@ msgstr "" #: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration_trigger__active +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_type__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_type_rule__active #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_view_search +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_view_search msgid "Active" @@ -244,11 +246,17 @@ msgstr "" msgid "Apply to this model" msgstr "" +#. module: edi_core_oca +#: model:ir.actions.server,name:edi_core_oca.ir_cron_archive_old_edi_records_ir_actions_server +msgid "Archive Old EDI Exchange Records" +msgstr "" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_trigger_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_view_form +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_view_form @@ -268,6 +276,30 @@ msgid "" "will take care of generating the output when not set yet. " msgstr "" +#. module: edi_core_oca +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_archive_records_after_days +msgid "Auto-archive records after (days)" +msgstr "" + +#. module: edi_core_oca +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_delete_records_after_days +msgid "Auto-delete archived records after (days)" +msgstr "" + +#. module: edi_core_oca +#: model:ir.model.fields,help:edi_core_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_core_oca +#: model:ir.model.fields,help:edi_core_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_core_oca #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__backend_id #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__backend_id @@ -420,6 +452,11 @@ msgstr "" msgid "Decoding Error Handler" msgstr "" +#. module: edi_core_oca +#: model:ir.actions.server,name:edi_core_oca.ir_cron_delete_old_archived_edi_records_ir_actions_server +msgid "Delete Old Archived EDI Exchange Records" +msgstr "" + #. module: edi_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__description #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration_trigger__description @@ -1373,6 +1410,11 @@ msgstr "" msgid "Record ID=%d is not meant to be sent!" msgstr "" +#. module: edi_core_oca +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form +msgid "Records retention" +msgstr "" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_form msgid "Regenerate" diff --git a/edi_core_oca/i18n/fr.po b/edi_core_oca/i18n/fr.po index dc7d2a607..28a967be4 100644 --- a/edi_core_oca/i18n/fr.po +++ b/edi_core_oca/i18n/fr.po @@ -201,10 +201,12 @@ msgstr "" #: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration_trigger__active +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_type__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_type_rule__active #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_view_search +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_view_search msgid "Active" @@ -251,11 +253,17 @@ msgstr "" msgid "Apply to this model" msgstr "" +#. module: edi_core_oca +#: model:ir.actions.server,name:edi_core_oca.ir_cron_archive_old_edi_records_ir_actions_server +msgid "Archive Old EDI Exchange Records" +msgstr "" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_trigger_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_view_form +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_view_form @@ -278,6 +286,30 @@ msgstr "" "utile est manquante. Si cette option est active, un cron se chargera de " "générer la sortie lorsqu'elle n'est pas encore définie. " +#. module: edi_core_oca +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_archive_records_after_days +msgid "Auto-archive records after (days)" +msgstr "" + +#. module: edi_core_oca +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_delete_records_after_days +msgid "Auto-delete archived records after (days)" +msgstr "" + +#. module: edi_core_oca +#: model:ir.model.fields,help:edi_core_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_core_oca +#: model:ir.model.fields,help:edi_core_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_core_oca #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__backend_id #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__backend_id @@ -430,6 +462,11 @@ msgstr "" msgid "Decoding Error Handler" msgstr "" +#. module: edi_core_oca +#: model:ir.actions.server,name:edi_core_oca.ir_cron_delete_old_archived_edi_records_ir_actions_server +msgid "Delete Old Archived EDI Exchange Records" +msgstr "" + #. module: edi_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__description #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration_trigger__description @@ -1391,6 +1428,11 @@ msgstr "" msgid "Record ID=%d is not meant to be sent!" msgstr "" +#. module: edi_core_oca +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form +msgid "Records retention" +msgstr "" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_form msgid "Regenerate" diff --git a/edi_core_oca/i18n/it.po b/edi_core_oca/i18n/it.po index 68ed9f741..550bf58dc 100644 --- a/edi_core_oca/i18n/it.po +++ b/edi_core_oca/i18n/it.po @@ -259,10 +259,12 @@ msgstr "Riemesso ACK: stato riportato a '%s'" #: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration_trigger__active +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_type__active #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_type_rule__active #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_view_search +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_view_search msgid "Active" @@ -309,11 +311,17 @@ msgstr "" msgid "Apply to this model" msgstr "Applica a questo modello" +#. module: edi_core_oca +#: model:ir.actions.server,name:edi_core_oca.ir_cron_archive_old_edi_records_ir_actions_server +msgid "Archive Old EDI Exchange Records" +msgstr "" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_trigger_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_configuration_view_form +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_form #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_view_form @@ -336,6 +344,30 @@ msgstr "" "contenuto. Se attiva, un cron gestirà la generazione dell'output quando non " "ancora impostato. " +#. module: edi_core_oca +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_archive_records_after_days +msgid "Auto-archive records after (days)" +msgstr "" + +#. module: edi_core_oca +#: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_delete_records_after_days +msgid "Auto-delete archived records after (days)" +msgstr "" + +#. module: edi_core_oca +#: model:ir.model.fields,help:edi_core_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_core_oca +#: model:ir.model.fields,help:edi_core_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_core_oca #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__backend_id #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__backend_id @@ -488,6 +520,11 @@ msgstr "Personalizzato" msgid "Decoding Error Handler" msgstr "Gestore errore decodifica" +#. module: edi_core_oca +#: model:ir.actions.server,name:edi_core_oca.ir_cron_delete_old_archived_edi_records_ir_actions_server +msgid "Delete Old Archived EDI Exchange Records" +msgstr "" + #. module: edi_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__description #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration_trigger__description @@ -1465,6 +1502,11 @@ msgstr "Record ID=%d non è previsto che sia elaborato" msgid "Record ID=%d is not meant to be sent!" msgstr "Record ID=%d non è previsto che sia inviato!" +#. module: edi_core_oca +#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form +msgid "Records retention" +msgstr "" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_form msgid "Regenerate" From 77cfa06c5a12b13380668e70a450e7aea339a51e Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 30 Apr 2026 08:36:43 +0000 Subject: [PATCH 38/58] Translated using Weblate (Italian) Currently translated at 100.0% (258 of 258 strings) Translation: edi-framework-18.0/edi-framework-18.0-edi_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_oca/it/ --- edi_oca/i18n/it.po | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/edi_oca/i18n/it.po b/edi_oca/i18n/it.po index b7c6df9cf..33faed5c2 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: 2026-02-10 15:09+0000\n" +"PO-Revision-Date: 2026-04-30 10:45+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -281,7 +281,7 @@ 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 "" +msgstr "Archivia i vecchi record EDI" #. module: edi_oca #: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form @@ -314,12 +314,12 @@ 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 "" +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 "" +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 @@ -327,6 +327,8 @@ 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 @@ -334,6 +336,8 @@ 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 @@ -470,7 +474,7 @@ 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 "" +msgstr "Cancella i vecchi record di scambio EDI archiviati" #. module: edi_oca #: model:ir.model.fields,help:edi_oca.field_edi_configuration__description @@ -1344,7 +1348,7 @@ msgstr "Record" #. module: edi_oca #: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form msgid "Records retention" -msgstr "" +msgstr "Cancellazione record" #. module: edi_oca #: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_form From 26b5d071e09ec216fae3f21ce33fcb831627a73f Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 30 Apr 2026 08:36:16 +0000 Subject: [PATCH 39/58] Translated using Weblate (Italian) Currently translated at 100.0% (283 of 283 strings) Translation: edi-framework-18.0/edi-framework-18.0-edi_core_oca Translate-URL: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_core_oca/it/ --- edi_core_oca/i18n/it.po | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/edi_core_oca/i18n/it.po b/edi_core_oca/i18n/it.po index 550bf58dc..14ad64c81 100644 --- a/edi_core_oca/i18n/it.po +++ b/edi_core_oca/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 17.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2026-02-10 15:09+0000\n" +"PO-Revision-Date: 2026-04-30 10:45+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -314,7 +314,7 @@ msgstr "Applica a questo modello" #. module: edi_core_oca #: model:ir.actions.server,name:edi_core_oca.ir_cron_archive_old_edi_records_ir_actions_server msgid "Archive Old EDI Exchange Records" -msgstr "" +msgstr "Archivia i vecchi record EDI" #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form @@ -347,12 +347,12 @@ msgstr "" #. module: edi_core_oca #: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_archive_records_after_days msgid "Auto-archive records after (days)" -msgstr "" +msgstr "Auto archivia i record dopo (giorni)" #. module: edi_core_oca #: model:ir.model.fields,field_description:edi_core_oca.field_edi_backend__auto_delete_records_after_days msgid "Auto-delete archived records after (days)" -msgstr "" +msgstr "Auto cancella i record archiviati dopo (giorni)" #. module: edi_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_backend__auto_archive_records_after_days @@ -360,6 +360,8 @@ 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_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_backend__auto_delete_records_after_days @@ -367,6 +369,8 @@ 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_core_oca #: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__backend_id @@ -523,7 +527,7 @@ msgstr "Gestore errore decodifica" #. module: edi_core_oca #: model:ir.actions.server,name:edi_core_oca.ir_cron_delete_old_archived_edi_records_ir_actions_server msgid "Delete Old Archived EDI Exchange Records" -msgstr "" +msgstr "Cancella i vecchi record di scambio EDI archiviati" #. module: edi_core_oca #: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__description @@ -1505,7 +1509,7 @@ msgstr "Record ID=%d non è previsto che sia inviato!" #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_backend_view_form msgid "Records retention" -msgstr "" +msgstr "Cancellazione record" #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_form From 6b4355ae166bd2ace76326ffa92bfb307d01d96c Mon Sep 17 00:00:00 2001 From: oca-ci Date: Wed, 6 May 2026 08:49:27 +0000 Subject: [PATCH 40/58] [UPD] Update edi_notification_oca.pot --- edi_notification_oca/i18n/edi_notification_oca.pot | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/edi_notification_oca/i18n/edi_notification_oca.pot b/edi_notification_oca/i18n/edi_notification_oca.pot index 43162d14e..f31e62e4a 100644 --- a/edi_notification_oca/i18n/edi_notification_oca.pot +++ b/edi_notification_oca/i18n/edi_notification_oca.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0\n" +"Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -56,7 +56,6 @@ msgstr "" #. 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 "" From d88e6f34f18b39e7032a73505c1b7cf25b6bb779 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 6 May 2026 08:56:16 +0000 Subject: [PATCH 41/58] [BOT] post-merge updates --- README.md | 1 + edi_notification_oca/README.rst | 26 +++++++++------- .../static/description/index.html | 30 +++++++++++-------- setup/_metapackage/pyproject.toml | 3 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4b9cfae86..7b91f650b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ addon | version | maintainers | summary [edi_endpoint_oca](edi_endpoint_oca/) | 18.0.1.0.3 | | Base module allowing configuration of custom endpoints for EDI framework. [edi_exchange_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 | simahawk | Allows definition of exchanges via templates. [edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 | simahawk | 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. [edi_oca](edi_oca/) | 18.0.1.5.2 | simahawk etobella | Integrate all EDI modules together [edi_party_data_oca](edi_party_data_oca/) | 18.0.1.0.1 | simahawk | Allow to configure and retrieve party information for EDI exchanges. [edi_queue_oca](edi_queue_oca/) | 18.0.1.0.2 | | Set Queue Jobs on EDI diff --git a/edi_notification_oca/README.rst b/edi_notification_oca/README.rst index 79b243d20..3c08c4141 100644 --- a/edi_notification_oca/README.rst +++ b/edi_notification_oca/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ================ EDI Notification ================ @@ -7,13 +11,13 @@ EDI Notification !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d8651cf2a0e90d542e65ce8083a2bccffd0b1a3549867bb7a8e91843bd62a770 + !! 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/licence-LGPL--3-blue.png +.. |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 @@ -33,13 +37,13 @@ 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 +- 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. @@ -72,8 +76,8 @@ Authors Contributors ------------ -- Duong (Tran Quoc) -- Simone Orsi +- Duong (Tran Quoc) +- Simone Orsi Maintainers ----------- diff --git a/edi_notification_oca/static/description/index.html b/edi_notification_oca/static/description/index.html index 0120c593b..cbf2538dc 100644 --- a/edi_notification_oca/static/description/index.html +++ b/edi_notification_oca/static/description/index.html @@ -3,7 +3,7 @@ -EDI Notification +README.rst -
    -

    EDI Notification

    +
    + + +Odoo Community Association + +
    +

    EDI Notification

    -

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

    +

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

    This module creates activities for users when an exchange record’s process fails.

    Exchange types must be configured properly to create such activities:

    @@ -379,8 +384,8 @@

    EDI Notification

  • 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
  • +Error” are used to define the users that will be assigned to the newly +created activity

    Important

    @@ -401,7 +406,7 @@

    EDI Notification

    -

    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 @@ -409,22 +414,22 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -437,5 +442,6 @@

    Maintainers

    +
    diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index ef3979096..9e80670a9 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "odoo-addons-oca-edi-framework" -version = "18.0.20251201.0" +version = "18.0.20260506.0" dependencies = [ "odoo-addon-edi_account_core_oca==18.0.*", "odoo-addon-edi_account_oca==18.0.*", @@ -9,6 +9,7 @@ dependencies = [ "odoo-addon-edi_endpoint_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_queue_oca==18.0.*", From e6688d2a73ee806a6017b09ff67b9b6e431aa0d5 Mon Sep 17 00:00:00 2001 From: Chetan Date: Wed, 4 Mar 2020 16:07:31 +0530 Subject: [PATCH 42/58] [MIG]Migration: edi_purchase_diapar --- edi_purchase_diapar/README.rst | 26 ++++ edi_purchase_diapar/__init__.py | 6 + edi_purchase_diapar/__manifest__.py | 21 +++ edi_purchase_diapar/description/icon.png | Bin 0 -> 7772 bytes .../i18n/edi_purchase_diapar.pot | 72 ++++++++++ edi_purchase_diapar/i18n/fr.po | 82 +++++++++++ edi_purchase_diapar/models/__init__.py | 7 + .../models/edi_config_system.py | 42 ++++++ edi_purchase_diapar/models/product.py | 23 +++ edi_purchase_diapar/models/purchase.py | 50 +++++++ .../static/description/icon.png | Bin 0 -> 130614 bytes .../views/res_partner_view.xml | 19 +++ edi_purchase_diapar/wizard/__init__.py | 1 + .../wizard/supplier_info_update.py | 135 ++++++++++++++++++ 14 files changed, 484 insertions(+) create mode 100644 edi_purchase_diapar/README.rst create mode 100644 edi_purchase_diapar/__init__.py create mode 100644 edi_purchase_diapar/__manifest__.py create mode 100644 edi_purchase_diapar/description/icon.png create mode 100644 edi_purchase_diapar/i18n/edi_purchase_diapar.pot create mode 100644 edi_purchase_diapar/i18n/fr.po create mode 100644 edi_purchase_diapar/models/__init__.py create mode 100644 edi_purchase_diapar/models/edi_config_system.py create mode 100644 edi_purchase_diapar/models/product.py create mode 100644 edi_purchase_diapar/models/purchase.py create mode 100644 edi_purchase_diapar/static/description/icon.png create mode 100644 edi_purchase_diapar/views/res_partner_view.xml create mode 100644 edi_purchase_diapar/wizard/__init__.py create mode 100644 edi_purchase_diapar/wizard/supplier_info_update.py diff --git a/edi_purchase_diapar/README.rst b/edi_purchase_diapar/README.rst new file mode 100644 index 000000000..7dd81cab7 --- /dev/null +++ b/edi_purchase_diapar/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/__init__.py b/edi_purchase_diapar/__init__.py new file mode 100644 index 000000000..d4bff2931 --- /dev/null +++ b/edi_purchase_diapar/__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 wizard diff --git a/edi_purchase_diapar/__manifest__.py b/edi_purchase_diapar/__manifest__.py new file mode 100644 index 000000000..28cf36b8f --- /dev/null +++ b/edi_purchase_diapar/__manifest__.py @@ -0,0 +1,21 @@ +# 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": "12.0.1.0.0", + "category": "Custom", + "author": "Druidoo", + "website": "http://www.druidoo.io", + "license": "AGPL-3", + "depends": [ + "product", + "edi_purchase_config", + "edi_purchase_base", + "purchase_package_qty", + "coop_purchase", + "coop_membership", + ], + "data": ["views/res_partner_view.xml"], +} diff --git a/edi_purchase_diapar/description/icon.png b/edi_purchase_diapar/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9a908cfea18816e78a6806d6cc9bc36c5db15196 GIT binary patch literal 7772 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 literal 0 HcmV?d00001 diff --git a/edi_purchase_diapar/i18n/edi_purchase_diapar.pot b/edi_purchase_diapar/i18n/edi_purchase_diapar.pot new file mode 100644 index 000000000..91c0732fc --- /dev/null +++ b/edi_purchase_diapar/i18n/edi_purchase_diapar.pot @@ -0,0 +1,72 @@ +# 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" +"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 +#: code:addons/edi_purchase_diapar/models/purchase.py:31 +#, python-format +msgid "Check price for lines with product %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:29 +#, python-format +msgid "Check taxes for lines with product %s!" +msgstr "" + +#. 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 "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:24 +#, python-format +msgid "No lines in this order %s!" +msgstr "" + +#. 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 "" + +#. 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 "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:18 +#, 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,name:edi_purchase_diapar.model_edi_config_system +msgid "edi.config.system" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_supplier_info_update +msgid "supplier.info.update" +msgstr "" + diff --git a/edi_purchase_diapar/i18n/fr.po b/edi_purchase_diapar/i18n/fr.po new file mode 100644 index 000000000..4b6a9ba9e --- /dev/null +++ b/edi_purchase_diapar/i18n/fr.po @@ -0,0 +1,82 @@ +# 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: 2018-12-26 10:56+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"Language: \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 +#: 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 "" + +#. 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/models/__init__.py b/edi_purchase_diapar/models/__init__.py new file mode 100644 index 000000000..850327c7b --- /dev/null +++ b/edi_purchase_diapar/models/__init__.py @@ -0,0 +1,7 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +# from . import product +from . import purchase +from . import edi_config_system diff --git a/edi_purchase_diapar/models/edi_config_system.py b/edi_purchase_diapar/models/edi_config_system.py new file mode 100644 index 000000000..0ac5142f1 --- /dev/null +++ b/edi_purchase_diapar/models/edi_config_system.py @@ -0,0 +1,42 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from odoo import models, fields + + +class EdiConfigSystem(models.Model): + _inherit = "edi.config.system" + + po_text_file_pattern = fields.Char( + string="Purchase order File pattern", + default="'LD%sH%s.C99' % self.env['edi.config.system'].get_datenow_format_for_file()", + required=True, + ) + customer_code = fields.Char( + string="Customer code", default="33513", required=True + ) + constant_file_start = fields.Char( + string="Constant file start", default="HDIAPAR ", required=True + ) + constant_file_end = fields.Char( + string="Constant file end", default="*DIAPAR*DIAPAR", required=True + ) + vrp_code = fields.Char(string="VRP Code", default="03", required=True) + ftp_host = fields.Char( + string="FTP Server Host", default="213.215.34.21", required=True + ) + ftp_login = fields.Char(string="FTP Login", default="33513", required=True) + ftp_password = fields.Char( + string="FTP Password", default="divers", required=True + ) + csv_relative_in_path = fields.Char( + string="Relative path for IN interfaces", + default="/Reception", + required=True, + ) + csv_relative_out_path = fields.Char( + string="Relative path for OUT interfaces", + default="/Envoi", + required=True, + ) diff --git a/edi_purchase_diapar/models/product.py b/edi_purchase_diapar/models/product.py new file mode 100644 index 000000000..6d2e7465b --- /dev/null +++ b/edi_purchase_diapar/models/product.py @@ -0,0 +1,23 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from odoo import models, api, _ +from odoo.exceptions import ValidationError + + +class ProductSupplierInfo(models.Model): + _inherit = "product.supplierinfo" + + @api.one + @api.constrains("product_code") + def _check_product_code(self): + if self.product_code: + if not self.product_code.isdigit(): + raise ValidationError( + _("Product code must be numeric for %s!") % self.name.name + ) + if len(self.product_code) != 6: + raise ValidationError( + _("Product code must be 6 digits for %s!") % self.name.name + ) diff --git a/edi_purchase_diapar/models/purchase.py b/edi_purchase_diapar/models/purchase.py new file mode 100644 index 000000000..aa57f2733 --- /dev/null +++ b/edi_purchase_diapar/models/purchase.py @@ -0,0 +1,50 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from datetime import datetime # Used when eval python codes !! + +from odoo import models, api, fields, _, tools +from odoo.exceptions import ValidationError + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + @api.multi + def _consolidate_products(self): + """ + Consolidate order lines by product. + Raise if Tax or price different. + @return: dict {product_id(record):[code_or_ean, qty, price, taxes(records)]} + """ + self.ensure_one() + if not self.order_line: + raise ValidationError(_("No lines in this order %s!") % self.name) + lines = {} + for line in self.order_line: + if line.product_id in lines: + if line.taxes_id != lines[line.product_id]["taxes_id"]: + raise ValidationError( + _("Check taxes for lines with product %s!") + % line.product_id.name + ) + if line.price_unit != lines[line.product_id]["price_unit"]: + raise ValidationError( + _("Check price for lines with product %s!") + % line.product_id.name + ) + lines[line.product_id]["quantity"] += line.product_qty + else: + code, origin_code = line.product_id._get_supplier_code_or_ean( + line.partner_id + ) + values = { + "code": code, + "origin_code": origin_code, + "quantity": line.product_qty_package, + "price_unit": line.price_unit, + "taxes_id": line.taxes_id, + } + lines.update({line.product_id: values}) + return lines diff --git a/edi_purchase_diapar/static/description/icon.png b/edi_purchase_diapar/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..67561cd2792079d2a772c17ab61cd751dda6ecfb GIT binary patch literal 130614 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 + + + + + res.partner.edi + res.partner + + + + + + + + + + + + \ No newline at end of file diff --git a/edi_purchase_diapar/wizard/__init__.py b/edi_purchase_diapar/wizard/__init__.py new file mode 100644 index 000000000..cfc5c9064 --- /dev/null +++ b/edi_purchase_diapar/wizard/__init__.py @@ -0,0 +1 @@ +from . import supplier_info_update diff --git a/edi_purchase_diapar/wizard/supplier_info_update.py b/edi_purchase_diapar/wizard/supplier_info_update.py new file mode 100644 index 000000000..d6379264c --- /dev/null +++ b/edi_purchase_diapar/wizard/supplier_info_update.py @@ -0,0 +1,135 @@ +# 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, _ +from odoo.exceptions import ValidationError + + +class SupplierInfoUpdate(models.TransientModel): + _inherit = "supplier.info.update" + + @api.model + def compute_edi_partner(self, partner_id): + """ + :param partner_id: purchase order/invoice supplier + :return: EDI supplier used in FTP prices operations + """ + ecs_obj = self.env["edi.config.system"] + if partner_id.is_edi: + edi_system = ecs_obj.search( + [("supplier_id", "=", partner_id.id)], limit=1 + ) + if not edi_system: + raise ValidationError( + _("No Config FTP for this supplier %s!") % partner_id.name + ) + if edi_system.parent_supplier_id: + return edi_system.parent_supplier_id + else: + return edi_system.supplier_id + else: + return True + + @api.model + def update_lines_prices(self, obj_lines_values): + """ + Updates PO/invoice lines prices depending on active_model + :param obj_lines_values: + :return: True + """ + active_model = self._context.get("active_model", "") + active_id = self._context.get("active_id", False) + active_obj = self.env[active_model].browse(active_id) + if active_model == "purchase.order": + active_obj.write({"order_line": obj_lines_values}) + elif active_model == "account.invoice": + active_obj.write({"invoice_line_ids": obj_lines_values}) + return True + + @api.model + def update_prices_edi( + self, lines, active_model, seller_values_lines, supplier, edi_supplier + ): + """ + :param lines: Wizard lines to Update. + :param seller_values_lines: List of dictionnary of lines to update. + :param supplier: Supplier. + :return: Updated lines. + """ + # Only for EDI suppliers + if supplier.is_edi: + # Clear old list elements + del lines[0:] + obj_lines_values = [] + for seller_values in seller_values_lines: + # Look for product and product.supplierinfo datas + supplier_id = supplier.id + product_id = seller_values.get("product_id", False) + product_tmpl_id = ( + self.env["product.product"] + .browse(product_id) + .product_tmpl_id.id + ) + product = self.env["product.template"].browse(product_tmpl_id) + selected_seller_id = product.seller_ids.search( + [ + ("name", "=", supplier_id), + ("product_tmpl_id", "=", product.id), + ] + ) + selected_seller_id = ( + selected_seller_id and selected_seller_id[0] or False + ) + # Look for product code + product_code = selected_seller_id.product_code + if not product_code: + raise ValidationError( + _( + "No supplier code given for product: %s for supplier: %s!, please give a " + "supplier code to continue prices operation update" + ) + % (product.name, supplier.name) + ) + # Look for EDI latest price + edi_price = self.env["supplier.price.list"].search( + [ + ("supplier_id", "=", edi_supplier.id), + ("supplier_code", "=", product_code), + ], + order="import_date DESC", + ) + if edi_price: + price = edi_price[0] + price_dict = {"price_unit": price.price} + line_key = ( + active_model == "purchase.order" + and "po_line_id" + or "invoice_line_id" + ) + line_id = seller_values.get(line_key, False) + obj_lines_values.append((1, line_id, price_dict)) + seller_values.update(price_dict) + # Reconstruct lines variable + lines.append((0, 0, seller_values)) + #### Update order/invoice lines with latest prices #### + self.update_lines_prices(obj_lines_values) + return lines + + @api.model + def compute_process_lines(self, active_model, active_obj): + """ + Overwritten to update product prices/order line prices based on latest EDI supplier prices (Only for EDI suppliers) + :param active_model: Current active model + :param active_obj: Current active object + :return: computed lines + """ + lines = super(SupplierInfoUpdate, self).compute_process_lines( + active_model, active_obj + ) + seller_values_lines = [line[2] for line in lines] + partner_id = active_obj.partner_id + edi_supplier = self.compute_edi_partner(partner_id) + return self.update_prices_edi( + lines, active_model, seller_values_lines, partner_id, edi_supplier + ) From fddeedbd54e2d4674347e9db1381bf89e866fb50 Mon Sep 17 00:00:00 2001 From: Chetan Date: Sat, 7 Mar 2020 11:49:48 +0530 Subject: [PATCH 43/58] [MIG]Migrated edi_purchase_diaper to v12. --- edi_purchase_diapar/__manifest__.py | 4 +- .../models/edi_config_system.py | 20 +++------ edi_purchase_diapar/models/product.py | 1 - edi_purchase_diapar/models/purchase.py | 7 ++-- .../views/res_partner_view.xml | 28 ++++++------- .../wizard/supplier_info_update.py | 41 ++++++++----------- 6 files changed, 41 insertions(+), 60 deletions(-) diff --git a/edi_purchase_diapar/__manifest__.py b/edi_purchase_diapar/__manifest__.py index 28cf36b8f..e08f59e41 100644 --- a/edi_purchase_diapar/__manifest__.py +++ b/edi_purchase_diapar/__manifest__.py @@ -17,5 +17,7 @@ "coop_purchase", "coop_membership", ], - "data": ["views/res_partner_view.xml"], + "data": [ + "views/res_partner_view.xml" + ], } diff --git a/edi_purchase_diapar/models/edi_config_system.py b/edi_purchase_diapar/models/edi_config_system.py index 0ac5142f1..ba65a2d38 100644 --- a/edi_purchase_diapar/models/edi_config_system.py +++ b/edi_purchase_diapar/models/edi_config_system.py @@ -13,23 +13,15 @@ class EdiConfigSystem(models.Model): default="'LD%sH%s.C99' % self.env['edi.config.system'].get_datenow_format_for_file()", required=True, ) - customer_code = fields.Char( - string="Customer code", default="33513", required=True - ) - constant_file_start = fields.Char( - string="Constant file start", default="HDIAPAR ", required=True - ) - constant_file_end = fields.Char( - string="Constant file end", default="*DIAPAR*DIAPAR", required=True - ) - vrp_code = fields.Char(string="VRP Code", default="03", required=True) + customer_code = fields.Char(default="33513", required=True) + constant_file_start = fields.Char(default="HDIAPAR ", required=True) + constant_file_end = fields.Char(default="*DIAPAR*DIAPAR", required=True) + vrp_code = fields.Char(default="03", required=True) ftp_host = fields.Char( string="FTP Server Host", default="213.215.34.21", required=True ) - ftp_login = fields.Char(string="FTP Login", default="33513", required=True) - ftp_password = fields.Char( - string="FTP Password", default="divers", required=True - ) + ftp_login = fields.Char(default="33513", required=True) + ftp_password = fields.Char(default="divers", required=True) csv_relative_in_path = fields.Char( string="Relative path for IN interfaces", default="/Reception", diff --git a/edi_purchase_diapar/models/product.py b/edi_purchase_diapar/models/product.py index 6d2e7465b..33e745487 100644 --- a/edi_purchase_diapar/models/product.py +++ b/edi_purchase_diapar/models/product.py @@ -9,7 +9,6 @@ class ProductSupplierInfo(models.Model): _inherit = "product.supplierinfo" - @api.one @api.constrains("product_code") def _check_product_code(self): if self.product_code: diff --git a/edi_purchase_diapar/models/purchase.py b/edi_purchase_diapar/models/purchase.py index aa57f2733..cceb1396d 100644 --- a/edi_purchase_diapar/models/purchase.py +++ b/edi_purchase_diapar/models/purchase.py @@ -2,9 +2,7 @@ # @author: Druidoo # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html -from datetime import datetime # Used when eval python codes !! - -from odoo import models, api, fields, _, tools +from odoo import models, api, _ from odoo.exceptions import ValidationError @@ -16,7 +14,8 @@ def _consolidate_products(self): """ Consolidate order lines by product. Raise if Tax or price different. - @return: dict {product_id(record):[code_or_ean, qty, price, taxes(records)]} + @return: dict {product_id(record):[code_or_ean, qty, price, + taxes(records)]} """ self.ensure_one() if not self.order_line: diff --git a/edi_purchase_diapar/views/res_partner_view.xml b/edi_purchase_diapar/views/res_partner_view.xml index 38984747c..ff99324d4 100644 --- a/edi_purchase_diapar/views/res_partner_view.xml +++ b/edi_purchase_diapar/views/res_partner_view.xml @@ -1,19 +1,15 @@ - - - res.partner.edi - res.partner - - - - - - - - - - - - \ No newline at end of file + + view.res.partner.edi.diapaar + res.partner + + + + + + + + + diff --git a/edi_purchase_diapar/wizard/supplier_info_update.py b/edi_purchase_diapar/wizard/supplier_info_update.py index d6379264c..12c1d03ad 100644 --- a/edi_purchase_diapar/wizard/supplier_info_update.py +++ b/edi_purchase_diapar/wizard/supplier_info_update.py @@ -2,7 +2,7 @@ # @author: Druidoo # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html -from odoo import api, fields, models, _ +from odoo import api, models, _ from odoo.exceptions import ValidationError @@ -72,40 +72,33 @@ def update_prices_edi( .product_tmpl_id.id ) product = self.env["product.template"].browse(product_tmpl_id) - selected_seller_id = product.seller_ids.search( - [ - ("name", "=", supplier_id), - ("product_tmpl_id", "=", product.id), - ] - ) + selected_seller_id = product.seller_ids.search([ + ("name", "=", supplier_id), + ("product_tmpl_id", "=", product.id), + ]) selected_seller_id = ( selected_seller_id and selected_seller_id[0] or False ) # Look for product code product_code = selected_seller_id.product_code if not product_code: - raise ValidationError( - _( - "No supplier code given for product: %s for supplier: %s!, please give a " - "supplier code to continue prices operation update" - ) - % (product.name, supplier.name) - ) + raise ValidationError(_( + "No supplier code given for product: %s for supplier: \ + %s!, please give a " + "supplier code to continue prices operation update" + ) % (product.name, supplier.name)) # Look for EDI latest price - edi_price = self.env["supplier.price.list"].search( - [ - ("supplier_id", "=", edi_supplier.id), - ("supplier_code", "=", product_code), - ], - order="import_date DESC", - ) + edi_price = self.env["supplier.price.list"].search([ + ("supplier_id", "=", edi_supplier.id), + ("supplier_code", "=", product_code), + ], order="import_date DESC") if edi_price: price = edi_price[0] price_dict = {"price_unit": price.price} line_key = ( - active_model == "purchase.order" - and "po_line_id" - or "invoice_line_id" + active_model == "purchase.order" and + "po_line_id" or + "invoice_line_id" ) line_id = seller_values.get(line_key, False) obj_lines_values.append((1, line_id, price_dict)) From 57028b6de873c6153428671098db371307245dc3 Mon Sep 17 00:00:00 2001 From: Chetan Date: Sat, 7 Mar 2020 12:09:25 +0530 Subject: [PATCH 44/58] Formate the code. --- edi_purchase_diapar/models/edi_config_system.py | 3 ++- edi_purchase_diapar/wizard/supplier_info_update.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/edi_purchase_diapar/models/edi_config_system.py b/edi_purchase_diapar/models/edi_config_system.py index ba65a2d38..1faa21772 100644 --- a/edi_purchase_diapar/models/edi_config_system.py +++ b/edi_purchase_diapar/models/edi_config_system.py @@ -10,7 +10,8 @@ class EdiConfigSystem(models.Model): po_text_file_pattern = fields.Char( string="Purchase order File pattern", - default="'LD%sH%s.C99' % self.env['edi.config.system'].get_datenow_format_for_file()", + default="'LD%sH%s.C99' % self.env['edi.config.system'].\ + get_datenow_format_for_file()", required=True, ) customer_code = fields.Char(default="33513", required=True) diff --git a/edi_purchase_diapar/wizard/supplier_info_update.py b/edi_purchase_diapar/wizard/supplier_info_update.py index 12c1d03ad..641f452e9 100644 --- a/edi_purchase_diapar/wizard/supplier_info_update.py +++ b/edi_purchase_diapar/wizard/supplier_info_update.py @@ -105,14 +105,15 @@ def update_prices_edi( seller_values.update(price_dict) # Reconstruct lines variable lines.append((0, 0, seller_values)) - #### Update order/invoice lines with latest prices #### + # Update order/invoice lines with latest prices self.update_lines_prices(obj_lines_values) return lines @api.model def compute_process_lines(self, active_model, active_obj): """ - Overwritten to update product prices/order line prices based on latest EDI supplier prices (Only for EDI suppliers) + Overwritten to update product prices/order line prices based on latest + EDI supplier prices (Only for EDI suppliers) :param active_model: Current active model :param active_obj: Current active object :return: computed lines From ec50312037e6801e5487c4febb161ddef6ed202c Mon Sep 17 00:00:00 2001 From: druidoo-transbot Date: Mon, 9 Mar 2020 12:57:04 +0000 Subject: [PATCH 45/58] [UPD] Update edi_purchase_diapar.pot --- .../i18n/edi_purchase_diapar.pot | 72 ++++++++++++++++--- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/edi_purchase_diapar/i18n/edi_purchase_diapar.pot b/edi_purchase_diapar/i18n/edi_purchase_diapar.pot index 91c0732fc..f686194d5 100644 --- a/edi_purchase_diapar/i18n/edi_purchase_diapar.pot +++ b/edi_purchase_diapar/i18n/edi_purchase_diapar.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 9.0c\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: <>\n" "Language-Team: \n" @@ -14,43 +14,78 @@ msgstr "" "Plural-Forms: \n" #. module: edi_purchase_diapar -#: code:addons/edi_purchase_diapar/models/purchase.py:31 +#: 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:29 +#: code:addons/edi_purchase_diapar/models/purchase.py:28 #, python-format msgid "Check taxes for lines with product %s!" msgstr "" #. module: edi_purchase_diapar -#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:23 +#: 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_edi_config_system__customer_code +msgid "Customer Code" +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_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 +#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:25 #, python-format msgid "No Config FTP for this supplier %s!" msgstr "" #. module: edi_purchase_diapar -#: code:addons/edi_purchase_diapar/models/purchase.py:24 +#: code:addons/edi_purchase_diapar/models/purchase.py:22 #, python-format msgid "No lines in this order %s!" msgstr "" #. module: edi_purchase_diapar -#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:72 +#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:85 #, python-format -msgid "No supplier code given for product: %s for supplier: %s!, please give a supplier code to continue prices operation update" +msgid "No supplier code given for product: %s for supplier: %s!, please give a supplier code to continue prices operation update" msgstr "" #. module: edi_purchase_diapar -#: code:addons/edi_purchase_diapar/models/product.py:20 +#: 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:18 +#: code:addons/edi_purchase_diapar/models/product.py:17 #, python-format msgid "Product code must be numeric for %s!" msgstr "" @@ -61,8 +96,23 @@ msgid "Purchase Order" msgstr "" #. module: edi_purchase_diapar -#: model:ir.model,name:edi_purchase_diapar.model_edi_config_system -msgid "edi.config.system" +#: 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_edi_config_system__vrp_code +msgid "VRP Code" msgstr "" #. module: edi_purchase_diapar From a37b4e198dfe2697519742cd994f841db47408db Mon Sep 17 00:00:00 2001 From: Chetan R Dhaduk Date: Fri, 10 Apr 2020 19:49:22 +0530 Subject: [PATCH 46/58] Diapr Improvment: add button to pull prices from history (#120) * Diapr Improvment: add button to pull prices from history * Improve code. --- edi_purchase_diapar/__manifest__.py | 3 +- edi_purchase_diapar/models/purchase.py | 9 +- .../views/res_partner_view.xml | 12 ++ edi_purchase_diapar/wizard/__init__.py | 2 +- .../wizard/invoice_supplier_price_update.py | 179 ++++++++++++++++++ .../wizard/invoice_supplier_price_update.xml | 70 +++++++ .../wizard/supplier_info_update.py | 129 ------------- 7 files changed, 272 insertions(+), 132 deletions(-) create mode 100644 edi_purchase_diapar/wizard/invoice_supplier_price_update.py create mode 100644 edi_purchase_diapar/wizard/invoice_supplier_price_update.xml delete mode 100644 edi_purchase_diapar/wizard/supplier_info_update.py diff --git a/edi_purchase_diapar/__manifest__.py b/edi_purchase_diapar/__manifest__.py index e08f59e41..1eeebeed3 100644 --- a/edi_purchase_diapar/__manifest__.py +++ b/edi_purchase_diapar/__manifest__.py @@ -18,6 +18,7 @@ "coop_membership", ], "data": [ - "views/res_partner_view.xml" + "wizard/invoice_supplier_price_update.xml", + "views/res_partner_view.xml", ], } diff --git a/edi_purchase_diapar/models/purchase.py b/edi_purchase_diapar/models/purchase.py index cceb1396d..48f57a145 100644 --- a/edi_purchase_diapar/models/purchase.py +++ b/edi_purchase_diapar/models/purchase.py @@ -2,7 +2,7 @@ # @author: Druidoo # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html -from odoo import models, api, _ +from odoo import models, api, _, fields from odoo.exceptions import ValidationError @@ -47,3 +47,10 @@ def _consolidate_products(self): } lines.update({line.product_id: values}) return lines + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + partner_is_edi = fields.Boolean(related='partner_id.is_edi', + string='Partner (Is Edit)', store=True) diff --git a/edi_purchase_diapar/views/res_partner_view.xml b/edi_purchase_diapar/views/res_partner_view.xml index ff99324d4..1ec6e1182 100644 --- a/edi_purchase_diapar/views/res_partner_view.xml +++ b/edi_purchase_diapar/views/res_partner_view.xml @@ -12,4 +12,16 @@ + + invoice.supplier.form.inherit.diapar + account.invoice + + + + +
    + + + 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 + } + + +
    From 92aca40fdecb85a185c092b2bf3f9eccb7dcee93 Mon Sep 17 00:00:00 2001 From: phucph Date: Wed, 6 May 2026 12:07:58 +0700 Subject: [PATCH 57/58] [DON'T MERGE] test-requirements.txt --- test-requirements.txt | 1 + 1 file changed, 1 insertion(+) 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 From f79096dc050c3ae4fb288dde61e0336f6b89d9ec Mon Sep 17 00:00:00 2001 From: phucph Date: Mon, 30 Mar 2026 15:26:44 +0700 Subject: [PATCH 58/58] [MIG] edi_purchase_diapar_oca: Migration data --- .../migrations/18.0.1.0.0/post-migration.py | 181 ++++++++ .../migrations/18.0.1.0.0/pre-migration.py | 436 ++++++++++++++++++ 2 files changed, 617 insertions(+) create mode 100644 edi_purchase_diapar_oca/migrations/18.0.1.0.0/post-migration.py create mode 100644 edi_purchase_diapar_oca/migrations/18.0.1.0.0/pre-migration.py diff --git a/edi_purchase_diapar_oca/migrations/18.0.1.0.0/post-migration.py b/edi_purchase_diapar_oca/migrations/18.0.1.0.0/post-migration.py new file mode 100644 index 000000000..c17687183 --- /dev/null +++ b/edi_purchase_diapar_oca/migrations/18.0.1.0.0/post-migration.py @@ -0,0 +1,181 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import SUPERUSER_ID, api +from odoo.tools import sql + +_logger = logging.getLogger(__name__) + + +MAPPING_EXCODE_PROCESS = { + "diapar_out_order": ("generate_model_id", "model_edi_output_diapar_handler"), + "diapar_in_despatch_advice": ( + "process_model_id", + "model_edi_input_diapar_despatch_advice_handler", + ), + "diapar_in_purchase_price": ( + "process_model_id", + "model_edi_input_diapar_purchase_price_handler", + ), +} + + +def _migrate_fs_storage(env): + tmp_table = "tbl_temp_fs_storage" + if any( + [ + not sql.table_exists("fs_storage"), + not sql.table_exists(tmp_table), + ] + ): + return + env.cr.execute( + sql.SQL( + """ + -- Insert values from temporary table to fs_storage + INSERT INTO fs_storage (name, code, protocol, options) + SELECT name, code, protocol, options + FROM %(tmp_table)s + WHERE code = 'diapar_ftp' + ON CONFLICT (code) DO NOTHING; + + -- Drop the temporary table + DROP TABLE IF EXISTS %(tmp_table)s; + """, + tmp_table=tmp_table, + ) + ) + + +def _migrate_exchange_type(env): + """ + Update the generate_model_id and process_model_id fields + """ + exchange_type_model = env["edi.exchange.type"] + for code, process in MAPPING_EXCODE_PROCESS.items(): + existing_type = exchange_type_model.search([("code", "=", code)], limit=1) + if not existing_type: + continue + mmodel = f"edi_purchase_diapar_oca.{process[1]}" + if getattr(existing_type, process[0]): + existing_type.write({process[0]: mmodel}) + _logger.info("Updated %s for exchange type '%s'", process[0], code) + + +def _migrate_field_mapping(env): + tmp_table = "tbl_temp_edi_field_mapping" + if not sql.table_exists(env.cr, tmp_table): + _logger.warning( + f"Temp table {tmp_table} does not exist. Skipping field mapping migration." + ) # noqa: E501 + return + + standard_cols = [ + "sequence", + "position", + "name", + "sequence_start", + "sequence_end", + "is_numeric", + "is_date", + "decimal_precision", + ] + env.cr.execute( + sql.SQL( + """ + -- Insert values from temporary table to edi_field_mapping + INSERT INTO edi_field_mapping ( + %(standard_cols)s, + field_mapping_id, + exchange_type_id + ) + SELECT + %(standard_cols)s, + imf.id AS field_mapping_id, + ext.id AS exchange_type_id + FROM tbl_temp_edi_field_mapping tfm + LEFT JOIN ir_model_fields imf ON imf.model = tfm.res_field_model + AND imf.name = tfm.res_field_name + LEFT JOIN edi_exchange_type ext ON ext.code = tfm.exchange_type_code + ON CONFLICT (name) DO NOTHING; + + -- Drop the temporary table + DROP TABLE IF EXISTS %(tmp_table)s; + """, + standard_cols=", ".join(standard_cols), + tmp_table=tmp_table, + ) + ) + + +def _migrate_template_output(env): + template_name = "edi_exchange_template_output_diapar_3" + template = env.ref( + f"edi_purchase_diapar_oca.{template_name}", + raise_if_not_found=False, + ) + if not template: + _logger.warning( + "Template '%s' not found. Skipping template output migration.", + template_name, + ) # noqa: E501 + return + + tmp_table = "tbl_temp_edi_template_output" + if not sql.table_exists(env.cr, tmp_table): + _logger.warning( + f"Temp table {tmp_table} does not exist. Skipping tmp output migration." + ) # noqa: E501 + return + + standard_cols = [ + "name", + "backend_type_id", + "backend_id", + "code", + "output_type", + "generator", + "customer_code", + "vrp_code", + "constant_file_start", + "constant_file_end", + ] + env.cr.execute( + sql.SQL( + """ + -- Insert values from temporary table to edi_exchange_template_output + INSERT INTO edi_exchange_template_output ( + %(standard_cols)s, + template_id + ) + SELECT + %(standard_cols)s, + %(template_id)s + FROM %(tmp_table)s + ON CONFLICT (code) DO NOTHING; + + -- Drop the temporary table + DROP TABLE IF EXISTS %(tmp_table)s; + """, + standard_cols=", ".join(standard_cols), + template_id=template.id, + tmp_table=tmp_table, + ) + ) + + +def migrate(cr, version): + """Post-migration script for Odoo 18 upgrade.""" + _logger.info("Starting post-migration data adjustments...") + + if not version: + return + + env = api.Environment(cr, SUPERUSER_ID, {}) + _migrate_fs_storage(env) + _migrate_exchange_type(env) + _migrate_field_mapping(env) + _migrate_template_output(env) + + _logger.info("Post-migration data adjustments completed.") diff --git a/edi_purchase_diapar_oca/migrations/18.0.1.0.0/pre-migration.py b/edi_purchase_diapar_oca/migrations/18.0.1.0.0/pre-migration.py new file mode 100644 index 000000000..5fca4e789 --- /dev/null +++ b/edi_purchase_diapar_oca/migrations/18.0.1.0.0/pre-migration.py @@ -0,0 +1,436 @@ +import logging + +from odoo import SUPERUSER_ID, api +from odoo.tools import sql + +_logger = logging.getLogger(__name__) + + +ENCODING = "iso-8859-1" +FILE_EXT = "txt" +EXCHANGE_CODES = [ + "diapar_out_order", + "diapar_in_despatch_advice", + "diapar_in_purchase_price", +] +DIRECTION_MAPPING = { + "input_dir_pending": "IN/PENDING", + "input_dir_done": "IN/DONE", + "input_dir_error": "IN/ERROR", + "output_dir_pending": "OUT/PENDING", + "output_dir_done": "OUT/DONE", + "output_dir_error": "OUT/ERROR", +} + + +def _skip_migration(env, version=None): + if not version: + return + tables_to_check = [ + "edi_config_system", + "edi_backend_type", + "edi_backend", + "edi_exchange_type", + ] + if any(not sql.table_exists(env.cr, table_name) for table_name in tables_to_check): + _logger.warning( + "One or more required tables do not exist. Skipping migration..." + ) + return True + + return False + + +def _get_edi_config_values(env): + cols = [ + "ftp_host", + "ftp_port", + "ftp_login", + "ftp_password", + "supplier_id", + "parent_supplier_id", + "constant_file_start", + "constant_file_end", + "vrp_code", + "customer_code", + "header_code", + "lines_code", + "delivery_sign", + ] + env.cr.execute( + sql.SQL( + """ + SELECT cfg.id AS config_id, %(col_names)s + FROM edi_config_system AS cfg + """, + col_names={", ".join(f"cfg.{col}" for col in cols)}, + ) + ) + config_infos = [] + for row in env.cr.fetchall(): + config_info = dict(zip(cols, row[1:], strict=False)) + config_infos.append(config_info) + return config_infos + + +def _check_fs_storage(env, config_info): + values_to_insert = { + "name": "Diapar FTP Storage", + "code": "diapar_ftp", + "protocol": "ftp", + "options": { + "host": f"{config_info.get('ftp_host', '')}", + "port": f"{config_info.get('ftp_port', '')}", + "login": f"{config_info.get('ftp_login', '')}", + "password": f"{config_info.get('ftp_password', '')}", + }, + } + values_to_insert["options"] = str(values_to_insert["options"]) + if sql.table_exists(env.cr, "tbl_temp_fs_storage"): + env.cr.execute( + sql.SQL( + """ + -- Insert values + INSERT + INTO tbl_temp_fs_storage (%(col_names)s) + VALUES (%(values_to_insert)s) + ON CONFLICT (code) DO NOTHING; + + -- Return the id of the inserted or existing record + SELECT id FROM tbl_temp_fs_storage WHERE code = 'diapar_ftp'; + """, + col_names=", ".join(values_to_insert.keys()), + values_to_insert=", ".join( + f"%({key})s" for key in values_to_insert.keys() + ), + **values_to_insert, + ) + ) + result = env.cr.fetchone() + return result[0] if result else None + # Create temp table if it does not exist + env.cr.execute( + sql.SQL( + """ + -- Create a temporary table + CREATE TEMPORARY TABLE + tbl_temp_fs_storage ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + code VARCHAR(255), + protocol VARCHAR(50), + options TEXT + ); + + -- Insert values + INSERT INTO tbl_temp_fs_storage (%(col_names)s) VALUES (%(values_to_insert)s) + ON CONFLICT (code) DO NOTHING; + + -- Return the id of the inserted or existing record + SELECT id FROM tbl_temp_fs_storage WHERE code = 'diapar_ftp'; + """, + col_names=", ".join(values_to_insert.keys()), + values_to_insert=", ".join(f"%({key})s" for key in values_to_insert.keys()), + **values_to_insert, + ) + ) + result = env.cr.fetchone() + return result[0] if result else None + + +def _check_edi_backend(env, fs_storage_id): + # Backend type + env.cr.execute( + sql.SQL( + """ + -- Insert values + INSERT INTO edi_backend_type (name, code) + VALUES ('DIAPAR', 'diapar') + ON CONFLICT (code) DO NOTHING; + + -- Return the id of the inserted or existing record + SELECT id FROM edi_backend_type WHERE code = 'diapar'; + """ + ) + ) + backend_type_id = env.cr.fetchone()[0] + # Backend + values_to_insert = { + "name": "Diapar", + "backend_type_id": backend_type_id, + "storage_id": fs_storage_id, + } + values_to_insert.update(DIRECTION_MAPPING) + env.cr.execute( + sql.SQL( + """ + SELECT id FROM edi_backend WHERE backend_type_id = %(backend_type_id)s + """, + backend_type_id=backend_type_id, + ) + ) + result = env.cr.fetchone() + if not result: + env.cr.execute( + sql.SQL( + """ + -- Insert values + INSERT INTO edi_backend (%(col_names)s) + VALUES (%(values_to_insert)s); + + -- Return the id of the inserted or existing record + SELECT id FROM edi_backend WHERE backend_type_id = %(backend_type_id)s; + """, + col_names=", ".join(values_to_insert.keys()), + backend_type_id=backend_type_id, + values_to_insert=", ".join( + f"%({key})s" for key in values_to_insert.keys() + ), + **values_to_insert, + ) + ) + result = env.cr.fetchone() + backend_id = result[0] + return backend_id, backend_type_id + + +def _prepare_exchange_type_datas(config_info, backend_id, backend_type_id): + common_values = { + "exchange_file_ext": FILE_EXT, + "encoding": ENCODING, + "backend_id": backend_id, + "backend_type_id": backend_type_id, + } + exchange_types_data = [] + for code in EXCHANGE_CODES: + exchange_type_data = { + "code": code, + "name": code, + } + exchange_type_data.update(common_values) + if code == "diapar_out_order": + exchange_type_data.update( + { + "direction": "output", + "exchange_filename_pattern": "LD{dt[0:8]}H{dt[9:13]}.C99", + "exchange_file_ext": "C99", + "generate_model_id": False, # To be set in post-migration + "advanced_settings_edit": """ + filename_pattern: + date_pattern: %Y%m%d %H%M%S + """, + } + ) + elif code == "diapar_in_despatch_advice": + exchange_type_data.update( + { + "direction": "input", + "exchange_filename_pattern": "BLE*", + "header_code": config_info["header_code"] or "H", + "lines_code": config_info["lines_code"] or "L", + "delivery_sign": config_info["delivery_sign"] or "+", + "process_model_id": False, # To be set in post-migration + } + ) + elif code == "diapar_in_purchase_price": + exchange_type_data.update( + { + "direction": "input", + "exchange_filename_pattern": "CH*", + "process_model_id": False, # To be set in post-migration + } + ) + exchange_types_data.append(exchange_type_data) + return exchange_types_data + + +def _check_exchange_types(env, config_info, backend_type_id, backend_id): + exchange_types_data = _prepare_exchange_type_datas( + config_info, backend_id, backend_type_id + ) + for exchange_type in exchange_types_data: + env.cr.execute( + sql.SQL( + """ + -- Insert values + INSERT INTO edi_exchange_type (%(col_names)s) + VALUES (%(values_to_insert)s) + ON CONFLICT (code) DO NOTHING; + """, + col_names=", ".join(exchange_type.keys()), + values_to_insert=", ".join( + f"%({key})s" for key in exchange_type.keys() + ), + **exchange_type, + ) + ) + + env.cr.execute( + sql.SQL( + """ + SELECT id, code FROM edi_exchange_type WHERE code IN %s + """, + (tuple(EXCHANGE_CODES),), + ) + ) + return {row[1]: row[0] for row in env.cr.fetchall()} + + +def _mapping_fields(env, exchange_type_infos): + """ + Two table (edi_price_mapping, edi_ble_mapping) is deprecated, + and in v18.0, create a new edi_field_mapping table to replace them. + """ + + if any( + [ + not sql.table_exists(env.cr, "edi_price_mapping"), + not sql.table_exists(env.cr, "edi_ble_mapping"), + ] + ): + # Skip if the old table does not exist as there is nothing to migrate + _logger.warning( + "Tables %s and %s do not exist. Skipping mapping fields migration...", + "edi_price_mapping", + "edi_ble_mapping", + ) # noqa: E501 + return + + env.cr.execute( + sql.SQL( + """ + -- Create a temporary table + CREATE TEMPORARY TABLE + tbl_temp_edi_field_mapping ( + id SERIAL PRIMARY KEY, + + -- Standard columns + sequence INTEGER, + position INTEGER NOT NULL, + name VARCHAR(255), + sequence_start INTEGER, + sequence_end INTEGER, + is_numeric BOOLEAN, + is_date BOOLEAN, + decimal_precision INTEGER, + + -- Relational columns + res_field_model VARCHAR(255), + res_field_name VARCHAR(255), + exchange_type_code VARCHAR(255) + ); + """ + ) + ) + standard_cols = [ + "sequence", + "position", + "name", + "sequence_start", + "sequence_end", + "is_numeric", + "is_date", + "decimal_precision", + ] + + def _insert_mapping_fields(table_name, code): + env.cr.execute( + sql.SQL( + """ + -- Insert values + INSERT INTO tbl_temp_edi_field_mapping ( + %(col_names)s, + res_field_model, res_field_name, exchange_type_code + ) + SELECT + %(col_names)s, + imf.model AS res_field_model, + imf.name AS res_field_name, + ext.code AS exchange_type_code + FROM %(table_name)s epm + LEFT JOIN ir_model_fields imf ON imf.id = epm.mapping_field_id + LEFT JOIN edi_exchange_type ext ON ext.code = %(code)s + """, + col_names=", ".join(standard_cols), + table_name=table_name, + code=code, + ) + ) + + for code, _id in exchange_type_infos.items(): + _insert_mapping_fields("edi_price_mapping", code) + _insert_mapping_fields("edi_ble_mapping", code) + + +def _template_output(env, config_info, backend_id, backend_type_id): + env.cr.execute( + sql.SQL( + """ + -- Create a temporary table + CREATE TEMPORARY TABLE + tbl_temp_edi_template_output ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + backend_type_id INTEGER NOT NULL, + backend_id INTEGER NOT NULL, + code VARCHAR(255) NOT NULL, + output_type VARCHAR(50) NOT NULL, + generator VARCHAR(50) NOT NULL, + customer_code VARCHAR(255), + vrp_code VARCHAR(255), + constant_file_start VARCHAR(255), + constant_file_end VARCHAR(255) + ); + + -- Insert values + INSERT INTO tbl_temp_edi_template_output ( + name, backend_type_id, backend_id, code, output_type, + generator, customer_code, vrp_code, + constant_file_start, constant_file_end + ) + VALUES ( + 'Diapar Output Exchange Template', + %(backend_type_id)s, + %(backend_id)s, + 'diapar.output.exchange.template', + 'text', + 'qweb', + %(customer_code)s, + %(vrp_code)s, + %(constant_file_start)s, + %(constant_file_end)s + ); + """, + backend_type_id=backend_type_id, + backend_id=backend_id, + customer_code=config_info["customer_code"], + vrp_code=config_info["vrp_code"], + constant_file_start=config_info["constant_file_start"], + constant_file_end=config_info["constant_file_end"], + ) + ) + + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + + if _skip_migration(env, version): + return + + config_infos = _get_edi_config_values(env) + if not config_infos: + _logger.warning( + "No configuration found in edi_config_system. Skipping migration..." + ) # noqa: E501 + return + + for config_info in config_infos: + fs_storage_id = _check_fs_storage(env, config_info) + backend_id, backend_type_id = _check_edi_backend(env, fs_storage_id) + exchange_type_infos = _check_exchange_types( + env, config_info, backend_type_id, backend_id + ) + _mapping_fields(env, exchange_type_infos) + _template_output(env, config_info, backend_id, backend_type_id) + + _logger.info("Pre-migration data adjustments completed.")