From 1b2a672ef6d747819946e616c15ca458af322a8a Mon Sep 17 00:00:00 2001 From: Edilio Escalona Almira Date: Thu, 13 Nov 2025 22:19:14 -0500 Subject: [PATCH] [ADD] edi_local: Add settings to generate .txt files --- edi_local/README.rst | 110 ++++ edi_local/__init__.py | 4 + edi_local/__manifest__.py | 22 + edi_local/data/edi_local_header_data.xml | 8 + edi_local/data/ir_cron_data.xml | 16 + edi_local/i18n/edi_local.pot | 710 +++++++++++++++++++++ edi_local/models/__init__.py | 6 + edi_local/models/edi_local.py | 439 +++++++++++++ edi_local/models/edi_local_header.py | 26 + edi_local/models/edi_local_line.py | 410 ++++++++++++ edi_local/pyproject.toml | 3 + edi_local/readme/CONTRIBUTORS.md | 2 + edi_local/readme/DESCRIPTION.md | 4 + edi_local/readme/ROADMAP.md | 1 + edi_local/readme/USAGE.md | 19 + edi_local/security/ir.model.access.csv | 4 + edi_local/static/description/icon.png | Bin 0 -> 9455 bytes edi_local/static/description/index.html | 462 ++++++++++++++ edi_local/utils.py | 28 + edi_local/views/edi_local_header_views.xml | 23 + edi_local/views/edi_local_line_views.xml | 62 ++ edi_local/views/edi_local_menus_views.xml | 20 + edi_local/views/edi_local_views.xml | 158 +++++ 23 files changed, 2537 insertions(+) create mode 100644 edi_local/README.rst create mode 100644 edi_local/__init__.py create mode 100644 edi_local/__manifest__.py create mode 100644 edi_local/data/edi_local_header_data.xml create mode 100644 edi_local/data/ir_cron_data.xml create mode 100644 edi_local/i18n/edi_local.pot create mode 100644 edi_local/models/__init__.py create mode 100644 edi_local/models/edi_local.py create mode 100644 edi_local/models/edi_local_header.py create mode 100644 edi_local/models/edi_local_line.py create mode 100644 edi_local/pyproject.toml create mode 100644 edi_local/readme/CONTRIBUTORS.md create mode 100644 edi_local/readme/DESCRIPTION.md create mode 100644 edi_local/readme/ROADMAP.md create mode 100644 edi_local/readme/USAGE.md create mode 100644 edi_local/security/ir.model.access.csv create mode 100644 edi_local/static/description/icon.png create mode 100644 edi_local/static/description/index.html create mode 100644 edi_local/utils.py create mode 100644 edi_local/views/edi_local_header_views.xml create mode 100644 edi_local/views/edi_local_line_views.xml create mode 100644 edi_local/views/edi_local_menus_views.xml create mode 100644 edi_local/views/edi_local_views.xml diff --git a/edi_local/README.rst b/edi_local/README.rst new file mode 100644 index 0000000000..901f25d3d9 --- /dev/null +++ b/edi_local/README.rst @@ -0,0 +1,110 @@ +========= +Edi local +========= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0e8cc9318b07f55cff944722ab96576f95706b578f7fd6ccee5c21cd6d75dab0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github + :target: https://github.com/OCA/edi/tree/17.0/edi_local + :alt: OCA/edi +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-17-0/edi-17-0-edi_local + :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&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +The module allows, through configuration, the following: + +1. Generating .txt files. +2. Reading .txt files. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Create headers for the configurations +------------------------------------- + +These headers allow us to have a configuration order independent of the +order of the configuration lines. + +Example: Header X - Sequence 1 Header Y - Sequence 2 Lines +(Configuration): + +- Header Y / Line 1 / Sequence 3 +- Header X / Line 2 / Sequence 5 +- Header Y / Line 3 / Sequence 1 + +Therefore, by grouping the lines by header, line 2 will be obtained +first, then line 3, and then line 1, in that order. + +1. Go to Settings > Technical > EDI > Edi local header +2. Create a new header +3. Save + +Known issues / Roadmap +====================== + +Add a new xml type and make the necessary modifications to generate and +read files of that type. + +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 +------- + +* Binhex + +Contributors +------------ + +- [Binhex] (https://www.binhex.cloud): + + - Edilio Escalona Almira e.escalona@binhex.cloud + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/edi_local/__init__.py b/edi_local/__init__.py new file mode 100644 index 0000000000..0077c8866c --- /dev/null +++ b/edi_local/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/edi_local/__manifest__.py b/edi_local/__manifest__.py new file mode 100644 index 0000000000..c8c5ddd0ba --- /dev/null +++ b/edi_local/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Edi local", + "summary": "Edi local", + "version": "17.0.1.0.0", + "author": "Binhex,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi", + "license": "AGPL-3", + "depends": ["base", "mail"], + "data": [ + "security/ir.model.access.csv", + "data/edi_local_header_data.xml", + "data/ir_cron_data.xml", + "views/edi_local_line_views.xml", + "views/edi_local_views.xml", + "views/edi_local_header_views.xml", + "views/edi_local_menus_views.xml", + ], + "installable": True, +} diff --git a/edi_local/data/edi_local_header_data.xml b/edi_local/data/edi_local_header_data.xml new file mode 100644 index 0000000000..7dac11bbb7 --- /dev/null +++ b/edi_local/data/edi_local_header_data.xml @@ -0,0 +1,8 @@ + + + + 0 + gen + General + + diff --git a/edi_local/data/ir_cron_data.xml b/edi_local/data/ir_cron_data.xml new file mode 100644 index 0000000000..e68c8d6c49 --- /dev/null +++ b/edi_local/data/ir_cron_data.xml @@ -0,0 +1,16 @@ + + + + Edi Local: Generate files + + 1 + days + -1 + + + model.with_context(generate_with_cron=True).generate_file_cron() + code + + diff --git a/edi_local/i18n/edi_local.pot b/edi_local/i18n/edi_local.pot new file mode 100644 index 0000000000..3c0aaa7a3b --- /dev/null +++ b/edi_local/i18n/edi_local.pot @@ -0,0 +1,710 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_local +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-11-13 18:43+0000\n" +"PO-Revision-Date: 2025-11-13 18:43+0000\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_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_line.py:0 +#, python-format +msgid "" +"\n" +"Type: %(type)s \n" +"Value: %(value)s \n" +"Field: %(field_name)s \n" +"Expression: %(expression)s \n" +"Record [%(model_name)s]: %(record_name)s \n" +"Error: %(message)s" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local_line__description +msgid "A description of the value the field accepts." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_ids +msgid "Activities" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_state +msgid "Activity State" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__after_header +msgid "After Header" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__type_data__alphanumeric +msgid "Alphanumeric" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__domain +msgid "Apply on" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__attachment_ids +msgid "Attachment" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__available_variables +msgid "Available Variables" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__orientation_fill_value__center +msgid "Center" +msgstr "" + +#. module: edi_local +#: model_terms:ir.ui.view,arch_db:edi_local.edi_local_view_form +msgid "" +"Check all lines without generating the final file or" +" taking any action after the evaluations." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__code +msgid "Code" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__create_uid +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__create_uid +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__create_uid +msgid "Created by" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__create_date +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__create_date +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__create_date +msgid "Created on" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__type_data__date +msgid "Date" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__decimal +msgid "Decimal" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "Define a filename" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "Define lines before generating a file" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__after_header +#: model:ir.model.fields,help:edi_local.field_edi_local_line__type_header +msgid "" +"Defines after which header the lines are generated, if not specified it is " +"generated at the end of everything" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local_line__type_data +msgid "" +"Defines the data type of the value\n" +"Alphanumeric: Only letters and numbers are allowed. Example: Alpha12pha345\n" +"Numeric: Only numbers are allowed. Example: 12345\n" +"Date: Only dates are allowed. Example: 20251112\n" +"General: Any value is allowed. Example: Alpha1_2pha345-*_" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local_line__size +msgid "Defines the field size. Example: 5 positions" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__dir_file +msgid "Defines the local directory where the generated files will be stored." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local_line__decimal +msgid "" +"Defines the number of decimal places in the number. These decimal places are" +" taken into account in the length (start + size)" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local_line__orientation_fill_value +msgid "Defines the orientation in which the filling will be carried out." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local_line__start +msgid "" +"Defines the position from which counting will begin based on the size value." +" By default, this is the last position of the previous line in the same " +"header." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local_line__value_fill +msgid "Defines the value to be filled, by default it is a space." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__override_file +msgid "Defines whether existing files are deleted when regenerating." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local_line__fill_value +msgid "" +"Defines whether the value size is filled with a specific character; if the " +"default character is not defined, it is filled with a space." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__description +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__description +msgid "Description" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__dir_file +msgid "Dir File" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__display_name +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__display_name +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__display_name +msgid "Display Name" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "" +"Documents of type %(document_type)s were not imported because the function " +"with structure %(func_structure)s does not exist." +msgstr "" + +#. module: edi_local +#: model:ir.ui.menu,name:edi_local.settings_edi_local +msgid "EDI" +msgstr "" + +#. module: edi_local +#: model:ir.model,name:edi_local.model_edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__edi_local_id +msgid "Edi Local" +msgstr "" + +#. module: edi_local +#: model:ir.model,name:edi_local.model_edi_local_type_header +msgid "Edi Local Type Header" +msgstr "" + +#. module: edi_local +#: model:ir.actions.server,name:edi_local.ir_cron_edi_local_generate_file_ir_actions_server +msgid "Edi Local: Generate files" +msgstr "" + +#. module: edi_local +#: model:ir.actions.act_window,name:edi_local.edi_local_tree_action +#: model:ir.ui.menu,name:edi_local.edi_local_tree_action_menu +msgid "Edi local" +msgstr "" + +#. module: edi_local +#: model:ir.model,name:edi_local.model_edi_local_line +msgid "Edi local line" +msgstr "" + +#. module: edi_local +#: model:ir.actions.act_window,name:edi_local.edi_local_type_header_tree_action +#: model:ir.ui.menu,name:edi_local.edi_local_type_header_tree_action_menu +msgid "Edi local type header" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__enabled +msgid "Enabled" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__end +msgid "End" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__field_id +msgid "Field" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__file_type +msgid "File Type" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__sequence_id +msgid "Filename" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__files_generated +msgid "Files Generated" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__fill_value +msgid "Fill Value" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__type_data__general +msgid "General" +msgstr "" + +#. module: edi_local +#: model_terms:ir.ui.view,arch_db:edi_local.edi_local_view_form +msgid "Generate file" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "Generated files:%(files_generated)s" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__has_message +msgid "Has Message" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__type__header +msgid "Header" +msgstr "" + +#. module: edi_local +#: model_terms:ir.ui.view,arch_db:edi_local.edi_local_view_form +msgid "Headers" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "" +"I do not generate documents of type %(document_type)s because the function " +"with structure %(func_structure)s does not exist." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__id +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__id +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__id +msgid "ID" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local__type__in +msgid "In" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__is_required +msgid "Is Required" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_line.py:0 +#, python-format +msgid "" +"It is not a valid number with %(partial_integer)s whole digits and " +"%(partial_decimal)s decimal places." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__write_uid +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__write_uid +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__write_date +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__write_date +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__write_date +msgid "Last Updated on" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__orientation_fill_value__left +msgid "Left" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__line_ids +msgid "Line" +msgstr "" + +#. module: edi_local +#: model_terms:ir.ui.view,arch_db:edi_local.edi_local_view_form +msgid "Lines" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__local_line_ids +msgid "Local Line" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_ids +msgid "Messages" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__model_id +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__model_id +msgid "Model" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__model_name +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__model_name +msgid "Model Name" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__name +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__name +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__name +msgid "Name" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_line.py:0 +#, python-format +msgid "No defined" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__type_data__numeric +msgid "Numeric" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__orientation_fill_value +msgid "Orientation Fill Value" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local__type__out +msgid "Out" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__override_file +msgid "Override File" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__orientation_fill_value__right +msgid "Right" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__sequence +#: model:ir.model.fields,field_description:edi_local.field_edi_local_type_header__sequence +msgid "Sequence" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__size +msgid "Size" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__start +msgid "Start" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__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_local +#: model_terms:ir.ui.view,arch_db:edi_local.edi_local_view_form +msgid "Test generate file" +msgstr "" + +#. module: edi_local +#: model_terms:ir.ui.view,arch_db:edi_local.edi_local_view_form +msgid "Test read file" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local__file_type__txt +msgid "Text" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_type_header.py:0 +#, python-format +msgid "The code must be unique." +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "The defined directory (%(dir_file)s) is not accessible." +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "" +"The document (%(document_name)s) extension is not the allowed one " +".%(file_type)s." +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "The documents were generated for %(model_name)s" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_line.py:0 +#, python-format +msgid "The field %(field)s is required" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_line.py:0 +#, python-format +msgid "" +"The start %(start)s and %(end)s values overlap with the %(line_name)s line.\n" +"Overlap: Start: %(start_overlap)s End: %(end_overlap)s" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_line.py:0 +#, python-format +msgid "The value is longer than the allowed size (%(size)s). " +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local.py:0 +#, python-format +msgid "" +"There are no records with the conditions defined in the Apply on field." +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_line.py:0 +#, python-format +msgid "" +"This is not a valid value for a date in YYYYMMDD format. Example: 20250902" +msgstr "" + +#. module: edi_local +#. odoo-python +#: code:addons/edi_local/models/edi_local_line.py:0 +#, python-format +msgid "" +"This is not a valid value for an alphanumeric character. Example: CAB159ED" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local__type +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__type +msgid "Type" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__type_data +msgid "Type Data" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__type_header +msgid "Type Header" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,help:edi_local.field_edi_local__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__value +msgid "Value" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields,field_description:edi_local.field_edi_local_line__value_fill +msgid "Value Fill" +msgstr "" + +#. module: edi_local +#: model:ir.model.fields.selection,name:edi_local.selection__edi_local_line__type__line +msgid "lines" +msgstr "" diff --git a/edi_local/models/__init__.py b/edi_local/models/__init__.py new file mode 100644 index 0000000000..be18536d47 --- /dev/null +++ b/edi_local/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import edi_local_header +from . import edi_local +from . import edi_local_line diff --git a/edi_local/models/edi_local.py b/edi_local/models/edi_local.py new file mode 100644 index 0000000000..ca4fe06093 --- /dev/null +++ b/edi_local/models/edi_local.py @@ -0,0 +1,439 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import io +import os +import textwrap +from pathlib import Path + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.safe_eval import safe_eval + +_AVAILABLE_VARIABLES = """ +# Available variables: +# - env: environment on which the action is triggered +# - record: +# When the type is 'out': it's the record on which the action is triggered. +# When the type is 'in': it's the value of the element retrieved from the +# file, usually a string. +# - model: model of the record on which the action is triggered; is a void recordset +# - time, datetime, dateutil, timezone: useful Python libraries +# - float_compare: utility function to compare floats based on specific precision +# - UserError: exception class for raising user-facing warning messages +""" + + +class EdiLocal(models.Model): + _name = "edi.local" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Edi Local" + + sequence_id = fields.Many2one("ir.sequence", string="Filename") + name = fields.Char(required=True, tracking=True) + model_id = fields.Many2one( + "ir.model", required=True, ondelete="cascade", tracking=True + ) + model_name = fields.Char( + related="model_id.model", + string="Model Name", + readonly=True, + inverse="_inverse_model_name", + ) + field_id = fields.Many2one( + "ir.model.fields", + domain="[('model', '=', model_name), " + "('ttype', 'in', ('one2many', 'many2many'))]", + ) + after_header = fields.Many2one( + "edi.local.header", + help="Defines after which header the lines are generated, " + "if not specified it is generated at the end of everything", + ) + local_line_ids = fields.One2many( + "edi.local.line", "edi_local_id", copy=True, domain=[("type", "=", "header")] + ) + line_ids = fields.One2many( + "edi.local.line", "edi_local_id", copy=True, domain=[("type", "=", "line")] + ) + description = fields.Char() + domain = fields.Char(string="Apply on", tracking=True) + enabled = fields.Boolean(default=False) + files_generated = fields.Text() + file_type = fields.Selection([("txt", "Text")], required=True, default="txt") + override_file = fields.Boolean( + default=True, + tracking=True, + help="Defines whether existing files are deleted when regenerating.", + ) + dir_file = fields.Char( + tracking=True, + help="Defines the local directory where the generated files will be stored.", + ) + type = fields.Selection( + [("in", "In"), ("out", "Out")], required=True, default="out" + ) + attachment_ids = fields.Many2many("ir.attachment") + available_variables = fields.Text(compute="_compute_available_variables") + + def _compute_available_variables(self): + available_variables = textwrap.dedent(_AVAILABLE_VARIABLES) + for local in self: + local.available_variables = available_variables + + @api.constrains("dir_file") + def _check_enabled(self): + for rec in self: + rec.valid_dir_file() + + def _inverse_model_name(self): + for rec in self: + rec.model_id = self.env["ir.model"]._get(rec.model_name) + + # ========================================================== + # GENERAL + # ========================================================== + + def get_eval_domain(self): + return self.env[self.model_name].search(safe_eval(self.domain)) + + def _message_error(self, message_error): + context = dict(self.env.context) + if context.get("generate_with_cron", False): + self.message_post(body=message_error) + else: + raise ValidationError(message_error) + + def _global_context_by_record(self, record=None): + """ + This method passes a dictionary as a context when + evaluating each of the record's values (values that match the domain). + For example: + The value of the Apply on field matches four records, + so these values are evaluated for each record, not for + each Header or Line. + """ + return {} + + def _context_by_line(self, record_line): + return {} + + def _get_values_grouped(self, headers=False, all_lines=False): + if all_lines: + values = self.local_line_ids | self.line_ids + else: + values = self.line_ids if not headers else self.local_line_ids + return values.sorted( + lambda local_line: local_line.type_header.sequence + ).grouped("type_header") + + # ========================================================== + # GENERATE FILE + # ========================================================== + + def valid_dir_file(self): + self.ensure_one() + if self.dir_file: + try: + os.makedirs(self.dir_file, exist_ok=True) + path = os.path.join(self.dir_file, ".perm_test.tmp") + with open(path, "w", encoding="utf-8") as f: + f.write("ok") + os.remove(path) + except OSError: + self._message_error( + _("The defined directory (%(dir_file)s) is not accessible.") + % { + "dir_file": self.dir_file, + } + ) + + def remove_files(self): + for file in self.files_generated.split(","): + file_path = Path(file) + if file_path.exists(): + file_path.unlink() + + def save_file(self, files): + files_generated = [] + self.valid_dir_file() + try: + for file in files: + filename, value_file = next(iter(file.items())) + path = os.path.join(self.dir_file, f"{filename}.txt") + with open(path, "w", encoding="utf-8") as f: + f.write(value_file) + files_generated.append(path) + except FileNotFoundError as ex: + self._message_error(str(ex)) + except Exception as ex: + self._message_error(ex) + return files_generated + + def get_filename(self): + return self.sequence_id.next_by_id() + + def _eval_lines(self, record, global_context_by_record): + """ + This method generates the lines according to the configuration of the Lines tab. + """ + eval_value = "" + line_grouped = self._get_values_grouped() + for _unused, values in line_grouped.items(): + for num_line, line in enumerate( + getattr(record, self.field_id.name, []), start=1 + ): + context_by_line = (global_context_by_record or {}).copy() + context_by_line.update(self._context_by_line(line)) + context_by_line.update({"num_line": num_line}) + for value in values: + eval_line = value._eval_line(line, context_by_line) + if eval_line is False: + return False + else: + eval_value += eval_line + eval_value += "\n" + return eval_value + + def _eval_headers_lines(self, record=None): + """ + This method generates the lines according + to the configuration of the Headers tab. + """ + local_line_grouped = self._get_values_grouped(headers=True) + global_context_by_record = self._global_context_by_record(record) + eval_value = "" + for type_header, values in local_line_grouped.items(): + for value in values: + eval_line = value._eval_line(record, global_context_by_record) + if eval_line is False: + return False + else: + eval_value += eval_line + + if self.after_header and self.after_header == type_header and self.field_id: + eval_value += "\n" + eval_value_line = self._eval_lines(record, global_context_by_record) + if eval_value_line is False: + return False + else: + eval_value += eval_value_line + eval_value += "\n" + return eval_value + + def _post_generate_file(self, record): + """ + params: + record: record that matches the configured domain + + This method is executed after generating a file for + one of the records that matches the configured domain. + For example: + When generating a file for an invoice, a 'Generated file' + field is set to allow the user to filter. + """ + pass + + def post_generate_file(self, record): + context = dict(self.env.context) + if not context.get("test_generate_file", False): + self._post_generate_file(record) + + def generate_file_txt(self, records): + files = [] + for record in records: + filename = self.get_filename() + eval_value = self._eval_headers_lines(record) + if eval_value: + files.append({filename: textwrap.dedent(eval_value).lstrip("\n")}) + self.post_generate_file(record) + return files + + def valid_generate_file(self): + self.ensure_one() + if not self.sequence_id: + self._message_error(_("Define a filename")) + + def generate_file(self): + context = dict(self.env.context) + for local in self: + if local.enabled or context.get("test_generate_file", False): + local.valid_before_read_or_generate_file() + local.valid_generate_file() + func_name = f"generate_file_{local.file_type}" + func_export_type = getattr(local, func_name, None) + if func_export_type: + try: + records = local.get_eval_domain() + if records: + result_files = func_export_type(records) + if result_files and not context.get( + "test_generate_file", False + ): + files_generated = local.save_file(result_files) + if self.override_file: + if self.files_generated: + self.remove_files() + self.files_generated = ",".join(files_generated) + else: + self.files_generated += "," + ",".join( + files_generated + ) + local.message_post( + body=_("Generated files:%(files_generated)s") + % { + "files_generated": "
".join( + files_generated + ) + }, + body_is_html=True, + ) + else: + continue + else: + local._message_error( + _( + "There are no records with " + "the conditions defined in the Apply on field." + ) + ) + except Exception as ex: + local._message_error(ex) + else: + local._message_error( + _( + "I do not generate documents of type %(document_type)s " + "because the function with structure " + " %(func_structure)s does not exist." + ) + % { + "document_type": local.file_type, + "func_structure": func_name, + } + ) + + # ========================================================== + # READ IMPORT FILE + # ========================================================== + + @api.constrains("attachment_ids", "file_type") + def _check_file(self): + for local in self: + for att in local.attachment_ids: + name = (att.name or "").lower() + ext = os.path.splitext(name)[1] + if ext != f".{local.file_type.lower()}": + raise ValidationError( + _( + "The document (%(document_name)s) extension is " + "not the allowed one .%(file_type)s." + ) + % { + "document_name": att.name, + "file_type": local.file_type, + } + ) + + def _post_read_import_file(self, value_list): + pass + + def post_read_import_file(self, value_list): + context = dict(self.env.context) + if not context.get("test_read_import_file", False): + self._post_read_import_file(value_list) + model_name = self.model_name.replace(".", " ").capitalize() + self.message_post( + body=_("The documents were generated for %(model_name)s") + % {"model_name": f"
{model_name}"}, + body_is_html=True, + ) + + def valid_read_file(self): + pass + + def read_file_txt(self, **values): + if values.get("bs4_data", False): + file_import_raw = base64.b64decode(values["bs4_data"]) + file_text = io.TextIOWrapper( + io.BytesIO(file_import_raw), + encoding=values.get("encoding", "utf-8"), + errors="replace", + ) + return file_text.readlines() + return [] + + def read_import_file_txt(self): + self.ensure_one() + lines = self._get_values_grouped(all_lines=True) + global_context_by_record = self._global_context_by_record() + value_list = [] + for attachment in self.attachment_ids: + lines_import = self.read_file_txt(**{"bs4_data": attachment.datas}) + field_name = self.field_id.name + item_line = 0 + value_dict = {} + for type_header, values in lines.items(): + line = lines_import[item_line].rstrip("\n") + if not value_dict.get(type_header.code, False): + value_dict[type_header.code] = {} + for value in values: + record_value = value._parse_value(line) + eval_line = value._eval_line(record_value, global_context_by_record) + if value.type == "line": + if not value_dict[type_header.code].get(field_name, False): + value_dict[type_header.code][field_name] = {} + value_dict[type_header.code][field_name].update(eval_line) + else: + value_dict[type_header.code].update(eval_line) + item_line += 1 + value_list.append(value_dict) + return value_list + + def read_import_file(self): + context = dict(self.env.context) + value_list = [] + for local in self: + if local.enabled or context.get("test_read_import_file", False): + local.valid_before_read_or_generate_file() + local.valid_read_file() + func_name = f"read_import_file_{local.file_type}" + func_import_type = getattr(local, func_name, None) + if func_import_type: + try: + value_list = func_import_type() + except Exception as ex: + local._message_error(ex) + else: + local._message_error( + _( + "Documents of type %(document_type)s " + "were not imported " + "because the function with structure " + " %(func_structure)s does not exist." + ) + % { + "document_type": local.file_type, + "func_structure": func_name, + } + ) + local.post_read_import_file(value_list) + + def valid_before_read_or_generate_file(self): + self.ensure_one() + if not self.local_line_ids: + self._message_error(_("Define lines before generating a file")) + + def get_domain_cron(self): + return [ + ("enabled", "=", True), + ("type", "=", "out"), + "|", + ("local_line_ids", "!=", False), + ("line_ids", "!=", False), + ] + + def generate_file_cron(self): + for local in self.search(self.get_domain_cron()): + if local.get_eval_domain(): + local.generate_file() diff --git a/edi_local/models/edi_local_header.py b/edi_local/models/edi_local_header.py new file mode 100644 index 0000000000..1e158fe7c2 --- /dev/null +++ b/edi_local/models/edi_local_header.py @@ -0,0 +1,26 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class EdiLocalHeader(models.Model): + _name = "edi.local.header" + _description = "Edi Local Header" + + sequence = fields.Integer() + code = fields.Char(required=True) + name = fields.Char(required=True) + + @api.constrains("code") + def _check_code(self): + for local_header in self: + if self.search_count( + [ + ("code", "=", local_header.code), + ("id", "!=", local_header.id), + ], + limit=1, + ): + raise ValidationError(_("The code must be unique.")) diff --git a/edi_local/models/edi_local_line.py b/edi_local/models/edi_local_line.py new file mode 100644 index 0000000000..5fc5f89660 --- /dev/null +++ b/edi_local/models/edi_local_line.py @@ -0,0 +1,410 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import inspect + +from pytz import timezone + +from odoo import _, api, fields, models, tools +from odoo.tools.float_utils import float_compare +from odoo.tools.safe_eval import safe_eval, test_python_expr + +from ..utils import is_alphanumeric, is_date, is_numeric + + +class EdiLocalLine(models.Model): + _name = "edi.local.line" + _description = "Edi local line" + + edi_local_id = fields.Many2one("edi.local") + model_name = fields.Char(related="edi_local_id.model_name") + model_id = fields.Many2one("ir.model", related="edi_local_id.model_id") + sequence = fields.Integer() + type = fields.Selection( + [ + ("header", "Header"), + ("line", "lines"), + ], + default="header", + help="", + ) + type_header = fields.Many2one( + "edi.local.header", + help="Defines after which header the lines are generated, " + "if not specified it is generated at the end of everything", + ) + name = fields.Char(required=True) + description = fields.Char(help="A description of the value the field accepts.") + type_data = fields.Selection( + [ + ("alphanumeric", "Alphanumeric"), + ("numeric", "Numeric"), + ("date", "Date"), + ("general", "General"), + ], + default="alphanumeric", + help="Defines the data type of the value\n" + "Alphanumeric: Only letters and numbers are allowed. Example: Alpha12pha345\n" + "Numeric: Only numbers are allowed. Example: 12345\n" + "Date: Only dates are allowed. Example: 20251112\n" + "General: Any value is allowed. Example: Alpha1_2pha345-*_", + ) + start = fields.Integer( + help="Defines the position from which counting will begin based on " + "the size value. By default, this is the last position " + "of the previous line in the same header.", + store=True, + ) + size = fields.Integer( + help="Defines the field size. Example: 5 positions", required=True, default=1 + ) + end = fields.Integer(compute="_compute_end", store=True) + decimal = fields.Integer( + default=0, + help="Defines the number of decimal places in the " + "number. These decimal places are taken into " + "account in the length (start + size)", + ) + is_required = fields.Boolean( + default=True, + ) + fill_value = fields.Boolean( + default=False, + help="Defines whether the value size is filled with " + "a specific character; if the default character " + "is not defined, it is filled with a space.", + ) + value_fill = fields.Char( + help="Defines the value to be filled, by default it is a space." + ) + orientation_fill_value = fields.Selection( + [("left", "Left"), ("center", "Center"), ("right", "Right")], + default="right", + help="Defines the orientation in which " "the filling will be carried out.", + ) + value = fields.Text() + + @api.depends("start", "size") + def _compute_end(self): + for line in self: + line.end = line.start + line.size + + @api.constrains("start", "size") + def _check_start(self): + for line in self: + overlap = self.search_read( + [ + ("type", "=", line.type), + ("type_header", "=", line.type_header.id), + ("start", "<", line.end), + ("model_name", "=", line.edi_local_id.model_name), + ("edi_local_id", "=", line.edi_local_id.id), + ("end", ">", line.start), + ("id", "!=", line.id), + ], + ["name", "start", "end"], + limit=1, + ) + if overlap: + self.edi_local_id._message_error( + _( + "The start %(start)s and %(end)s values overlap with the " + "%(line_name)s line." + "\nOverlap: Start: %(start_overlap)s End: %(end_overlap)s" + ) + % { + "start": line.start, + "end": line.end, + "line_name": overlap[0]["name"], + "start_overlap": overlap[0]["start"], + "end_overlap": overlap[0]["end"], + } + ) + + @api.onchange("value") + def _onchange_value(self): + for local in self: + local._normalize_code() + if local.value: + local._test_python_expr() + + def _normalize_code(self): + format_value = self.value or "" + format_value = format_value.replace("\r\n", "\n") + format_value = format_value.expandtabs(4) + self.value = inspect.cleandoc(format_value).strip() + + def _not_check_value(self, record, value): + return False + + def _show_message_error_line(self, record, value, message_error): + self.edi_local_id._message_error( + _( + "\nType: %(type)s " + "\nValue: %(value)s " + "\nField: %(field_name)s " + "\nExpression: %(expression)s " + "\nRecord [%(model_name)s]: %(record_name)s " + "\nError: %(message)s" + ) + % { + "type": self.type.capitalize(), + "field_name": self.name, + "model_name": self.model_id.name, + "record_name": getattr(record, "name", ""), + "value": value if value else _("No defined"), + "expression": self.value, + "message": message_error, + } + ) + + def _valid_is_numeric(self, **values): + value = values.get("value") + partial_integer = self.size - self.decimal + partial_decimal = self.decimal + valid = True + if not is_numeric( + value=value, + partial_integer=values.get("partial_integer", partial_integer), + partial_decimal=values.get("partial_decimal", partial_decimal), + ) or (values.get("is_digit", False) and not value.isdigit()): + valid = False + self._show_message_error_line( + self, + value, + _( + "It is not a valid number with %(partial_integer)s " + "whole digits and %(partial_decimal)s decimal places." + ) + % { + "partial_integer": partial_integer, + "partial_decimal": partial_decimal, + }, + ) + return valid + + def _valid_is_alphanumeric(self, **values): + value = values.get("value") + valid = True + if not is_alphanumeric( + value=value, + allow_character=" " + if self.fill_value and not self.value_fill + else self.value_fill, + ): + valid = False + self._show_message_error_line( + self, + value, + _( + "This is not a valid value for an alphanumeric character. " + "Example: CAB159ED" + ), + ) + return valid + + def _valid_is_date(self, **values): + value = values.get("value") + valid = True + if not is_date(value=value): + valid = False + self._show_message_error_line( + self, + value, + _( + "This is not a valid value for a date in YYYYMMDD format. " + "Example: 20250902" + ), + ) + return valid + + def _check_value(self, record, value): + check_valid = True + if not self.is_required and not value: + return check_valid + value_size = ( + self.size + 1 + if self.type_data == "numeric" and self.decimal > 0 + else self.size + ) + if len(value) > value_size: + self._show_message_error_line( + record, + value, + _("The value is longer than the allowed size (%(size)s). ") + % { + "size": value_size, + }, + ) + check_valid = False + elif self.is_required and not value: + self.edi_local_id._message_error( + _("The field %(field)s is required") % {"field": self.name} + ) + check_valid = False + elif self.type_data == "numeric": + check_valid = self._valid_is_numeric(**{"value": value}) + elif self.type_data == "alphanumeric": + check_valid = self._valid_is_alphanumeric(**{"value": value}) + elif self.type_data == "date": + check_valid = self._valid_is_date(**{"value": value}) + return check_valid + + def check_value(self, record, value): + if self._not_check_value(record, value): + return True + return self._check_value(record, value) + + def _get_eval_context(self, eval_context=None): + return { + # orm + "env": self.env, + "model": self.env[self.model_name], + # record + "record": None, + # tools + "time": tools.safe_eval.time, + "datetime": tools.safe_eval.datetime, + "dateutil": tools.safe_eval.dateutil, + "timezone": timezone, + "float_compare": float_compare, + } | (eval_context or {}) + + def _test_python_expr(self): + # Evaluating expression syntax + if self.value: + msg = test_python_expr(expr=self.value, mode="eval") + if msg: + self.edi_local_id._message_error(msg) + return True + + def _trunc_value(self, value): + if self.type_data == "alphanumeric": + return value[0 : self.size] + return value + + def _fill_value_numeric(self, **values): + eval_value = values.get("eval_value", None) + size = values.get("size", None) + number_str = str(eval_value) + if "." in number_str: + integer, decimal = number_str.split(".", 1) + else: + integer, decimal = number_str, "" + partial_integer = integer.zfill(size) + if self.decimal: + partial_decimal = decimal.ljust(self.decimal, "0")[: self.decimal] + value_complete = f"{partial_integer}.{partial_decimal}" + else: + value_complete = partial_integer + return value_complete + + def _fill_value_alphanumeric(self, **values): + eval_value = values.get("eval_value", None) + size = values.get("size", None) + if self.orientation_fill_value == "left": + value_complete = ( + str(eval_value).rjust(size, str(self.value_fill)) + if self.value_fill + else str(eval_value).rjust(size) + ) + elif self.orientation_fill_value == "center": + value_complete = ( + str(eval_value).center(size, str(self.value_fill)) + if self.value_fill + else str(eval_value).center(size) + ) + else: + value_complete = ( + str(eval_value).ljust(size, str(self.value_fill)) + if self.value_fill + else str(eval_value).ljust(size) + ) + return value_complete + + def _fill_value_date(self, **values): + eval_value = values.get("eval_value", None) + size = values.get("size", None) + if self.value_fill: + value_complete = str(eval_value).rjust(size, str(self.value_fill)) + else: + value_complete = str(eval_value).rjust(size) + return value_complete + + def fill_value_by_size(self, eval_value): + size = self.size - self.decimal + if self.type_data == "alphanumeric": + value_complete = self._fill_value_alphanumeric( + **{"eval_value": eval_value, "size": size} + ) + elif self.type_data == "numeric": + value_complete = self._fill_value_numeric( + **{"eval_value": eval_value, "size": size} + ) + elif self.type_data == "date": + value_complete = self._fill_value_date( + **{"eval_value": eval_value, "size": size} + ) + else: + value_complete = eval_value + return value_complete + + def _eval_value(self, record, global_context_by_record): + self._normalize_code() + self._test_python_expr() + try: + global_context_by_record.update( + { + "record": record, + } + ) + if not self.value: + return "" if self.edi_local_id.type == "out" else {self.name: record} + + value = safe_eval( + self.value, + mode="eval", + nocopy=True, + globals_dict=self._get_eval_context( + eval_context=global_context_by_record + ), + ) + if self.edi_local_id.type == "out": + result = self._trunc_value(value) + if self.fill_value and len(str(result)) < self.size: + result = self.fill_value_by_size(result) + result_check = self.check_value(record, result) + if not result_check: + return False + else: + result = {self.name: value} + return result + except TypeError as e: + self._show_message_error_line(record, None, str(e)) + + def _eval_line(self, record, global_context_by_record): + context = dict(self.env.context) + eval_value = self.with_context(**context)._eval_value( + record, global_context_by_record + ) + return eval_value + + def _parse_value(self, value): + self.ensure_one() + parse_value = value[self.start - 1 : self.end - 1] + parse_value = parse_value.replace(self.value_fill or " ", "") + if self.type_data == "numeric" and parse_value: + self._valid_is_numeric(**{"value": parse_value, "is_digit": True}) + parse_entire = parse_value[0 : self.size] + partial_decimal = False + if self.decimal: + partial_decimal = parse_value[self.size : self.size + self.decimal :] + parse_value = float( + f"{parse_entire}.{partial_decimal or 0}" + if self.decimal + else parse_entire + ) + elif self.type_data == "alphanumeric" and parse_value: + self._valid_is_alphanumeric(**{"value": parse_value}) + elif self.type_data == "date" and parse_value: + self._valid_is_date(**{"value": parse_value}) + return parse_value diff --git a/edi_local/pyproject.toml b/edi_local/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/edi_local/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/edi_local/readme/CONTRIBUTORS.md b/edi_local/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..ab4e0fb41b --- /dev/null +++ b/edi_local/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Binhex] (https://www.binhex.cloud): + - Edilio Escalona Almira diff --git a/edi_local/readme/DESCRIPTION.md b/edi_local/readme/DESCRIPTION.md new file mode 100644 index 0000000000..569c2db076 --- /dev/null +++ b/edi_local/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +The module allows, through configuration, the following: + +1. Generating .txt files. +2. Reading .txt files. diff --git a/edi_local/readme/ROADMAP.md b/edi_local/readme/ROADMAP.md new file mode 100644 index 0000000000..7ac99240db --- /dev/null +++ b/edi_local/readme/ROADMAP.md @@ -0,0 +1 @@ +Add a new xml type and make the necessary modifications to generate and read files of that type. diff --git a/edi_local/readme/USAGE.md b/edi_local/readme/USAGE.md new file mode 100644 index 0000000000..deaec40cf7 --- /dev/null +++ b/edi_local/readme/USAGE.md @@ -0,0 +1,19 @@ +Create headers for the configurations +----------------------------------------- +These headers allow us to have a configuration order independent of the order of the configuration lines. + +Example: +Header X - Sequence 1 +Header Y - Sequence 2 +Lines (Configuration): +- Header Y / Line 1 / Sequence 3 +- Header X / Line 2 / Sequence 5 +- Header Y / Line 3 / Sequence 1 + +Therefore, by grouping the lines by header, line 2 will be obtained first, then line 3, and then line 1, in that order. + +1. Go to Settings > Technical > EDI > Edi local header +2. Create a new header +3. Save + + diff --git a/edi_local/security/ir.model.access.csv b/edi_local/security/ir.model.access.csv new file mode 100644 index 0000000000..277ea08131 --- /dev/null +++ b/edi_local/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_edi_local_manager,edi_local.edi_local,model_edi_local,base.group_user,1,1,1,1 +access_edi_local_line_manager,edi_local.edi_local.line,model_edi_local_line,base.group_user,1,1,1,1 +access_edi_local_header_manager,edi_local.edi_local.header,model_edi_local_header,base.group_user,1,1,1,1 diff --git a/edi_local/static/description/icon.png b/edi_local/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/edi_local/static/description/index.html b/edi_local/static/description/index.html new file mode 100644 index 0000000000..02cf2d7f9a --- /dev/null +++ b/edi_local/static/description/index.html @@ -0,0 +1,462 @@ + + + + + +Edi local + + + +
+

Edi local

+ + +

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

+

The module allows, through configuration, the following:

+
    +
  1. Generating .txt files.
  2. +
  3. Reading .txt files.
  4. +
+

Table of contents

+ +
+

Usage

+
+

Create headers for the configurations

+

These headers allow us to have a configuration order independent of the +order of the configuration lines.

+

Example: Header X - Sequence 1 Header Y - Sequence 2 Lines +(Configuration):

+
    +
  • Header Y / Line 1 / Sequence 3
  • +
  • Header X / Line 2 / Sequence 5
  • +
  • Header Y / Line 3 / Sequence 1
  • +
+

Therefore, by grouping the lines by header, line 2 will be obtained +first, then line 3, and then line 1, in that order.

+
    +
  1. Go to Settings > Technical > EDI > Edi local header
  2. +
  3. Create a new header
  4. +
  5. Save
  6. +
+
+
+
+

Known issues / Roadmap

+

Add a new xml type and make the necessary modifications to generate and +read files of that type.

+
+
+

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

+
    +
  • Binhex
  • +
+
+
+

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 project on GitHub.

+

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

+
+
+
+ + diff --git a/edi_local/utils.py b/edi_local/utils.py new file mode 100644 index 0000000000..4becc0f88c --- /dev/null +++ b/edi_local/utils.py @@ -0,0 +1,28 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import re + + +def is_numeric(value, partial_integer=8, partial_decimal=None): + partial_integer = partial_integer if "-" not in value else partial_integer - 1 + if partial_decimal: + regex = re.compile( + rf"^-?\d{{1,{partial_integer}}}(?:\.\d{{{partial_decimal}}})?$" + ) + else: + regex = re.compile(rf"^-?\d{{1,{partial_integer}}}$") + return regex.match(str(value)) + + +def is_alphanumeric(value, allow_character=None): + ex_regex = "^[A-Za-z0-9 ]+$" + if allow_character: + ex_regex = rf"^[A-Za-z0-9 {allow_character}]+$" + regex = re.compile(ex_regex) + return regex.match(str(value)) + + +def is_date(value): + regex = re.compile(r"^\d{4}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$") + return regex.match(str(value)) diff --git a/edi_local/views/edi_local_header_views.xml b/edi_local/views/edi_local_header_views.xml new file mode 100644 index 0000000000..866930cc57 --- /dev/null +++ b/edi_local/views/edi_local_header_views.xml @@ -0,0 +1,23 @@ + + + + + edi.local.header.view.tree + edi.local.header + + + + + + + + + + + Edi local header + edi.local.header + tree,form + + + + diff --git a/edi_local/views/edi_local_line_views.xml b/edi_local/views/edi_local_line_views.xml new file mode 100644 index 0000000000..3d4d332ec1 --- /dev/null +++ b/edi_local/views/edi_local_line_views.xml @@ -0,0 +1,62 @@ + + + + edi.local.line.view.tree + edi.local.line + + + + + + + + + + + + + + + + + + + + edi.local.line.view.form + edi.local.line + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
diff --git a/edi_local/views/edi_local_menus_views.xml b/edi_local/views/edi_local_menus_views.xml new file mode 100644 index 0000000000..caa4608936 --- /dev/null +++ b/edi_local/views/edi_local_menus_views.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/edi_local/views/edi_local_views.xml b/edi_local/views/edi_local_views.xml new file mode 100644 index 0000000000..1c4cbb95fb --- /dev/null +++ b/edi_local/views/edi_local_views.xml @@ -0,0 +1,158 @@ + + + + + edi.local.view.tree + edi.local + + + + + + + + + + + + + + edi.local.view.form + edi.local + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+ + +
+
+
+
+ + + Edi local + edi.local + tree,form + + + +