diff --git a/hr_planning_resources/README.rst b/hr_planning_resources/README.rst new file mode 100644 index 0000000..a9f9812 --- /dev/null +++ b/hr_planning_resources/README.rst @@ -0,0 +1,91 @@ +=================== +HR Resource Planner +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f8c267bf44bdd476c5d1521e536405cf86ddbed1c66d84d65fd1df159b50805d + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fresource-lightgray.png?logo=github + :target: https://github.com/OCA/resource/tree/16.0/hr_planning_resources + :alt: OCA/resource +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/resource-16-0/resource-16-0-hr_planning_resources + :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/resource&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is designed to improve human resources planning and management within an organization. +It's provides advanced tools for task scheduling, permission management, and resource planning, +allowing managers and employees to organize and visualize their schedules efficiently. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Navigate to the "Planning" section in the main menu. +2. Select "My Planning" to view your assigned tasks. +3. To assign a new task, select "My Planning", click on the New button. +4. You can assign it from the same task, ticket or project using the assign to planning button. +5. In the task, if the "Force recalculation" checkbox is checked, all hours that have elapsed will be calculated. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Binhex + +Contributors +~~~~~~~~~~~~ + +* `Binhex `_: + + * Antonio Ruban + * Adasat Torres de Leon + * Zuzanna Elzbieta Szalaty Szalaty + +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/resource `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_planning_resources/__init__.py b/hr_planning_resources/__init__.py new file mode 100644 index 0000000..9b42961 --- /dev/null +++ b/hr_planning_resources/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/hr_planning_resources/__manifest__.py b/hr_planning_resources/__manifest__.py new file mode 100644 index 0000000..110c8cf --- /dev/null +++ b/hr_planning_resources/__manifest__.py @@ -0,0 +1,32 @@ +{ + "name": "HR Resource Planner", + "summary": "", + "version": "16.0.1.0.7", + "category": "Human Resources", + "website": "https://github.com/OCA/resource", + "author": "Binhex, Odoo Community Association (OCA)", + "depends": [ + "project", + "web_timeline", + "helpdesk_mgmt", + "hr_holidays", + ], + "data": [ + "security/ir.model.access.csv", + "data/hr_task_ir_cron.xml", + "views/hr_task_views.xml", + "views/project_views.xml", + "wizard/create_hr_task_views.xml", + "views/menus.xml", + ], + "license": "AGPL-3", + "application": False, + "installable": True, + "assets": { + "web.assets_backend": [ + "hr_planning_resources/static/src/scss/hr_planning_resources.scss", + "hr_planning_resources/static/src/js/*.js", + "hr_planning_resources/static/src/xml/*.xml", + ], + }, +} diff --git a/hr_planning_resources/data/hr_task_ir_cron.xml b/hr_planning_resources/data/hr_task_ir_cron.xml new file mode 100644 index 0000000..97c50ee --- /dev/null +++ b/hr_planning_resources/data/hr_task_ir_cron.xml @@ -0,0 +1,22 @@ + + + + Update Task + + code + model.cron_update_task_state() + 1 + days + -1 + False + True + + + HR Planning Resources: generate next recurring shifts + + code + model._cron_schedule_next() + weeks + -1 + + diff --git a/hr_planning_resources/i18n/es.po b/hr_planning_resources/i18n/es.po new file mode 100644 index 0000000..85eae2e --- /dev/null +++ b/hr_planning_resources/i18n/es.po @@ -0,0 +1,853 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_planning_resources +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-01-06 20:38+0000\n" +"PO-Revision-Date: 2025-01-06 20:38+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_leave.py:0 +#, python-format +msgid " and" +msgstr "Error translating text" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.create_hr_task_view_form +msgid "" +"\n" +" Create a new HR Task for the current user.\n" +" " +msgstr "" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_form_view +msgid "" +"" +msgstr "" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.res_config_settings_view_form +msgid "" +"\n" +" Recurring Tasks\n" +" " +msgstr "" +"\n" +" Tareas recurrentes\n" +" " + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.helpdesk_ticket_edit_view_form +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.view_edit_project_form +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.view_project_task_form +msgid "Planning Task" +msgstr "" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.res_config_settings_view_form +msgid "months ahead" +msgstr "meses por delante" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_needaction +msgid "Action Needed" +msgstr "Acción necesaria" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_ids +msgid "Activities" +msgstr "Actividades" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Emblema de actividad de excepción" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_state +msgid "Activity State" +msgstr "Estado de la actividad" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_type_icon +msgid "Activity Type Icon" +msgstr "Ícono de tipo de actvidad" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__allocated_hours +msgid "Allocated Time" +msgstr "Tiempo asignado" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__allocated_percentage +msgid "Allocated Time %" +msgstr "Tiempo asignado" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_task_recurrency.py:0 +#, python-format +msgid "An shift must be in the same company as its recurrency." +msgstr "Un turno debe estar en la misma empresa que su recurrencia." + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.helpdesk_ticket_edit_view_form +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.view_edit_project_form +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.view_project_task_form +msgid "Assing Planning" +msgstr "" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_attachment_count +msgid "Attachment Count" +msgstr "Nº de archivos adjuntos" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.create_hr_task_view_form +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_form_view +msgid "Cancel" +msgstr "Cancelar" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__state__cancel +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "Cancelled" +msgstr "Cancelado" + +#. module: hr_planning_resources +#: model:ir.model,name:hr_planning_resources.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__company_id +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__company_id +msgid "Company" +msgstr "Empresa" + +#. module: hr_planning_resources +#: model:ir.model,name:hr_planning_resources.model_res_config_settings +msgid "Config Settings" +msgstr "Opciones de configuración" + +#. module: hr_planning_resources +#: model:ir.ui.menu,name:hr_planning_resources.menu_settings +msgid "Configuration" +msgstr "Configuración" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.create_hr_task_view_form +msgid "Confirm" +msgstr "Confirmar" + +#. module: hr_planning_resources +#: model:ir.model,name:hr_planning_resources.model_create_hr_task +msgid "Create HR Task" +msgstr "Crear tarea de RR. HH" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__create_uid +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__create_uid +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__create_date +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__create_date +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_form_view +msgid "Date" +msgstr "Fecha" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__repeat_unit__day +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task_recurrency__repeat_unit__day +msgid "Days" +msgstr "Días" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__department_id +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "Department" +msgstr "Departamento" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__display_name +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__display_name +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__duration +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__planned_hours +msgid "Duration" +msgstr "Duración" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__email_cc +msgid "Email cc" +msgstr "Correo electrónico cc" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__employee_id +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "Employee" +msgstr "Empleado" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__date_end +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__date_end +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "End Date" +msgstr "Fecha de Final" + +#. module: hr_planning_resources +#: model:ir.model.constraint,message:hr_planning_resources.constraint_hr_task_date_check +msgid "Error: End date must be greater than start date!" +msgstr "La fecha de finalización debe ser posterior a la de Inicio" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_form_view +msgid "Every" +msgstr "Cada" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_form_view +msgid "Filter by Project" +msgstr "Filtrar por proyecto:" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__filtered_project_id +msgid "Filtered Project" +msgstr "Proyecto filtrado" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__state__finished +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_form_view +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "Finished" +msgstr "Terminado" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_follower_ids +msgid "Followers" +msgstr "Seguidores" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguidores (Contactos)" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icono de Font Awesome ej. fa-tasks" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__repeat_type__forever +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task_recurrency__repeat_type__forever +msgid "Forever" +msgstr "Siempre" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.res_config_settings_view_form +msgid "Generate shifts" +msgstr "Generar turnos" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "Group By" +msgstr "GroupBy" + +#. module: hr_planning_resources +#: model:ir.model,name:hr_planning_resources.model_hr_task +msgid "HR Planning Resource" +msgstr "Recurso de planificación de RR. HH" + +#. module: hr_planning_resources +#: model:ir.actions.server,name:hr_planning_resources.ir_cron_hr_task_schedule_ir_actions_server +#: model:ir.cron,cron_name:hr_planning_resources.ir_cron_hr_task_schedule +msgid "HR Planning Resources: generate next recurring shifts" +msgstr "" +"Recursos de planificación de RR. HH.: generar próximos turnos recurrentes" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_helpdesk_ticket__hr_task_count +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_mixin__hr_task_count +#: model:ir.model.fields,field_description:hr_planning_resources.field_project_project__hr_task_count +#: model:ir.model.fields,field_description:hr_planning_resources.field_project_task__hr_task_count +msgid "HR Task Count" +msgstr "" + +#. module: hr_planning_resources +#: model:ir.model,name:hr_planning_resources.model_hr_task_mixin +msgid "HR Task Mixin" +msgstr "hr.task.mixin" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__has_message +msgid "Has Message" +msgstr "Tiene mensaje" + +#. module: hr_planning_resources +#: model:ir.model,name:hr_planning_resources.model_helpdesk_ticket +msgid "Helpdesk Ticket" +msgstr "Ticket de Mesa de Servicio" + +#. module: hr_planning_resources +#: model:ir.model,name:hr_planning_resources.model_hr_task_recurrency +msgid "Hr Task Recurrency" +msgstr "HrTaskRecurrency" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__id +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__id +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__id +msgid "ID" +msgstr "" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_exception_icon +msgid "Icon" +msgstr "Ícono" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Ícono para indicar una actividad de excepción." + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Si está marcado hay nuevos mensajes que requieren su atención." + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__message_has_error +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "Si se encuentra marcado, algunos mensajes tienen error de envío." + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__state__in_progress +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "In Progress" +msgstr "En progreso" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_form_view +msgid "In progress" +msgstr "En curso" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_is_follower +msgid "Is Follower" +msgstr "Es un seguidor" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__last_generated_end_datetime +msgid "Last Generated End Date" +msgstr "Última fecha de generación" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task____last_update +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task____last_update +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency____last_update +msgid "Last Modified on" +msgstr "Última modificación el" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__write_uid +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__write_uid +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__write_date +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__write_date +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__write_date +msgid "Last Updated on" +msgstr "Ultima actualización en" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__leave_warning +msgid "Leave Warning" +msgstr "Error translating text" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_main_attachment_id +msgid "Main Attachment" +msgstr "Adjuntos principales" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__employee_parent_id +msgid "Manager" +msgstr "Administrador" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__member_of_department +msgid "Member of department" +msgstr "Miembro del departamento" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_has_error +msgid "Message Delivery error" +msgstr "Error de Envío de Mensaje" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_ids +msgid "Messages" +msgstr "Mensajes" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__repeat_unit__month +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task_recurrency__repeat_unit__month +msgid "Months" +msgstr "Meses" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Fecha límite de Mi Actividad" + +#. module: hr_planning_resources +#: model:ir.actions.act_window,name:hr_planning_resources.my_department_timeline_view +#: model:ir.ui.menu,name:hr_planning_resources.menu_my_department +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "My Department" +msgstr "Mi departamento" + +#. module: hr_planning_resources +#: model:ir.actions.act_window,name:hr_planning_resources.my_planning_timeline_view +#: model:ir.ui.menu,name:hr_planning_resources.menu_my_planning +msgid "My Planning" +msgstr "Mi planificación" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "My Shifts" +msgstr "Mis turnos" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__name +msgid "Name" +msgstr "Nombre" + +#. module: hr_planning_resources +#. odoo-javascript +#: code:addons/hr_planning_resources/static/src/xml/web_timeline.xml:0 +#: code:addons/hr_planning_resources/static/src/xml/web_timeline.xml:0 +#, python-format +msgid "New" +msgstr "Noticia" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_calendar_event_id +msgid "Next Activity Calendar Event" +msgstr "Siguiente evento en el calendario de actividades" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Fecha de vencimiento de siguiente actividad" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_summary +msgid "Next Activity Summary" +msgstr "Resumen de la siguiente actividad" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo de actividad Siguiente" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task_recurrency__repeat_number +msgid "No Of Repetitions of the plannings" +msgstr "Nº de repeticiones de las planificaciones" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/wizard/create_hr_task.py:0 +#, python-format +msgid "No active record id provided." +msgstr "" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/wizard/create_hr_task.py:0 +#, python-format +msgid "No default resource model specified." +msgstr "" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_needaction_counter +msgid "Number of Actions" +msgstr "Número de acciones" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__repeat_type__x_times +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task_recurrency__repeat_type__x_times +msgid "Number of Repetitions" +msgstr "Número de repeticiones" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_has_error_counter +msgid "Number of errors" +msgstr "Numero de errores" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Número de mensajes que requieren acción" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Número de mensajes con error de envío" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__state__planified +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "Planified" +msgstr "Planificado" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.res_config_settings_view_form +msgid "Planning Resources" +msgstr "Recursos de planificación" + +#. module: hr_planning_resources +#: model:ir.actions.act_window,name:hr_planning_resources.planner_timeline_view +msgid "Planning resource" +msgstr "Recurso de planificación" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_task.py:0 +#: model:ir.model,name:hr_planning_resources.model_project_project +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__project_id +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__type__project +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.view_edit_project_form +#, python-format +msgid "Project" +msgstr "Proyecto" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_res_company__task_generation_interval +#: model:ir.model.fields,field_description:hr_planning_resources.field_res_config_settings__task_generation_interval +msgid "Rate Of Shift Generation" +msgstr "Tasa de generación de turnos" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__is_recompute_forced +msgid "Recompute Forced?" +msgstr "Forzar recalculo" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__recurrency_id +msgid "Recurrency" +msgstr "Recurrencia" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__task_ids +msgid "Related Planning Tasks" +msgstr "Tareas de planificación relacionadas" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__user_id +msgid "Related user name for the resource to manage its access." +msgstr "Usuario relacionado con el recurso para gestionar su acceso." + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__repeat +msgid "Repeat" +msgstr "Repetir" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__repeat_interval +msgid "Repeat Every" +msgstr "Error translating text" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__repeat_type +msgid "Repeat Type" +msgstr "Repetir tipo" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__repeat_unit +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__repeat_unit +msgid "Repeat Unit" +msgstr "Repetir unidad" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__repeat_until +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__repeat_until +msgid "Repeat Until" +msgstr "Repetir hasta" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__repeat_interval +msgid "Repeat every" +msgstr "Repetir Cada" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__repeat_number +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__repeat_number +msgid "Repetitions" +msgstr "Repeticiones" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__resource_id +msgid "Resource" +msgstr "Recurso" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__activity_user_id +msgid "Responsible User" +msgstr "Usuario responsable" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__message_has_sms_error +msgid "SMS Delivery error" +msgstr "Error de entrega del SMS" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_form_view +msgid "Set to Planified" +msgstr "Configurar como planificado" + +#. module: hr_planning_resources +#: model:ir.actions.act_window,name:hr_planning_resources.planning_action_settings +#: model:ir.ui.menu,name:hr_planning_resources.menu_settings_config +msgid "Settings" +msgstr "Ajustes" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__date_start +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__date_start +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "Start Date" +msgstr "Fecha de inicio" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__state +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "State" +msgstr "Estado" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Estado basado en actividades\n" +"Vencida: la fecha límite ya pasó\n" +"Hoy: la fecha límite es hoy\n" +"Planificada: actividades futuras." + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_task.py:0 +#: model:ir.model,name:hr_planning_resources.model_project_task +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__task_id +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__type__task +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +#, python-format +msgid "Task" +msgstr "Tarea" + +#. module: hr_planning_resources +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.helpdesk_ticket_edit_view_form +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.view_project_task_form +msgid "Tasks" +msgstr "Tareas" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_task_mixin.py:0 +#, python-format +msgid "The method _compute_hr_task_count must be implemented in the subclass." +msgstr "El método _compute_hr_task_count debe implementarse en la subclase." + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_task_recurrency.py:0 +#, python-format +msgid "The number of repetitions cannot be negative." +msgstr "El número de repeticiones no puede ser negativo." + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/wizard/create_hr_task.py:0 +#, python-format +msgid "The record does not exist." +msgstr "El registro no existe." + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/wizard/create_hr_task.py:0 +#, python-format +msgid "The selected user does not have an associated employee." +msgstr "El usuario seleccionado no tiene un empleado asociado." + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_task.py:0 +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__ticket_id +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__type__ticket +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +#, python-format +msgid "Ticket" +msgstr "Entrada" + +#. module: hr_planning_resources +#: model:ir.model,name:hr_planning_resources.model_hr_leave +msgid "Time Off" +msgstr "Ausencias" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__title +msgid "Title" +msgstr "Sr. / Sra." + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__type +#: model_terms:ir.ui.view,arch_db:hr_planning_resources.hr_task_search_view +msgid "Type" +msgstr "Tipo" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo de actividad de excepción registrada." + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/wizard/create_hr_task.py:0 +#, python-format +msgid "Unsupported resource model: %s" +msgstr "No se admite el modelo de recurso: %s" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__repeat_type__until +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task_recurrency__repeat_type__until +msgid "Until" +msgstr "Hasta" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task_recurrency__repeat_until +msgid "Up to which date should the plannings be repeated" +msgstr "Hasta qué fecha deben repetirse las planificaciones" + +#. module: hr_planning_resources +#: model:ir.actions.server,name:hr_planning_resources.ir_cron_hr_task_ir_actions_server +#: model:ir.cron,cron_name:hr_planning_resources.ir_cron_hr_task +msgid "Update Task" +msgstr "Actualizar tarea " + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__user_id +msgid "User" +msgstr "Usuario" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_create_hr_task__user_id +msgid "Users" +msgstr "Usuarios" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__website_message_ids +msgid "Website Messages" +msgstr "Mensajes del Sitio Web" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__website_message_ids +msgid "Website communication history" +msgstr "Historial de comunicaciones del sitio web" + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task_recurrency__repeat_type +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__repeat_unit__week +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task_recurrency__repeat_unit__week +msgid "Weeks" +msgstr "Semanas" + +#. module: hr_planning_resources +#: model:ir.model.fields,help:hr_planning_resources.field_hr_task__member_of_department +msgid "" +"Whether the employee is a member of the active user's department or one of " +"it's child department." +msgstr "" +"Si el empleado es miembro del departamento del usuario activo o uno de sus " +"departamentos secundarios." + +#. module: hr_planning_resources +#: model:ir.model.fields,field_description:hr_planning_resources.field_hr_task__working_days_count +msgid "Working Days" +msgstr "Días laborables" + +#. module: hr_planning_resources +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task__repeat_unit__year +#: model:ir.model.fields.selection,name:hr_planning_resources.selection__hr_task_recurrency__repeat_unit__year +msgid "Years" +msgstr "Años" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_leave.py:0 +#, python-format +msgid "has requested time off" +msgstr "ha solicitado tiempo libre" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_leave.py:0 +#, python-format +msgid "is on time off" +msgstr "está en tiempo libre" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_leave.py:0 +#, python-format +msgid "{employee} {time_off_type}{period_leaves}. \n" +msgstr "" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_leave.py:0 +#, python-format +msgid "" +"{prefix} from the {dfrom_date} at {dfrom_time} to the {dto_date} at " +"{dto_time}" +msgstr "{prefix} desde el {dfrom_date} a las {dfrom_time} hasta el {dto_date} a las {dto_time}" + +#. module: hr_planning_resources +#. odoo-python +#: code:addons/hr_planning_resources/models/hr_leave.py:0 +#, python-format +msgid "{prefix} from the {dfrom} to the {dto}" +msgstr "{prefix} desde el {dfrom} hasta el {dto}" diff --git a/hr_planning_resources/models/__init__.py b/hr_planning_resources/models/__init__.py new file mode 100644 index 0000000..ea0c1d0 --- /dev/null +++ b/hr_planning_resources/models/__init__.py @@ -0,0 +1,7 @@ +from . import hr_task +from . import hr_leave +from . import hr_task_mixin +from . import project_project +from . import project_task +from . import helpdesk_ticket +from . import hr_task_recurrency diff --git a/hr_planning_resources/models/helpdesk_ticket.py b/hr_planning_resources/models/helpdesk_ticket.py new file mode 100644 index 0000000..48f1b12 --- /dev/null +++ b/hr_planning_resources/models/helpdesk_ticket.py @@ -0,0 +1,20 @@ +from odoo import api, models + + +class HelpdeskTicket(models.Model): + _name = "helpdesk.ticket" + _inherit = ["helpdesk.ticket", "hr.task.mixin"] + + def _compute_hr_task_count(self): + for record in self: + ticket_count = self.env["hr.task"].search_count( + [("ticket_id", "=", record.id)] + ) + record.hr_task_count = ticket_count + + @api.model_create_multi + def create(self, vals_list): + if self.env.context.get("default_user_id", False): + for vals in vals_list: + vals["user_id"] = self.env.context["default_user_id"] + return super().create(vals_list) diff --git a/hr_planning_resources/models/hr_leave.py b/hr_planning_resources/models/hr_leave.py new file mode 100644 index 0000000..9d9d595 --- /dev/null +++ b/hr_planning_resources/models/hr_leave.py @@ -0,0 +1,188 @@ +from collections import defaultdict +from datetime import timedelta +from functools import lru_cache +from itertools import groupby + +from pytz import timezone, utc + +from odoo import _, api, models +from odoo.tools.misc import get_lang + + +def format_time(env, time): + return time.strftime(get_lang(env).time_format) + + +def format_date(env, date): + return date.strftime(get_lang(env).date_format) + + +class HrLeave(models.Model): + _inherit = "hr.leave" + + @api.model + def _get_leaves(self, date_from, date_to, employee_ids): + calendar_leaves = self._get_calendar_leaves(date_from, date_to, employee_ids) + leaves = self._group_leaves_by_employee(calendar_leaves, employee_ids) + hr_leaves = self._get_hr_leaves(date_from, date_to, employee_ids) + for leave in hr_leaves: + leaves[leave.employee_id.id].append(leave) + return leaves + + def _get_calendar_leaves(self, date_from, date_to, employee_ids): + return self.env["resource.calendar.leaves"].search( + [ + ("time_type", "=", "leave"), + "|", + ("company_id", "in", employee_ids.mapped("company_id").ids), + ("company_id", "=", False), + "|", + ("resource_id", "in", employee_ids.mapped("resource_id").ids), + ("resource_id", "=", False), + ("date_from", "<=", date_to), + ("date_to", ">=", date_from), + ], + order="date_from", + ) + + def _group_leaves_by_employee(self, calendar_leaves, employee_ids): + leaves = defaultdict(list) + for leave in calendar_leaves: + for employee in employee_ids: + if self._is_leave_relevant_to_employee(leave, employee): + leaves[employee.id].append(leave) + return leaves + + def _is_leave_relevant_to_employee(self, leave, employee): + return ( + (not leave.company_id or leave.company_id == employee.company_id) + and (not leave.resource_id or leave.resource_id == employee.resource_id) + and ( + not leave.calendar_id + or leave.calendar_id == employee.resource_calendar_id + ) + ) + + def _get_hr_leaves(self, date_from, date_to, employee_ids): + return self.env["hr.leave"].search( + [ + ("employee_id", "in", employee_ids.ids), + ("state", "in", ["confirm", "validate1"]), + ("date_from", "<=", date_to), + ("date_to", ">=", date_from), + ], + order="date_from", + ) + + def _get_leave_message_warning(self, leaves, employee, date_from, date_to): + @lru_cache(None) + def localize(date): + return ( + utc.localize(date) + .astimezone(timezone(self.env.user.tz or "UTC")) + .replace(tzinfo=None) + ) + + def format_period_leave(period, prefix): + dfrom = period["from"] + dto = period["to"] + if period.get("show_hours", False): + return _( + "{prefix} from the {dfrom_date} at {dfrom_time} to " + "the {dto_date} at {dto_time}" + ).format( + prefix=prefix, + dfrom_date=format_date(self.env, localize(dfrom)), + dfrom_time=format_time(self.env, localize(dfrom)), + dto_date=format_date(self.env, localize(dto)), + dto_time=format_time(self.env, localize(dto)), + ) + else: + return _("{prefix} from the {dfrom} to the {dto}").format( + prefix=prefix, + dfrom=format_date(self.env, localize(dfrom)), + dto=format_date(self.env, localize(dto)), + ) + + warning = "" + periods = self._group_leaves(leaves, employee, date_from, date_to) + periods_by_states = [ + list(b) for _, b in groupby(periods, key=lambda x: x["is_validated"]) + ] + + for periods in periods_by_states: + period_leaves = "" + for period in periods: + prefix = "" + if period != periods[0]: + prefix = _(" and") if period == periods[-1] else "," + period_leaves += format_period_leave(period, prefix) + + time_off_type = ( + _("is on time off") + if periods[0].get("is_validated") + else _("has requested time off") + ) + warning += _("{employee} {time_off_type}{period_leaves}. \n").format( + employee=employee.name, + time_off_type=time_off_type, + period_leaves=period_leaves, + ) + return warning + + def _group_leaves(self, leaves, employee_id, date_from, date_to): + work_times = self._get_work_times(employee_id, date_from, date_to) + periods = [] + + for leave in leaves: + if self._is_leave_outside_range(leave, date_from, date_to): + continue + + number_of_days, is_validated = self._get_leave_details(leave) + if not periods or self._has_working_hours( + periods[-1]["from"], leave.date_to, work_times + ): + periods.append( + { + "is_validated": is_validated, + "from": leave.date_from, + "to": leave.date_to, + "show_hours": number_of_days <= 1, + } + ) + else: + self._update_existing_period( + periods[-1], leave, is_validated, number_of_days + ) + + return periods + + def _get_work_times(self, employee_id, date_from, date_to): + return { + wk[0]: wk[1] + for wk in employee_id.list_work_time_per_day(date_from, date_to) + } + + def _is_leave_outside_range(self, leave, date_from, date_to): + return leave.date_from > date_to or leave.date_to < date_from + + def _get_leave_details(self, leave): + if isinstance(leave, self.pool["hr.leave"]): + return leave.number_of_days, False + else: + dt_delta = leave.date_to - leave.date_from + number_of_days = dt_delta.days + (dt_delta.seconds / 3600) / 24 + return number_of_days, True + + def _has_working_hours(self, start_dt, end_dt, work_times): + diff_days = (end_dt - start_dt).days + all_dates = [ + start_dt.date() + timedelta(days=delta) for delta in range(diff_days + 1) + ] + return any(d in work_times for d in all_dates) + + def _update_existing_period(self, period, leave, is_validated, number_of_days): + period["is_validated"] = is_validated + if period["to"] < leave.date_to: + period["to"] = leave.date_to + period["show_hours"] = period.get("show_hours") or number_of_days <= 1 diff --git a/hr_planning_resources/models/hr_task.py b/hr_planning_resources/models/hr_task.py new file mode 100644 index 0000000..5696bd0 --- /dev/null +++ b/hr_planning_resources/models/hr_task.py @@ -0,0 +1,519 @@ +from datetime import datetime, time + +import pytz +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models + +TASK_TYPES = [ + ("task", _("Task")), + ("project", _("Project")), + ("ticket", _("Ticket")), +] + + +class HrTask(models.Model): + _name = "hr.task" + _description = "HR Planning Resource" + _inherit = ["mail.thread.cc", "mail.activity.mixin"] + _sql_constraints = [ + ( + "date_check", + "CHECK (date_start <= date_end)", + "Error: End date must be greater than start date!", + ), + ] + _order = "date_start desc, id desc" + + def _default_date_start(self): + return datetime.combine(fields.Date.context_today(self), time.min) + + def _default_date_end(self): + return datetime.combine(fields.Date.context_today(self), time.max) + + def _get_default_employee(self): + return self.env["hr.employee"].search([("user_id", "=", self.env.uid)], limit=1) + + name = fields.Char(compute="_compute_name", store=True) + title = fields.Char(compute="_compute_title", store=True) + type = fields.Selection(selection=TASK_TYPES, required=True, tracking=True) + employee_id = fields.Many2one( + "hr.employee", + required=True, + tracking=True, + default=lambda self: self._get_default_employee(), + ) + resource_id = fields.Many2one( + "resource.resource", related="employee_id.resource_id" + ) + user_id = fields.Many2one("res.users", related="employee_id.user_id") + department_id = fields.Many2one( + "hr.department", + related="employee_id.department_id", + ) + member_of_department = fields.Boolean(related="employee_id.member_of_department") + company_id = fields.Many2one( + "res.company", + default=lambda self: self.env.user.company_id.id, + required=True, + ) + state = fields.Selection( + [ + ("planified", "Planified"), + ("in_progress", "In Progress"), + ("finished", "Finished"), + ("cancel", "Cancelled"), + ], + default="planified", + tracking=True, + ) + date_start = fields.Datetime( + string="Start Date", + required=True, + tracking=True, + ) + + date_end = fields.Datetime(string="End Date", required=True, tracking=True) + + allocated_hours = fields.Float( + "Allocated Time", + compute="_compute_allocated_hours", + store=True, + readonly=False, + ) + + project_id = fields.Many2one("project.project", string="Project") + filtered_project_id = fields.Many2one("project.project") + task_id = fields.Many2one("project.task", string="Task") + ticket_id = fields.Many2one("helpdesk.ticket", string="Ticket") + + leave_warning = fields.Char(compute="_compute_leave_warning") + + # Recurrency + recurrency_id = fields.Many2one("hr.task.recurrency", string="Recurrency") + repeat = fields.Boolean( + compute="_compute_repeat", inverse="_inverse_repeat", copy=True + ) + repeat_interval = fields.Integer( + "Repeat every", + default=1, + compute="_compute_repeat_task_interval", + inverse="_inverse_repeat", + copy=True, + ) + repeat_unit = fields.Selection( + [ + ("day", "Days"), + ("week", "Weeks"), + ("month", "Months"), + ("year", "Years"), + ], + default="week", + compute="_compute_repeat_task_unit", + inverse="_inverse_repeat", + required=True, + ) + repeat_type = fields.Selection( + [ + ("forever", "Forever"), + ("until", "Until"), + ("x_times", "Number of Repetitions"), + ], + default="forever", + compute="_compute_repeat_type", + inverse="_inverse_repeat", + copy=True, + ) + repeat_until = fields.Date( + compute="_compute_repeat_task_until", + inverse="_inverse_repeat", + copy=True, + ) + repeat_number = fields.Integer( + "Repetitions", + default=1, + compute="_compute_repeat_task_number", + inverse="_inverse_repeat", + copy=True, + ) + + is_recompute_forced = fields.Boolean(default=False, string="Recompute Forced?") + + @api.onchange("filtered_project_id") + def _onchange_filtered_project_id(self): + res = {"domain": {"task_id": []}} + if self.filtered_project_id: + res["domain"].update( + {"task_id": [("project_id", "=", self.filtered_project_id.id)]} + ) + return res + + def default_get(self, fields_list): + res = super().default_get(fields_list) + + if "date_start" in fields_list: + date_start = ( + fields.Datetime.from_string(res.get("date_start")) + or self._default_date_start() + ) + date_end = ( + fields.Datetime.from_string(res.get("date_end")) + or self._default_date_end() + ) + + start = pytz.utc.localize(date_start) + end = pytz.utc.localize(date_end) + opening_hours = self._company_task_working_hours(start, end) + res["date_start"] = ( + opening_hours[0].astimezone(pytz.utc).replace(tzinfo=None) + ) + + if "date_end" in fields_list: + res["date_end"] = ( + opening_hours[1].astimezone(pytz.utc).replace(tzinfo=None) + ) + + return res + + def _company_task_working_hours(self, start, end): + company = self.company_id or self.env.company + work_interval = company.resource_calendar_id._work_intervals_batch(start, end)[ + False + ] + intervals = [ + (date_start, date_stop) for date_start, date_stop, _ in work_interval + ] + date_start, date_end = (start, end) + if intervals: + if (date_end - date_start).days == 0: + date_start = intervals[0][0] + date_end = [ + stop for _, stop in intervals if stop.date() == date_start.date() + ][-1] + else: + date_start = intervals[0][0] + date_end = intervals[-1][1] + + return (date_start, date_end) + + @api.depends("recurrency_id") + def _compute_repeat(self): + for task in self: + task.repeat = bool(task.recurrency_id) + + @api.depends("recurrency_id.repeat_interval") + def _compute_repeat_task_interval(self): + recurrency_tasks = self.filtered("recurrency_id") + for task in recurrency_tasks: + task.repeat_interval = task.recurrency_id.repeat_interval + (self - recurrency_tasks).update(self.default_get(["repeat_interval"])) + + @api.depends("recurrency_id.repeat_until", "repeat", "repeat_type") + def _compute_repeat_task_until(self): + for task in self: + repeat_until = False + if task.repeat and task.repeat_type == "until": + if task.recurrency_id and task.recurrency_id.repeat_until: + repeat_until = task.recurrency_id.repeat_until + elif task.date_start: + repeat_until = task.date_start + relativedelta(weeks=1) + task.repeat_until = repeat_until + + @api.depends("recurrency_id.repeat_number", "repeat_type") + def _compute_repeat_task_number(self): + recurrency_tasks = self.filtered("recurrency_id") + for task in recurrency_tasks: + task.repeat_number = task.recurrency_id.repeat_number + (self - recurrency_tasks).update(self.default_get(["repeat_number"])) + + @api.depends("recurrency_id.repeat_unit") + def _compute_repeat_task_unit(self): + non_recurrent_tasks = self.filtered(lambda task: not task.recurrency_id) + recurrent_tasks = self - non_recurrent_tasks + + for task in recurrent_tasks: + task.repeat_unit = task.recurrency_id.repeat_unit + + non_recurrent_tasks.update(self.default_get(["repeat_unit"])) + + @api.depends("recurrency_id.repeat_type") + def _compute_repeat_type(self): + recurrency_tasks = self.filtered("recurrency_id") + for task in recurrency_tasks: + task.repeat_type = task.recurrency_id.repeat_type + (self - recurrency_tasks).update(self.default_get(["repeat_type"])) + + def _inverse_repeat(self): + for task in self: + if task.repeat and not task.recurrency_id.id: # create the recurrence + repeat_until = False + repeat_number = 0 + if task.repeat_type == "until": + repeat_until = fields.Datetime.to_datetime(task.repeat_until) + repeat_until = ( + repeat_until.replace( + tzinfo=pytz.timezone( + task.company_id.resource_calendar_id.tz or "UTC" + ) + ) + .astimezone(pytz.utc) + .replace(tzinfo=None) + ) + if task.repeat_type == "x_times": + repeat_number = task.repeat_number + recurrency_values = { + "repeat_interval": task.repeat_interval, + "repeat_unit": task.repeat_unit, + "repeat_until": repeat_until, + "repeat_number": repeat_number, + "repeat_type": task.repeat_type, + "company_id": task.company_id.id, + } + recurrence = self.env["hr.task.recurrency"].create(recurrency_values) + task.recurrency_id = recurrence + task.recurrency_id._repeat_task() + elif not task.repeat and task.recurrency_id.id: + task.recurrency_id._delete_task(task.date_end) + task.recurrency_id.unlink() + + @api.depends("date_start", "date_end", "employee_id") + def _compute_leave_warning(self): + assigned_tasks = self.filtered(lambda s: s.employee_id and s.date_start) + unassigned_tasks = self - assigned_tasks + unassigned_tasks.update( + { + "leave_warning": False, + } + ) + + if not assigned_tasks: + return + + min_date = min(assigned_tasks.mapped("date_start")) + date_from = max(min_date, fields.Datetime.today()) + date_to = max(assigned_tasks.mapped("date_end")) + employee_ids = assigned_tasks.mapped("employee_id") + + leaves = self.env["hr.leave"]._get_leaves( + date_from=date_from, + date_to=date_to, + employee_ids=employee_ids, + ) + + for task in assigned_tasks: + task_leaves = leaves.get(task.employee_id.id) + if task_leaves: + warning = self.env["hr.leave"]._get_leave_message_warning( + leaves=task_leaves, + employee=task.employee_id, + date_from=task.date_start, + date_to=task.date_end, + ) + task.leave_warning = warning + else: + task.leave_warning = False + + def _compute_title(self): + for record in self: + if record.name: + record.title = record.name + else: + record.title = "" + + @api.depends("type", "task_id", "project_id", "ticket_id") + def _compute_name(self): + for record in self: + if record.type == "task": + record.name = record.task_id.name + elif record.type == "project": + record.name = record.project_id.name + elif record.type == "ticket": + record.name = record.ticket_id.name + else: + record.name = "" + + @api.depends( + "date_start", + "date_end", + "employee_id", + "employee_id.resource_calendar_id", + "is_recompute_forced", + ) + def _compute_allocated_hours(self): + """ + Compute working hours considering: + - Employee's work schedule (unless is_recompute_forced is True) + - Approved time off (vacations, leaves) + - Public holidays + """ + for record in self: + if not (record.date_start and record.date_end and record.employee_id): + record.allocated_hours = 0.0 + continue + + if record.is_recompute_forced: + total_seconds = (record.date_end - record.date_start).total_seconds() + record.allocated_hours = round(total_seconds / 3600.0, 2) + continue + + # Normal calculation for non-forced records + calendar = ( + record.employee_id.resource_calendar_id + or record.env.company.resource_calendar_id + ) + if not calendar: + record.allocated_hours = 0.0 + continue + + # Get base working hours without considering leaves + work_days_data = record.employee_id._get_work_days_data_batch( + record.date_start, record.date_end + )[record.employee_id.id] + base_hours = work_days_data["hours"] + + # Get leaves (including vacations) + domain = [ + ("employee_id", "=", record.employee_id.id), + ("state", "=", "validate"), + ("date_from", "<=", record.date_end), + ("date_to", ">=", record.date_start), + ] + leaves = record.env["hr.leave"].search(domain) + + # Calculate leave hours + leave_hours = 0 + for leave in leaves: + # Get overlapping period + leave_start = max(leave.date_from, record.date_start) + leave_end = min(leave.date_to, record.date_end) + + # Calculate leave hours for the overlapping period + leave_days_data = record.employee_id._get_work_days_data_batch( + leave_start, leave_end + )[record.employee_id.id] + leave_hours += leave_days_data["hours"] + + # Subtract leave hours from base hours + record.allocated_hours = max(0, base_hours - leave_hours) + + def _get_tz(self): + return ( + self.env.user.tz + or self.employee_id.tz + or self.employee_id.tz + or self._context.get("tz") + or self.company_id.resource_calendar_id.tz + or "UTC" + ) + + def _add_delta_with_dst(self, start, delta): + try: + tz = pytz.timezone(self._get_tz()) + except pytz.UnknownTimeZoneError: + tz = pytz.UTC + start = start.replace(tzinfo=pytz.utc).astimezone(tz).replace(tzinfo=None) + result = start + delta + return tz.localize(result).astimezone(pytz.utc).replace(tzinfo=None) + + @api.onchange("type") + def _onchange_type(self): + if self.type == "task": + self.write({"project_id": False, "ticket_id": False}) + elif self.type == "project": + self.write({"task_id": False, "ticket_id": False}) + elif self.type == "ticket": + self.write({"project_id": False, "task_id": False}) + + def write(self, values): + result = super().write(values) + if any( + key + in ( + "repeat", + "repeat_unit", + "repeat_type", + "repeat_until", + "repeat_interval", + "repeat_number", + ) + for key in values + ): + for task in self: + if task.recurrency_id and values.get("repeat") is None: + repeat_type = ( + values.get("repeat_type") or task.recurrency_id.repeat_type + ) + repeat_until = ( + values.get("repeat_until") or task.recurrency_id.repeat_until + ) + repeat_number = values.get("repeat_number", 0) or task.repeat_number + if repeat_type == "until": + repeat_until = datetime.combine( + fields.Date.to_date(repeat_until), + datetime.max.time(), + ) + repeat_until = ( + repeat_until.replace( + tzinfo=pytz.timezone( + task.company_id.resource_calendar_id.tz or "UTC" + ) + ) + .astimezone(pytz.utc) + .replace(tzinfo=None) + ) + recurrency_values = { + "repeat_interval": values.get("repeat_interval") + or task.recurrency_id.repeat_interval, + "repeat_unit": values.get("repeat_unit") + or task.recurrency_id.repeat_unit, + "repeat_until": ( + repeat_until if repeat_type == "until" else False + ), + "repeat_number": repeat_number, + "repeat_type": repeat_type, + "company_id": task.company_id.id, + } + task.recurrency_id.write(recurrency_values) + if task.repeat_type == "x_times": + recurrency_values[ + "repeat_until" + ] = task.recurrency_id._get_recurrence_last_datetime() + date_end = ( + task.date_end + if values.get("repeat_unit") + else recurrency_values.get("repeat_until") + ) + task.recurrency_id._delete_task(date_end) + task.recurrency_id._repeat_task() + return result + + def action_cancel(self): + self.write({"state": "cancel"}) + return True + + def action_planified(self): + self.write({"state": "planified"}) + return True + + def action_in_progress(self): + self.write({"state": "in_progress"}) + return True + + def action_finished(self): + self.write({"state": "finished"}) + return True + + def cron_update_task_state(self): + self.search( + [ + ("date_end", "<", fields.Datetime.now()), + ("state", "=", "in_progress"), + ] + ).write({"state": "finished"}) + + self.search( + [ + ("date_start", "<=", fields.Datetime.now()), + ("state", "=", "planified"), + ] + ).write({"state": "in_progress"}) diff --git a/hr_planning_resources/models/hr_task_mixin.py b/hr_planning_resources/models/hr_task_mixin.py new file mode 100644 index 0000000..3dcaa4d --- /dev/null +++ b/hr_planning_resources/models/hr_task_mixin.py @@ -0,0 +1,62 @@ +from odoo import _, fields, models + + +class HrTaskMixin(models.AbstractModel): + _name = "hr.task.mixin" + _description = "HR Task Mixin" + + hr_task_count = fields.Integer( + compute="_compute_hr_task_count", string="HR Task Count" + ) + + def _compute_hr_task_count(self): + """ + Compute the number of HR tasks related to this record. + Should be implemented by concrete models that inherit this mixin. + """ + raise NotImplementedError( + _("The method _compute_hr_task_count must be implemented in the subclass.") + ) + + def action_view_hr_task(self): + self.ensure_one() + + # Determine the domain based on the model type + domain_map = { + "project.project": [("project_id", "=", self.id)], + "helpdesk.ticket": [("ticket_id", "=", self.id)], + } + domain = domain_map.get(self._name, [("task_id", "=", self.id)]) + + # Prepare and return the action to view the HR tasks + return { + "type": "ir.actions.act_window", + "res_model": "hr.task", + "view_mode": "tree,form", + "views": [[False, "tree"], [False, "form"]], + "context": dict(self.env.context), + "domain": domain, + } + + def action_create_hr_task(self): + self.ensure_one() + + # Fetch the form view for creating HR tasks + view_id = self.env.ref("hr_planning_resources.create_hr_task_view_form").id + + # Update context with default resource model and default resource ID + ctx = dict( + self.env.context, + default_res_model=self._name, + default_res_id=self.id, + ) + + # Return the action to open the create HR task form + return { + "type": "ir.actions.act_window", + "res_model": "create.hr.task", + "view_mode": "form", + "views": [[view_id, "form"]], + "context": ctx, + "target": "new", + } diff --git a/hr_planning_resources/models/hr_task_recurrency.py b/hr_planning_resources/models/hr_task_recurrency.py new file mode 100644 index 0000000..fc96f6e --- /dev/null +++ b/hr_planning_resources/models/hr_task_recurrency.py @@ -0,0 +1,310 @@ +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.date_utils import get_timedelta + +TASK_GENERATION_INTERVAL = 1 +_logger = logging.getLogger(__name__) + + +class HrTaskRecurrency(models.Model): + _name = "hr.task.recurrency" + _description = "Hr Task Recurrency" + + task_ids = fields.One2many( + comodel_name="hr.task", + inverse_name="recurrency_id", + string="Related Planning Tasks", + ) + repeat_interval = fields.Integer("Repeat Every", default=1, required=True) + repeat_unit = fields.Selection( + [ + ("day", "Days"), + ("week", "Weeks"), + ("month", "Months"), + ("year", "Years"), + ], + default="week", + required=True, + ) + repeat_type = fields.Selection( + [ + ("forever", "Forever"), + ("until", "Until"), + ("x_times", "Number of Repetitions"), + ], + string="Weeks", + default="forever", + ) + repeat_until = fields.Datetime( + help="Up to which date should the plannings be repeated", + ) + repeat_number = fields.Integer( + string="Repetitions", help="No Of Repetitions of the plannings" + ) + last_generated_end_datetime = fields.Datetime( + "Last Generated End Date", readonly=True + ) + company_id = fields.Many2one( + "res.company", + string="Company", + readonly=True, + required=True, + default=lambda self: self.env.company, + ) + + @api.constrains("repeat_number", "repeat_type") + def _check_repeat_number(self): + if self.filtered(lambda t: t.repeat_type == "x_times" and t.repeat_number < 0): + raise ValidationError(_("The number of repetitions cannot be negative.")) + + @api.constrains("company_id", "task_ids") + def _check_multi_company(self): + for recurrency in self: + if any( + recurrency.company_id != planning.company_id + for planning in recurrency.task_ids + ): + raise ValidationError( + _("An shift must be in the same company as its recurrency.") + ) + + def name_get(self): + result = [] + for recurrency in self: + if recurrency.repeat_type == "forever": + name = _(f"Forever, every {recurrency.repeat_interval} week(s)") + else: + name = _( + f"Every {recurrency.repeat_interval} " + "week(s) until {recurrency.repeat_until}" + ) + result.append([recurrency.id, name]) + return result + + @api.model + def _cron_schedule_next(self): + companies = self.env["res.company"].search([]) + now = fields.Datetime.now() + for company in companies: + delta = get_timedelta(TASK_GENERATION_INTERVAL, "month") + + recurrencies = self.search( + [ + "&", + "&", + ("company_id", "=", company.id), + ("last_generated_end_datetime", "<", now + delta), + "|", + ("repeat_until", "=", False), + ("repeat_until", ">", now - delta), + ] + ) + recurrencies._repeat_task(now + delta) + + def _repeat_task(self, stop_datetime=False): + """ + Repeats tasks based on recurrency settings. + + Args: + stop_datetime (datetime, optional): Limit datetime for task generation + + Returns: + list: Created task IDs + + Note: + - Creates new tasks based on the latest task template + - Updates last generated datetime + - Removes recurrency if no template task exists + """ + HrTask = self.env["hr.task"] + created_task_ids = [] + + for recurrency in self: + try: + # Get template task + template_task = self._get_latest_task(recurrency) + if not template_task: + _logger.info( + "No template task found for recurrency %s. " + "Removing recurrency.", + recurrency.id, + ) + recurrency.unlink() + continue + + # Calculate date limits + date_limits = self._calculate_date_limits(recurrency, stop_datetime) + if not date_limits: + continue + + # Generate new tasks + new_tasks = self._create_recurring_tasks( + template_task, recurrency, date_limits, HrTask + ) + + if new_tasks: + created_task_ids.extend(new_tasks.ids) + + except Exception as e: + _logger.error( + "Error processing recurrency %s: %s", + recurrency.id, + str(e), + exc_info=True, + ) + + return created_task_ids + + def _calculate_date_limits(self, recurrency, stop_datetime): + """ + Calculates the effective date limits for task generation. + + Returns: + dict: Contains 'range_limit' and 'task_duration' + """ + recurrence_end_dt = self._get_recurrence_end_datetime(recurrency) + effective_stop_dt = self._get_stop_datetime(recurrency, stop_datetime) + + # Filter out None values and get minimum + valid_dates = [dt for dt in [recurrence_end_dt, effective_stop_dt] if dt] + if not valid_dates: + return None + + return { + "range_limit": min(valid_dates), + "task_duration": recurrency.task_ids[0].date_end + - recurrency.task_ids[0].date_start, + } + + def _create_recurring_tasks(self, template_task, recurrency, date_limits, HrTask): + """ + Creates recurring tasks based on template and limits. + + Args: + template_task: The task to use as template + recurrency: The recurrency record + date_limits: Dictionary with range_limit and task_duration + HrTask: Task model environment + + Returns: + recordset: Created tasks + """ + task_values_list = self._generate_task_values_list( + template_task, + recurrency, + date_limits["range_limit"], + date_limits["task_duration"], + ) + + if not task_values_list: + return HrTask.browse() + + # Create tasks in batch + new_tasks = HrTask.create(task_values_list) + + # Update recurrency last generated datetime + if new_tasks: + last_task_start = task_values_list[-1]["date_start"] + recurrency.write({"last_generated_end_datetime": last_task_start}) + + return new_tasks + + def _generate_task_values_list(self, task, recurrency, range_limit, task_duration): + """ + Generates list of values for creating recurring tasks. + + Improved version with batch processing and validation. + """ + + def get_next_start_dates(): + for i in range(1, 365 * 5): # 5 years limit + next_start = self.env["hr.task"]._add_delta_with_dst( + task.date_start, + get_timedelta( + recurrency.repeat_interval * i, + recurrency.repeat_unit, + ), + ) + if next_start >= range_limit: + break + yield next_start + + # Generate all dates first + start_dates = list(get_next_start_dates()) + + if not start_dates: + return [] + + # Prepare base values from template task + base_values = task.copy_data( + { + "recurrency_id": recurrency.id, + "company_id": recurrency.company_id.id, + "repeat": True, + "state": "planified", + } + )[0] + + # Generate all task values in batch + return [ + { + **base_values, + "date_start": start, + "date_end": start + task_duration, + } + for start in start_dates + ] + + def _get_latest_task(self, recurrency): + return self.env["hr.task"].search( + [("recurrency_id", "=", recurrency.id)], + limit=1, + order="date_start DESC", + ) + + def _get_recurrence_end_datetime(self, recurrency): + if recurrency.repeat_type == "until": + return recurrency.repeat_until + if recurrency.repeat_type == "x_times": + return recurrency._get_recurrence_last_datetime() + return False + + def _get_stop_datetime(self, recurrency, stop_datetime): + if not stop_datetime: + stop_datetime = fields.Datetime.now() + get_timedelta( + TASK_GENERATION_INTERVAL, + "month", + ) + return stop_datetime + + def _delete_task(self, date_start): + tasks = self.env["hr.task"].search( + [ + ("recurrency_id", "in", self.ids), + ("date_start", ">=", date_start), + ("state", "=", "planified"), + ] + ) + tasks.unlink() + + def _get_recurrence_last_datetime(self): + self.ensure_one() + date_end = self.env["hr.task"].search_read( + [("recurrency_id", "=", self.id)], + ["date_end"], + order="date_end", + limit=1, + ) + timedelta = get_timedelta( + self.repeat_number * self.repeat_interval, self.repeat_unit + ) + if timedelta.days > 999: + raise ValidationError( + _( + "Recurring shifts cannot be planned further than 999 days in the " + "future." + ) + ) + return date_end[0]["date_end"] + timedelta diff --git a/hr_planning_resources/models/project_project.py b/hr_planning_resources/models/project_project.py new file mode 100644 index 0000000..14aaba0 --- /dev/null +++ b/hr_planning_resources/models/project_project.py @@ -0,0 +1,19 @@ +from odoo import api, models + + +class ProjectProject(models.Model): + _name = "project.project" + _inherit = ["project.project", "hr.task.mixin"] + + def _compute_hr_task_count(self): + for record in self: + project_count = self.env["hr.task"].search_count( + [("project_id", "=", record.id)] + ) + record.hr_task_count = project_count + + @api.model_create_multi + def create(self, vals_list): + if self.env.context.get("default_user_id", False): + vals_list[0]["user_id"] = [(4, self.env.context["default_user_id"])] + return super().create(vals_list) diff --git a/hr_planning_resources/models/project_task.py b/hr_planning_resources/models/project_task.py new file mode 100644 index 0000000..f8ab7b1 --- /dev/null +++ b/hr_planning_resources/models/project_task.py @@ -0,0 +1,17 @@ +from odoo import api, models + + +class ProjectTask(models.Model): + _name = "project.task" + _inherit = ["project.task", "hr.task.mixin"] + + def _compute_hr_task_count(self): + for record in self: + task_count = self.env["hr.task"].search_count([("task_id", "=", record.id)]) + record.hr_task_count = task_count + + @api.model_create_multi + def create(self, vals_list): + if self.env.context.get("default_user_id", False): + vals_list[0]["user_ids"] = [(4, self.env.context["default_user_id"])] + return super().create(vals_list) diff --git a/hr_planning_resources/readme/CONTRIBUTORS.rst b/hr_planning_resources/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..ca15340 --- /dev/null +++ b/hr_planning_resources/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* `Binhex `_: + + * Antonio Ruban + * Adasat Torres de Leon + * Zuzanna Elzbieta Szalaty Szalaty diff --git a/hr_planning_resources/readme/DESCRIPTION.rst b/hr_planning_resources/readme/DESCRIPTION.rst new file mode 100644 index 0000000..76f9d08 --- /dev/null +++ b/hr_planning_resources/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module is designed to improve human resources planning and management within an organization. +It's provides advanced tools for task scheduling, permission management, and resource planning, +allowing managers and employees to organize and visualize their schedules efficiently. diff --git a/hr_planning_resources/readme/USAGE.rst b/hr_planning_resources/readme/USAGE.rst new file mode 100644 index 0000000..7dea8a2 --- /dev/null +++ b/hr_planning_resources/readme/USAGE.rst @@ -0,0 +1,5 @@ +1. Navigate to the "Planning" section in the main menu. +2. Select "My Planning" to view your assigned tasks. +3. To assign a new task, select "My Planning", click on the New button. +4. You can assign it from the same task, ticket or project using the assign to planning button. +5. In the task, if the "Force recalculation" checkbox is checked, all hours that have elapsed will be calculated. diff --git a/hr_planning_resources/security/ir.model.access.csv b/hr_planning_resources/security/ir.model.access.csv new file mode 100644 index 0000000..4c1ede7 --- /dev/null +++ b/hr_planning_resources/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_task,hr.task.access,model_hr_task,base.group_user,1,1,1,1 +access_create_hr_task_manager,create_hr_task_manager,model_create_hr_task,,1,1,1,1 +access_hr_task_recurrency_manager,hr_task_recurrency_manager,model_hr_task_recurrency,,1,1,1,1 diff --git a/hr_planning_resources/static/description/icon.png b/hr_planning_resources/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/hr_planning_resources/static/description/icon.png differ diff --git a/hr_planning_resources/static/description/index.html b/hr_planning_resources/static/description/index.html new file mode 100644 index 0000000..74981d6 --- /dev/null +++ b/hr_planning_resources/static/description/index.html @@ -0,0 +1,441 @@ + + + + + +HR Resource Planner + + + +
+

HR Resource Planner

+ + +

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

+

This module is designed to improve human resources planning and management within an organization. +It’s provides advanced tools for task scheduling, permission management, and resource planning, +allowing managers and employees to organize and visualize their schedules efficiently.

+

Table of contents

+ +
+

Usage

+
    +
  1. Navigate to the “Planning” section in the main menu.
  2. +
  3. Select “My Planning” to view your assigned tasks.
  4. +
  5. To assign a new task, select “My Planning”, click on the New button.
  6. +
  7. You can assign it from the same task, ticket or project using the assign to planning button.
  8. +
  9. In the task, if the “Force recalculation” checkbox is checked, all hours that have elapsed will be calculated.
  10. +
+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Binhex
  • +
+
+
+

Contributors

+
    +
  • Binhex:
      +
    • Antonio Ruban
    • +
    • Adasat Torres de Leon
    • +
    • Zuzanna Elzbieta Szalaty Szalaty
    • +
    +
  • +
+
+
+

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

+

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

+
+
+
+ + diff --git a/hr_planning_resources/static/description/planning_icon.png b/hr_planning_resources/static/description/planning_icon.png new file mode 100644 index 0000000..84dbdba Binary files /dev/null and b/hr_planning_resources/static/description/planning_icon.png differ diff --git a/hr_planning_resources/static/src/js/timeline_controller.esm.js b/hr_planning_resources/static/src/js/timeline_controller.esm.js new file mode 100644 index 0000000..fb3deaa --- /dev/null +++ b/hr_planning_resources/static/src/js/timeline_controller.esm.js @@ -0,0 +1,27 @@ +/** @odoo-module */ + +import TimelineController from "web_timeline.TimelineController"; + +TimelineController.include({ + create_completed: function (id) { + const self = this; + return this._rpc({ + model: this.model.modelName, + method: "read", + args: [id, this.model.fieldNames], + context: this.context, + }).then((records) => { + const new_event = this.renderer.event_data_transform(records[0]); + const items = this.renderer.timeline.itemsData; + items.add(new_event); + + self.model.data.data.push(records[0]); + const params = { + domain: this.renderer.last_domains, + context: this.context, + groupBy: this.renderer.last_group_bys, + }; + this.update(params, {adjust_window: false}); + }); + }, +}); diff --git a/hr_planning_resources/static/src/js/timeline_renderer.js b/hr_planning_resources/static/src/js/timeline_renderer.js new file mode 100644 index 0000000..5ddfe22 --- /dev/null +++ b/hr_planning_resources/static/src/js/timeline_renderer.js @@ -0,0 +1,17 @@ +odoo.define("hr_task_planner.timeline_renderer", function (require) { + "use strict"; + + const TimelineRenderer = require("web_timeline.TimelineRenderer"); + + TimelineRenderer.include({ + events: _.extend({}, TimelineRenderer.prototype.events, { + "click .oe_hr_task_planner_new_task": "_onNewTask", + }), + _onNewTask: function (ev) { + ev.preventDefault(); + this.on_add(ev, () => { + console.log("on_add"); + }); + }, + }); +}); diff --git a/hr_planning_resources/static/src/scss/hr_planning_resources.scss b/hr_planning_resources/static/src/scss/hr_planning_resources.scss new file mode 100644 index 0000000..06a84ad --- /dev/null +++ b/hr_planning_resources/static/src/scss/hr_planning_resources.scss @@ -0,0 +1,8 @@ +.oe_timeline_buttons { + padding-left: 16px; + padding-right: 16px; +} + +.oe_timeline_view .vis-timeline .vis-item .vis-item-overflow { + overflow: hidden; +} diff --git a/hr_planning_resources/static/src/xml/web_timeline.xml b/hr_planning_resources/static/src/xml/web_timeline.xml new file mode 100644 index 0000000..3d83ef6 --- /dev/null +++ b/hr_planning_resources/static/src/xml/web_timeline.xml @@ -0,0 +1,18 @@ + + diff --git a/hr_planning_resources/tests/__init__.py b/hr_planning_resources/tests/__init__.py new file mode 100644 index 0000000..5bd1d58 --- /dev/null +++ b/hr_planning_resources/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_task diff --git a/hr_planning_resources/tests/common.py b/hr_planning_resources/tests/common.py new file mode 100644 index 0000000..17e9731 --- /dev/null +++ b/hr_planning_resources/tests/common.py @@ -0,0 +1,94 @@ +from dateutil.relativedelta import relativedelta + +from odoo import Command, fields +from odoo.tests import common, tagged + + +@tagged("post_install", "-at_install") +class TestHrPlanningCommon(common.TransactionCase): + def setUp(self): + super().setUp() + + # create a new user + self.john_doe_user = self.env["res.users"].create( + { + "name": "John Doe", + "login": "test_user", + "email": "test_user@example.com", + "password": "test_password", + "company_id": self.env.ref("base.main_company").id, + } + ) + # create a new department + self.department = self.env["hr.department"].create( + { + "name": "Department", + } + ) + # create a new employee + self.john_doe_employee = self.env["hr.employee"].create( + { + "name": "John Doe", + "user_id": self.john_doe_user.id, + "department_id": self.department.id, + } + ) + + # create a new project + self.project = self.env["project.project"].create( + { + "name": "Project", + "user_id": self.john_doe_user.id, + "date_start": fields.Date.today(), + } + ) + + # create a new task + self.task = self.env["project.task"].create( + { + "name": "Task", + "user_ids": [Command.link(self.john_doe_user.id)], + "project_id": self.project.id, + "date_assign": fields.Datetime.now(), + "date_deadline": fields.Datetime.now() + relativedelta(days=1), + } + ) + + # Create a new helpdesk.team + self.helpdesk_team = self.env["helpdesk.ticket.team"].create( + { + "name": "Team", + "user_ids": [Command.link(self.john_doe_user.id)], + } + ) + + # create a new ticket + self.ticket = self.env["helpdesk.ticket"].create( + { + "name": "Ticket", + "user_id": self.john_doe_user.id, + "team_id": self.helpdesk_team.id, + "description": "Ticket description", + } + ) + + def create_hr_task(self, ttype="task"): + # Create a new hr.taskk + name = "Task" + if ttype == "project": + name = "Project" + elif ttype == "ticket": + name = "Ticket" + + return self.env["hr.task"].create( + { + "name": name, + "employee_id": self.john_doe_employee.id, + "project_id": (self.project.id if ttype == "project" else False), + "task_id": self.task.id if ttype == "task" else False, + "ticket_id": self.ticket.id if ttype == "ticket" else False, + "type": ttype, + "date_start": fields.Datetime.now(), + "date_end": fields.Datetime.now() + relativedelta(days=1), + } + ) diff --git a/hr_planning_resources/tests/test_hr_task.py b/hr_planning_resources/tests/test_hr_task.py new file mode 100644 index 0000000..14897cc --- /dev/null +++ b/hr_planning_resources/tests/test_hr_task.py @@ -0,0 +1,31 @@ +from .common import TestHrPlanningCommon + + +class TestHrTask(TestHrPlanningCommon): + def test_00_hr_task_type_task(self): + # Create a new hr.task + hr_task_instance = self.create_hr_task() + self.assertEqual(hr_task_instance.name, "Task") + self.assertEqual(hr_task_instance.employee_id, self.john_doe_employee) + self.assertEqual(hr_task_instance.ticket_id.id, False) + self.assertEqual(hr_task_instance.project_id.id, False) + self.assertEqual(hr_task_instance.type, "task") + self.assertEqual(hr_task_instance.task_id, self.task) + + def test_01_hr_task_type_project(self): + # Create a new hr.task + hr_task_project = self.create_hr_task("project") + self.assertEqual(hr_task_project.name, "Project") + self.assertEqual(hr_task_project.employee_id, self.john_doe_employee) + self.assertEqual(hr_task_project.project_id, self.project) + self.assertEqual(hr_task_project.ticket_id.id, False) + self.assertEqual(hr_task_project.task_id.id, False) + + def test_02_hr_task_type_ticket(self): + # Create a new hr.task + hr_task_ticket = self.create_hr_task("ticket") + self.assertEqual(hr_task_ticket.name, "Ticket") + self.assertEqual(hr_task_ticket.employee_id, self.john_doe_employee) + self.assertEqual(hr_task_ticket.ticket_id, self.ticket) + self.assertEqual(hr_task_ticket.project_id.id, False) + self.assertEqual(hr_task_ticket.task_id.id, False) diff --git a/hr_planning_resources/views/hr_task_views.xml b/hr_planning_resources/views/hr_task_views.xml new file mode 100644 index 0000000..20c04c9 --- /dev/null +++ b/hr_planning_resources/views/hr_task_views.xml @@ -0,0 +1,348 @@ + + + + hr.task.view.tree + hr.task + + + + + + + + + + + + + hr.task.form.view + hr.task + +
+
+
+ + + + +
+ + + +
+ +
+
+ + hr.task.timeline.view + hr.task + timeline + + + + + + + + + +
+ + + + + + +
+
+
+
+
+
+ + hr.task.search.view + hr.task + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Planning resource + hr.task + timeline,tree,form,search + + + My Planning + hr.task + timeline,tree,form,search + {'search_default_my_shifts': 1} + + + My Department + hr.task + timeline,tree,form,search + {'search_default_my_department': 1} + + + +
diff --git a/hr_planning_resources/views/menus.xml b/hr_planning_resources/views/menus.xml new file mode 100644 index 0000000..ac32585 --- /dev/null +++ b/hr_planning_resources/views/menus.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/hr_planning_resources/views/project_views.xml b/hr_planning_resources/views/project_views.xml new file mode 100644 index 0000000..3f5dd7f --- /dev/null +++ b/hr_planning_resources/views/project_views.xml @@ -0,0 +1,113 @@ + + + + + view.edit.project.form + project.project + + + + + + + + + + + + view.project.task.form + project.task + + + + + + + + + + +