diff --git a/project_key/README.rst b/project_key/README.rst new file mode 100644 index 0000000000..3b4f420f5a --- /dev/null +++ b/project_key/README.rst @@ -0,0 +1,118 @@ +=========== +Project key +=========== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproject-lightgray.png?logo=github + :target: https://github.com/OCA/project/tree/15.0/project_key + :alt: OCA/project +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/project-14-0/project-14-0-project_key + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/140/15.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides functionality to uniquely identify projects and tasks by simple ``key`` field. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module functionality you just need to: + +On ``project.project`` level: + +In Kanban View: + +#. Go to Project > Dashboard +#. Create +#. Enter project name and use auto generated key or simply override value by entering your own key value. + +In Tree View: + +#. Go to Project > Configuration > Projects +#. Create +#. Enter project name and use auto generated key or simply override value by entering your own key value. + +In form View: + +#. Go to Project > Dashboard +#. Open the projects settings +#. Modify the "key" value +#. After modifying project key the key of any existing tasks related to that project will be updated automatically. + +When you create a project, under the hood a ir.sequence record gets creted with prefix: ``-``. + +On ``project.task`` level: + +#. Actually there is nothing to be done here +#. Task keys are auto generated based on project key value with per project auto incremented number (i.e. PA-1, PA-2, etc) + +In browser address bar: + +#. Navigate to your project by entering following url: http://<>/projects/PROJECT-KEY +#. Navigate to your task by entering following url: http://<>/tasks/TASK-KEY + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Modoolar + +Contributors +~~~~~~~~~~~~ + +* Petar Najman +* Sladjan Kantar +* `CorporateHub `__ + + * Alexey Pelykh + +* Saran Lim. +* Tharathip Chaweewongphan + +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/project `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/project_key/__init__.py b/project_key/__init__.py new file mode 100644 index 0000000000..2474bef694 --- /dev/null +++ b/project_key/__init__.py @@ -0,0 +1,5 @@ +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from . import models +from . import controllers +from .hooks import post_init_hook diff --git a/project_key/__manifest__.py b/project_key/__manifest__.py new file mode 100644 index 0000000000..2c4486d340 --- /dev/null +++ b/project_key/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +{ + "name": "Project key", + "summary": "Module decorates projects and tasks with Project Key", + "category": "Project", + "version": "15.0.1.0.0", + "license": "LGPL-3", + "author": "Modoolar, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/project", + "depends": ["project"], + "data": ["views/project_key_views.xml"], + "post_init_hook": "post_init_hook", +} diff --git a/project_key/controllers/__init__.py b/project_key/controllers/__init__.py new file mode 100644 index 0000000000..4e80e131bf --- /dev/null +++ b/project_key/controllers/__init__.py @@ -0,0 +1,3 @@ +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from . import main diff --git a/project_key/controllers/main.py b/project_key/controllers/main.py new file mode 100644 index 0000000000..e70d1f1e9c --- /dev/null +++ b/project_key/controllers/main.py @@ -0,0 +1,41 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +import werkzeug + +from odoo import http + +# from odoo.http import request + + +class ProjectBrowser(http.Controller): + def get_record_url(self, model, domain, action_xml_id): + env = http.request.env() + + records = env[model].search(domain) + record_id = records and records.id or -1 + action_id = env.ref(action_xml_id).id + + return "/web#id={}&view_type=form&model={}&action={}".format( + record_id, model, action_id + ) + + def get_task_url(self, key): + return self.get_record_url( + "project.task", [("key", "=ilike", key)], "project.action_view_task" + ) + + def get_project_url(self, key): + return self.get_record_url( + "project.project", + [("key", "=ilike", key)], + "project.open_view_project_all_config", + ) + + @http.route(["/projects/"], type="http", auth="user") + def open_project(self, key, **kwargs): + return werkzeug.utils.redirect(self.get_project_url(key), 301) + + @http.route(["/tasks/"], type="http", auth="user") + def open_task(self, key, **kwargs): + return werkzeug.utils.redirect(self.get_task_url(key), 301) diff --git a/project_key/hooks.py b/project_key/hooks.py new file mode 100644 index 0000000000..a3f5141551 --- /dev/null +++ b/project_key/hooks.py @@ -0,0 +1,9 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + + +def post_init_hook(cr, registry): + from odoo import SUPERUSER_ID, api + + env = api.Environment(cr, SUPERUSER_ID, {}) + env["project.project"]._set_default_project_key() diff --git a/project_key/i18n/de.po b/project_key/i18n/de.po new file mode 100644 index 0000000000..4d866bdfe2 --- /dev/null +++ b/project_key/i18n/de.po @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2019-07-12 15:43+0000\n" +"Last-Translator: Maria Sparenberg \n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 3.7.1\n" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__key +msgid "Key" +msgstr "Nummerierungsmuster" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__task_key_sequence_id +msgid "Key Sequence" +msgstr "Musterfolge" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_project +msgid "Project" +msgstr "Projekt" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_project_project_key_unique +msgid "Project key must be unique" +msgstr "Das Nummerierungsmuster für Projekte muss eindeutig sein." + +#. module: project_key +#: code:addons/project_key/models/project_project.py:0 +#, python-format +msgid "Project task sequence for project " +msgstr "Aufgabennummerierung für Projekt " + +#. module: project_key +#: model:ir.model,name:project_key.model_project_task +msgid "Task" +msgstr "Aufgabe" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_task_task_key_unique +msgid "Task key must be unique!" +msgstr "Aufgabennummerierung muss eindeutig sein!" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__url +msgid "URL" +msgstr "URL" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__key +msgid "key" +msgstr "Nummer" diff --git a/project_key/i18n/es_AR.po b/project_key/i18n/es_AR.po new file mode 100644 index 0000000000..706440e568 --- /dev/null +++ b/project_key/i18n/es_AR.po @@ -0,0 +1,81 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2021-04-01 03:48+0000\n" +"Last-Translator: Ignacio Buioli \n" +"Language-Team: none\n" +"Language: es_AR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.3.2\n" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__display_name +#: model:ir.model.fields,field_description:project_key.field_project_task__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__id +#: model:ir.model.fields,field_description:project_key.field_project_task__id +msgid "ID" +msgstr "ID" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__key +msgid "Key" +msgstr "Clave" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__task_key_sequence_id +msgid "Key Sequence" +msgstr "Secuencia de la Clave" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project____last_update +#: model:ir.model.fields,field_description:project_key.field_project_task____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_project +msgid "Project" +msgstr "Proyecto" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_project_project_key_unique +msgid "Project key must be unique" +msgstr "La clave del proyecto debe ser única" + +#. module: project_key +#: code:addons/project_key/models/project_project.py:0 +#, python-format +msgid "Project task sequence for project" +msgstr "Secuencia de tareas del proyecto para el proyecto" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_task +msgid "Task" +msgstr "Tarea" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_task_task_key_unique +msgid "Task key must be unique!" +msgstr "¡La clave de la tarea debe ser única!" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__url +msgid "URL" +msgstr "URL" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__key +msgid "key" +msgstr "clave" diff --git a/project_key/i18n/fr.po b/project_key/i18n/fr.po new file mode 100644 index 0000000000..2959bef693 --- /dev/null +++ b/project_key/i18n/fr.po @@ -0,0 +1,66 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-18 15:37+0000\n" +"PO-Revision-Date: 2023-01-18 16:37+0100\n" +"Last-Translator: Yves Le Doeuff \n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Poedit 3.0.1\n" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__key +#: model:ir.model.fields,field_description:project_key.field_project_task__key +msgid "Key" +msgstr "Clé" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__task_key_sequence_id +msgid "Key Sequence" +msgstr "Séquence de clé" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_project +msgid "Project" +msgstr "Projet" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_project_project_key_unique +msgid "Project key must be unique" +msgstr "La clé de projet doit être unique" + +#. module: project_key +#: code:addons/project_key/models/project_project.py:0 +#, python-format +msgid "Project task sequence for project" +msgstr "Séquence des tâches pour le projet" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_task +msgid "Task" +msgstr "Tâche" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_task_task_key_unique +msgid "Task key must be unique!" +msgstr "La clé de tâche doit être unique !" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__url +msgid "URL" +msgstr "URL" + +#~ msgid "key" +#~ msgstr "Clé" + +#~ msgid "Display Name" +#~ msgstr "Nom affiché" diff --git a/project_key/i18n/fr_FR.po b/project_key/i18n/fr_FR.po new file mode 100644 index 0000000000..66564f2977 --- /dev/null +++ b/project_key/i18n/fr_FR.po @@ -0,0 +1,81 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2021-04-11 16:46+0000\n" +"Last-Translator: Yves Le Doeuff \n" +"Language-Team: none\n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.3.2\n" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__display_name +#: model:ir.model.fields,field_description:project_key.field_project_task__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__id +#: model:ir.model.fields,field_description:project_key.field_project_task__id +msgid "ID" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__key +msgid "Key" +msgstr "Clé" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__task_key_sequence_id +msgid "Key Sequence" +msgstr "Séquence de clé" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project____last_update +#: model:ir.model.fields,field_description:project_key.field_project_task____last_update +msgid "Last Modified on" +msgstr "" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_project +msgid "Project" +msgstr "Projet" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_project_project_key_unique +msgid "Project key must be unique" +msgstr "La clé de projet doit être unique" + +#. module: project_key +#: code:addons/project_key/models/project_project.py:0 +#, python-format +msgid "Project task sequence for project" +msgstr "Séquence des tâches pour le projet" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_task +msgid "Task" +msgstr "Tâche" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_task_task_key_unique +msgid "Task key must be unique!" +msgstr "La clé de tâche doit être unique" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__url +msgid "URL" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__key +msgid "key" +msgstr "Clé" diff --git a/project_key/i18n/it.po b/project_key/i18n/it.po new file mode 100644 index 0000000000..e2629fd24c --- /dev/null +++ b/project_key/i18n/it.po @@ -0,0 +1,81 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2022-10-19 14:43+0000\n" +"Last-Translator: Sergio Zanchetta \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14.1\n" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__display_name +#: model:ir.model.fields,field_description:project_key.field_project_task__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__id +#: model:ir.model.fields,field_description:project_key.field_project_task__id +msgid "ID" +msgstr "ID" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__key +msgid "Key" +msgstr "Chiave" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__task_key_sequence_id +msgid "Key Sequence" +msgstr "Sequenza chiave" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project____last_update +#: model:ir.model.fields,field_description:project_key.field_project_task____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_project +msgid "Project" +msgstr "Progetto" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_project_project_key_unique +msgid "Project key must be unique" +msgstr "La chiave del progetto deve essere univoca" + +#. module: project_key +#: code:addons/project_key/models/project_project.py:0 +#, python-format +msgid "Project task sequence for project" +msgstr "" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_task +msgid "Task" +msgstr "Lavoro" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_task_task_key_unique +msgid "Task key must be unique!" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__url +msgid "URL" +msgstr "URL" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__key +msgid "key" +msgstr "chiave" diff --git a/project_key/i18n/project_key.pot b/project_key/i18n/project_key.pot new file mode 100644 index 0000000000..50c0a9e976 --- /dev/null +++ b/project_key/i18n/project_key.pot @@ -0,0 +1,78 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.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: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__display_name +#: model:ir.model.fields,field_description:project_key.field_project_task__display_name +msgid "Display Name" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__id +#: model:ir.model.fields,field_description:project_key.field_project_task__id +msgid "ID" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__key +msgid "Key" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project__task_key_sequence_id +msgid "Key Sequence" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_project____last_update +#: model:ir.model.fields,field_description:project_key.field_project_task____last_update +msgid "Last Modified on" +msgstr "" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_project +msgid "Project" +msgstr "" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_project_project_key_unique +msgid "Project key must be unique" +msgstr "" + +#. module: project_key +#: code:addons/project_key/models/project_project.py:0 +#, python-format +msgid "Project task sequence for project" +msgstr "" + +#. module: project_key +#: model:ir.model,name:project_key.model_project_task +msgid "Task" +msgstr "" + +#. module: project_key +#: model:ir.model.constraint,message:project_key.constraint_project_task_task_key_unique +msgid "Task key must be unique!" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__url +msgid "URL" +msgstr "" + +#. module: project_key +#: model:ir.model.fields,field_description:project_key.field_project_task__key +msgid "key" +msgstr "" diff --git a/project_key/models/__init__.py b/project_key/models/__init__.py new file mode 100644 index 0000000000..443c6f46cb --- /dev/null +++ b/project_key/models/__init__.py @@ -0,0 +1,4 @@ +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from . import project_project +from . import project_task diff --git a/project_key/models/project_project.py b/project_key/models/project_project.py new file mode 100644 index 0000000000..49a4a004bf --- /dev/null +++ b/project_key/models/project_project.py @@ -0,0 +1,223 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from odoo import _, api, fields, models +from odoo.osv import expression +from odoo.tools import config + + +class Project(models.Model): + _inherit = "project.project" + + task_key_sequence_id = fields.Many2one( + comodel_name="ir.sequence", string="Key Sequence", ondelete="restrict" + ) + + key = fields.Char(size=10, required=False, index=True, copy=False) + + _sql_constraints = [ + ("project_key_unique", "UNIQUE(key)", "Project key must be unique") + ] + + @api.onchange("name") + def _onchange_project_name(self): + for rec in self: + if rec.key: + continue + + if rec.name: + rec.key = self.generate_project_key(rec.name) + else: + rec.key = "" + + @api.model + def create(self, vals): + if "key" not in vals: + vals["key"] = self.generate_project_key(vals["name"]) + + # Tasks must be created after the project. + if "task_ids" in vals: + task_vals = vals.pop("task_ids") + else: + task_vals = [] + + # The key sequences to create stories and tasks with keys, created with + # a project, must be linked to the project company to avoid security + # issues. + # Propagate the company ID, using the context key, to fill the + # sequences company. + company_id = vals.get("company_id") + if company_id: + self = self.with_context(project_sequence_company=company_id) + + new_project = super(Project, self).create(vals) + new_project.create_sequence() + + # Tasks must be created after the project. + if task_vals: + new_project.write({"task_ids": task_vals}) + + return new_project + + def write(self, values): + update_key = False + + if "key" in values: + key = values["key"] + update_key = self.key != key + + res = super(Project, self).write(values) + + if update_key: + # Here we don't expect to have more than one record + # because we can not have multiple projects with the same KEY. + self.update_sequence() + self._update_task_keys() + + return res + + def unlink(self): + for project in self: + sequence = project.task_key_sequence_id + project.task_key_sequence_id = False + sequence.sudo().unlink() + return super(Project, self).unlink() + + @api.model + def name_search(self, name, args=None, operator="ilike", limit=100): + res = super(Project, self).name_search(name, args, operator, limit) + if name: + domain = [ + "|", + ("key", "ilike", name + "%"), + ("id", "in", [x[0] for x in res]), + ] + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = ["&", "!"] + domain[1:] + projects = self.search(domain + (args or []), limit=limit) + return projects.name_get() + else: + return res + + def create_sequence(self): + """ + This method creates ir.sequence fot the current project + :return: Returns create sequence + """ + self.ensure_one() + sequence_data = self._prepare_sequence_data() + sequence = self.env["ir.sequence"].sudo().create(sequence_data) + self.write({"task_key_sequence_id": sequence.id}) + return sequence + + def update_sequence(self): + """ + This method updates existing task sequence + :return: + """ + sequence_data = self._prepare_sequence_data(init=False) + self.task_key_sequence_id.sudo().write(sequence_data) + + def _prepare_sequence_data(self, init=True): + """ + This method prepares data for create/update_sequence methods + :param init: Set to False in case you don't want to set initial values + for number_increment and number_next_actual + """ + values = { + "name": "{} {}".format(_("Project task sequence for project"), self.name), + "implementation": "standard", + "code": "project.task.key.{}".format(self.id), + "prefix": "{}-".format(self.key), + "use_date_range": False, + } + + # The key sequences to create stories and tasks with keys, created with + # a project, must be linked to the project company to avoid security + # issues. + company_id = self.env.context.get("project_sequence_company") + if company_id: + values["company_id"] = company_id + + if init: + values.update(dict(number_increment=1, number_next_actual=1)) + + return values + + def get_next_task_key(self): + test_project_key = self.env.context.get("test_project_key") + if (config["test_enable"] and not test_project_key) or ( + config["demo"].get("project_key") and not test_project_key + ): + return False + return self.sudo().task_key_sequence_id.next_by_id() + + def generate_project_key(self, text): + test_project_key = self.env.context.get("test_project_key") + if (config["test_enable"] and not test_project_key) or ( + config["demo"].get("project_key") and not test_project_key + ): + return False + + if not text: + return "" + + data = text.split(" ") + if len(data) == 1: + return self._generate_project_unique_key(data[0][:3].upper()) + + key = [] + for item in data: + key.append(item[:1].upper()) + return self._generate_project_unique_key("".join(key)) + + def _generate_project_unique_key(self, text): + res = text + unique_key = False + counter = 0 + while not unique_key: + if counter != 0: + res = "%s%s" % (text, counter) + unique_key = not bool(self.search([("key", "=", res)])) + counter += 1 + + return res + + def _update_task_keys(self): + """ + This method will update task keys of the current project. + """ + self.ensure_one() + self.flush() + reindex_query = """ + UPDATE project_task + SET key = x.key + FROM ( + SELECT t.id, p.key || '-' || split_part(t.key, '-', 2) AS key + FROM project_task t + INNER JOIN project_project p ON t.project_id = p.id + WHERE t.project_id = %s + ) AS x + WHERE project_task.id = x.id; + """ + + self.env.cr.execute(reindex_query, (self.id,)) + self.env["project.task"].invalidate_cache(["key"], self.task_ids.ids) + + @api.model + def _set_default_project_key(self): + """ + This method will be called from the post_init hook in order to set + default values on project.project and + project.task, so we leave those tables nice and clean after module + installation. + :return: + """ + for project in self.with_context(active_test=False).search( + [("key", "=", False)] + ): + project.key = self.generate_project_key(project.name) + project.create_sequence() + + for task in project.task_ids: + task.key = project.get_next_task_key() diff --git a/project_key/models/project_task.py b/project_key/models/project_task.py new file mode 100644 index 0000000000..7e46289525 --- /dev/null +++ b/project_key/models/project_task.py @@ -0,0 +1,100 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from odoo import api, fields, models +from odoo.osv import expression + +TASK_URL = "/web#id=%s&view_type=form&model=project.task&action=%s" + + +class Task(models.Model): + _inherit = "project.task" + + key = fields.Char(size=20, required=False, index=True) + + url = fields.Char(string="URL", compute="_compute_task_url") + + _sql_constraints = [("task_key_unique", "UNIQUE(key)", "Task key must be unique!")] + + def _compute_task_url(self): + action_id = self.env.ref("project.action_view_task").id + for task in self: + task.url = TASK_URL % (task.id, action_id) + + @api.model + def create(self, vals): + get = self.env.context.get + + project_id = vals.get("project_id", False) + if not project_id: + project_id = get("default_project_id", False) + + if not project_id and get("active_model", False) == "project.project": + project_id = get("active_id", False) + + if project_id: + project = self.env["project.project"].browse(project_id) + vals["key"] = project.get_next_task_key() + return super(Task, self).create(vals) + + def write(self, vals): + project_id = vals.get("project_id", False) + if not project_id: + return super(Task, self).write(vals) + + project = self.env["project.project"].browse(project_id) + for task in self: + if task.key and task.project_id.id == project.id: + continue + + values = self.prepare_task_for_project_switch(task, project) + super(Task, task).write(values) + + return super(Task, self).write(vals) + + def prepare_task_for_project_switch(self, task, project): + data = {"key": project.get_next_task_key(), "project_id": project.id} + + if len(task.child_ids) > 0: + data["child_ids"] = [ + (1, child.id, self.prepare_task_for_project_switch(child, project)) + for child in task.child_ids + ] + return data + + @api.model + def _name_search( + self, name="", args=None, operator="ilike", limit=100, name_get_uid=None + ): + if name: + if operator in ["=", "ilike", "=ilike", "like", "=like"]: + args = ( + args + ["|", ("key", operator, name)] + if args + else ["|", ("key", operator, name)] + ) + if operator in expression.NEGATIVE_TERM_OPERATORS: + args = ( + expression.AND([[("key", operator, name)], args]) + if args + else [("key", operator, name)] + ) + return super()._name_search( + name=name, + args=args, + operator=operator, + limit=limit, + name_get_uid=name_get_uid, + ) + + def name_get(self): + result = [] + + for record in self: + task_name = [] + if record.key: + task_name.append(record.key) + task_name.append(record.name) + result.append((record.id, " - ".join(task_name))) + + return result diff --git a/project_key/readme/CONTRIBUTORS.rst b/project_key/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..7585716600 --- /dev/null +++ b/project_key/readme/CONTRIBUTORS.rst @@ -0,0 +1,8 @@ +* Petar Najman +* Sladjan Kantar +* `CorporateHub `__ + + * Alexey Pelykh + +* Saran Lim. +* Tharathip Chaweewongphan diff --git a/project_key/readme/DESCRIPTION.rst b/project_key/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..9b7adc76ca --- /dev/null +++ b/project_key/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module provides functionality to uniquely identify projects and tasks by simple ``key`` field. diff --git a/project_key/readme/USAGE.rst b/project_key/readme/USAGE.rst new file mode 100644 index 0000000000..206ca8b2eb --- /dev/null +++ b/project_key/readme/USAGE.rst @@ -0,0 +1,34 @@ +To use this module functionality you just need to: + +On ``project.project`` level: + +In Kanban View: + +#. Go to Project > Dashboard +#. Create +#. Enter project name and use auto generated key or simply override value by entering your own key value. + +In Tree View: + +#. Go to Project > Configuration > Projects +#. Create +#. Enter project name and use auto generated key or simply override value by entering your own key value. + +In form View: + +#. Go to Project > Dashboard +#. Open the projects settings +#. Modify the "key" value +#. After modifying project key the key of any existing tasks related to that project will be updated automatically. + +When you create a project, under the hood a ir.sequence record gets creted with prefix: ``-``. + +On ``project.task`` level: + +#. Actually there is nothing to be done here +#. Task keys are auto generated based on project key value with per project auto incremented number (i.e. PA-1, PA-2, etc) + +In browser address bar: + +#. Navigate to your project by entering following url: http://<>/projects/PROJECT-KEY +#. Navigate to your task by entering following url: http://<>/tasks/TASK-KEY diff --git a/project_key/static/description/icon.png b/project_key/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/project_key/static/description/icon.png differ diff --git a/project_key/static/description/index.html b/project_key/static/description/index.html new file mode 100644 index 0000000000..e005de7e05 --- /dev/null +++ b/project_key/static/description/index.html @@ -0,0 +1,462 @@ + + + + + + +Project key + + + +
+

Project key

+ + +

Beta License: LGPL-3 OCA/project Translate me on Weblate Try me on Runbot

+

This module provides functionality to uniquely identify projects and tasks by simple key field.

+

Table of contents

+ +
+

Usage

+

To use this module functionality you just need to:

+

On project.project level:

+

In Kanban View:

+
    +
  1. Go to Project > Dashboard
  2. +
  3. Create
  4. +
  5. Enter project name and use auto generated key or simply override value by entering your own key value.
  6. +
+

In Tree View:

+
    +
  1. Go to Project > Configuration > Projects
  2. +
  3. Create
  4. +
  5. Enter project name and use auto generated key or simply override value by entering your own key value.
  6. +
+

In form View:

+
    +
  1. Go to Project > Dashboard
  2. +
  3. Open the projects settings
  4. +
  5. Modify the “key” value
  6. +
  7. After modifying project key the key of any existing tasks related to that project will be updated automatically.
  8. +
+

When you create a project, under the hood a ir.sequence record gets creted with prefix: <project-key>-.

+

On project.task level:

+
    +
  1. Actually there is nothing to be done here
  2. +
  3. Task keys are auto generated based on project key value with per project auto incremented number (i.e. PA-1, PA-2, etc)
  4. +
+

In browser address bar:

+
    +
  1. Navigate to your project by entering following url: http://<<your-domain>>/projects/PROJECT-KEY
  2. +
  3. Navigate to your task by entering following url: http://<<your-domain>>/tasks/TASK-KEY
  4. +
+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Modoolar
  • +
+
+
+

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

+

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

+
+
+
+ + diff --git a/project_key/tests/__init__.py b/project_key/tests/__init__.py new file mode 100644 index 0000000000..31f0029620 --- /dev/null +++ b/project_key/tests/__init__.py @@ -0,0 +1,5 @@ +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from . import test_project +from . import test_task +from . import test_controller diff --git a/project_key/tests/test_common.py b/project_key/tests/test_common.py new file mode 100644 index 0000000000..08c3fd528e --- /dev/null +++ b/project_key/tests/test_common.py @@ -0,0 +1,55 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from odoo.tests.common import HttpCase, TransactionCase + + +class TestMixin(object): + @staticmethod + def _setup_records(class_or_instance): + self = class_or_instance + self.Project = self.env["project.project"].with_context(test_project_key=True) + self.Task = self.env["project.task"].with_context(test_project_key=True) + + self.project_action = self.env.ref("project.open_view_project_all_config") + self.task_action = self.env.ref("project.action_view_task") + + self.project_1 = self.Project.create({"name": "OCA"}) + self.project_2 = self.Project.create({"name": "Odoo", "key": "ODOO"}) + self.project_3 = self.Project.create({"name": "Python"}) + + self.task11 = self.Task.create({"name": "1", "project_id": self.project_1.id}) + + self.task12 = self.Task.create( + {"name": "2", "parent_id": self.task11.id, "project_id": self.project_1.id} + ) + + self.task21 = self.Task.create({"name": "3", "project_id": self.project_2.id}) + + self.task30 = self.Task.create({"name": "3"}) + + def get_record_url(self, record, model, action): + return "/web#id={}&view_type=form&model={}&action={}".format( + record.id, model, action + ) + + def get_task_url(self, task): + return self.get_record_url(task, task._name, self.task_action.id) + + def get_project_url(self, project): + return self.get_record_url(project, project._name, self.project_action.id) + + +class TestCommon(TransactionCase, TestMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls._setup_records(cls) + + +class HttpTestCommon(HttpCase, TestMixin): + def setUp(self): + super().setUp() + self.env = self.env(context=dict(self.env.context, tracking_disable=True)) + self._setup_records(self) diff --git a/project_key/tests/test_controller.py b/project_key/tests/test_controller.py new file mode 100644 index 0000000000..dc65dbd5cd --- /dev/null +++ b/project_key/tests/test_controller.py @@ -0,0 +1,22 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from .test_common import HttpTestCommon + + +class TestController(HttpTestCommon): + def test_01_project_browse(self): + self.authenticate("admin", "admin") + response = self.url_open("/projects/" + self.project_1.key) + self.assertEqual(response.status_code, 200) + self.assertTrue( + response.url.endswith(self.get_project_url(self.project_1)), response.url + ) + + def test_02_task_browse(self): + self.authenticate("admin", "admin") + response = self.url_open("/tasks/" + self.task11.key) + self.assertEqual(response.status_code, 200) + self.assertTrue( + response.url.endswith(self.get_task_url(self.task11)), response.url + ) diff --git a/project_key/tests/test_project.py b/project_key/tests/test_project.py new file mode 100644 index 0000000000..2d089aa7a9 --- /dev/null +++ b/project_key/tests/test_project.py @@ -0,0 +1,71 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from odoo.tools import mute_logger + +from .test_common import TestCommon + + +class TestProject(TestCommon): + def test_01_key(self): + self.assertEqual(self.project_1.key, "OCA") + self.assertEqual(self.project_2.key, "ODOO") + self.assertEqual(self.project_3.key, "PYT") + + def test_02_change_key(self): + self.project_1.key = "XXX" + + self.assertEqual(self.task11.key, "XXX-1") + self.assertEqual(self.task12.key, "XXX-2") + + def test_03_name_search(self): + + projects = self.Project.name_search("ODO") + self.assertEqual(len(projects), 1) + + non_odoo_projects = [ + x[0] for x in self.Project.name_search("ODO", operator="not ilike") + ] + + odoo_projects = self.Project.browse(non_odoo_projects).filtered( + lambda x: x.id == self.project_2.id + ) + + self.assertEqual(len(odoo_projects), 0) + + def test_04_name_search_empty(self): + projects = self.Project.name_search("") + self.assertGreater(len(projects), 0) + + def test_05_name_onchange(self): + project = self.Project.new({"name": "Software Development"}) + project._onchange_project_name() + self.assertEqual(project.key, "SD") + + def test_06_name_onchange(self): + project = self.Project.new({}) + project._onchange_project_name() + self.assertEqual(project.key, "") + + @mute_logger("odoo.models.unlink") + def test_07_delete(self): + self.project_1.task_ids.unlink() + self.project_1.unlink() + + self.project_2.task_ids.unlink() + self.project_2.unlink() + + self.project_3.unlink() + + def test_08_generate_empty_project_key(self): + empty_key = self.Project.generate_project_key(False) + self.assertEqual(empty_key, "") + + def test_09_name_onchange_with_key(self): + project = self.Project.new({"name": "Software Development", "key": "TEST"}) + project._onchange_project_name() + self.assertEqual(project.key, "TEST") + + def test_10_generate_unique_key_with_counter(self): + project = self.Project.create({"name": "OCA"}) + self.assertEqual(project.key, "OCA1") diff --git a/project_key/tests/test_task.py b/project_key/tests/test_task.py new file mode 100644 index 0000000000..7ad37a2158 --- /dev/null +++ b/project_key/tests/test_task.py @@ -0,0 +1,54 @@ +# Copyright 2017 - 2018 Modoolar +# License LGPLv3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from .test_common import TestCommon + + +class TestTask(TestCommon): + def test_01_key(self): + self.assertEqual(self.task11.key, "OCA-1") + self.assertEqual(self.task12.key, "OCA-2") + self.assertEqual(self.task21.key, "ODOO-1") + self.assertEqual(self.task30.key, False) + + def test_02_compute_task_url(self): + task_url = self.get_task_url(self.task11) + + self.task11._compute_task_url() + self.assertEqual(self.task11.url, task_url) + + def test_03_create_task_project_in_context(self): + self.Task.with_context( + active_model="project.project", active_id=self.project_1.id + ).create({"name": "4"}) + + def test_04_no_switch_project(self): + self.task11.write({"project_id": self.project_1.id}) + self.assertEqual(self.task11.key, "OCA-1") + self.assertEqual(self.task12.key, "OCA-2") + + def test_05_switch_project(self): + self.task11.write({"project_id": self.project_2.id}) + self.assertEqual(self.task11.key, "ODOO-2") + self.assertEqual(self.task12.key, "ODOO-3") + + def test_06_name_search(self): + oca_tasks = self.Task.name_search("OCA") + self.assertEqual(len(oca_tasks), 2) + + non_oca_task_ids = [ + x[0] for x in self.Task.name_search("OCA", operator="not ilike") + ] + + oca_tasks = self.Task.browse(non_oca_task_ids).filtered( + lambda x: x.project_id.id == self.project_1.id + ) + + self.assertEqual(len(oca_tasks), 0) + + def test_07_name_search_empty(self): + tasks = self.Task.name_search("") + self.assertGreater(len(tasks), 0) + + def test_08_create_new_company(self): + self.env["res.company"].create({"name": "New company"}) diff --git a/project_key/views/project_key_views.xml b/project_key/views/project_key_views.xml new file mode 100644 index 0000000000..26f94c5da4 --- /dev/null +++ b/project_key/views/project_key_views.xml @@ -0,0 +1,114 @@ + + + + + project.edit.project.inherited + project.project + + + + + + + + + project.project.tree + project.project + + + + + + + + + project.project.select + project.project + + + + ['|',('name','ilike',self),('key','ilike',self)] + + + + + project.task.form.key + project.task + + + + + + + + + project.task.tree + project.task + + + + + + + + + + project.task.search.key + project.task + + + + ['|',('name','ilike',self),('key','ilike',self)] + + + + + project.task.kanban.key + project.task + + + + + + + + + + + + + + + project.project.view.form.simplified + project.project + + +
+ +
+
+
+ + project.project.kanban + project.project + + + + + + + - + + + +
diff --git a/setup/project_key/odoo/addons/project_key b/setup/project_key/odoo/addons/project_key new file mode 120000 index 0000000000..4305bc669d --- /dev/null +++ b/setup/project_key/odoo/addons/project_key @@ -0,0 +1 @@ +../../../../project_key \ No newline at end of file diff --git a/setup/project_key/setup.py b/setup/project_key/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/project_key/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)