diff --git a/dms_auto_classification/README.rst b/dms_auto_classification/README.rst new file mode 100644 index 000000000..6df5f15f2 --- /dev/null +++ b/dms_auto_classification/README.rst @@ -0,0 +1,116 @@ +================================ +Auto classify documents into DMS +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4b8df517cdad94a806c89521ecfd60b1505a9bf4f71ac75b281321313237b66e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fdms-lightgray.png?logo=github + :target: https://github.com/OCA/dms/tree/19.0/dms_auto_classification + :alt: OCA/dms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/dms-19-0/dms-19-0-dms_auto_classification + :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/dms&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Automatically classify files within a .zip file to the corresponding DMS +directory(s). + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. Go to Documents / Configuration / Classification Templates and create + a new template. +2. You can set patterns (regex expressions) for filenames and + directories. + +Example of filename pattern to use only .txt files: .txt$ Directory +pattern example 1: Documents Directory pattern example 1: Documents / +Data + +If the pattern doesn't contain path separators (/), the file name will +be search across all the subdirectories. + +Usage +===== + +1. Go to Documents / Auto Classification and select a template and a + .zip file. +2. Press the Analyze button +3. As many lines will be set as the number of files contained in the + .zip file and apply the filename pattern. +4. The full path to the file will be displayed in each detail line. +5. Press the Classify button +6. The files (dms.file) will be created in the corresponding + directories. + +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 +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - Víctor Martínez + - Pedro M. Baeza + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-victoralmau| image:: https://github.com/victoralmau.png?size=40px + :target: https://github.com/victoralmau + :alt: victoralmau + +Current `maintainer `__: + +|maintainer-victoralmau| + +This module is part of the `OCA/dms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/dms_auto_classification/__init__.py b/dms_auto_classification/__init__.py new file mode 100644 index 000000000..e1e144406 --- /dev/null +++ b/dms_auto_classification/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizards diff --git a/dms_auto_classification/__manifest__.py b/dms_auto_classification/__manifest__.py new file mode 100644 index 000000000..a371d58f4 --- /dev/null +++ b/dms_auto_classification/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Auto classify documents into DMS", + "version": "19.0.1.0.0", + "category": "Document Management", + "website": "https://github.com/OCA/dms", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["dms"], + "installable": True, + "data": [ + "security/ir.model.access.csv", + "security/security.xml", + "views/dms_classification_template_views.xml", + "wizards/wizard_dms_classification_views.xml", + ], + "demo": ["demo/dms_classification_template_demo.xml"], + "maintainers": ["victoralmau"], +} diff --git a/dms_auto_classification/demo/dms_classification_template_demo.xml b/dms_auto_classification/demo/dms_classification_template_demo.xml new file mode 100644 index 000000000..93cd98700 --- /dev/null +++ b/dms_auto_classification/demo/dms_classification_template_demo.xml @@ -0,0 +1,11 @@ + + + + Documents template + .txt$ + Documents + + diff --git a/dms_auto_classification/i18n/dms_auto_classification.pot b/dms_auto_classification/i18n/dms_auto_classification.pot new file mode 100644 index 000000000..bf3c7f122 --- /dev/null +++ b/dms_auto_classification/i18n/dms_auto_classification.pot @@ -0,0 +1,207 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * dms_auto_classification +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: dms_auto_classification +#: model:ir.model.fields.selection,name:dms_auto_classification.selection__wizard_dms_classification__state__analyze +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "Analyze" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.actions.act_window,name:dms_auto_classification.action_wizard_dms_classification +#: model:ir.ui.menu,name:dms_auto_classification.menu_wizard_dms_classification +msgid "Auto Classification" +msgstr "" + +#. module: dms_auto_classification +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "Cancel" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.actions.act_window,name:dms_auto_classification.action_dms_classification_template +#: model:ir.ui.menu,name:dms_auto_classification.menu_dms_classification_template +msgid "Classification Templates" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields.selection,name:dms_auto_classification.selection__wizard_dms_classification_detail__state__classified +msgid "Classified" +msgstr "" + +#. module: dms_auto_classification +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "Classify" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__company_id +msgid "Company" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__create_uid +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__create_uid +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__create_uid +msgid "Created by" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__create_date +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__create_date +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__create_date +msgid "Created on" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__data_filename +msgid "Data Filename" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__detail_ids +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "Details" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__directory_id +msgid "Directory" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__directory_pattern +msgid "Directory Pattern" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__display_name +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__display_name +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__display_name +msgid "Display Name" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model,name:dms_auto_classification.model_dms_classification_template +msgid "Dms Classification Template" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields.selection,name:dms_auto_classification.selection__wizard_dms_classification__state__draft +msgid "Draft" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__data_file +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__file_id +msgid "File" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__data_file +msgid "File content" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__file_name +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "File name" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__filename_pattern +msgid "Filename Pattern" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__full_path +msgid "Full path" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__id +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__id +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__id +msgid "ID" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template____last_update +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification____last_update +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail____last_update +msgid "Last Modified on" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__write_uid +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__write_uid +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__write_date +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__write_date +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__write_date +msgid "Last Updated on" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__name +msgid "Name" +msgstr "" + +#. module: dms_auto_classification +#. odoo-python +#: code:addons/dms_auto_classification/wizards/wizard_dms_classification.py:0 +#, python-format +msgid "Only .zip files are allowed" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__parent_id +msgid "Parent" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__sequence +msgid "Sequence" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__state +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__state +msgid "State" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__template_id +msgid "Template" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields.selection,name:dms_auto_classification.selection__wizard_dms_classification_detail__state__to_classify +msgid "To classify" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model,name:dms_auto_classification.model_wizard_dms_classification +msgid "Wizard Dms Classification" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model,name:dms_auto_classification.model_wizard_dms_classification_detail +msgid "Wizard Dms Classification Detail" +msgstr "" diff --git a/dms_auto_classification/i18n/es.po b/dms_auto_classification/i18n/es.po new file mode 100644 index 000000000..73b7f9af0 --- /dev/null +++ b/dms_auto_classification/i18n/es.po @@ -0,0 +1,211 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * dms_auto_classification +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-02-05 08:37+0000\n" +"PO-Revision-Date: 2024-02-05 09:38+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 3.0.1\n" + +#. module: dms_auto_classification +#: model:ir.model.fields.selection,name:dms_auto_classification.selection__wizard_dms_classification__state__analyze +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "Analyze" +msgstr "Analizar" + +#. module: dms_auto_classification +#: model:ir.actions.act_window,name:dms_auto_classification.action_wizard_dms_classification +#: model:ir.ui.menu,name:dms_auto_classification.menu_wizard_dms_classification +msgid "Auto Classification" +msgstr "Auto-clasificación" + +#. module: dms_auto_classification +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "Cancel" +msgstr "Cancelar" + +#. module: dms_auto_classification +#: model:ir.actions.act_window,name:dms_auto_classification.action_dms_classification_template +#: model:ir.ui.menu,name:dms_auto_classification.menu_dms_classification_template +msgid "Classification Templates" +msgstr "Plantillas de clasificación" + +#. module: dms_auto_classification +#: model:ir.model.fields.selection,name:dms_auto_classification.selection__wizard_dms_classification_detail__state__classified +msgid "Classified" +msgstr "Clasificado" + +#. module: dms_auto_classification +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "Classify" +msgstr "Clasificar" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__company_id +msgid "Company" +msgstr "Compañía" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__create_uid +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__create_uid +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__create_date +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__create_date +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__data_filename +msgid "Data Filename" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__detail_ids +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "Details" +msgstr "Detalles" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__directory_id +msgid "Directory" +msgstr "Directorio" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__directory_pattern +msgid "Directory Pattern" +msgstr "Patrón del directorio" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__display_name +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__display_name +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__display_name +msgid "Display Name" +msgstr "Nombre a Mostrar" + +#. module: dms_auto_classification +#: model:ir.model,name:dms_auto_classification.model_dms_classification_template +msgid "Dms Classification Template" +msgstr "Plantilla de clasificación Dms" + +#. module: dms_auto_classification +#: model:ir.model.fields.selection,name:dms_auto_classification.selection__wizard_dms_classification__state__draft +msgid "Draft" +msgstr "Borrador" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__data_file +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__file_id +msgid "File" +msgstr "Archivo" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__data_file +msgid "File content" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__file_name +#: model_terms:ir.ui.view,arch_db:dms_auto_classification.view_wizard_dms_classification_form +msgid "File name" +msgstr "Nombre de archivo" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__filename_pattern +msgid "Filename Pattern" +msgstr "Patrón de nombre de archivo" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__full_path +msgid "Full path" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__id +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__id +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__id +msgid "ID" +msgstr "ID" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template____last_update +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification____last_update +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__write_uid +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__write_uid +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__write_date +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__write_date +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__write_date +msgid "Last Updated on" +msgstr "Ultima actualización el" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__name +msgid "Name" +msgstr "Nombrr" + +#. module: dms_auto_classification +#. odoo-python +#: code:addons/dms_auto_classification/wizards/wizard_dms_classification.py:0 +#, python-format +msgid "Only .zip files are allowed" +msgstr "Solo se permiten archivos .zip" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__parent_id +msgid "Parent" +msgstr "Padre" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_dms_classification_template__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__state +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification_detail__state +msgid "State" +msgstr "Estado" + +#. module: dms_auto_classification +#: model:ir.model.fields,field_description:dms_auto_classification.field_wizard_dms_classification__template_id +msgid "Template" +msgstr "Plantilla" + +#. module: dms_auto_classification +#: model:ir.model.fields.selection,name:dms_auto_classification.selection__wizard_dms_classification_detail__state__to_classify +msgid "To classify" +msgstr "A clasificar" + +#. module: dms_auto_classification +#: model:ir.model,name:dms_auto_classification.model_wizard_dms_classification +msgid "Wizard Dms Classification" +msgstr "" + +#. module: dms_auto_classification +#: model:ir.model,name:dms_auto_classification.model_wizard_dms_classification_detail +msgid "Wizard Dms Classification Detail" +msgstr "" diff --git a/dms_auto_classification/models/__init__.py b/dms_auto_classification/models/__init__.py new file mode 100644 index 000000000..cb95092bf --- /dev/null +++ b/dms_auto_classification/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import dms_classification_template diff --git a/dms_auto_classification/models/dms_classification_template.py b/dms_auto_classification/models/dms_classification_template.py new file mode 100644 index 000000000..23ff91f03 --- /dev/null +++ b/dms_auto_classification/models/dms_classification_template.py @@ -0,0 +1,21 @@ +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class DmsClassificationTemplate(models.Model): + _name = "dms.classification.template" + _order = "sequence,name,id" + _description = "Dms Classification Template" + + name = fields.Char(required=True) + sequence = fields.Integer(required=True, default=10) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + store=True, + index=True, + ) + filename_pattern = fields.Char() + directory_pattern = fields.Char() diff --git a/dms_auto_classification/pyproject.toml b/dms_auto_classification/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/dms_auto_classification/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/dms_auto_classification/readme/CONFIGURE.md b/dms_auto_classification/readme/CONFIGURE.md new file mode 100644 index 000000000..a2f733cf8 --- /dev/null +++ b/dms_auto_classification/readme/CONFIGURE.md @@ -0,0 +1,11 @@ +1. Go to Documents / Configuration / Classification Templates and + create a new template. +2. You can set patterns (regex expressions) for filenames and + directories. + +Example of filename pattern to use only .txt files: .txt\$ Directory +pattern example 1: Documents Directory pattern example 1: Documents / +Data + +If the pattern doesn't contain path separators (/), the file name will +be search across all the subdirectories. diff --git a/dms_auto_classification/readme/CONTRIBUTORS.md b/dms_auto_classification/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..5fee39042 --- /dev/null +++ b/dms_auto_classification/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Tecnativa](https://www.tecnativa.com): + - Víctor Martínez + - Pedro M. Baeza diff --git a/dms_auto_classification/readme/DESCRIPTION.md b/dms_auto_classification/readme/DESCRIPTION.md new file mode 100644 index 000000000..2a8faa89d --- /dev/null +++ b/dms_auto_classification/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +Automatically classify files within a .zip file to the corresponding DMS +directory(s). diff --git a/dms_auto_classification/readme/USAGE.md b/dms_auto_classification/readme/USAGE.md new file mode 100644 index 000000000..25514718d --- /dev/null +++ b/dms_auto_classification/readme/USAGE.md @@ -0,0 +1,9 @@ +1. Go to Documents / Auto Classification and select a template and a + .zip file. +2. Press the Analyze button +3. As many lines will be set as the number of files contained in the + .zip file and apply the filename pattern. +4. The full path to the file will be displayed in each detail line. +5. Press the Classify button +6. The files (dms.file) will be created in the corresponding + directories. diff --git a/dms_auto_classification/security/ir.model.access.csv b/dms_auto_classification/security/ir.model.access.csv new file mode 100644 index 000000000..23e9e5a83 --- /dev/null +++ b/dms_auto_classification/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_dms_classification_template_user,dms_classification_template_user,model_dms_classification_template,dms.group_dms_user,1,0,0,0 +access_dms_classification_template_manager,dms_classification_template_manager,model_dms_classification_template,dms.group_dms_manager,1,1,1,1 +access_wizard_dms_classification_manager,wizard_dms_classification_manager,model_wizard_dms_classification,dms.group_dms_user,1,1,1,1 +access_wizard_dms_classification_detail_user,wizard_dms_classification_detail_user,model_wizard_dms_classification_detail,dms.group_dms_user,1,1,1,0 +access_wizard_dms_classification_detail_manager,wizard_dms_classification_detail_manager,model_wizard_dms_classification_detail,dms.group_dms_user,1,1,1,1 diff --git a/dms_auto_classification/security/security.xml b/dms_auto_classification/security/security.xml new file mode 100644 index 000000000..a8332fbdd --- /dev/null +++ b/dms_auto_classification/security/security.xml @@ -0,0 +1,9 @@ + + + + DMS Classification Template multi-company + + + [('company_id', 'in', [False] + company_ids)] + + diff --git a/dms_auto_classification/static/description/icon.png b/dms_auto_classification/static/description/icon.png new file mode 100644 index 000000000..9fd5b327a Binary files /dev/null and b/dms_auto_classification/static/description/icon.png differ diff --git a/dms_auto_classification/static/description/icon.svg b/dms_auto_classification/static/description/icon.svg new file mode 100644 index 000000000..38e821a45 --- /dev/null +++ b/dms_auto_classification/static/description/icon.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dms_auto_classification/static/description/index.html b/dms_auto_classification/static/description/index.html new file mode 100644 index 000000000..05cbfb4c4 --- /dev/null +++ b/dms_auto_classification/static/description/index.html @@ -0,0 +1,460 @@ + + + + + +Auto classify documents into DMS + + + +
+

Auto classify documents into DMS

+ + +

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

+

Automatically classify files within a .zip file to the corresponding DMS +directory(s).

+

Table of contents

+ +
+

Configuration

+
    +
  1. Go to Documents / Configuration / Classification Templates and create +a new template.
  2. +
  3. You can set patterns (regex expressions) for filenames and +directories.
  4. +
+

Example of filename pattern to use only .txt files: .txt$ Directory +pattern example 1: Documents Directory pattern example 1: Documents / +Data

+

If the pattern doesn’t contain path separators (/), the file name will +be search across all the subdirectories.

+
+
+

Usage

+
    +
  1. Go to Documents / Auto Classification and select a template and a +.zip file.
  2. +
  3. Press the Analyze button
  4. +
  5. As many lines will be set as the number of files contained in the +.zip file and apply the filename pattern.
  6. +
  7. The full path to the file will be displayed in each detail line.
  8. +
  9. Press the Classify button
  10. +
  11. The files (dms.file) will be created in the corresponding +directories.
  12. +
+
+
+

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

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Víctor Martínez
    • +
    • Pedro M. Baeza
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

victoralmau

+

This module is part of the OCA/dms project on GitHub.

+

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

+
+
+
+ + diff --git a/dms_auto_classification/tests/__init__.py b/dms_auto_classification/tests/__init__.py new file mode 100644 index 000000000..7ad1c277a --- /dev/null +++ b/dms_auto_classification/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from . import test_dms_auto_classification diff --git a/dms_auto_classification/tests/data/test.zip b/dms_auto_classification/tests/data/test.zip new file mode 100644 index 000000000..78f317a00 Binary files /dev/null and b/dms_auto_classification/tests/data/test.zip differ diff --git a/dms_auto_classification/tests/test_dms_auto_classification.py b/dms_auto_classification/tests/test_dms_auto_classification.py new file mode 100644 index 000000000..7b1ac3daf --- /dev/null +++ b/dms_auto_classification/tests/test_dms_auto_classification.py @@ -0,0 +1,206 @@ +# Copyright 2024-2025 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from base64 import b64encode +from os import path + +from odoo import Command +from odoo.tests import Form, new_test_user +from odoo.tests.common import users + +from odoo.addons.base.tests.common import BaseCommon + + +class TestDmsAutoClassification(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create fixtures directly — OCA CI runs without demo data. + cls.storage = cls.env["dms.storage"].create( + {"name": "Test Storage", "save_type": "database"} + ) + cls.access_group = cls.env["dms.access.group"].create( + { + "name": "Test access group", + "perm_create": True, + "perm_write": True, + } + ) + cls.directory = cls.env["dms.directory"].create( + { + "name": "Documents", + "is_root_directory": True, + "storage_id": cls.storage.id, + "group_ids": [Command.link(cls.access_group.id)], + } + ) + cls.template = cls.env["dms.classification.template"].create( + { + "name": "Documents template", + "filename_pattern": ".txt$", + "directory_pattern": "Documents", + } + ) + cls.user = new_test_user( + cls.env, login="test_dms_user", groups="dms.group_dms_user" + ) + cls.access_group.explicit_user_ids = [Command.link(cls.user.id)] + cls.wizard = cls._create_wizard_dms_classification(cls.template) + cls.extra_wizard = cls._create_wizard_dms_classification(cls.template) + + @classmethod + def _data_file(cls, filename, encoding=None): + mode = "rt" if encoding else "rb" + with open(path.join(path.dirname(__file__), filename), mode) as file: + data = file.read() + if encoding: + data = data.encode(encoding) + return b64encode(data) + + @classmethod + def _create_wizard_dms_classification(cls, template): + wizard_form = Form(cls.env["wizard.dms.classification"]) + wizard_form.template_id = template + wizard_form.data_file = cls._data_file("data/test.zip") + return wizard_form.save() + + @users("test_dms_user") + def test_wizard_dms_clasification_process_01(self): + self.wizard = self.wizard.with_user(self.env.user) + self.assertEqual(self.wizard.state, "draft") + # Wizard - Analyze process + self.wizard.action_analyze() + self.assertEqual(self.wizard.state, "analyze") + self.assertEqual(len(self.wizard.detail_ids), 2) + full_paths = self.wizard.mapped("detail_ids.full_path") + self.assertIn("test/file-1.txt", full_paths) + self.assertIn("test/file-2.txt", full_paths) + file_names = self.wizard.mapped("detail_ids.file_name") + self.assertIn("file-1.txt", file_names) + self.assertIn("file-2.txt", file_names) + self.assertIn(self.directory, self.wizard.mapped("detail_ids.directory_id")) + detail_1 = self.wizard.detail_ids.filtered( + lambda x: x.file_name == "file-1.txt" + ) + self.assertEqual(detail_1.state, "to_classify") + detail_2 = self.wizard.detail_ids.filtered( + lambda x: x.file_name == "file-2.txt" + ) + self.assertEqual(detail_2.state, "to_classify") + # Wizard - Classify process + res = self.wizard.action_classify() + dms_files = self.env[res["res_model"]].search(res["domain"]) + self.assertEqual(len(dms_files), 2) + self.assertEqual(detail_1.state, "classified") + self.assertEqual(detail_1.file_id.directory_id, self.directory) + self.assertEqual(detail_1.file_id.name, "file-1.txt") + self.assertTrue(detail_1.file_id.content) + self.assertEqual(detail_2.state, "classified") + self.assertEqual(detail_2.file_id.directory_id, self.directory) + self.assertEqual(detail_2.file_id.name, "file-2.txt") + self.assertTrue(detail_2.file_id.content) + # Extra wizard + self.assertEqual(self.extra_wizard.state, "draft") + # New Wizard - Analyze process + self.extra_wizard = self.extra_wizard.with_user(self.env.user) + self.extra_wizard.action_analyze() + self.assertEqual(self.extra_wizard.state, "analyze") + self.assertEqual(len(self.extra_wizard.detail_ids), 2) + file_names = self.extra_wizard.mapped("detail_ids.file_name") + self.assertIn("file-1.txt", file_names) + self.assertIn("file-2.txt", file_names) + self.assertIn( + self.directory, self.extra_wizard.mapped("detail_ids.directory_id") + ) + detail_1 = self.extra_wizard.detail_ids.filtered( + lambda x: x.file_name == "file-1.txt" + ) + self.assertTrue(detail_1.file_id) + self.assertEqual(detail_1.state, "classified") + detail_2 = self.extra_wizard.detail_ids.filtered( + lambda x: x.file_name == "file-2.txt" + ) + self.assertTrue(detail_2.file_id) + self.assertEqual(detail_1.state, "classified") + # New Wizard - Classify process + res = self.extra_wizard.action_classify() + dms_files = self.env[res["res_model"]].search(res["domain"]) + self.assertEqual(len(dms_files), 2) + + def test_wizard_dms_clasification_process_filename_pattern_01(self): + self.template.filename_pattern = ".pdf$" + self.assertEqual(self.wizard.state, "draft") + self.wizard.action_analyze() + self.assertEqual(self.wizard.state, "analyze") + self.assertEqual(len(self.wizard.detail_ids), 0) + + def test_wizard_dms_clasification_process_filename_pattern_02(self): + self.template.filename_pattern = "test2/.txt$" + self.assertEqual(self.wizard.state, "draft") + self.wizard.action_analyze() + self.assertEqual(self.wizard.state, "analyze") + self.assertEqual(len(self.wizard.detail_ids), 0) + + def test_wizard_dms_clasification_process_filename_pattern_03(self): + self.template.filename_pattern = "test.*.txt$" + self.assertEqual(self.wizard.state, "draft") + self.wizard.action_analyze() + self.assertEqual(self.wizard.state, "analyze") + self.assertEqual(len(self.wizard.detail_ids), 2) + full_paths = self.wizard.mapped("detail_ids.full_path") + self.assertIn("test/file-1.txt", full_paths) + self.assertIn("test/file-2.txt", full_paths) + file_names = self.wizard.mapped("detail_ids.file_name") + self.assertIn("file-1.txt", file_names) + self.assertIn("file-2.txt", file_names) + + def test_wizard_dms_clasification_process_filename_pattern_04(self): + self.template.filename_pattern = "test1.*.txt$" + self.assertEqual(self.wizard.state, "draft") + self.wizard.action_analyze() + self.assertEqual(self.wizard.state, "analyze") + self.assertEqual(len(self.wizard.detail_ids), 0) + + def test_wizard_dms_clasification_process_filename_pattern_05(self): + self.template.filename_pattern = "test.*.file-1.txt$" + self.assertEqual(self.wizard.state, "draft") + self.wizard.action_analyze() + self.assertEqual(self.wizard.state, "analyze") + self.assertEqual(len(self.wizard.detail_ids), 1) + full_paths = self.wizard.mapped("detail_ids.full_path") + self.assertIn("test/file-1.txt", full_paths) + self.assertNotIn("test/file-2.txt", full_paths) + file_names = self.wizard.mapped("detail_ids.file_name") + self.assertIn("file-1.txt", file_names) + self.assertNotIn("file-2.txt", file_names) + + @users("test_dms_user") + def test_wizard_dms_clasification_process_directory_pattern(self): + self.template.directory_pattern = "Documents2" + self.assertEqual(self.wizard.state, "draft") + # Analyze process + self.wizard = self.wizard.with_user(self.env.user) + self.wizard.action_analyze() + self.assertEqual(self.wizard.state, "analyze") + self.assertEqual(len(self.wizard.detail_ids), 2) + full_paths = self.wizard.mapped("detail_ids.full_path") + self.assertIn("test/file-1.txt", full_paths) + self.assertIn("test/file-2.txt", full_paths) + file_names = self.wizard.mapped("detail_ids.file_name") + self.assertIn("file-1.txt", file_names) + self.assertIn("file-2.txt", file_names) + self.assertNotIn(self.directory, self.wizard.mapped("detail_ids.directory_id")) + detail_1 = self.wizard.detail_ids.filtered( + lambda x: x.file_name == "file-1.txt" + ) + self.assertEqual(detail_1.state, "to_classify") + detail_2 = self.wizard.detail_ids.filtered( + lambda x: x.file_name == "file-2.txt" + ) + self.assertEqual(detail_2.state, "to_classify") + # Classify process + res = self.wizard.action_classify() + dms_files = self.env[res["res_model"]].search(res["domain"]) + self.assertEqual(len(dms_files), 0) + self.assertFalse(detail_1.file_id) + self.assertFalse(detail_2.file_id) diff --git a/dms_auto_classification/views/dms_classification_template_views.xml b/dms_auto_classification/views/dms_classification_template_views.xml new file mode 100644 index 000000000..28c250f31 --- /dev/null +++ b/dms_auto_classification/views/dms_classification_template_views.xml @@ -0,0 +1,50 @@ + + + + + dms.classification.template + + + + + + + + + + + + dms.classification.template + +
+ +
+

+ +

+
+ + + + + +
+
+
+
+ + Classification Templates + dms.classification.template + list,form + + + +
diff --git a/dms_auto_classification/wizards/__init__.py b/dms_auto_classification/wizards/__init__.py new file mode 100644 index 000000000..4ba9cc23a --- /dev/null +++ b/dms_auto_classification/wizards/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import wizard_dms_classification diff --git a/dms_auto_classification/wizards/wizard_dms_classification.py b/dms_auto_classification/wizards/wizard_dms_classification.py new file mode 100644 index 000000000..eaab6b38d --- /dev/null +++ b/dms_auto_classification/wizards/wizard_dms_classification.py @@ -0,0 +1,216 @@ +# Copyright 2024-2026 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import base64 +import re +import zipfile +from io import BytesIO + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class WizardDmsClassification(models.TransientModel): + _name = "wizard.dms.classification" + _description = "Wizard Dms Classification" + + company_id = fields.Many2one( + comodel_name="res.company", + required=True, + default=lambda self: self.env.company, + string="Company", + ) + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("analyze", "Analyze"), + ], + default="draft", + ) + template_id = fields.Many2one( + comodel_name="dms.classification.template", + string="Template", + required=True, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", + ) + data_file = fields.Binary( + string="File", + required=True, + ) + data_filename = fields.Char() + detail_ids = fields.One2many( + comodel_name="wizard.dms.classification.detail", + inverse_name="parent_id", + string="Details", + ) + + def _is_zipfile(self): + try: + zipfile.ZipFile(BytesIO(base64.b64decode(self.data_file))) + return True + except (zipfile.BadZipFile, ValueError): + return False + + @api.onchange("data_file") + def _onchange_data_file(self): + for item in self.filtered("data_file"): + if not item._is_zipfile(): + raise UserError(self.env._("Only .zip files are allowed")) + + def _return_item(self): + return { + "context": self.env.context, + "view_type": "form", + "view_mode": "form", + "res_model": self._name, + "res_id": self.id, + "view_id": False, + "type": "ir.actions.act_window", + "target": "new", + } + + def _get_directory_from_pattern(self, pattern, directories): + directory = False + for d in directories: + if re.search(pattern, d.complete_name): + directory = d + break + return directory + + def action_analyze(self): + """Process the zip file and generate details.""" + details = self._prepare_details_vals() + self.state = "analyze" + self.detail_ids = [(0, 0, vals) for vals in details] + return self._return_item() + + def _prepare_details_vals(self): + """Method that gets the files from .zip and if it apply the filename pattern + it will set it as detail with the corresponding values.""" + details = [] + zip_file = zipfile.ZipFile(BytesIO(base64.b64decode(self.data_file))) + filename_pattern = self.template_id.filename_pattern + for zip_info in zip_file.infolist(): + if zip_info.is_dir(): + continue + filename = zip_info.filename + if re.search(filename_pattern, filename): + file_content = zip_file.read(filename) + data_file = base64.b64encode(file_content) + details.append(self._prepare_detail_vals(filename, data_file)) + return details + + def _prepare_detail_vals(self, full_path, data_file): + """Method to set the values of each detail. May be extended by other modules. + Clean full_path (remove / from folders).""" + return { + "full_path": full_path, + "data_file": data_file, + } + + def _action_classify(self): + """Create the files (dms.file) in the corresponding directory. + Details that do not have a directory or already have a linked + file are skipped.""" + for detail in self.detail_ids.filtered( + lambda x: x.state == "to_classify" and x.directory_id + ): + detail._create_dms_file() + + def action_classify(self): + self._action_classify() + action = self.env["ir.actions.act_window"]._for_xml_id("dms.action_dms_file") + action["view_mode"] = "list" + action["views"] = [(False, "list")] + action["domain"] = [("id", "in", self.mapped("detail_ids.file_id").ids)] + return action + + +class WizardDmsClassificationDetail(models.TransientModel): + _name = "wizard.dms.classification.detail" + _description = "Wizard Dms Classification Detail" + + parent_id = fields.Many2one( + comodel_name="wizard.dms.classification", + string="Parent", + ) + full_path = fields.Char( + string="Full path", + required=True, + readonly=True, + ) + file_name = fields.Char( + compute="_compute_file_name", + store=True, + string="File name", + ) + data_file = fields.Binary( + string="File content", + required=True, + ) + directory_id = fields.Many2one( + comodel_name="dms.directory", + string="Directory", + compute="_compute_directory_id", + store=True, + readonly=False, + ) + file_id = fields.Many2one( + comodel_name="dms.file", + string="File", + compute="_compute_file_id", + store=True, + readonly=True, + ) + state = fields.Selection( + selection=[ + ("to_classify", "To classify"), + ("classified", "Classified"), + ], + compute="_compute_state", + store=True, + readonly=True, + ) + + @api.depends("full_path") + def _compute_file_name(self): + """File_name field is used to set file_id.""" + for item in self: + name = item.full_path + if "/" in name: + name = name.split("/")[-1] + item.file_name = name + + @api.depends("file_name") + def _compute_directory_id(self): + domain = [("company_id", "=", self.parent_id.company_id.id)] + directories = self.env["dms.directory"].search(domain) + for item in self: + item.directory_id = self.parent_id._get_directory_from_pattern( + self.parent_id.template_id.directory_pattern, directories + ) + + @api.depends("file_name", "directory_id") + def _compute_file_id(self): + for item in self.filtered(lambda x: x.file_name and x.directory_id): + files = item.directory_id.file_ids.filtered( + lambda x, item=item: x.name == item.file_name + ) + item.file_id = files[:1] + + @api.depends("file_id") + def _compute_state(self): + items_with_file = self.filtered("file_id") + items_with_file.state = "classified" + (self - items_with_file).state = "to_classify" + + def _create_dms_file(self): + self.ensure_one() + if not self.directory_id or self.file_id: + return + self.file_id = self.env["dms.file"].create( + { + "name": self.file_name, + "directory_id": self.directory_id.id, + "content": self.data_file, + } + ) diff --git a/dms_auto_classification/wizards/wizard_dms_classification_views.xml b/dms_auto_classification/wizards/wizard_dms_classification_views.xml new file mode 100644 index 000000000..b25123d26 --- /dev/null +++ b/dms_auto_classification/wizards/wizard_dms_classification_views.xml @@ -0,0 +1,69 @@ + + + + + wizard.dms.classification + +
+
+ +
+ + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + Auto Classification + wizard.dms.classification + form + new + + + +