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 35b61245d..7b91f650b 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 @@ -23,22 +24,23 @@ 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_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.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.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_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.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.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. [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/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/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_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/README.rst b/edi_core_oca/README.rst index 7b34951d6..bb985bd0d 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:c609033733302fa71a3c01c11e2729fd2b47ccde0b9a1d0619bed03cc26db4fe !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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..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.5.6", + "version": "18.0.1.6.6", "website": "https://github.com/OCA/edi-framework", "development_status": "Beta", "license": "LGPL-3", @@ -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/i18n/edi_core_oca.pot b/edi_core_oca/i18n/edi_core_oca.pot index c77ee2f77..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" @@ -1608,7 +1650,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/es.po b/edi_core_oca/i18n/es.po index 36f9a0c0d..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" @@ -1618,7 +1660,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..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" @@ -1636,7 +1678,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..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: 2025-12-02 15:42+0000\n" +"PO-Revision-Date: 2026-04-30 10:45+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 @@ -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 "Archivia i vecchi record EDI" + #. 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,34 @@ 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 "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 "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 +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 +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 #: model:ir.model.fields,field_description:edi_core_oca.field_edi_exchange_record__backend_id @@ -488,6 +524,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 "Cancella i vecchi record di scambio EDI archiviati" + #. 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 +1506,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 "Cancellazione record" + #. module: edi_core_oca #: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_form msgid "Regenerate" @@ -1721,10 +1767,13 @@ 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." +"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 @@ -1746,6 +1795,13 @@ msgstr "" msgid "error on send" msgstr "errore nell'invio" +#~ msgid "" +#~ "When active, records of this type will be processed immediately without " +#~ "waiting for the cron to pass by." +#~ msgstr "" +#~ "Quando attiva, i record di questo tipo verranno elaborati immediatamente " +#~ "senza attendere il cron." + #~ msgid "" #~ "For output exchange types this should be a formatting string with the " #~ "following variables available (to be used between brackets, `{}`): " diff --git a/edi_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/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index bdea2e24f..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): @@ -373,6 +385,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): @@ -427,6 +448,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: diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 8e4f697ed..a87b4506e 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", @@ -133,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" @@ -142,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."), @@ -379,10 +382,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): @@ -635,8 +639,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/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/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/static/description/index.html b/edi_core_oca/static/description/index.html index fa279ca4c..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:05f11d491e9ac910591ac4fbb7dc7372d0efb74d2b81b38dd84b58a526739cfc +!! source digest: sha256:c609033733302fa71a3c01c11e2729fd2b47ccde0b9a1d0619bed03cc26db4fe !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Base EDI backend.

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 db227b5ad..9c6aff0fb 100644 --- a/edi_core_oca/tests/test_edi_backend_cron.py +++ b/edi_core_oca/tests/test_edi_backend_cron.py @@ -16,50 +16,40 @@ class EDIBackendTestCronCase(EDIBackendCommonTestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.partner2 = cls.env.ref("base.res_partner_10") - cls.partner3 = cls.env.ref("base.res_partner_12") - cls.record1 = cls.backend.create_record( - "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner.id} - ) - cls.record2 = cls.backend.create_record( - "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner2.id} - ) - cls.record3 = cls.backend.create_record( - "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner3.id} - ) - cls.records = cls.record1 + cls.record1 + cls.record3 - - @classmethod - def _setup_records(cls): # pylint:disable=missing-return - super()._setup_records() - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() from .fake_models import EdiTestExecution - cls.loader.update_registry((EdiTestExecution,)) - cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"] - cls.model = cls.env["ir.model"].search( + self.loader.update_registry((EdiTestExecution,)) + self.ExecutionAbstractModel = self.env["edi.framework.test.execution"] + self.model = self.env["ir.model"].search( [("model", "=", "edi.framework.test.execution")] ) - cls.exchange_type_out.generate_model_id = cls.model - cls.exchange_type_out.send_model_id = cls.model - cls.exchange_type_out.output_validate_model_id = cls.model - - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() - - def setUp(self): - super().setUp() + self.exchange_type_out.generate_model_id = self.model + self.exchange_type_out.send_model_id = self.model + self.exchange_type_out.output_validate_model_id = self.model + self.partner2 = self.env.ref("base.res_partner_10") + self.partner3 = self.env.ref("base.res_partner_12") + self.record1 = self.backend.create_record( + "test_csv_output", {"model": self.partner._name, "res_id": self.partner.id} + ) + self.record2 = self.backend.create_record( + "test_csv_output", {"model": self.partner._name, "res_id": self.partner2.id} + ) + self.record3 = self.backend.create_record( + "test_csv_output", {"model": self.partner._name, "res_id": self.partner3.id} + ) + self.records = self.record1 + self.record1 + self.record3 self.ExecutionAbstractModel.reset_faked("generate") self.ExecutionAbstractModel.reset_faked("send") self.ExecutionAbstractModel.reset_faked("check") + def tearDown(self): + self.loader.restore_registry() + super().tearDown() + @mute_logger(*LOGGERS) def test_exchange_generate_new_no_auto(self): # No content ready to be sent, no auto-generate, nothing happens @@ -87,14 +77,8 @@ def test_exchange_generate_new_auto_skip_send(self): # TODO: test better? self.assertFalse(rec.ack_exchange_id) - @mute_logger(*LOGGERS) - def test_exchange_generate_new_auto_send(self): - self.exchange_type_out.exchange_file_auto_generate = True - # No content ready to be sent, will get the content and send it - for rec in self.records: - self.assertEqual(rec.edi_exchange_state, "new") - self.backend._cron_check_output_exchange_sync() - for rec in self.records: + def _test_generate_new_auto_send(self, records): + for rec in records: self.assertEqual(rec.edi_exchange_state, "output_sent") self.assertTrue( self.ExecutionAbstractModel.check_called_for(rec, "generate") @@ -104,6 +88,26 @@ def test_exchange_generate_new_auto_send(self): ) self.assertTrue(self.ExecutionAbstractModel.check_called_for(rec, "send")) + @mute_logger(*LOGGERS) + def test_exchange_generate_new_auto_send(self): + self.exchange_type_out.exchange_file_auto_generate = True + # No content ready to be sent, will get the content and send it + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "new") + self.backend._cron_check_output_exchange_sync() + self._test_generate_new_auto_send(self.records) + + @mute_logger(*LOGGERS) + def test_exchange_generate_new_quick_exec_skip_cron(self): + self.exchange_type_out.exchange_file_auto_generate = True + self.exchange_type_out.quick_exec = True + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "new") + # Records w/ quick exec should be skipped by the cron + self.backend._cron_check_output_exchange_sync() + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "new") + @mute_logger(*LOGGERS) def test_exchange_generate_output_ready_auto_send(self): # No content ready to be sent, will get the content and send it diff --git a/edi_core_oca/tests/test_edi_configuration.py b/edi_core_oca/tests/test_edi_configuration.py index 88f1a2918..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..c26c62e4e 100644 --- a/edi_core_oca/tests/test_security.py +++ b/edi_core_oca/tests/test_security.py @@ -11,65 +11,57 @@ class TestEDIExchangeRecordSecurity(EDIBackendCommonTestCase): - @classmethod - def _setup_env(cls): - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() from .fake_models import EdiExchangeConsumerTest - cls.loader.update_registry((EdiExchangeConsumerTest,)) - return super()._setup_env() - - # pylint: disable=W8110 - @classmethod - def _setup_records(cls): - super()._setup_records() - cls.group = cls.env["res.groups"].create({"name": "Demo Group"}) - cls.ir_access = cls.env["ir.model.access"].create( + self.loader.update_registry((EdiExchangeConsumerTest,)) + self.group = self.env["res.groups"].create({"name": "Demo Group"}) + self.ir_access = self.env["ir.model.access"].create( { "name": "model access", - "model_id": cls.env.ref( + "model_id": self.env.ref( "edi_core_oca.model_edi_exchange_consumer_test" ).id, - "group_id": cls.group.id, + "group_id": self.group.id, "perm_read": True, "perm_write": True, "perm_create": True, "perm_unlink": True, } ) - cls.rule = cls.env["ir.rule"].create( + self.rule = self.env["ir.rule"].create( { "name": "Exchange Record rule demo", - "model_id": cls.env.ref( + "model_id": self.env.ref( "edi_core_oca.model_edi_exchange_consumer_test" ).id, "domain_force": "[('name', '=', 'test')]", - "groups": [(4, cls.group.id)], + "groups": [(4, self.group.id)], } ) - cls.user = ( - cls.env["res.users"] + self.user = ( + self.env["res.users"] .with_context(no_reset_password=True, mail_notrack=True) .create( { "name": "Poor Partner (not integrating one)", "email": "poor.partner@ododo.com", "login": "poorpartner", - "groups_id": [(6, 0, [cls.env.ref("base_edi.group_edi_user").id])], + "groups_id": [(6, 0, [self.env.ref("base_edi.group_edi_user").id])], } ) ) - cls.consumer_record = cls.env["edi.exchange.consumer.test"].create( + self.consumer_record = self.env["edi.exchange.consumer.test"].create( {"name": "test"} ) - cls.exchange_type_out.exchange_filename_pattern = "{record.id}" + self.exchange_type_out.exchange_filename_pattern = "{record.id}" - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() + def tearDown(self): + self.loader.restore_registry() + super().tearDown() def create_record(self, user=False): vals = { @@ -238,3 +230,108 @@ def test_no_group_no_read_child(self): msg = rf"not allowed to access '{model._description}' \({model._name}\)" with self.assertRaisesRegex(AccessError, msg): child_exchange_record.with_user(self.user).read() + + def test_search_pagination_with_inaccessible_middle_records(self): + """ + Regression test: + If some records in the first page are filtered out due to access rules, + _search must fetch additional records from next pages without truncating them. + """ + + self.user.write({"groups_id": [(4, self.group.id)]}) + + # Two different companies are used to trigger multi-company access filtering + company_1 = self.env.ref("base.main_company") + company_2 = self.env["res.company"].create({"name": "Other Company"}) + + # Three target records: + # - consumer_c1 and consumer_c3 belong to the active company and are readable + # - consumer_c2 belongs to another company and will be filtered out + # by access rules + consumer_c1 = self.env["res.partner"].create( + {"name": "c1-a", "company_id": company_1.id} + ) + consumer_c2 = self.env["res.partner"].create( + {"name": "c2", "company_id": company_2.id} + ) + consumer_c3 = self.env["res.partner"].create( + {"name": "c1-b", "company_id": company_1.id} + ) + + # One EDI records pointing to readable target records + self.backend.create_record( + "test_csv_output", + {"model": consumer_c1._name, "res_id": consumer_c1.id}, + ) + + # One EDI records pointing to records from another company + self.backend.create_record( + "test_csv_output", + {"model": consumer_c2._name, "res_id": consumer_c2.id}, + ) + + # One EDI records pointing to readable target records + visible_id_2 = self.backend.create_record( + "test_csv_output", + {"model": consumer_c3._name, "res_id": consumer_c3.id}, + ).id + + # Restrict the environment to company_1 only, activating the multi-company rule + # that will hide records pointing to consumer_c2 + env_company_1 = self.env( + context=dict(self.env.context, allowed_company_ids=[company_1.id]) + ) + + # Execute the search as a non-superuser: + # - super()._search returns the first 2 IDs (1 visible + 1 hidden) + # - custom logic removes the 1 hidden + # - pagination logic fetches 1 more record from the next page + records = ( + env_company_1["edi.exchange.record"] + .with_user(self.user) + .search([], limit=2, order="id asc") + ) + + # The result must NOT be truncated: the search should still return ` + # limit` records + self.assertEqual( + len(records), + 2, + "Search results were truncated when inaccessible records were " + "present in the first page", + ) + + # The records fetched from the second page must be present in the final result + self.assertIn(visible_id_2, records.ids) + + def test_search_no_res_id(self): + """Test Exc Rec visibility for internal users when ``res_id`` is False-ish + + Exchange Record's ``res_id`` is a ``Many2onReference`` field, which internally + converts False-ish values to 0 before storing them to the cache and the DB. + The rule's domain old leaf ``('res_id', '=', False)`` was instead converted to a + SQL query clause ``WHERE "edi_exchange_record.res_id" IS NULL``. + Since all ``edi_exchange_record`` rows contain a non-negative integer in the + ``res_id`` column, the rule old domain leaf always failed to fetch any record. + + Changing the leaf to ``('res_id', '=', 0)`` fixes the issue, making such + Exchange Records visible again for internal users. + """ + # Add the test user to the internal users group + self.user.write({"groups_id": [(4, self.env.ref("base.group_user").id)]}) + + # Create Exchange Records with no model (condition ``('model', '!=', False)`` + # will fail) and False-ish record ID (to test condition ``('res_id', '=', 0)``): + # such False-ish values are all converted to 0 by ``fields.Many2oneReference`` + # methods (and methods of its superclasses) when updating the cache values and + # preparing SQL queries to flush to the DB + exc_recs = self.env["edi.exchange.record"] + type_code = "test_csv_output" + vals = {"model": False} + for res_id in (0, 0.00, False, None, "", self.env["base"]): + exc_recs += self.backend.create_record(type_code, vals | {"res_id": res_id}) + self.assertEqual(exc_recs.mapped("res_id"), [0] * len(exc_recs)) + + # Check that the test user can actually fetch such records + exc_recs_model = self.env["edi.exchange.record"].with_user(self.user) + self.assertEqual(exc_recs_model.search([("id", "in", exc_recs.ids)]), exc_recs) diff --git a/edi_core_oca/views/edi_backend_views.xml b/edi_core_oca/views/edi_backend_views.xml index 2f557e0d3..5c4aea4f4 100644 --- a/edi_core_oca/views/edi_backend_views.xml +++ b/edi_core_oca/views/edi_backend_views.xml @@ -57,7 +57,14 @@ - + + + + + + + + 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" /> + + + 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 @@ - + + - 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_exchange_template_oca/tests/test_edi_backend_output.py b/edi_exchange_template_oca/tests/test_edi_backend_output.py index 89a91bb37..117f170e4 100644 --- a/edi_exchange_template_oca/tests/test_edi_backend_output.py +++ b/edi_exchange_template_oca/tests/test_edi_backend_output.py @@ -242,13 +242,21 @@ def test_generate_file(self): self.assertEqual(file_content.strip(), expected) def test_prettify(self): - self.tmpl_out2.template_id.arch = ( + tmpl_out2 = self.env["edi.exchange.template.output"].browse(self.tmpl_out2.id) + record2 = self.env["edi.exchange.record"].browse(self.record2.id) + self.assertTrue( + tmpl_out2.exists(), "Template output record vanished during test execution" + ) + self.assertTrue( + record2.exists(), "Exchange record vanished during test execution" + ) + tmpl_out2.template_id.arch = ( '1' ) - output = self.tmpl_out2.exchange_generate(self.record2) + output = tmpl_out2.exchange_generate(record2) self.assertEqual(output, b"1") - self.tmpl_out2.prettify = True - output = self.tmpl_out2.exchange_generate(self.record2) + tmpl_out2.prettify = True + output = tmpl_out2.exchange_generate(record2) self.assertEqual(output, b"\n 1\n\n") def test_generate_file_report(self): diff --git a/edi_notification_oca/README.rst b/edi_notification_oca/README.rst new file mode 100644 index 000000000..3c08c4141 --- /dev/null +++ b/edi_notification_oca/README.rst @@ -0,0 +1,97 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================ +EDI Notification +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d865a9ca8287a26c0e9185d73c56951401b600d75381622a3f3474f5ee2c0992 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi--framework-lightgray.png?logo=github + :target: https://github.com/OCA/edi-framework/tree/18.0/edi_notification_oca + :alt: OCA/edi-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_notification_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module creates activities for users when an exchange record's +process fails. + +Exchange types must be configured properly to create such activities: + +- field "Notify On Process Error" must be checked to activate the + feature for the current exchange type +- field "Activity Type Used When Notify On Process Error" is used to + define the type of the newly created activity +- fields "Notify Groups On Process Error" and "Notify Users On Process + Error" are used to define the users that will be assigned to the newly + created activity + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Duong (Tran Quoc) +- Simone Orsi + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/edi-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/edi_notification_oca/__init__.py b/edi_notification_oca/__init__.py new file mode 100644 index 000000000..f24d3e242 --- /dev/null +++ b/edi_notification_oca/__init__.py @@ -0,0 +1,2 @@ +from . import components +from . import models diff --git a/edi_notification_oca/__manifest__.py b/edi_notification_oca/__manifest__.py new file mode 100644 index 000000000..5fc8183f9 --- /dev/null +++ b/edi_notification_oca/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2024 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "EDI Notification", + "summary": """Define notification activities on exchange records.""", + "version": "18.0.1.0.0", + "development_status": "Alpha", + "license": "LGPL-3", + "website": "https://github.com/OCA/edi-framework", + "author": "Camptocamp,Odoo Community Association (OCA)", + # TODO v19: consider getting rid off `edi_component_oca` dep + "depends": ["edi_core_oca", "edi_component_oca"], + "data": ["data/mail_activity_type.xml", "views/edi_exchange_type.xml"], + "installable": True, +} diff --git a/edi_notification_oca/components/__init__.py b/edi_notification_oca/components/__init__.py new file mode 100644 index 000000000..9430369c2 --- /dev/null +++ b/edi_notification_oca/components/__init__.py @@ -0,0 +1 @@ +from . import listener diff --git a/edi_notification_oca/components/listener.py b/edi_notification_oca/components/listener.py new file mode 100644 index 000000000..30945e128 --- /dev/null +++ b/edi_notification_oca/components/listener.py @@ -0,0 +1,44 @@ +# Copyright 2024 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.addons.component.core import Component + + +class EdiNotificationListener(Component): + _name = "edi.notification.component.listener" + _inherit = "base.event.listener" + + def on_edi_exchange_error(self, record): + exc_type = record.type_id + notify_on_process_error = exc_type.notify_on_process_error + activity_type = exc_type.notify_on_process_error_activity_type_id + if ( + not notify_on_process_error + or not activity_type + or not ( + exc_type.notify_on_process_error_groups_ids + or exc_type.notify_on_process_error_users_ids + ) + ): + return True + users = self._get_users_to_notify(exc_type) + # Send notification to defined users + for user in users: + record.activity_schedule( + activity_type_id=activity_type.id, + summary=self.env._( + "EDI: Process error on record '%(identifier)s'.", + identifier=record.identifier, + ), + note=record.exchange_error, + user_id=user.id, + automated=True, + ) + return True + + def _get_users_to_notify(self, exc_type): + exc_type.ensure_one() + return ( + exc_type.notify_on_process_error_groups_ids.users + | exc_type.notify_on_process_error_users_ids + ) diff --git a/edi_notification_oca/data/mail_activity_type.xml b/edi_notification_oca/data/mail_activity_type.xml new file mode 100644 index 000000000..7fa92c0d4 --- /dev/null +++ b/edi_notification_oca/data/mail_activity_type.xml @@ -0,0 +1,12 @@ + + + + EDI Exchange Record: Failed + fa-warning + edi.exchange.record + warning + + diff --git a/edi_notification_oca/i18n/edi_notification_oca.pot b/edi_notification_oca/i18n/edi_notification_oca.pot new file mode 100644 index 000000000..f31e62e4a --- /dev/null +++ b/edi_notification_oca/i18n/edi_notification_oca.pot @@ -0,0 +1,149 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_notification_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_ids +msgid "Activities" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_state +msgid "Activity State" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_activity_type_id +msgid "Activity Type Used When Notify On Process Error" +msgstr "" + +#. module: edi_notification_oca +#: model:mail.activity.type,name:edi_notification_oca.mail_activity_failed_exchange_record_warning +msgid "EDI Exchange Record: Failed" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model,name:edi_notification_oca.model_edi_exchange_type +msgid "EDI Exchange Type" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model,name:edi_notification_oca.model_edi_exchange_record +msgid "EDI exchange Record" +msgstr "" + +#. module: edi_notification_oca +#. odoo-python +#: code:addons/edi_notification_oca/components/listener.py:0 +msgid "EDI: Process error on record '%(identifier)s'." +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_type__notify_on_process_error +msgid "" +"If an error happens on process, a notification will be sent to all selected " +"users. If active, please select the specific groups and specific users in " +"the 'Notifications' page." +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: edi_notification_oca +#: model_terms:ir.ui.view,arch_db:edi_notification_oca.edi_exchange_type_view_form +msgid "Notification" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_groups_ids +msgid "Notify Groups On Process Error" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error +msgid "Notify On Process Error" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_users_ids +msgid "Notify Users On Process Error" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_users_ids +msgid "" +"Select users to send notifications to. If 'Notification Groups' have been " +"selected, notifications will also be sent to users selected in here." +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" diff --git a/edi_notification_oca/i18n/it.po b/edi_notification_oca/i18n/it.po new file mode 100644 index 000000000..5a02a680f --- /dev/null +++ b/edi_notification_oca/i18n/it.po @@ -0,0 +1,163 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_notification_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-09-10 14:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_ids +msgid "Activities" +msgstr "Attività" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decorazione eccezione attività" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_state +msgid "Activity State" +msgstr "Stato attività" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icona tipo attività" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_activity_type_id +msgid "Activity Type Used When Notify On Process Error" +msgstr "Tipo attività utilizzata nelle notifiche di errori di processo" + +#. module: edi_notification_oca +#: model:mail.activity.type,name:edi_notification_oca.mail_activity_failed_exchange_record_warning +msgid "EDI Exchange Record: Failed" +msgstr "Record scambio EDI: fallito" + +#. module: edi_notification_oca +#: model:ir.model,name:edi_notification_oca.model_edi_exchange_type +msgid "EDI Exchange Type" +msgstr "Tipo scambio EDI" + +#. module: edi_notification_oca +#: model:ir.model,name:edi_notification_oca.model_edi_exchange_record +msgid "EDI exchange Record" +msgstr "Record di scambio EDI" + +#. module: edi_notification_oca +#. odoo-python +#: code:addons/edi_notification_oca/components/listener.py:0 +#, python-format +msgid "EDI: Process error on record '%(identifier)s'." +msgstr "EDI: errore di processo nel record '%(identifier)s'." + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icona Font Awesome es. fa-tasks" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_exception_icon +msgid "Icon" +msgstr "Icona" + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icona per indicare un'attività eccezione." + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_type__notify_on_process_error +msgid "" +"If an error happens on process, a notification will be sent to all selected " +"users. If active, please select the specific groups and specific users in " +"the 'Notifications' page." +msgstr "" +"Se si verifica un errore durante il processo, verrà inviata una notifica a " +"tutti gli utenti selezionati. Se attiva, selezionare i gruppi e gli utenti " +"specifici nella pagina \"Notifiche\"." + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Scadenza mia attività" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Scadenza prossima attività" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_summary +msgid "Next Activity Summary" +msgstr "Riepilogo prossima attività" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo prossima attività" + +#. module: edi_notification_oca +#: model_terms:ir.ui.view,arch_db:edi_notification_oca.edi_exchange_type_view_form +msgid "Notification" +msgstr "Notifica" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_groups_ids +msgid "Notify Groups On Process Error" +msgstr "Avvisa i gruppi all'errore di processo" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error +msgid "Notify On Process Error" +msgstr "Avvisa all'errore di processo" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_users_ids +msgid "Notify Users On Process Error" +msgstr "Avvisa gli utenti all'errore di processo" + +#. module: edi_notification_oca +#: model:ir.model.fields,field_description:edi_notification_oca.field_edi_exchange_record__activity_user_id +msgid "Responsible User" +msgstr "Utente responsabile" + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_type__notify_on_process_error_users_ids +msgid "" +"Select users to send notifications to. If 'Notification Groups' have been " +"selected, notifications will also be sent to users selected in here." +msgstr "" +"Seleziona gli utenti a cui inviare le notifiche. Se sono stati selezionati " +"'Gruppi di notifica', le notifiche saranno inviate anche agli utenti " +"selezionati qui." + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Stato in base alle attività\n" +"Scaduto: la data richiesta è trascorsa\n" +"Oggi: la data attività è oggi\n" +"Pianificato: attività future." + +#. module: edi_notification_oca +#: model:ir.model.fields,help:edi_notification_oca.field_edi_exchange_record__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo di attività eccezione sul record." diff --git a/edi_notification_oca/models/__init__.py b/edi_notification_oca/models/__init__.py new file mode 100644 index 000000000..6128f744d --- /dev/null +++ b/edi_notification_oca/models/__init__.py @@ -0,0 +1,2 @@ +from . import edi_exchange_type +from . import edi_exchange_record diff --git a/edi_notification_oca/models/edi_exchange_record.py b/edi_notification_oca/models/edi_exchange_record.py new file mode 100644 index 000000000..e6b48bc68 --- /dev/null +++ b/edi_notification_oca/models/edi_exchange_record.py @@ -0,0 +1,9 @@ +# Copyright 2024 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import models + + +class EDIExchangeRecord(models.Model): + _name = "edi.exchange.record" + _inherit = ["edi.exchange.record", "mail.activity.mixin"] diff --git a/edi_notification_oca/models/edi_exchange_type.py b/edi_notification_oca/models/edi_exchange_type.py new file mode 100644 index 000000000..3f29d606e --- /dev/null +++ b/edi_notification_oca/models/edi_exchange_type.py @@ -0,0 +1,52 @@ +# Copyright 2024 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class EDIExchangeType(models.Model): + _inherit = "edi.exchange.type" + + notify_on_process_error = fields.Boolean( + help="If an error happens on process, a notification will be sent to all" + " selected users. If active, please select the specific groups and" + " specific users in the 'Notifications' page.", + default=False, + ) + notify_on_process_error_groups_ids = fields.Many2many( + comodel_name="res.groups", + string="Notify Groups On Process Error", + inverse="_inverse_notify_on_process_error_groups_users", + ) + notify_on_process_error_users_ids = fields.Many2many( + comodel_name="res.users", + string="Notify Users On Process Error", + inverse="_inverse_notify_on_process_error_groups_users", + help="Select users to send notifications to." + " If 'Notification Groups' have been selected, notifications will also be sent" + " to users selected in here.", + ) + notify_on_process_error_activity_type_id = fields.Many2one( + "mail.activity.type", + string="Activity Type Used When Notify On Process Error", + default=lambda self: self._default_notify_on_process_error_activity_type_id(), + ) + + def _default_notify_on_process_error_activity_type_id(self): + return self.env.ref( + "edi_notification_oca.mail_activity_failed_exchange_record_warning", False + ) + + @api.onchange("notify_on_process_error") + def _onchange_notify_on_process_error(self): + if not self.notify_on_process_error: + self.notify_on_process_error_groups_ids = None + self.notify_on_process_error_users_ids = None + + def _inverse_notify_on_process_error_groups_users(self): + for rec in self: + if ( + rec.notify_on_process_error_groups_ids + or rec.notify_on_process_error_users_ids + ): + rec.notify_on_process_error = True diff --git a/edi_notification_oca/pyproject.toml b/edi_notification_oca/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/edi_notification_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/edi_notification_oca/readme/CONTRIBUTORS.md b/edi_notification_oca/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..289708028 --- /dev/null +++ b/edi_notification_oca/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Duong (Tran Quoc) \<\> +- Simone Orsi \<\> diff --git a/edi_notification_oca/readme/DESCRIPTION.md b/edi_notification_oca/readme/DESCRIPTION.md new file mode 100644 index 000000000..3f250b798 --- /dev/null +++ b/edi_notification_oca/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +This module creates activities for users when an exchange record's process fails. + +Exchange types must be configured properly to create such activities: + +- field "Notify On Process Error" must be checked to activate the feature + for the current exchange type +- field "Activity Type Used When Notify On Process Error" is used to define + the type of the newly created activity +- fields "Notify Groups On Process Error" and "Notify Users On Process Error" are used + to define the users that will be assigned to the newly created activity diff --git a/edi_notification_oca/static/description/icon.png b/edi_notification_oca/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/edi_notification_oca/static/description/icon.png differ diff --git a/edi_notification_oca/static/description/index.html b/edi_notification_oca/static/description/index.html new file mode 100644 index 000000000..cbf2538dc --- /dev/null +++ b/edi_notification_oca/static/description/index.html @@ -0,0 +1,447 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

EDI Notification

+ +

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:

+
    +
  • field “Notify On Process Error” must be checked to activate the +feature for the current exchange type
  • +
  • field “Activity Type Used When Notify On Process Error” is used to +define the type of the newly created activity
  • +
  • fields “Notify Groups On Process Error” and “Notify Users On Process +Error” are used to define the users that will be assigned to the newly +created activity
  • +
+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/edi-framework project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/edi_notification_oca/tests/__init__.py b/edi_notification_oca/tests/__init__.py new file mode 100644 index 000000000..ab8b6e8bd --- /dev/null +++ b/edi_notification_oca/tests/__init__.py @@ -0,0 +1 @@ +from . import test_edi_notification diff --git a/edi_notification_oca/tests/test_edi_notification.py b/edi_notification_oca/tests/test_edi_notification.py new file mode 100644 index 000000000..cd9ee4f88 --- /dev/null +++ b/edi_notification_oca/tests/test_edi_notification.py @@ -0,0 +1,177 @@ +# Copyright 2024 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +import base64 + +from odoo.tests.common import RecordCapturer + +from odoo.addons.edi_oca.tests.common import EDIBackendCommonComponentRegistryTestCase +from odoo.addons.edi_oca.tests.fake_components import FakeInputProcess + + +class TestEDINotification(EDIBackendCommonComponentRegistryTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_env() + cls._build_components( + cls, + FakeInputProcess, + ) + cls._load_module_components(cls, "edi_notification_oca") + vals = { + "model": cls.partner._name, + "res_id": cls.partner.id, + "exchange_file": base64.b64encode(b"1234"), + } + cls.record = cls.backend.create_record("test_csv_input", vals) + cls.group_portal = cls.env.ref("base.group_portal") + cls.user_a = cls._create_user("A") + cls.user_b = cls._create_user("B") + cls.user_c = cls._create_user("C") + + def setUp(self): + super().setUp() + FakeInputProcess.reset_faked() + + @classmethod + def _create_user(cls, letter: str): + return ( + cls.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "name": f"User {letter}", + "login": f"user_{letter}", + "groups_id": [(6, 0, [cls.group_portal.id])], + } + ) + ) + + def test_inverse_notify_on_process_error(self): + self.exchange_type_in.notify_on_process_error = False + # If we forgot to enable notify_on_process_error + self.exchange_type_in.write( + { + "notify_on_process_error_groups_ids": [(6, 0, [self.group_portal.id])], + "notify_on_process_error_users_ids": [(6, 0, [self.user_c.id])], + } + ) + # Make sure notify_on_process_error should be enabled + self.assertTrue(self.exchange_type_in.notify_on_process_error) + + def test_dont_notify_on_process_error(self): + self.exchange_type_in.notify_on_process_error = False + self.record.write({"edi_exchange_state": "input_received"}) + self.record._set_file_content("TEST %d" % self.record.id) + with RecordCapturer(self.env["mail.activity"], []) as capture: + self.record.with_context( + test_break_process="OOPS! Something went wrong :(" + ).action_exchange_process() + self.assertRecordValues( + self.record, + [ + { + "edi_exchange_state": "input_processed_error", + } + ], + ) + self.assertIn("OOPS! Something went wrong :(", self.record.exchange_error) + # We don't expect any notification + self.assertEqual(len(capture.records), 0) + + def test_notify_on_process_error_to_group(self): + self.exchange_type_in.write( + { + "notify_on_process_error": True, + "notify_on_process_error_groups_ids": [(6, 0, [self.group_portal.id])], + } + ) + # Remove group on user C to test + self.user_c.groups_id = None + self.record.write({"edi_exchange_state": "input_received"}) + self.record._set_file_content("TEST %d" % self.record.id) + with RecordCapturer(self.env["mail.activity"], []) as capture: + self.record.with_context( + test_break_process="OOPS! Something went wrong :(" + ).action_exchange_process() + # Send notification to all users in defined groups when error + a_noti = capture.records.filtered(lambda x: x.user_id == self.user_a) + self.assertEqual(len(a_noti), 1) + self.assertEqual( + a_noti.summary, + f"EDI: Process error on record '{self.record.identifier}'.", + ) + self.assertIn( + "OOPS! Something went wrong :(", + a_noti.note, + ) + b_noti = capture.records.filtered(lambda x: x.user_id == self.user_b) + self.assertEqual(len(a_noti), 1) + self.assertEqual( + b_noti.summary, + f"EDI: Process error on record '{self.record.identifier}'.", + ) + self.assertIn( + "OOPS! Something went wrong :(", + b_noti.note, + ) + # We don't send notification to user C + # because C is not belonging to the group_portal + c_noti = capture.records.filtered(lambda x: x.user_id == self.user_c) + self.assertEqual(len(c_noti), 0) + + def test_notify_on_process_error_to_users(self): + self.exchange_type_in.write( + { + "notify_on_process_error": True, + "notify_on_process_error_users_ids": [(6, 0, [self.user_c.id])], + } + ) + self.record.write({"edi_exchange_state": "input_received"}) + self.record._set_file_content("TEST %d" % self.record.id) + with RecordCapturer(self.env["mail.activity"], []) as capture: + self.record.with_context( + test_break_process="OOPS! Something went wrong :(" + ).action_exchange_process() + # Send notification to all users in defined users when error + a_b_noti = capture.records.filtered( + lambda x: x.user_id in (self.user_a | self.user_b) + ) + self.assertEqual(len(a_b_noti), 0) + c_noti = capture.records.filtered(lambda x: x.user_id == self.user_c) + self.assertEqual(len(c_noti), 1) + self.assertEqual( + c_noti.summary, + f"EDI: Process error on record '{self.record.identifier}'.", + ) + self.assertIn( + "OOPS! Something went wrong :(", + c_noti.note, + ) + + def test_notify_on_process_error_to_groups_and_users(self): + self.exchange_type_in.write( + { + "notify_on_process_error": True, + "notify_on_process_error_groups_ids": [(6, 0, [self.group_portal.id])], + "notify_on_process_error_users_ids": [(6, 0, [self.user_c.id])], + } + ) + # Remove group on user C to test + self.user_c.groups_id = None + self.record.write({"edi_exchange_state": "input_received"}) + self.record._set_file_content("TEST %d" % self.record.id) + with RecordCapturer(self.env["mail.activity"], []) as capture: + self.record.with_context( + test_break_process="OOPS! Something went wrong :(" + ).action_exchange_process() + # Send notification to all users in defined users when error + a_b_noti = capture.records.filtered( + lambda x: x.user_id in (self.user_a | self.user_b) + ) + self.assertEqual(len(a_b_noti), 2) + # also send notification to user C + c_noti = capture.records.filtered(lambda x: x.user_id == self.user_c) + self.assertEqual(len(c_noti), 1) diff --git a/edi_notification_oca/views/edi_exchange_type.xml b/edi_notification_oca/views/edi_exchange_type.xml new file mode 100644 index 000000000..e052b6eb5 --- /dev/null +++ b/edi_notification_oca/views/edi_exchange_type.xml @@ -0,0 +1,36 @@ + + + + edi.exchange.type + + + + + + + + + + + + + + + + + diff --git a/edi_oca/i18n/edi_oca.pot b/edi_oca/i18n/edi_oca.pot index bd4283165..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" @@ -1453,7 +1495,8 @@ msgstr "" #: model:ir.model.fields,help:edi_oca.field_edi_exchange_type__quick_exec msgid "" "When active, records of this type will be processed immediately without " -"waiting for the cron to pass by." +"waiting for the cron to pass by. Requires auto generate flag to be active as" +" well. The cron will skip these records unless forced." msgstr "" #. module: edi_oca diff --git a/edi_oca/i18n/it.po b/edi_oca/i18n/it.po index 9f91ca259..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: 2025-12-02 15:42+0000\n" +"PO-Revision-Date: 2026-04-30 10:45+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 @@ -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 "Archivia i vecchi record EDI" + #. module: edi_oca #: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form #: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi_oca.edi_configuration_trigger_view_form #: model_terms:ir.ui.view,arch_db:edi_oca.edi_configuration_view_form +#: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_form #: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_rule_view_search #: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_type_view_form @@ -303,6 +311,34 @@ msgstr "" "contenuto. Se attiva, un cron gestirà la generazione dell'output quando non " "ancora impostato. " +#. module: edi_oca +#: model:ir.model.fields,field_description:edi_oca.field_edi_backend__auto_archive_records_after_days +msgid "Auto-archive records after (days)" +msgstr "Auto archivia i record dopo (giorni)" + +#. module: edi_oca +#: model:ir.model.fields,field_description:edi_oca.field_edi_backend__auto_delete_records_after_days +msgid "Auto-delete archived records after (days)" +msgstr "Auto cancella i record archiviati dopo (giorni)" + +#. module: edi_oca +#: model:ir.model.fields,help:edi_oca.field_edi_backend__auto_archive_records_after_days +msgid "" +"Automatically archive EDI exchange records after X days. Set to <= 0 to " +"disable auto-archiving." +msgstr "" +"Archivia automaticamente i record di scambio EDI dopo X giorni. Impostare " +"inferiore o uguale a 0 per disabilitare l'auto archiviazione." + +#. module: edi_oca +#: model:ir.model.fields,help:edi_oca.field_edi_backend__auto_delete_records_after_days +msgid "" +"Automatically delete archived EDI exchange records after X days. Set to <= 0 " +"to disable auto-deletion." +msgstr "" +"Cancella automaticamente i record di scambio EDI archiviati dopo X giorni. " +"Impostare inferiore o uguale a 0 per disabilitare l'auto cancellazione." + #. module: edi_oca #: model:ir.model.fields,field_description:edi_oca.field_edi_configuration__backend_id #: model:ir.model.fields,field_description:edi_oca.field_edi_exchange_record__backend_id @@ -435,6 +471,11 @@ msgstr "Personalizzato" msgid "Decoding Error Handler" msgstr "Gestore errore decodifica" +#. module: edi_oca +#: model:ir.actions.server,name:edi_oca.ir_cron_delete_old_archived_edi_records_ir_actions_server +msgid "Delete Old Archived EDI Exchange Records" +msgstr "Cancella i vecchi record di scambio EDI archiviati" + #. module: edi_oca #: model:ir.model.fields,help:edi_oca.field_edi_configuration__description #: model:ir.model.fields,help:edi_oca.field_edi_configuration_trigger__description @@ -1304,6 +1345,11 @@ msgstr "Scambi recenti" msgid "Record" msgstr "Record" +#. module: edi_oca +#: model_terms:ir.ui.view,arch_db:edi_oca.edi_backend_view_form +msgid "Records retention" +msgstr "Cancellazione record" + #. module: edi_oca #: model_terms:ir.ui.view,arch_db:edi_oca.edi_exchange_record_view_form msgid "Regenerate" @@ -1560,10 +1606,13 @@ msgstr "Cronologia comunicazioni sito web" #: model:ir.model.fields,help:edi_oca.field_edi_exchange_type__quick_exec msgid "" "When active, records of this type will be processed immediately without " -"waiting for the cron to pass by." +"waiting for the cron to pass by. Requires auto generate flag to be active as " +"well. The cron will skip these records unless forced." msgstr "" -"Quando attiva, i record di questo tipo verranno elaborati immediatamente " -"senza attendere il cron." +"Quando attivi, i record di questo tipo verranno elaborati immediatamente, " +"senza attendere il completamento del cron. Richiede che anche il flag di " +"generazione automatica sia attivo. Il cron ignorerà questi record, a meno " +"che non venga forzato." #. module: edi_oca #: model:ir.model.fields,help:edi_oca.field_edi_exchange_consumer_mixin__edi_disable_auto @@ -1585,6 +1634,13 @@ msgstr "" msgid "error on send" msgstr "errore nell'invio" +#~ msgid "" +#~ "When active, records of this type will be processed immediately without " +#~ "waiting for the cron to pass by." +#~ msgstr "" +#~ "Quando attiva, i record di questo tipo verranno elaborati immediatamente " +#~ "senza attendere il cron." + #~ msgid "" #~ "For output exchange types this should be a formatting string with the " #~ "following variables available (to be used between brackets, `{}`): " diff --git a/edi_purchase_diapar_oca/README.rst b/edi_purchase_diapar_oca/README.rst new file mode 100644 index 000000000..7dd81cab7 --- /dev/null +++ b/edi_purchase_diapar_oca/README.rst @@ -0,0 +1,26 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +====================== +EDI Purchase DIAPAR +====================== + +Features +-------- + +This module contains all specific and necessary conditions and treatments to DIAPAR relationship. + + +Credits +======= + +Yassine TEIMI +Simon Mas + +Funders +------- + +The development of this module has been financially supported by: + +* La Louve (https://cooplalouve.fr/) diff --git a/edi_purchase_diapar_oca/__init__.py b/edi_purchase_diapar_oca/__init__.py new file mode 100644 index 000000000..3d5be67ea --- /dev/null +++ b/edi_purchase_diapar_oca/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from . import models +from . import components diff --git a/edi_purchase_diapar_oca/__manifest__.py b/edi_purchase_diapar_oca/__manifest__.py new file mode 100644 index 000000000..b1651e333 --- /dev/null +++ b/edi_purchase_diapar_oca/__manifest__.py @@ -0,0 +1,39 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +{ + "name": "EDI Purchase DIAPAR", + "version": "18.0.1.0.0", + "category": "Custom", + "author": "Druidoo, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi-framework", + "license": "AGPL-3", + "depends": [ + "product", + "purchase_stock", + "edi_storage_oca", + "edi_purchase_oca", + "edi_exchange_template_oca", + ], + "data": [ + "security/ir.model.access.csv", + "views/supplier_price_list_views.xml", + "views/picking_update_views.xml", + "views/product_template_views.xml", + "views/res_partner_views.xml", + "views/exchange_type_views.xml", + "views/edi_exchange_template_output.xml", + "views/res_config_settings_views.xml", + "views/menus.xml", + "templates/exchange_template_output_diapar.xml", + ], + "demo": [ + "demo/fs_storage_demo.xml", + "demo/edi_backend_type_demo.xml", + "demo/edi_exchange_template_output_demo.xml", + "demo/edi_exchange_type_demo.xml", + "demo/edi_field_mapping_demo.xml", + "demo/edi_configuration_demo.xml", + ], +} diff --git a/edi_purchase_diapar_oca/components/__init__.py b/edi_purchase_diapar_oca/components/__init__.py new file mode 100644 index 000000000..b871c10f8 --- /dev/null +++ b/edi_purchase_diapar_oca/components/__init__.py @@ -0,0 +1,3 @@ +from . import input_process_ble +from . import input_process_ch +from . import generate_diapar_output diff --git a/edi_purchase_diapar_oca/components/generate_diapar_output.py b/edi_purchase_diapar_oca/components/generate_diapar_output.py new file mode 100644 index 000000000..6256a3489 --- /dev/null +++ b/edi_purchase_diapar_oca/components/generate_diapar_output.py @@ -0,0 +1,30 @@ +from odoo import models + +from odoo.addons.edi_core_oca.exceptions import EDINotImplementedError + + +class EdiOcaHandlerGenerate(models.AbstractModel): + _name = "edi.output.diapar.handler" + _inherit = [ + "edi.oca.handler.generate", + ] + _description = "EDI Handler Generate Output For Diapar" + + def generate(self, exchange_record): + exchange_record = self.env["edi.exchange.record"].browse(exchange_record.id) + tmpl = exchange_record.backend_id._get_output_template(exchange_record) + if tmpl: + exchange_record = exchange_record.with_context( + edi_framework_action="generate" + ) + tmpl = tmpl.with_context(edi_framework_action="generate") + if exchange_record.model == "purchase.order" and exchange_record.res_id: + order = self.env["purchase.order"].browse(exchange_record.res_id) + if order: + return order.generate_template_output_diapar( + tmpl, + exchange_record, + ) + raise EDINotImplementedError( + self.env._("Only purchase order with Diapar output template are supported.") + ) diff --git a/edi_purchase_diapar_oca/components/input_process_ble.py b/edi_purchase_diapar_oca/components/input_process_ble.py new file mode 100644 index 000000000..ec51a3a99 --- /dev/null +++ b/edi_purchase_diapar_oca/components/input_process_ble.py @@ -0,0 +1,192 @@ +import logging + +from odoo import Command, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class EDIInputProcessDiaparDespatchAdvice(models.AbstractModel): + """ + INPUT PROCESSOR BLE + """ + + _name = "input.process.diapar.despatch.advice" + _inherit = ["edi.oca.handler.process"] + _description = "Input process despatch advice from Diapar" + + def process(self, exchange_record): + _logger.info(">>>>>>>>>>>>>>>>>> Reading BLE file >>>>>>>>>>>>>>>>>>>>>") + picking_update, purchase_order = self._handle_create_picking_update( + exchange_record + ) + if picking_update: + picking_update.button_update_picking_order() + _related = self.env["edi.exchange.related.record"].create( + { + "exchange_record_id": exchange_record.id, + "res_id": purchase_order.id, + "model": "purchase.order", + } + ) + return self.env._( + "Stock Picking %s updated. Related Order: %s", + picking_update.name.name, + purchase_order.name, + ) + return self.env._("No picking update created for this file.") + + def _parse_file_content_ble(self, exchange_record): + datas = exchange_record._get_file_content() + return datas.split("\n") + + def _get_mapping_field_position(self, exchange_type, field_name): + mapping = exchange_type.field_mapping_ids.filtered( + lambda rec: rec.mapping_field_id.name == field_name + ) + pos_from = mapping.sequence_start + pos_to = mapping.sequence_end + return pos_from, pos_to + + def _get_picking_order_and_supplier_info( + self, line, edi_exchange_type, picking_order, supplier_info, delivery_date + ): + pos_from, pos_to = self._get_mapping_field_position( + edi_exchange_type, "product_code" + ) + product_code = line[pos_from:pos_to] + purchase_order = self.env["purchase.order"] + supplier_info = self.env["product.supplierinfo"].search( + [("product_code", "=", product_code)], + limit=1, + ) + supplier_ids = supplier_info.mapped("partner_id").ids + # Look for corresponding purchase order, normally it + # should be just one even if there is more than one + # supplier associated to product + if supplier_ids: + sdate = f"{delivery_date} 00:00:00" + edate = f"{delivery_date} 23:59:59" + purchase_order = self.env["purchase.order"].search( + [ + ("partner_id", "in", supplier_ids), + ("state", "in", ["purchase", "done"]), + ("date_planned", ">=", sdate), + ("date_planned", "<=", edate), + ], + limit=1, + ) + # Assuming purchase order has only one picking order associated. + if purchase_order.exists(): + picking_order = purchase_order.picking_ids[0] + return picking_order, supplier_info, purchase_order + + def _get_updated_quantity_values( + self, line, edi_exchange_type, picking_order, supplier_info + ): + # Look for updated quantity + pos_from, pos_to = self._get_mapping_field_position( + edi_exchange_type, "product_qty" + ) + new_quantity = line[pos_from:pos_to] + # Look for ordered quantity + product_tmpl = self.env["product.template"] + if supplier_info: + product_tmpl = supplier_info[0].product_tmpl_id + ordered_product = self.env["product.product"].search( + [("product_tmpl_id", "=", product_tmpl.id)], + limit=1, + ) + ordered_operation = self.env["stock.move"].search( + [ + ("picking_id", "=", picking_order.id), + ("product_id", "=", ordered_product.id), + ] + ) + ordered_quantity = ordered_operation.quantity + # Construct one2many values + vals = dict() + if ordered_quantity != float(new_quantity): + vals.update( + { + "line_to_update_id": ordered_operation.id, + "product_id": ordered_product.id, + "ordered_quantity": ordered_quantity, + "product_qty": float(new_quantity), + } + ) + return vals + + def _reprepare_delivery_date(self, line, edi_exchange_type): + pos_from, pos_to = self._get_mapping_field_position( + edi_exchange_type, "date_planned" + ) + data = line[pos_from:pos_to] + return self.env["edi.configuration"].get_date_format_ble_yyyymmdd(data) + + def _prepare_picking_update_values(self, exchange_record): + lines = self._parse_file_content_ble(exchange_record) + edi_exchange_type = exchange_record.type_id + + if not lines: + raise ValidationError( + self.env._( + "Please configure fields mapping for BLE interface on your" + " EDI system!" + ) + ) + + delivery_date = delivery_sign = "" + picking_order = self.env["stock.picking"] + supplier_info = self.env["product.supplierinfo"] + purchase_order = self.env["purchase.order"] + proposition_vals = dict() + values_list = [] + for line in lines: + if not line or not isinstance(line, str): + continue + # This condition ensures that this job + # consider only one picking per EDI File + line = line.lstrip() + if line.startswith(edi_exchange_type.header_code) and not delivery_date: + # Header processing + delivery_date = self._reprepare_delivery_date(line, edi_exchange_type) + elif line.startswith(edi_exchange_type.lines_code): + # Look if it's a first delivery + delivery_sign = edi_exchange_type.delivery_sign + if delivery_sign == "-": + break + + # Look for picking_order, supplier_info + picking_order, supplier_info, purchase_order = ( + self._get_picking_order_and_supplier_info( + line, + edi_exchange_type, + picking_order, + supplier_info, + delivery_date, + ) + ) + # Look for updated quantity + updated_values = self._get_updated_quantity_values( + line, edi_exchange_type, picking_order, supplier_info + ) + if updated_values: + values_list += [Command.create(updated_values)] + if delivery_sign == "+": + proposition_vals.update( + { + "name": picking_order.id, + "values_proposed_ids": values_list, + } + ) + return proposition_vals, purchase_order + + def _handle_create_picking_update(self, exchange_record): + picking_update = self.env["picking.update"] + proposition_vals, purchase_order = self._prepare_picking_update_values( + exchange_record + ) + if proposition_vals: + picking_update = picking_update.create(proposition_vals) + return picking_update, purchase_order diff --git a/edi_purchase_diapar_oca/components/input_process_ch.py b/edi_purchase_diapar_oca/components/input_process_ch.py new file mode 100644 index 000000000..0593fc4a4 --- /dev/null +++ b/edi_purchase_diapar_oca/components/input_process_ch.py @@ -0,0 +1,126 @@ +import datetime +import logging + +from odoo import models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class EDIInputProcessDiaparPurchasePrice(models.AbstractModel): + """ + INPUT PROCESSOR CH + """ + + _name = "input.process.diapar.purchase.price" + _inherit = ["edi.oca.handler.process"] + _description = "Input process purchase price from Diapar" + + def process(self, exchange_record): + _logger.info(">>>>>>>>>>>>>>>>>> Reading CH file >>>>>>>>>>>>>>>>>>>>>") + SupplierPriceList = self.env["supplier.price.list"] + + self.create_supplier_price_list(exchange_record) + args = [ + ("product_tmpl_id", "!=", False), + ("price_updated", "=", False), + ("supplier_id", "!=", False), + ] + recs = self.env["supplier.price.list"].search(args) + products = recs.mapped("product_tmpl_id") + for product in products: + splists = recs.filtered( + lambda rec, product=product: rec.product_tmpl_id == product + ) + try: + SupplierPriceList.update_product_price_list(product, splists) + except Exception as e: + _logger.error( + "Could not update price list for the product %s: %s", + product, + str(e), + ) + + return self.env._("Process update purchase price from Diapar done!") + + def _parse_file_content_ch(self, exchange_record): + datas = exchange_record._get_file_content() + return datas.split("\n") + + def _get_mapping_field_position(self, exchange_type, field_name): + mapping = exchange_type.field_mapping_ids.filtered( + lambda rec: rec.mapping_field_id.name == field_name + ) + pos_from = mapping.sequence_start + pos_to = mapping.sequence_end + return pos_from, pos_to + + def _prepare_supplier_price_list_values(self, exchange_record): + lines = self._parse_file_content_ch(exchange_record) + if not lines: + raise ValidationError( + self.env._( + "Please configure fields mapping for prices interface on your \ + EDI Exchange Type!" + ) + ) + + edi_exchange_type = exchange_record.type_id + + EDIConfig = self.env["edi.configuration"] + ProductSupplierInfo = self.env["product.supplierinfo"] + + prices = [] + product_codes = [] + today = datetime.date.today() + for line in lines: + if not line or not isinstance(line, str): + continue + line = line.lstrip() + # check if this line is already imported. + pos_from, pos_to = self._get_mapping_field_position( + edi_exchange_type, "supplier_code" + ) + product_code = line[pos_from:pos_to] + if product_code in product_codes: + continue + else: + product_codes.append(product_code) + key = ["supplier_id", "import_date"] + value = [edi_exchange_type.partner_ids.ids[0], today] + for mapping in edi_exchange_type.field_mapping_ids: + slice_from = mapping.sequence_start + slice_to = mapping.sequence_end + # construct dictionary + key.append(mapping.mapping_field_id.name) + data = line[slice_from:slice_to] + # Product test + if mapping.mapping_field_id.name == "supplier_code": + # appending supplier_code data + value.append(data) + # appending product_id data + supp_info = ProductSupplierInfo.search( + [("product_code", "=", data)], limit=1 + ) + product_id = supp_info.product_tmpl_id.id + key.append("product_tmpl_id") + value.append(product_id) + # Date test + elif mapping.is_date: + # slice dates + apply_date = EDIConfig.get_date_format_yyyymmdd(data) + value.append(apply_date) + # numeric test + elif mapping.is_numeric: + decimal_precision = mapping.decimal_precision + price = EDIConfig.insert_separator(data, -decimal_precision, ".") + value.append(float(price)) + elif mapping.mapping_field_id.name == "product_name": + value.append(data) + prices_dict = {k: v for k, v in zip(key, value, strict=False)} + prices.append(prices_dict) + return prices + + def create_supplier_price_list(self, exchange_record): + prices = self._prepare_supplier_price_list_values(exchange_record) + return self.env["supplier.price.list"].create(prices) diff --git a/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml b/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml new file mode 100644 index 000000000..dcecad37a --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml @@ -0,0 +1,22 @@ + + + + DIAPAR + diapar + + + + Diapar + + + IN/PENDING + IN/DONE + IN/ERROR + OUT/PENDING + OUT/DONE + OUT/ERROR + + diff --git a/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml b/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml new file mode 100644 index 000000000..64ac55f9c --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml @@ -0,0 +1,23 @@ + + + + Diapar Purchase Order + + + + + +if record.state == 'purchase': + record._edi_send_via_edi(conf.type_id) + + + diff --git a/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml b/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml new file mode 100644 index 000000000..ebb3dc919 --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml @@ -0,0 +1,28 @@ + + + + Diapar Output Exchange Template + + + diapar.output.exchange.template + text + qweb + + 33513 + 03 + HDIAPAR + *DIAPAR*DIAPAR + + diff --git a/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml b/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml new file mode 100644 index 000000000..d9bef0bc6 --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml @@ -0,0 +1,82 @@ + + + + diapar_out_order + diapar_out_order + + + output + LD{dt}H{time} + C99 + iso-8859-1 + + + + filename_pattern: + date_pattern: "%Y%m%d" + time_pattern: "%H%M%S" + + + + diapar_in_despatch_advice + diapar_in_despatch_advice + + + input + BLE.* + txt + iso-8859-1 + 1 + 2 + + + + + + + + diapar_in_purchase_price + diapar_in_purchase_price + + + input + CH* + txt + iso-8859-1 + + + + + diff --git a/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml b/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml new file mode 100644 index 000000000..335ca3ee7 --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml @@ -0,0 +1,107 @@ + + + + + Date Planned + 1 + 24 + 32 + True + + + + + Product Code + 2 + 16 + 22 + + + + + Product Quantity + 3 + 52 + 57 + + + + + + Product Code + 1 + 2 + 8 + + + + + Product Name + 2 + 13 + 38 + + + + + Price HT + 3 + 38 + 47 + True + + + 4 + + + Apply Date + 4 + 96 + 102 + True + + + + diff --git a/edi_purchase_diapar_oca/demo/fs_storage_demo.xml b/edi_purchase_diapar_oca/demo/fs_storage_demo.xml new file mode 100644 index 000000000..e1397c28c --- /dev/null +++ b/edi_purchase_diapar_oca/demo/fs_storage_demo.xml @@ -0,0 +1,14 @@ + + + + Diapar FTP Storage + diapar_ftp + ftp + { + "host": "127.0.0.1", + "username": "admin", + "password": "admin", + "port": 5000 + } + + diff --git a/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot b/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot new file mode 100644 index 000000000..8c7aa2156 --- /dev/null +++ b/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot @@ -0,0 +1,205 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_purchase_diapar +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: edi_purchase_diapar +#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update +msgid "Cancel" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:33 +#, python-format +msgid "Check price for lines with product %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:28 +#, python-format +msgid "Check taxes for lines with product %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__constant_file_end +msgid "Constant File End" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__constant_file_start +msgid "Constant File Start" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__create_uid +msgid "Created by" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__create_date +msgid "Created on" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__customer_code +msgid "Customer Code" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__display_name +msgid "Display Name" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_edi_config_system +msgid "EDI Config System" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__edi_line_ids +msgid "Edi Line" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_login +msgid "FTP Login" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_password +msgid "FTP Password" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_host +msgid "FTP Server Host" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__id +msgid "ID" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_supplier_info_update_line__inv_supplier_price_id +msgid "Inv Supplier Price" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_account_invoice +msgid "Invoice" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_invoice_supplier_price_update +msgid "Invoice Supplier Price Update" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update____last_update +msgid "Last Modified on" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__write_date +msgid "Last Updated on" +msgstr "" + +#. module: edi_purchase_diapar +#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update +msgid "Lines" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:22 +#, python-format +msgid "No lines in this order %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__partner_id +msgid "Partner" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_account_invoice__partner_is_edi +msgid "Partner (Is Edit)" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:21 +#, python-format +msgid "Product code must be 6 digits for %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:17 +#, python-format +msgid "Product code must be numeric for %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_purchase_order +msgid "Purchase Order" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__po_text_file_pattern +msgid "Purchase order File pattern" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__csv_relative_in_path +msgid "Relative path for IN interfaces" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__csv_relative_out_path +msgid "Relative path for OUT interfaces" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__show_discount +msgid "Show Discount" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_supplier_info_update_line +msgid "Supplier Information Update Line" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.actions.act_window,name:edi_purchase_diapar.invoice_supplier_price_update_act +msgid "Update Prices From EDI" +msgstr "" + +#. module: edi_purchase_diapar +#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.invoice_supplier_form_inherit_diapar +msgid "Update Prices from EDI" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__vrp_code +msgid "VRP Code" +msgstr "" + +#. module: edi_purchase_diapar +#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update +msgid "Validate" +msgstr "" + diff --git a/edi_purchase_diapar_oca/i18n/fr.po b/edi_purchase_diapar_oca/i18n/fr.po new file mode 100644 index 000000000..15fa78b85 --- /dev/null +++ b/edi_purchase_diapar_oca/i18n/fr.po @@ -0,0 +1,84 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_purchase_diapar +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-26 10:56+0000\n" +"PO-Revision-Date: 2020-07-01 10:27+0000\n" +"Last-Translator: Simon Mas \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 3.8\n" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:31 +#, python-format +msgid "Check price for lines with product %s!" +msgstr "Check price for lines with product %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:29 +#, python-format +msgid "Check taxes for lines with product %s!" +msgstr "Merci de vérifier la taxe pour les lignes au produit: %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:23 +#, python-format +msgid "No Config FTP for this supplier %s!" +msgstr "Aucune configuration trouvée pour le fournisseur: %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:24 +#, python-format +msgid "No lines in this order %s!" +msgstr "Aucune ligne pour cette commande: %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:72 +#, python-format +msgid "" +"No supplier code given for product: %s for supplier: %s!, please give a " +"supplier code to continue prices operation update" +msgstr "" +"Aucun code fournisseur renseigné pour ce produit : %s avec le fournisseur: " +"%s!, merci de mettre à jour le codde fournisseur pour continuer la mise à " +"jour des prix." + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:20 +#, python-format +msgid "Product code must be 6 digits for %s!" +msgstr "Le code produit doit être de 6 numériques pour : %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:18 +#, python-format +msgid "Product code must be numeric for %s!" +msgstr "Le code produit doit être numérique pour le produit : %s!" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_purchase_order +msgid "Purchase Order" +msgstr "Bon de commande" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_edi_config_system +msgid "edi.config.system" +msgstr "edi.config.system" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_supplier_info_update +msgid "supplier.info.update" +msgstr "" + +#~ msgid "Information about a product vendor" +#~ msgstr "Information sur le vendeur de l'article" diff --git a/edi_purchase_diapar_oca/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.") diff --git a/edi_purchase_diapar_oca/models/__init__.py b/edi_purchase_diapar_oca/models/__init__.py new file mode 100644 index 000000000..9c3912f3b --- /dev/null +++ b/edi_purchase_diapar_oca/models/__init__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from . import account_move +from . import res_partner +from . import edi_configuration +from . import edi_field_mapping +from . import edi_exchange_type +from . import edi_exchange_template_output +from . import picking_update +from . import purchase_order +from . import product_product +from . import product_supplierinfo +from . import supplier_price_list +from . import res_company +from . import res_config_settings +from . import stock_move diff --git a/edi_purchase_diapar_oca/models/account_move.py b/edi_purchase_diapar_oca/models/account_move.py new file mode 100644 index 000000000..23188f3d0 --- /dev/null +++ b/edi_purchase_diapar_oca/models/account_move.py @@ -0,0 +1,15 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html + +from odoo import fields, models + + +class AccountMove(models.Model): + _inherit = "account.move" + + partner_is_edi = fields.Boolean( + related="partner_id.is_edi", + string="Partner (Is Edi)", + store=True, + ) diff --git a/edi_purchase_diapar_oca/models/edi_configuration.py b/edi_purchase_diapar_oca/models/edi_configuration.py new file mode 100644 index 000000000..a7fe93981 --- /dev/null +++ b/edi_purchase_diapar_oca/models/edi_configuration.py @@ -0,0 +1,59 @@ +import datetime +import time + +from odoo import api, models + + +class EDIConfiguration(models.Model): + _inherit = "edi.configuration" + + @api.model + def get_datenow_format_for_file(self): + now = time.strftime("%Y-%m-%d %H:%M:%S") + date = now.split(" ")[0].replace("-", "") + hour = now.split(" ")[1].replace(":", "") + return date, hour + + @api.model + def get_datetime_format_ddmmyyyy(self, do_date): + if not isinstance(do_date, datetime.datetime): + do_date = datetime.datetime.strptime(do_date, "%Y-%m-%d %H:%M:%S") + return "%02d%02d%s" % ( + do_date.day, + do_date.month, + str(do_date.year)[2:], + ) + + @api.model + def get_date_format_yyyymmdd(self, date): + """ + Transform a string date to datetime and format it to standard odoo + date format + """ + return datetime.datetime.strptime(date, "%y%m%d").strftime("%Y-%m-%d") + + @api.model + def get_date_format_ble_yyyymmdd(self, date): + """ + Transform a string date (specific to delivery order interface format) + to datetime object and format it to standard odoo date format + """ + return datetime.datetime.strptime(date, "%Y%m%d").strftime("%Y-%m-%d") + + @api.model + def insert_separator(self, string, index, separator): + """ + This method is to insert a separator inside string on a certain + position + """ + return string[:index] + separator + string[index:] + + def _fix_lenght(self, value, lenght, mode="float", replace="", position="before"): + value = str(value) + if mode == "float": + value = value.split(".")[0] + if position == "before": + value = "".join([replace for i in range(lenght - len(value))]) + value + else: + value += "".join([replace for i in range(lenght - len(value))]) + return value[0:lenght] diff --git a/edi_purchase_diapar_oca/models/edi_exchange_template_output.py b/edi_purchase_diapar_oca/models/edi_exchange_template_output.py new file mode 100644 index 000000000..511a5b515 --- /dev/null +++ b/edi_purchase_diapar_oca/models/edi_exchange_template_output.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class EDIExchangeTemplateOutput(models.Model): + _inherit = "edi.exchange.template.output" + + customer_code = fields.Char() + constant_file_start = fields.Char() + constant_file_end = fields.Char() + vrp_code = fields.Char() diff --git a/edi_purchase_diapar_oca/models/edi_exchange_type.py b/edi_purchase_diapar_oca/models/edi_exchange_type.py new file mode 100644 index 000000000..4bfad0ad2 --- /dev/null +++ b/edi_purchase_diapar_oca/models/edi_exchange_type.py @@ -0,0 +1,54 @@ +from datetime import datetime + +from pytz import timezone, utc + +from odoo import fields, models + +DEFAULT_DATE_FMT = "%Y%m%d" +DEFAULT_TIME_FMT = "%H%M%S" + + +class EDIExchangeType(models.Model): + _inherit = "edi.exchange.type" + + field_mapping_ids = fields.One2many( + comodel_name="edi.field.mapping", + inverse_name="exchange_type_id", + string="Field Mappings", + ) + header_code = fields.Char() + lines_code = fields.Char() + delivery_sign = fields.Char() + + def _make_exchange_filename_time(self): + self.ensure_one() + pattern_settings = self.advanced_settings.get("filename_pattern", {}) + force_tz = pattern_settings.get("force_tz", self.env.user.tz) + time_pattern = pattern_settings.get("time_pattern", DEFAULT_TIME_FMT) + tz = timezone(force_tz) if force_tz else None + now = datetime.now(utc).astimezone(tz) + return self.env["ir.http"]._slugify(now.strftime(time_pattern)) + + def _make_exchange_filename(self, exchange_record): + pattern = self.exchange_filename_pattern + if "{time}" in pattern: + ext = self.exchange_file_ext + if ext: + pattern += ".{ext}" + dt = self._make_exchange_filename_datetime() + seq = self._make_exchange_filename_sequence() + record_name = self._get_record_name(exchange_record) + record = exchange_record + if exchange_record.model and exchange_record.res_id: + record = exchange_record.record + return pattern.format( + exchange_record=exchange_record, + record=record, + record_name=record_name, + type=self, + dt=dt, + seq=seq, + ext=ext, + time=self._make_exchange_filename_time(), + ) + return super()._make_exchange_filename(exchange_record) diff --git a/edi_purchase_diapar_oca/models/edi_field_mapping.py b/edi_purchase_diapar_oca/models/edi_field_mapping.py new file mode 100644 index 000000000..0ddd2ee06 --- /dev/null +++ b/edi_purchase_diapar_oca/models/edi_field_mapping.py @@ -0,0 +1,25 @@ +from odoo import fields, models + + +class EDIPriceMapping(models.Model): + _name = "edi.field.mapping" + _order = "position" + _description = "EDI Price Mapping" + + sequence = fields.Integer(default=1) + position = fields.Integer(required=True) + exchange_type_id = fields.Many2one( + comodel_name="edi.exchange.type", + string="Exchange type", + required=True, + ) + mapping_field_id = fields.Many2one( + comodel_name="ir.model.fields", + string="Prices mapping field", + ) + name = fields.Char(string="Zone description") + sequence_start = fields.Integer() + sequence_end = fields.Integer() + is_numeric = fields.Boolean(string="Is numeric ?") + is_date = fields.Boolean(string="Is a date?") + decimal_precision = fields.Integer() diff --git a/edi_purchase_diapar_oca/models/picking_update.py b/edi_purchase_diapar_oca/models/picking_update.py new file mode 100644 index 000000000..6f725c127 --- /dev/null +++ b/edi_purchase_diapar_oca/models/picking_update.py @@ -0,0 +1,51 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html +from odoo import Command, fields, models + + +class PickingEdi(models.Model): + _name = "picking.edi" + _description = "Picking EDI" + + product_id = fields.Many2one("product.product") + ordered_quantity = fields.Float() + product_qty = fields.Float(string="EDI Quantity") + package_qty = fields.Float(string="Product package") + line_to_update_id = fields.Many2one("stock.move") + picking_update_id = fields.Many2one("picking.update") + + +class PickingUpdate(models.Model): + _name = "picking.update" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Picking Update" + + done = fields.Boolean(readonly=True) + name = fields.Many2one("stock.picking", string="Order picking", readonly=True) + values_proposed_ids = fields.One2many( + "picking.edi", + inverse_name="picking_update_id", + string="Quantities to update", + readonly=True, + ) + + def _prepare_values_proposed(self, proposition): + self.ensure_one() + return { + "quantity": proposition.product_qty, + } + + def button_update_picking_order(self): + self.ensure_one() + commands = [] + for proposition in self.values_proposed_ids: + commands += [ + Command.update( + proposition.line_to_update_id.id, + self._prepare_values_proposed(proposition), + ) + ] + self.done = True + self.name.write({"move_ids": commands}) + return True diff --git a/edi_purchase_diapar_oca/models/product_product.py b/edi_purchase_diapar_oca/models/product_product.py new file mode 100644 index 000000000..c7e393ade --- /dev/null +++ b/edi_purchase_diapar_oca/models/product_product.py @@ -0,0 +1,22 @@ +from odoo import models +from odoo.exceptions import ValidationError + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def _get_supplier_code_or_ean(self, seller_id): + self.ensure_one() + code, origin_code = "", "" + seller_line = self.seller_ids.filtered( + lambda seller: seller.partner_id.id == seller_id and seller.product_code + ) + if seller_line and seller_line[0].product_code: + code = seller_line[0].product_code + origin_code = "supplier" + elif self.barcode: + code = self.barcode + origin_code = "barcode" + if not code: + raise ValidationError(self.env._("No code for this product %s!", self.name)) + return code, origin_code diff --git a/edi_purchase_diapar_oca/models/product_supplierinfo.py b/edi_purchase_diapar_oca/models/product_supplierinfo.py new file mode 100644 index 000000000..52ca22637 --- /dev/null +++ b/edi_purchase_diapar_oca/models/product_supplierinfo.py @@ -0,0 +1,80 @@ +from odoo import api, models +from odoo.exceptions import ValidationError + + +class SupplierInfo(models.Model): + _inherit = "product.supplierinfo" + + @api.model + def compute_edi_partner(self, partner): + """ + :param partner: purchase order/invoice supplier + :return: EDI supplier used + """ + edi_exchange_type_obj = self.env["edi.exchange.type"] + exchange_type = edi_exchange_type_obj.search( + [("partner_ids", "=", partner.id)], limit=1 + ) + if not exchange_type: + raise ValidationError( + self.env._("No EDI Exchange Type for this supplier %s!") % partner.name + ) + if partner.edi_purchase_supplier_id: + return partner.edi_purchase_supplier_id + else: + return partner + + def _get_price_field(self): + return "price" + + @api.model + def update_purchase_price(self, vals): + """ + Looks for most recent price on purchase table of prices, only for + EDI suppliers + :param vals: + :return: updated values with product price + """ + supplier_id = vals.get("partner_id", False) + supplier = self.env["res.partner"].browse(supplier_id) + if supplier.is_edi: + edi_supplier = self.compute_edi_partner(supplier) + supplier_code = vals.get("product_code", False) + if not supplier_code: + raise ValidationError( + self.env._("Please give a supplier code to create the product!") + ) + price = self.env["supplier.price.list"].search( + [ + ("supplier_id", "=", edi_supplier.id), + ("supplier_code", "=", supplier_code), + ], + order="apply_date DESC", + ) + if price: + price_field = self._get_price_field() + vals.update({price_field: price[0].price}) + price.sudo().write({"price_updated": True}) + return vals + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + vals = self.update_purchase_price(vals) + return super().create(vals_list) + + @api.constrains("product_code", "partner_id") + def _check_product_code(self): + if self.product_code and self.partner_id.is_edi: + if not self.product_code.isdigit(): + raise ValidationError( + self.env._( + "Product code must be numeric for %s!", self.partner_id.name + ) + ) from None + if len(self.product_code) != 6: + raise ValidationError( + self.env._( + "Product code must be 6 digits for %s!", self.partner_id.name + ) + ) from None diff --git a/edi_purchase_diapar_oca/models/purchase_order.py b/edi_purchase_diapar_oca/models/purchase_order.py new file mode 100644 index 000000000..86e27ffd1 --- /dev/null +++ b/edi_purchase_diapar_oca/models/purchase_order.py @@ -0,0 +1,83 @@ +from odoo import models +from odoo.exceptions import ValidationError + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + def _should_apply_receipt_qty_policy(self): + """Condition to apply the policy of using the quantity from + the EDI file instead of the one in the order line.""" + self.ensure_one() + exchange_types = self.env["edi.exchange.type"].search( + [ + ("partner_ids", "in", self.partner_id.ids), + ("direction", "=", "input"), + ] + ) + return ( + not self.edi_disable_auto + and self.partner_id.is_edi + and self.partner_id.edi_purchase_conf_ids + and exchange_types + ) + + def _consolidate_product_qty(self, order_line): + return order_line.product_qty + + def _consolidate_products(self): + self.ensure_one() + if not self.order_line: + raise ValidationError( + self.env._("No lines in this order %s!", self.name) + ) from None + lines = {} + for line in self.order_line: + quantity = self._consolidate_product_qty(line) + if line.product_id in lines: + if line.taxes_id != lines[line.product_id]["taxes_id"]: + raise ValidationError( + self.env._( + "Check taxes for lines with product %s!", + line.product_id.name, + ) + ) + if line.price_unit != lines[line.product_id]["price_unit"]: + raise ValidationError( + self.env._( + "Check price for lines with product %s!", + line.product_id.name, + ) + ) + lines[line.product_id]["quantity"] += quantity + else: + code, origin_code = line.product_id._get_supplier_code_or_ean( + line.partner_id.id + ) + values = { + "code": code, + "origin_code": origin_code, + "quantity": quantity, + "price_unit": line.price_unit, + "taxes_id": line.taxes_id, + } + lines.update({line.product_id: values}) + return lines + + def generate_template_output_diapar(self, output_template, exchange_record): + self.ensure_one() + return output_template.exchange_generate( + exchange_record, + order_lines=self._consolidate_products(), + env=self.env, + ) + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + def _create_stock_moves(self, picking): + if self.order_id._should_apply_receipt_qty_policy(): + if self.company_id.edi_receipt_qty_policy == "received_file": + self = self.with_context(qty_from_file_policy=True) + return super()._create_stock_moves(picking) diff --git a/edi_purchase_diapar_oca/models/res_company.py b/edi_purchase_diapar_oca/models/res_company.py new file mode 100644 index 000000000..6cc529364 --- /dev/null +++ b/edi_purchase_diapar_oca/models/res_company.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + edi_receipt_qty_policy = fields.Selection( + selection=[ + ("ordered", "From PO"), + ("received_file", "From EDI Receipt File"), + ], + string="Receipt Quantity Policy", + default="ordered", + help="Policy to determine the quantity to be used in " + "receipt when processing EDI file for purchase order.", + ) diff --git a/edi_purchase_diapar_oca/models/res_config_settings.py b/edi_purchase_diapar_oca/models/res_config_settings.py new file mode 100644 index 000000000..cd67ab420 --- /dev/null +++ b/edi_purchase_diapar_oca/models/res_config_settings.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + edi_receipt_qty_policy = fields.Selection( + related="company_id.edi_receipt_qty_policy", readonly=False + ) diff --git a/edi_purchase_diapar_oca/models/res_partner.py b/edi_purchase_diapar_oca/models/res_partner.py new file mode 100644 index 000000000..baea45a0a --- /dev/null +++ b/edi_purchase_diapar_oca/models/res_partner.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + edi_purchase_supplier_id = fields.Many2one( + "res.partner", + domain=[("supplier_rank", ">", 0), ("is_edi", "=", True)], + ) + is_edi = fields.Boolean(string="Is an EDI supplier") + show_discount = fields.Boolean() diff --git a/edi_purchase_diapar_oca/models/stock_move.py b/edi_purchase_diapar_oca/models/stock_move.py new file mode 100644 index 000000000..8ab945ae3 --- /dev/null +++ b/edi_purchase_diapar_oca/models/stock_move.py @@ -0,0 +1,12 @@ +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _action_assign(self, force_qty=False): + res = super()._action_assign(force_qty=force_qty) + if self.env.context.get("qty_from_file_policy"): + # Set the quantity to 0 and it will be updated later in the process. + self.quantity = 0 + return res diff --git a/edi_purchase_diapar_oca/models/supplier_price_list.py b/edi_purchase_diapar_oca/models/supplier_price_list.py new file mode 100644 index 000000000..a6b60b1fa --- /dev/null +++ b/edi_purchase_diapar_oca/models/supplier_price_list.py @@ -0,0 +1,114 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from odoo import api, fields, models + + +class SupplierPriceList(models.Model): + _name = "supplier.price.list" + _description = "Supplier Price List" + + import_date = fields.Date(readonly=True) + supplier_id = fields.Many2one( + comodel_name="res.partner", + string="EDI Supplier", + domain=[("is_edi", "=", True), ("supplier_rank", ">", 0)], + readonly=True, + required=True, + ) + product_tmpl_id = fields.Many2one( + comodel_name="product.template", string="Product", ondelete="set null" + ) + product_name = fields.Char(readonly=True, required=True) + supplier_code = fields.Char(readonly=True, required=True) + price = fields.Float( + digits="Product Price", + readonly=True, + required=True, + help="The price HT to purchase a product", + ) + apply_date = fields.Date(readonly=True, required=True) + barcode = fields.Char(string="Ean") + price_updated = fields.Boolean() + + def _get_price_field(self): + return "price" + + def _create_supplierinfo_with_price(self, vals): + price_field = self._get_price_field() + vals.update({price_field: self.price}) + return self.env["product.supplierinfo"].create(vals) + + def button_create_product(self): + self.ensure_one() + # create new product + product_tmpl_id = self.env["product.template"].create( + { + "name": self.product_name, + "sale_ok": True, + "purchase_ok": True, + "type": "consu", + "default_code": self.supplier_code, + "barcode": self.barcode, + } + ) + # link product with current supplier price list + self.sudo().product_tmpl_id = product_tmpl_id.id + # create product supplier info + self._create_supplierinfo_with_price( + { + "partner_id": self.supplier_id.id, + "product_code": self.supplier_code, + "product_tmpl_id": product_tmpl_id.id, + } + ) + self.sudo().price_updated = True + # find similar supplier price list + supplier_price_list_ids = self.search( + [ + ("supplier_id", "=", self.supplier_id.id), + ("product_name", "=", self.product_name), + ("supplier_code", "=", self.supplier_code), + ("product_tmpl_id", "=", False), + ] + ) + # link product to similar supplier price list + supplier_price_list_ids.sudo().write({"product_tmpl_id": product_tmpl_id.id}) + # create action to open newly created product form view + action = { + "name": self.env._("Product Form"), + "res_model": "product.template", + "type": "ir.actions.act_window", + "target": "current", + "res_id": product_tmpl_id.id, + } + return action + + @api.model + def update_product_price_list(self, product, splists): + splists = splists.sorted("apply_date") + splist = splists[-1] + price = splist.price + args = [ + ("product_code", "=", splist.supplier_code), + ("product_tmpl_id", "=", product.id), + ] + supplierinfos = self.env["product.supplierinfo"].search(args, limit=1) + if supplierinfos: + price_field = self._get_price_field() + if getattr(supplierinfos, price_field) != price: + supplierinfos.write( + { + price_field: price, + } + ) + else: + self._create_supplierinfo_with_price( + { + "partner_id": splist.supplier_id.id, + "product_code": splist.supplier_code, + "product_tmpl_id": product.id, + } + ) + splists.sudo().write({"price_updated": True}) diff --git a/edi_purchase_diapar_oca/pyproject.toml b/edi_purchase_diapar_oca/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/edi_purchase_diapar_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md b/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..68accf590 --- /dev/null +++ b/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Druidoo +- [Trobz](https://www.trobz.com) + - Phan Hong Phuc \<\> diff --git a/edi_purchase_diapar_oca/security/ir.model.access.csv b/edi_purchase_diapar_oca/security/ir.model.access.csv new file mode 100644 index 000000000..9138190da --- /dev/null +++ b/edi_purchase_diapar_oca/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_model_edi_field_mapping_all,access_model_edi_field_mapping_all,model_edi_field_mapping,base.group_user,1,0,0,0 +access_model_edi_field_mapping_manager,access_model_edi_field_mapping_manager,model_edi_field_mapping,base_edi.group_edi_manager,1,1,1,1 +access_model_picking_edi_all,access_model_picking_edi_all,model_picking_edi,base.group_user,1,0,0,0 +access_model_picking_edi_manager,access_model_picking_edi_manager,model_picking_edi,base_edi.group_edi_manager,1,1,0,0 +access_model_picking_update_all,access_model_picking_update_all,model_picking_update,base.group_user,1,0,0,0 +access_model_picking_update_manager,access_model_picking_update_manager,model_picking_update,base_edi.group_edi_manager,1,1,0,0 +access_model_supplier_price_list_all,access_model_supplier_price_list_all,model_supplier_price_list,base.group_user,1,0,0,0 diff --git a/edi_purchase_diapar_oca/static/description/icon.png b/edi_purchase_diapar_oca/static/description/icon.png new file mode 100644 index 000000000..fd60220fb Binary files /dev/null and b/edi_purchase_diapar_oca/static/description/icon.png differ diff --git a/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml b/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml new file mode 100644 index 000000000..78aef15e0 --- /dev/null +++ b/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml @@ -0,0 +1,10 @@ + + + + diff --git a/edi_purchase_diapar_oca/tests/__init__.py b/edi_purchase_diapar_oca/tests/__init__.py new file mode 100644 index 000000000..bd5bea96f --- /dev/null +++ b/edi_purchase_diapar_oca/tests/__init__.py @@ -0,0 +1 @@ +from . import test_edi_purchase_diapar_oca diff --git a/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py b/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py new file mode 100644 index 000000000..deac5987c --- /dev/null +++ b/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py @@ -0,0 +1,238 @@ +from odoo import Command +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestEdiPurchaseDiaparOCA(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.edi_config_diapar = cls.env.ref( + "edi_purchase_diapar_oca.demo_edi_config_diapar" + ) + cls.backend = cls.env.ref("edi_purchase_diapar_oca.demo_edi_backend_diapar") + cls.edi_template_output_diapar = cls.env.ref( + "edi_purchase_diapar_oca.demo_edi_exchange_template_output_diapar" + ) + cls.exchange_type_ble = cls.env.ref( + "edi_purchase_diapar_oca.demo_exchange_diapar_in_despatch_advice" + ) + cls.exchange_type_ch = cls.env.ref( + "edi_purchase_diapar_oca.demo_exchange_diapar_in_purchase_price" + ) + cls.edi_supplier = cls.env.ref("base.res_partner_12") + cls.edi_supplier.write( + { + "is_edi": True, + "edi_purchase_conf_ids": [Command.set(cls.edi_config_diapar.ids)], + } + ) + cls.exchange_type_ble.partner_ids = [Command.set(cls.edi_supplier.ids)] + cls.exchange_type_ch.partner_ids = [Command.set(cls.edi_supplier.ids)] + cls.normal_supplier = cls.env.ref("base.res_partner_4") + cls.product_4 = cls.env.ref("product.product_product_4") + cls.product_5 = cls.env.ref("product.product_product_5") + cls.supplier_info_5 = cls.env["product.supplierinfo"].create( + { + "product_tmpl_id": cls.product_5.product_tmpl_id.id, + "partner_id": cls.edi_supplier.id, + "product_code": "987654", + "min_qty": 1, + "price": 10, + } + ) + + def _create_purchase_order(self, partner, product): + return self.env["purchase.order"].create( + { + "partner_id": partner.id, + "order_line": [ + Command.create( + { + "product_id": product.id, + "product_qty": 10, + "price_unit": 10, + } + ) + ], + } + ) + + def test_purchase_order_confirmation_without_product_code(self): + order = self._create_purchase_order(self.edi_supplier, self.product_4) + with self.assertRaises(ValidationError, msg="Please give a supplier code to"): + order.button_confirm() + + def test_purchase_order_confirmation_with_edi_supplier(self): + order = self._create_purchase_order(self.edi_supplier, self.product_5) + order.button_confirm() + self.assertEqual(order.state, "purchase") + self.assertEqual(order.exchange_record_count, 1) + + def test_purchase_order_confirmation_with_normal_supplier(self): + order = self._create_purchase_order(self.normal_supplier, self.product_5) + order.button_confirm() + self.assertEqual(order.state, "purchase") + self.assertEqual(order.exchange_record_count, 0) + + def test_edi_ouput_process_diapar_purchase_order(self): + order = self._create_purchase_order(self.edi_supplier, self.product_5) + order.button_confirm() + exchange_record = order.exchange_record_ids[0] + exchange_record.backend_id.exchange_generate(exchange_record) + file_content = exchange_record._get_file_content() + self.assertTrue(file_content, "The generated file content should not be empty.") + # Test template output diapar, this shoule be removed? + constant_file_start = self.edi_template_output_diapar.constant_file_start + constant_file_end = self.edi_template_output_diapar.constant_file_end + customer_code = self.edi_template_output_diapar.customer_code + vrp_code = self.edi_template_output_diapar.vrp_code + mapping_line_1 = "C" + self.edi_config_diapar.get_datetime_format_ddmmyyyy( + order.date_planned + ) + mapping_line_2 = "".join( + [ + ("D" if vals["origin_code"] == "supplier" else "E") + + self.edi_config_diapar._fix_lenght( + vals["code"], 6, mode="string", replace="0", position="after" + ) + + self.edi_config_diapar._fix_lenght( + vals["quantity"], 3, mode="float", replace="0", position="before" + ) + for _product, vals in order._consolidate_products().items() + ] + ) + self.assertEqual( + str(file_content).strip(), + f"{constant_file_start}A{vrp_code}B{customer_code}" + f"{mapping_line_1}{mapping_line_2}{constant_file_end}", + ) + + def _generate_fake_input_ble_content(self, order, new_quantity): + """ + Example: + 1AAAAABBBBBCCDDD2026030420260305EEEE + 2AAAAABBBBBCCDDD987654UUUUUUUUUUUUUUUUUUUUUUUUUVVVVV00007-GG + (where: + - 1 is the header code, 2 is the lines code + - 20260305 is the planned date + - 987654 is the product code + - 000007 is the new quantity) + """ + partner_id = order.partner_id.id + date_planned = order.date_planned.strftime("%Y%m%d") + header = f"1AAAAABBBBBCCDDD20260304{date_planned}EEEEEE" + lines = [] + for order_line in order.order_line: + product = order_line.product_id + code, _origin_code = product._get_supplier_code_or_ean(partner_id) + line = ( + f"2AAAAABBBBBCCDDD{code}UUUUUUUUUUUUUUUUUUUUUUUUU" + + f"VVVVV{str(new_quantity).zfill(5)}-GG" + ) + lines.append(line) + return "\n".join([header] + lines) + + def test_edi_input_process_ble(self): + new_quantity = 7 + order = self._create_purchase_order(self.edi_supplier, self.product_5) + order.write({"date_planned": "2026-03-05"}) + order.button_confirm() + exchange_record_out = order.exchange_record_ids[0] + exchange_record_out.backend_id.exchange_generate(exchange_record_out) + + exchange_record_ble = self.env["edi.exchange.record"].create( + { + "backend_id": self.backend.id, + "type_id": self.exchange_type_ble.id, + "model": "purchase.order", + "res_id": order.id, + } + ) + self.assertEqual(order.exchange_record_count, 2) + + fake_content = self._generate_fake_input_ble_content(order, new_quantity) + exchange_record_ble._set_file_content(fake_content) + exchange_record_ble.write({"edi_exchange_state": "input_received"}) + exchange_record_ble.backend_id.exchange_process(exchange_record_ble) + self.assertEqual( + exchange_record_ble.edi_exchange_state, + "input_processed", + "The EDI exchange record should be processed successfully.", + ) + order_picking = order.picking_ids[0] + picking_update = self.env["picking.update"].search( + [("name", "=", order_picking.id)] + ) + self.assertTrue(picking_update, "A picking update record should be created.") + self.assertTrue( + picking_update.done, "The picking update should be marked as done." + ) + moves = order_picking.move_ids + self.assertEqual( + len(moves), 1, "There should be one stock move in the picking." + ) + self.assertEqual( + moves[0].quantity, + new_quantity, + "The stock move quantity should be updated to the new quantity.", + ) + + def _generate_fake_input_ch_content(self): + product_code = self.supplier_info_5.product_code + product_name = "BISCOTTE 6CEREALE.300HEUD" + price_ht = "000015500" + apply_date = "20060305" + return ( + f"07{product_code}00015{product_name}{price_ht}" + + f"0000214100024000033924604808270011013700000030000{apply_date}0001403" + ) + + def test_edi_input_process_ch(self): + order = self._create_purchase_order(self.edi_supplier, self.product_5) + order.button_confirm() + exchange_record_ch = self.env["edi.exchange.record"].create( + { + "backend_id": self.backend.id, + "type_id": self.exchange_type_ch.id, + "model": "purchase.order", + "res_id": order.id, + } + ) + fake_content = self._generate_fake_input_ch_content().strip() + exchange_record_ch._set_file_content(fake_content) + exchange_record_ch.write({"edi_exchange_state": "input_received"}) + exchange_record_ch.backend_id.exchange_process(exchange_record_ch) + self.assertEqual( + exchange_record_ch.edi_exchange_state, + "input_processed", + "The EDI exchange record should be processed successfully.", + ) + + supplier_price_list = self.env["supplier.price.list"].search( + [ + ("supplier_id", "=", self.edi_supplier.id), + ("product_tmpl_id", "=", self.product_5.product_tmpl_id.id), + ] + ) + self.assertTrue( + supplier_price_list, "A supplier price list record should be created." + ) + self.assertEqual( + supplier_price_list.price, + 1.55, + "The price in the supplier price list should be updated to the new price.", + ) + supllier_info = self.env["product.supplierinfo"].search( + [ + ("partner_id", "=", self.edi_supplier.id), + ("product_code", "=", self.supplier_info_5.product_code), + ] + ) + self.assertTrue(supllier_info, "The supplier info record should exist.") + self.assertEqual( + supllier_info.price, + 1.55, + "The base price in the supplier info should be updated to the new price.", + ) diff --git a/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml b/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml new file mode 100644 index 000000000..2dd1b03ae --- /dev/null +++ b/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml @@ -0,0 +1,18 @@ + + + + edi.exchange.template.output + + + + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/exchange_type_views.xml b/edi_purchase_diapar_oca/views/exchange_type_views.xml new file mode 100644 index 000000000..280da8e30 --- /dev/null +++ b/edi_purchase_diapar_oca/views/exchange_type_views.xml @@ -0,0 +1,35 @@ + + + + edi.exchange.type.form.inherit + edi.exchange.type + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/menus.xml b/edi_purchase_diapar_oca/views/menus.xml new file mode 100644 index 000000000..ca0a422a9 --- /dev/null +++ b/edi_purchase_diapar_oca/views/menus.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/picking_update_views.xml b/edi_purchase_diapar_oca/views/picking_update_views.xml new file mode 100644 index 000000000..43837f8cc --- /dev/null +++ b/edi_purchase_diapar_oca/views/picking_update_views.xml @@ -0,0 +1,57 @@ + + + + picking.update.tree + picking.update + + + + + + + + + + + picking.update.form + picking.update + +
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ + + Delivery Order Update + ir.actions.act_window + picking.update + list,form + +
diff --git a/edi_purchase_diapar_oca/views/product_template_views.xml b/edi_purchase_diapar_oca/views/product_template_views.xml new file mode 100644 index 000000000..f38dd4289 --- /dev/null +++ b/edi_purchase_diapar_oca/views/product_template_views.xml @@ -0,0 +1,19 @@ + + + + product.template + + 20 + +
+
+
+
+
diff --git a/edi_purchase_diapar_oca/views/res_config_settings_views.xml b/edi_purchase_diapar_oca/views/res_config_settings_views.xml new file mode 100644 index 000000000..c1e4399a1 --- /dev/null +++ b/edi_purchase_diapar_oca/views/res_config_settings_views.xml @@ -0,0 +1,23 @@ + + + + res.config.settings.view.form.inherit.purchase + res.config.settings + + + + + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/res_partner_views.xml b/edi_purchase_diapar_oca/views/res_partner_views.xml new file mode 100644 index 000000000..01df2b7fd --- /dev/null +++ b/edi_purchase_diapar_oca/views/res_partner_views.xml @@ -0,0 +1,17 @@ + + + + view.partner.form.inherit + res.partner + + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/supplier_price_list_views.xml b/edi_purchase_diapar_oca/views/supplier_price_list_views.xml new file mode 100644 index 000000000..3feb0856b --- /dev/null +++ b/edi_purchase_diapar_oca/views/supplier_price_list_views.xml @@ -0,0 +1,81 @@ + + + + product.price.history.search + supplier.price.list + + + + + + + + + + + product.price.history.tree + supplier.price.list + + + + + + + + + + + + + + product.price.history.form + supplier.price.list + +
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ + + Price History + ir.actions.act_window + supplier.price.list + list,form + + + + Product Price History + ir.actions.act_window + supplier.price.list + { + 'search_default_product_tmpl_id': [active_id], + 'default_product_tmpl_id': active_id + } + + +
diff --git a/edi_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_queue_oca/tests/test_backend_jobs.py b/edi_queue_oca/tests/test_backend_jobs.py index 6998d08eb..44c90eac7 100644 --- a/edi_queue_oca/tests/test_backend_jobs.py +++ b/edi_queue_oca/tests/test_backend_jobs.py @@ -20,25 +20,27 @@ class EDIBackendTestJobsCase(EDIBackendCommonTestCase, JobMixin): def _setup_context(cls): return dict(super()._setup_context(), queue_job__no_delay=None) - @classmethod - def _setup_records(cls): # pylint:disable=missing-return - super()._setup_records() - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() from odoo.addons.edi_core_oca.tests.fake_models import EdiTestExecution - cls.loader.update_registry((EdiTestExecution,)) - cls.ExecutionAbstractModel = cls.env["edi.framework.test.execution"] - cls.model = cls.env["ir.model"].search( + self.loader.update_registry((EdiTestExecution,)) + self.ExecutionAbstractModel = self.env["edi.framework.test.execution"] + self.model = self.env["ir.model"].search( [("model", "=", "edi.framework.test.execution")] ) - cls.exchange_type_out.generate_model_id = cls.model - cls.exchange_type_out.send_model_id = cls.model - cls.exchange_type_out.output_validate_model_id = cls.model - cls.exchange_type_in.receive_model_id = cls.model - cls.exchange_type_in.process_model_id = cls.model - cls.exchange_type_in.input_validate_model_id = cls.model + self.exchange_type_out.generate_model_id = self.model + self.exchange_type_out.send_model_id = self.model + self.exchange_type_out.output_validate_model_id = self.model + self.exchange_type_in.receive_model_id = self.model + self.exchange_type_in.process_model_id = self.model + self.exchange_type_in.input_validate_model_id = self.model + + def tearDown(self): + self.loader.restore_registry() + super().tearDown() def _get_related_jobs(self, record): # Use domain in action to find all related jobs diff --git a/edi_record_metadata_oca/__manifest__.py b/edi_record_metadata_oca/__manifest__.py index 9b162ace3..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.2", + "version": "18.0.1.0.4", "development_status": "Alpha", "license": "LGPL-3", "website": "https://github.com/OCA/edi-framework", diff --git a/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py b/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py index ba9080a6e..87b6af99c 100644 --- a/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_record_metadata_oca/models/edi_exchange_consumer_mixin.py @@ -36,11 +36,11 @@ def _edi_get_metadata_to_store(self, orig_vals): def _edi_store_metadata(self, metadata): if self.origin_exchange_record_id: - self.origin_exchange_record_id.set_metadata(metadata) + self.origin_exchange_record_id.edi_set_metadata(metadata) @api.model def _edi_store_metadata_before_create(self, origin_id, metadata): - self.env["edi.exchange.record"].browse(origin_id).set_metadata(metadata) + self.env["edi.exchange.record"].browse(origin_id).edi_set_metadata(metadata) def _edi_get_metadata(self): - return self.origin_exchange_record_id.get_metadata() + return self.origin_exchange_record_id.edi_get_metadata() diff --git a/edi_record_metadata_oca/models/edi_exchange_record.py b/edi_record_metadata_oca/models/edi_exchange_record.py index 960fafef7..cd9be86cb 100644 --- a/edi_record_metadata_oca/models/edi_exchange_record.py +++ b/edi_record_metadata_oca/models/edi_exchange_record.py @@ -25,8 +25,8 @@ def _compute_metadata_display(self): for rec in self: rec.metadata_display = json.dumps(rec.metadata, sort_keys=True, indent=4) - def set_metadata(self, data): + def edi_set_metadata(self, data): self.metadata = data - def get_metadata(self): + def edi_get_metadata(self): return self.metadata diff --git a/edi_record_metadata_oca/tests/test_metadata.py b/edi_record_metadata_oca/tests/test_metadata.py index 62144fe94..28daf71a6 100644 --- a/edi_record_metadata_oca/tests/test_metadata.py +++ b/edi_record_metadata_oca/tests/test_metadata.py @@ -10,32 +10,28 @@ class TestEDIMetadata(EDIBackendCommonTestCase): - @classmethod - def _setup_records(cls): - res = super()._setup_records() - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() from .fake_models import EDIMetadataConsumerFake - cls.loader.update_registry((EDIMetadataConsumerFake,)) - cls.consumer_model = cls.env[EDIMetadataConsumerFake._name] + self.loader.update_registry((EDIMetadataConsumerFake,)) + self.consumer_model = self.env[EDIMetadataConsumerFake._name] - cls.exc_type = cls._create_exchange_type( + self.exc_type = self._create_exchange_type( name="Metadata test", code="metadata_test", direction="output", ) - cls.exc_record = cls.backend.create_record(cls.exc_type.code, {}) - return res + self.exc_record = self.backend.create_record(self.exc_type.code, {}) - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() + def tearDown(self): + self.loader.restore_registry() + super().tearDown() def test_fields(self): - self.exc_record.set_metadata({"foo": "baz", "bar": "waa"}) + self.exc_record.edi_set_metadata({"foo": "baz", "bar": "waa"}) self.assertTrue(self.exc_record.metadata) self.assertTrue(self.exc_record.metadata_display) @@ -48,7 +44,7 @@ def test_no_store(self): } ) self.assertFalse(consumer_record._edi_get_metadata()) - self.assertFalse(self.exc_record.get_metadata()) + self.assertFalse(self.exc_record.edi_get_metadata()) def test_store(self): vals = { @@ -70,4 +66,4 @@ def test_store(self): "additional": True, } self.assertEqual(consumer_record._edi_get_metadata(), expected) - self.assertEqual(self.exc_record.get_metadata(), expected) + self.assertEqual(self.exc_record.edi_get_metadata(), expected) diff --git a/edi_sale_input_oca/README.rst b/edi_sale_input_oca/README.rst index 1b89a27a9..99ac47dd6 100644 --- a/edi_sale_input_oca/README.rst +++ b/edi_sale_input_oca/README.rst @@ -11,7 +11,7 @@ EDI Sales input !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:1458f5d439bfde955a3c1dc00e8c8ab99b674d8bdb518b9bfe6e475df80742a6 + !! source digest: sha256:031a6af4aecc4f3f856b85d4d793f21481e7f45e181875c59435bf59e17b3239 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png diff --git a/edi_sale_input_oca/__manifest__.py b/edi_sale_input_oca/__manifest__.py index b182ce1fc..a523ea605 100644 --- a/edi_sale_input_oca/__manifest__.py +++ b/edi_sale_input_oca/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ Process incoming sale orders with the EDI framework. """, - "version": "18.0.1.0.1", + "version": "18.0.1.0.2", "development_status": "Alpha", "license": "AGPL-3", "author": "Camptocamp,Odoo Community Association (OCA)", diff --git a/edi_sale_input_oca/static/description/index.html b/edi_sale_input_oca/static/description/index.html index 1ce64058d..2187525cd 100644 --- a/edi_sale_input_oca/static/description/index.html +++ b/edi_sale_input_oca/static/description/index.html @@ -372,7 +372,7 @@

EDI Sales input

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:1458f5d439bfde955a3c1dc00e8c8ab99b674d8bdb518b9bfe6e475df80742a6 +!! source digest: sha256:031a6af4aecc4f3f856b85d4d793f21481e7f45e181875c59435bf59e17b3239 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

diff --git a/edi_sale_input_oca/tests/test_process.py b/edi_sale_input_oca/tests/test_process.py index 2fdea5f5b..bb5ab5c23 100644 --- a/edi_sale_input_oca/tests/test_process.py +++ b/edi_sale_input_oca/tests/test_process.py @@ -130,7 +130,7 @@ def test_metadata(self): order=dict(origin_exchange_record_id=self.record.id) ), ).create_order(parsed_order, "pricelist") - metadata = self.record.get_metadata() + metadata = self.record.edi_get_metadata() # Lines are mapped via `edi_id` (coming from `order_line_ref` by default) line_metadata = metadata["orig_values"]["lines"]["1111"] for k in ( diff --git a/edi_sale_ubl_output_oca/i18n/edi_sale_ubl_output_oca.pot b/edi_sale_ubl_output_oca/i18n/edi_sale_ubl_output_oca.pot index 197b4f2d4..28651d779 100644 --- a/edi_sale_ubl_output_oca/i18n/edi_sale_ubl_output_oca.pot +++ b/edi_sale_ubl_output_oca/i18n/edi_sale_ubl_output_oca.pot @@ -13,46 +13,6 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "1.0" -msgstr "" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "12:30:00" -msgstr "" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "2.2" -msgstr "" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "2010-01-21" -msgstr "" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "2010-02-10" -msgstr "" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "2010-02-25" -msgstr "" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qweb_tmpl_ubl_party -msgid "7300070011115" -msgstr "" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qweb_tmpl_ubl_party -msgid "7302347231111" -msgstr "" - #. module: edi_sale_ubl_output_oca #: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out msgid "EUR" diff --git a/edi_sale_ubl_output_oca/i18n/it.po b/edi_sale_ubl_output_oca/i18n/it.po index 09016fe82..6342ce979 100644 --- a/edi_sale_ubl_output_oca/i18n/it.po +++ b/edi_sale_ubl_output_oca/i18n/it.po @@ -16,46 +16,6 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.10.4\n" -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "1.0" -msgstr "1.0" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "12:30:00" -msgstr "12:30:00" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "2.2" -msgstr "2.2" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "2010-01-21" -msgstr "2010-01-21" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "2010-02-10" -msgstr "2010-02-10" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out -msgid "2010-02-25" -msgstr "2010-02-25" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qweb_tmpl_ubl_party -msgid "7300070011115" -msgstr "7300070011115" - -#. module: edi_sale_ubl_output_oca -#: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qweb_tmpl_ubl_party -msgid "7302347231111" -msgstr "7302347231111" - #. module: edi_sale_ubl_output_oca #: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out msgid "EUR" @@ -85,3 +45,27 @@ msgstr "urn:www.cenbii.eu:profile:BIIXYZ:ver1.0" #: model_terms:ir.ui.view,arch_db:edi_sale_ubl_output_oca.qwb_tmpl_ubl_order_response_out msgid "urn:www.cenbii.eu:transaction:biicoretrdmXYZ:ver1.0" msgstr "urn:www.cenbii.eu:transaction:biicoretrdmXYZ:ver1.0" + +#~ msgid "1.0" +#~ msgstr "1.0" + +#~ msgid "12:30:00" +#~ msgstr "12:30:00" + +#~ msgid "2.2" +#~ msgstr "2.2" + +#~ msgid "2010-01-21" +#~ msgstr "2010-01-21" + +#~ msgid "2010-02-10" +#~ msgstr "2010-02-10" + +#~ msgid "2010-02-25" +#~ msgstr "2010-02-25" + +#~ msgid "7300070011115" +#~ msgstr "7300070011115" + +#~ msgid "7302347231111" +#~ msgstr "7302347231111" diff --git a/edi_state_oca/README.rst b/edi_state_oca/README.rst index 21e8056ad..62956dee6 100644 --- a/edi_state_oca/README.rst +++ b/edi_state_oca/README.rst @@ -11,7 +11,7 @@ EDI state !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:b94605243de3a33cc1d60443e16739b5e3c1a135859aab0b21154ff1173e1f0a + !! source digest: sha256:e8a144d5155e42c0155d565988d9fecd87ed66b45db2090a072b8829eab6ab34 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png diff --git a/edi_state_oca/__manifest__.py b/edi_state_oca/__manifest__.py index 51e33f736..2fc7bf39e 100644 --- a/edi_state_oca/__manifest__.py +++ b/edi_state_oca/__manifest__.py @@ -7,7 +7,7 @@ "summary": """ Allow to assign specific EDI states to related records. """, - "version": "18.0.1.0.2", + "version": "18.0.1.0.3", "development_status": "Alpha", "license": "LGPL-3", "website": "https://github.com/OCA/edi-framework", diff --git a/edi_state_oca/static/description/index.html b/edi_state_oca/static/description/index.html index efa1a7a75..20ea1443b 100644 --- a/edi_state_oca/static/description/index.html +++ b/edi_state_oca/static/description/index.html @@ -372,7 +372,7 @@

EDI state

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

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 diff --git a/edi_state_oca/tests/test_edi_state.py b/edi_state_oca/tests/test_edi_state.py index 08ba3ec56..55f719643 100644 --- a/edi_state_oca/tests/test_edi_state.py +++ b/edi_state_oca/tests/test_edi_state.py @@ -10,63 +10,59 @@ class TestEDIState(EDIBackendCommonTestCase): - @classmethod - def _setup_records(cls): - res = super()._setup_records() - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() from .fake_models import EDIStateConsumerFake - cls.loader.update_registry((EDIStateConsumerFake,)) - cls.consumer_model = cls.env[EDIStateConsumerFake._name] - cls.consumer_record = cls.consumer_model.create( + self.loader.update_registry((EDIStateConsumerFake,)) + self.consumer_model = self.env[EDIStateConsumerFake._name] + self.consumer_record = self.consumer_model.create( { "name": "State Test Consumer", } ) # Suitable workflow - cls.wf1_ok = cls.env["edi.state.workflow"].create( + self.wf1_ok = self.env["edi.state.workflow"].create( { "name": "WF1", - "backend_type_id": cls.backend.backend_type_id.id, - "model_id": cls.env["ir.model"]._get(cls.consumer_record._name).id, + "backend_type_id": self.backend.backend_type_id.id, + "model_id": self.env["ir.model"]._get(self.consumer_record._name).id, } ) for i in range(1, 4): - cls.env["edi.state"].create( - {"name": f"OK {i}", "code": f"OK_{i}", "workflow_id": cls.wf1_ok.id} + self.env["edi.state"].create( + {"name": f"OK {i}", "code": f"OK_{i}", "workflow_id": self.wf1_ok.id} ) # Non suitable workflow - cls.wf2_ko = cls.env["edi.state.workflow"].create( + self.wf2_ko = self.env["edi.state.workflow"].create( { "name": "WF2", - "backend_type_id": cls.backend.backend_type_id.id, - "model_id": cls.env["ir.model"]._get("res.partner").id, + "backend_type_id": self.backend.backend_type_id.id, + "model_id": self.env["ir.model"]._get("res.partner").id, } ) for i in range(1, 4): - cls.env["edi.state"].create( - {"name": f"KO {i}", "code": f"KO_{i}", "workflow_id": cls.wf2_ko.id} + self.env["edi.state"].create( + {"name": f"KO {i}", "code": f"KO_{i}", "workflow_id": self.wf2_ko.id} ) - cls.exc_type = cls._create_exchange_type( + self.exc_type = self._create_exchange_type( name="State test", code="state_test", direction="output", - state_workflow_ids=[(6, 0, cls.wf1_ok.ids)], + state_workflow_ids=[(6, 0, self.wf1_ok.ids)], ) vals = { - "model": cls.consumer_record._name, - "res_id": cls.consumer_record.id, + "model": self.consumer_record._name, + "res_id": self.consumer_record.id, } - record = cls.backend.create_record("state_test", vals) - cls.consumer_record._edi_set_origin(record) - return res + record = self.backend.create_record("state_test", vals) + self.consumer_record._edi_set_origin(record) - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() + def tearDown(self): + self.loader.restore_registry() + super().tearDown() def test_is_state_valid(self): self.assertTrue(self.wf1_ok.is_valid_for_model(self.consumer_model._name)) diff --git a/eslint.config.cjs b/eslint.config.cjs index 0d5731f89..dd0cbe0ae 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -1,3 +1,4 @@ +var globals = require('globals'); jsdoc = require("eslint-plugin-jsdoc"); const config = [{ @@ -16,6 +17,8 @@ const config = [{ openerp: "readonly", owl: "readonly", luxon: "readonly", + QUnit: "readonly", + ...globals.browser, }, ecmaVersion: 2024, @@ -191,7 +194,7 @@ const config = [{ }, }, { - files: ["**/*.esm.js"], + files: ["**/*.esm.js", "**/*test.js"], languageOptions: { ecmaVersion: 2024, diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index ef3979096..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.*", 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