diff --git a/project_forecast_line/README.rst b/project_forecast_line/README.rst new file mode 100644 index 0000000000..619b18badf --- /dev/null +++ b/project_forecast_line/README.rst @@ -0,0 +1,174 @@ +====================== +Project Forecast Lines +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f6a3feb1d9b6593b5322c73cc13b475f38f01898496e691b2b2efc14fe320404 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/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%2Fproject-lightgray.png?logo=github + :target: https://github.com/OCA/project/tree/16.0/project_forecast_line + :alt: OCA/project +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/project-16-0/project-16-0-project_forecast_line + :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/project&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to plan your resources using forecast lines. + +For each employee of the company, the module will generate forecast line +records with a positive capacity based on their working time schedules. Then, +tasks assigned to employees will generate forecast lines with a negative +capacity which will "consume" the work time capacity of the employees. + +The idea is that you can then see the work capacity and scheduled work of +people by summing the "forecasts" per time period. If you have more resources +(positive forecast) than work (negative forecast) you will have a positive net +sum. Otherwise you are in trouble and need to recruit or reschedule your +work. Another way to use the report is checking when the work capacity of a +department becomes positive (or high enough) in order to provide you potential +customers with an estimate of when a project would be able to start. + +Forecast lines also come in two states "forecast" or "confirmed", depending on +whether the consumption is confirmed or not. For instance, holidays requests +and sales quotation lines create lines of type "forecast", whereas tasks for +project which are in a running state create lines with type "confirmed". + +To get the best experience using the Forecast application you may want to install: + +* project_forecast_line_holidays_public module which takes public holidays into + account during forecast lines creation + +* project_forecast_line_bokeh_chart module which improves the reports of + project_forecast_line module by using the bokeh widget available in OCA/web + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Forecast lines have the following data: + +* Forecast hours: it is positive for resources (employees) and negative for + things which consume time (project tasks, for instance) + +* From and To date which are the beginning and ending of the period of the + capacity + +* Consolidated forecast: this is a computed field, which is computed as follows: + + * for costs (project tasks for instance) we take the absolute value of the + forecast hours (so it is a positive number) + + * for resources (employee capacity for a period), we take the capacity and + substract all the costs for that employee on the same period. So it will be + positive if the employee still has some free time, and negative if he is + overloaded with work. + + * this consolidated forecast is currently converted to days to ease + readability of the forecast report + + +Objects creating forecast lines: + +* employees with a forecast role will create forecast line with a positive + capacity and type "confirmed" for each day on which they work. This + information comes from their work calendar, and the different roles that are + linked to the employee. + +* draft sale orders (if enabled in the settings) will create forecast lines of + type "forecast" for each sale order line having a product with a forecast + role and start and end dates. The forecast hours are negative + +* confirmed sale orders don't create forecast lines. This is handled by the + tasks created at the confirmation of the sale order + +* project tasks create forecast lines if they have a linked role and planned start/end + dates. The type of the line will depend on the related project's stage. The + `forecast_hours` field is based on the remaining time of the task, which is spread + on the work days of the planned start and end date of the task. If the + current date is in the middle of the planned duration of the task, it is used + as the start date. If the planned end date is in the past the task does not + generate forecast lines (and you need to fix your planning). In case multiple + employees are assigned to the task the forecast is split equally between + them. + +* holiday requests create negative forecast lines with type "forecast" when + they are pending manager validation. + +* Validated holiday requests do not generate forecast lines, as they alter the + work calendar of the employee: the employee will not have a positive line + associated to his leave days. + +The creation of forecast lines is done either in real time when some actions +are performed by the user (requesting leaves, updating the remaining time on a +project task, timesheeting) and also via a cron that runs on a daily basis. The +cron is required to cleanup lines related to dates in the past and to recompute +the lines related to project tasks by computing the ratio of remaing time on +the tasks on the remaining days, for tasks which are in progress. So, to start +using consolidated forecast report you first need to set everything mentioned +in Usage section. Then, probably run Forecast recomputation cron manually from +Scheduled Actions or wait till cron creates records. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp SA + +Contributors +~~~~~~~~~~~~ + +* Alexandre Fayolle +* Maksym Yankin + +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_forecast_line/__init__.py b/project_forecast_line/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/project_forecast_line/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_forecast_line/__manifest__.py b/project_forecast_line/__manifest__.py new file mode 100644 index 0000000000..34d6bca66e --- /dev/null +++ b/project_forecast_line/__manifest__.py @@ -0,0 +1,38 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Project Forecast Lines", + "summary": "Project Forecast Lines", + "version": "16.0.1.0.0", + "author": "Camptocamp SA, Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Project", + "website": "https://github.com/OCA/project", + "depends": ["sale_timesheet", "sale_project", "hr_holidays"], + "data": [ + "security/forecast_line_security.xml", + "security/ir.model.access.csv", + "views/sale_order_views.xml", + "views/hr_employee_views.xml", + "views/forecast_line_views.xml", + "views/forecast_role_views.xml", + "views/product_views.xml", + "views/project_task_views.xml", + "views/project_project_stage_views.xml", + "views/res_config_settings_views.xml", + "data/ir_cron.xml", + "data/project_data.xml", + ], + "demo": [ + "demo/res_users.xml", + "demo/forecast_role.xml", + "demo/hr_job.xml", + "demo/hr_employee.xml", + "demo/product.xml", + "demo/project.xml", + "demo/sale.xml", + ], + "installable": True, + "development_status": "Alpha", + "application": True, +} diff --git a/project_forecast_line/data/ir_cron.xml b/project_forecast_line/data/ir_cron.xml new file mode 100644 index 0000000000..21563c6143 --- /dev/null +++ b/project_forecast_line/data/ir_cron.xml @@ -0,0 +1,14 @@ + + + + Forecast recomputation + + + code + model._cron_recompute_all() + 1 + days + -1 + + + diff --git a/project_forecast_line/data/project_data.xml b/project_forecast_line/data/project_data.xml new file mode 100644 index 0000000000..1dd857c7af --- /dev/null +++ b/project_forecast_line/data/project_data.xml @@ -0,0 +1,19 @@ + + + + + forecast + + + + confirmed + + + + + + + + + + diff --git a/project_forecast_line/demo/forecast_role.xml b/project_forecast_line/demo/forecast_role.xml new file mode 100644 index 0000000000..bc7cdbf70a --- /dev/null +++ b/project_forecast_line/demo/forecast_role.xml @@ -0,0 +1,15 @@ + + + + Project Manager + Takes care of project management tasks + + + Consultant + Takes care of business analysis + + + Software Development + Takes care of writing code and solving bugs + + diff --git a/project_forecast_line/demo/hr_employee.xml b/project_forecast_line/demo/hr_employee.xml new file mode 100644 index 0000000000..4317334c21 --- /dev/null +++ b/project_forecast_line/demo/hr_employee.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project_forecast_line/demo/hr_job.xml b/project_forecast_line/demo/hr_job.xml new file mode 100644 index 0000000000..bdade411b3 --- /dev/null +++ b/project_forecast_line/demo/hr_job.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/project_forecast_line/demo/product.xml b/project_forecast_line/demo/product.xml new file mode 100644 index 0000000000..659f27c981 --- /dev/null +++ b/project_forecast_line/demo/product.xml @@ -0,0 +1,40 @@ + + + + Consultant + + 80 + 120 + service + + + + task_in_project + delivered_timesheet + + + Project Manager + + 950 + 150 + service + + + + task_in_project + delivered_timesheet + + + Software Developer + + 80 + 120 + service + + + + task_in_project + delivered_timesheet + + + diff --git a/project_forecast_line/demo/project.xml b/project_forecast_line/demo/project.xml new file mode 100644 index 0000000000..8ffd3452df --- /dev/null +++ b/project_forecast_line/demo/project.xml @@ -0,0 +1,122 @@ + + + + confirmed + + + + + Odoo implementation + 3 + + + + portal + + + + + + + + + 0 + + Project Management + + 3 + + + + + + + + + 0 + + Workshops + + 3 + + + + + + + + + 0 + + Software Development + + 3 + + + + + + + + + 0 + + Internal Testing + + 3 + + + + + + + + + 0 + + Customer Tests + + 3 + + + + + + + diff --git a/project_forecast_line/demo/res_users.xml b/project_forecast_line/demo/res_users.xml new file mode 100644 index 0000000000..9a5e7fdce5 --- /dev/null +++ b/project_forecast_line/demo/res_users.xml @@ -0,0 +1,69 @@ + + + + Audrey Peterson + audrey.peterson25@example.com + + audrey + + + + + + + Walter Horton + walter.horton80@example.com + + walter + + + + + + + Doris Cole + doris.cole31@example.com + + doris + + + + + + Jennie Fletcher + jennie.fletcher76@example.com + + jennie + + + + + diff --git a/project_forecast_line/demo/sale.xml b/project_forecast_line/demo/sale.xml new file mode 100644 index 0000000000..ccef71e96b --- /dev/null +++ b/project_forecast_line/demo/sale.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + 30 + + 295.00 + + + + + + + + + 300 + + 200.00 + + + + + + + + + 200 + + 200.00 + + + + + diff --git a/project_forecast_line/i18n/es.po b/project_forecast_line/i18n/es.po new file mode 100644 index 0000000000..e369b7bbf9 --- /dev/null +++ b/project_forecast_line/i18n/es.po @@ -0,0 +1,614 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_forecast_line +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-10-09 07:43+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\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.17\n" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Allow to see forecast dates on quotations" +msgstr "Permitir ver las fechas previstas en los presupuestos" + +#. module: project_forecast_line +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_dev +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_pm +msgid "Blocked" +msgstr "Bloqueado" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__company_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__company_id +msgid "Company" +msgstr "Companía" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_res_config_settings +msgid "Config Settings" +msgstr "Configurar Ajustes" + +#. module: project_forecast_line +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_config +msgid "Configuration" +msgstr "Configuración" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__forecast_line__type__confirmed +#: model:ir.model.fields.selection,name:project_forecast_line.selection__project_project_stage__forecast_line_type__confirmed +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Confirmed" +msgstr "Confirmado" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__confirmed_consolidated_forecast +msgid "Confirmed lines consolidated forecast" +msgstr "Líneas confirmadas previsión consolidada" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__consolidated_forecast +msgid "Consolidated Forecast" +msgstr "Previsión consolidada" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__consolidated_forecast +msgid "Consolidated forecast for lines of all types consumed" +msgstr "Previsión consolidada de líneas de todo tipo consumidas" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__confirmed_consolidated_forecast +msgid "Consolidated forecast for lines of type confirmed" +msgstr "Previsión consolidada de líneas de tipo confirmadas" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_consultant +#: model:product.template,name:project_forecast_line.product_product_consultant_product_template +msgid "Consultant" +msgstr "Consultor" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__cost +msgid "Cost" +msgstr "Coste" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__cost +msgid "" +"Cost, in company currency. Cost is positive for things which add forecast, " +"such as employees and negative for things which consume forecast such as " +"holidays, sales, or tasks. " +msgstr "" +"Coste, en divisa de la compañía. El coste es positivo para las cosas que " +"añaden previsiones, como los empleados, y negativo para las cosas que " +"consumen previsiones, como las vacaciones, las ventas o las tareas. " + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__create_date +msgid "Created On" +msgstr "Creado el" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__currency_id +msgid "Currency" +msgstr "Divisa" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__date_end +msgid "Date End" +msgstr "Fecha Fin" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__date_from +msgid "Date From" +msgstr "Fecha Desde" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__date_start +msgid "Date Start" +msgstr "Fecha Inicio" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__date_to +msgid "Date To" +msgstr "Fecha hasta" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Date from" +msgstr "Fecha desde" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__date_from +msgid "Date of the period start for this line" +msgstr "Fecha de inicio del período para esta línea" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__day +msgid "Day" +msgstr "Día" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order__default_forecast_date_end +msgid "Default Forecast Date End" +msgstr "Fecha de previsión por defecto Fin" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order__default_forecast_date_start +msgid "Default Forecast Date Start" +msgstr "Fecha de previsión por defecto Inicio" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__description +msgid "Description" +msgstr "Descripción" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_employee +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__employee_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Employee" +msgstr "Empleado/a" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_forecast_role_id +msgid "Employee Forecast Role" +msgstr "Función de previsión de empleados" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_resource_consumption_ids +msgid "Employee Resource Consumption" +msgstr "Consumo de recursos de los empleados" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_resource_forecast_line_id +msgid "Employee Resource Forecast Line" +msgstr "Línea de previsión de recursos para los empleados" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_employee_forecast_role +msgid "Employee forecast role" +msgstr "Función de previsión de los empleados" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_forecast_role +msgid "Employee role for task matching" +msgstr "Función del empleado para la asignación de tareas" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_lines +#: model:ir.model,name:project_forecast_line.model_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__forecast_hours +#: model:ir.model.fields.selection,name:project_forecast_line.selection__forecast_line__type__forecast +#: model:ir.model.fields.selection,name:project_forecast_line.selection__project_project_stage__forecast_line_type__forecast +#: model:ir.ui.menu,name:project_forecast_line.forecast_menu_root +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_line_consolidated +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Forecast" +msgstr "Previsión" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_lines_consolidated +msgid "Forecast (Consolidated)" +msgstr "Previsión (consolidada)" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__forecast_hours +msgid "" +"Forecast (in hours). Forecast is positive for resources which add forecast, " +"such as employees, and negative for things which consume forecast, such as " +"holidays, sales, or tasks." +msgstr "" +"Previsión (en horas). La previsión es positiva para los recursos que añaden " +"previsión, como los empleados, y negativa para las cosas que consumen " +"previsión, como las vacaciones, las ventas o las tareas." + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__forecast_date_end +msgid "Forecast Date End" +msgstr "Fecha prevista Fin" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__forecast_date_start +msgid "Forecast Date Start" +msgstr "Fecha prevista Inicio" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_company__forecast_line_granularity +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__forecast_line_granularity +msgid "Forecast Line Granularity" +msgstr "Granularidad de la línea de previsión" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_company__forecast_line_horizon +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__forecast_line_horizon +msgid "Forecast Line Horizon" +msgstr "Línea de previsión Horizonte" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_project_stage__forecast_line_type +msgid "Forecast Line Type" +msgstr "Tipo de línea de previsión" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__group_forecast_line_on_quotation +msgid "Forecast Line on Quotations" +msgstr "Línea de previsión en las cotizaciones" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Forecast Management" +msgstr "Gestión de previsiones" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_recomputation_trigger +msgid "Forecast Recomputation Trigger" +msgstr "Activación del nuevo cálculo de previsiones" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_product_product__forecast_role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_product_template__forecast_role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_role_id +msgid "Forecast Role" +msgstr "Función de previsión" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_roles +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_role +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_employee_form +msgid "Forecast Roles" +msgstr "Funciones de previsión" + +#. module: project_forecast_line +#: model:ir.actions.server,name:project_forecast_line.cron_forecast_lines_ir_actions_server +#: model:ir.cron,cron_name:project_forecast_line.cron_forecast_lines +#: model:ir.cron,name:project_forecast_line.cron_forecast_lines +msgid "Forecast recomputation" +msgstr "Recálculo de previsiones" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__forecast_role_id +msgid "Forecast role" +msgstr "Función de previsión" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Group By" +msgstr "Agrupado por" + +#. module: project_forecast_line +#: model:product.product,uom_name:project_forecast_line.product_product_consultant +#: model:product.product,uom_name:project_forecast_line.product_product_dev +#: model:product.product,uom_name:project_forecast_line.product_product_pm +#: model:product.template,uom_name:project_forecast_line.product_product_consultant_product_template +#: model:product.template,uom_name:project_forecast_line.product_product_dev_product_template +#: model:product.template,uom_name:project_forecast_line.product_product_pm_product_template +msgid "Hours" +msgstr "Horas" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__id +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__id +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__id +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__id +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__id +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: project_forecast_line +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_normal:project_forecast_line.software_project_task_dev +#: model:project.task,legend_normal:project_forecast_line.software_project_task_pm +msgid "In Progress" +msgstr "En Progreso" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_job +msgid "Job Position" +msgstr "Posición de trabajo" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__write_date +msgid "Last Updated On" +msgstr "Última Actualización el" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: project_forecast_line +#: code:addons/project_forecast_line/models/hr_leave.py:0 +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__hr_leave_id +#, python-format +msgid "Leave" +msgstr "Dejar" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee__main_role_id +msgid "Main Role" +msgstr "Función principal" + +#. module: project_forecast_line +#: model:res.groups,name:project_forecast_line.group_forecast_line_on_quotation +msgid "Manage Forecast Dates on Quotations" +msgstr "Gestionar las fechas de previsión en los presupuestos" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__res_model +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Model" +msgstr "Modelo" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__month +msgid "Month" +msgstr "Mes" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__name +msgid "Name" +msgstr "Nombre" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_res_company__forecast_line_horizon +#: model:ir.model.fields,help:project_forecast_line.field_res_config_settings__forecast_line_horizon +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Number of month for the forecast planning" +msgstr "Número de meses para la planificación de previsiones" + +#. module: project_forecast_line +#: model:project.project,name:project_forecast_line.software_project_1 +msgid "Odoo implementation" +msgstr "Implementación de Odoo" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_res_company__forecast_line_granularity +#: model:ir.model.fields,help:project_forecast_line.field_res_config_settings__forecast_line_granularity +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Periodicity of the forecast that will be generated" +msgstr "Periodicidad de la previsión que se generará" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_date_planned_end +msgid "Planned end date" +msgstr "Fecha de finalización prevista" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_date_planned_start +msgid "Planned start date" +msgstr "Fecha de inicio prevista" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_product_template +msgid "Product Template" +msgstr "Plantilla del Producto" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__project_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Project" +msgstr "Proyecto" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_pm +#: model:product.template,name:project_forecast_line.product_product_pm_product_template +msgid "Project Manager" +msgstr "Director de proyecto" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_project_project_stage +msgid "Project Stage" +msgstr "Etapa del proyecto" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__rate +msgid "Rate" +msgstr "Tarifa" + +#. module: project_forecast_line +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_done:project_forecast_line.software_project_task_dev +#: model:project.task,legend_done:project_forecast_line.software_project_task_pm +msgid "Ready" +msgstr "Listo" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__res_id +msgid "Record ID" +msgstr "ID de Registro" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_resource_calendar_leaves +msgid "Resource Time Off Detail" +msgstr "Detalle del tiempo libre de los recursos" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee__role_ids +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_job__role_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Role" +msgstr "Rol" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__sale_id +msgid "Sale" +msgstr "Venta" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__sale_line_id +msgid "Sale line" +msgstr "Línea de venta" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_sale_order +msgid "Sales Order" +msgstr "Orden de venta" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de Orden de Venta" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.forecast_config_settings_action +#: model:ir.ui.menu,name:project_forecast_line.forecast_config_settings_menu_action +msgid "Settings" +msgstr "Configuraciones" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_dev +#: model:product.template,name:project_forecast_line.product_product_dev_product_template +msgid "Software Developer" +msgstr "Desarrollador de programa informático" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_project_task +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__task_id +msgid "Task" +msgstr "Tarea" + +#. module: project_forecast_line +#: model:project.project,label_tasks:project_forecast_line.software_project_1 +msgid "Tasks" +msgstr "Tareas" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_project_task__forecast_recomputation_trigger +msgid "Technical field used to trigger the forecast recomputation" +msgstr "" +"Campo técnico utilizado para activar el nuevo cálculo de las previsiones" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_leave +msgid "Time Off" +msgstr "Tiempo Libre" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__type +msgid "Type" +msgstr "Tipo" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__week +msgid "Week" +msgstr "Semana" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_forecast_line_mixin +msgid "mixin for models which generate forecast lines" +msgstr "mezclador para modelos que generan líneas de previsión" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__employee_resource_forecast_line_id +msgid "" +"technical field giving the name of the resource (model=hr.employee.forecast." +"role) line for that employee and that period" +msgstr "" +"campo técnico que da el nombre de la línea de recursos (model=hr.employee." +"forecast.role) para ese empleado y ese periodo" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_project_project_stage__forecast_line_type +msgid "type of forecast lines created by the tasks of projects in that stage" +msgstr "" +"tipo de líneas de previsión creadas por las tareas de los proyectos en esa " +"fase" diff --git a/project_forecast_line/i18n/fr.po b/project_forecast_line/i18n/fr.po new file mode 100644 index 0000000000..bfb56a50cd --- /dev/null +++ b/project_forecast_line/i18n/fr.po @@ -0,0 +1,635 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_forecast_line +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-08-09 11:54+0000\n" +"PO-Revision-Date: 2021-09-10 14:59+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Allow to see forecast dates on quotations" +msgstr "" + +#. module: project_forecast_line +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_dev +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_pm +msgid "Blocked" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_res_company +msgid "Companies" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__company_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__company_id +msgid "Company" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: project_forecast_line +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_config +msgid "Configuration" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__forecast_line__type__confirmed +#: model:ir.model.fields.selection,name:project_forecast_line.selection__project_project_stage__forecast_line_type__confirmed +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Confirmed" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__confirmed_consolidated_forecast +msgid "Confirmed lines consolidated forecast" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__consolidated_forecast +msgid "Consolidated Forecast" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__consolidated_forecast +msgid "Consolidated forecast for lines of all types consumed" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__confirmed_consolidated_forecast +msgid "Consolidated forecast for lines of type confirmed" +msgstr "" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_consultant +#: model:product.template,name:project_forecast_line.product_product_consultant_product_template +msgid "Consultant" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__cost +msgid "Cost" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__cost +msgid "" +"Cost, in company currency. Cost is positive for things which add forecast, " +"such as employees and negative for things which consume forecast such as " +"holidays, sales, or tasks. " +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__create_date +msgid "Created On" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__create_uid +msgid "Created by" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__create_date +msgid "Created on" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__currency_id +msgid "Currency" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__date_end +msgid "Date End" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__date_from +msgid "Date From" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__date_start +msgid "Date Start" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__date_to +msgid "Date To" +msgstr "" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Date from" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__date_from +msgid "Date of the period start for this line" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__day +msgid "Day" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order__default_forecast_date_end +msgid "Default Forecast Date End" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order__default_forecast_date_start +msgid "Default Forecast Date Start" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__description +msgid "Description" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_employee +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__employee_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Employee" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_forecast_role_id +msgid "Employee Forecast Role" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_resource_consumption_ids +msgid "Employee Resource Consumption" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_resource_forecast_line_id +msgid "Employee Resource Forecast Line" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_employee_forecast_role +msgid "Employee forecast role" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_forecast_role +msgid "Employee role for task matching" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_lines +#: model:ir.model,name:project_forecast_line.model_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__forecast_hours +#: model:ir.model.fields.selection,name:project_forecast_line.selection__forecast_line__type__forecast +#: model:ir.model.fields.selection,name:project_forecast_line.selection__project_project_stage__forecast_line_type__forecast +#: model:ir.ui.menu,name:project_forecast_line.forecast_menu_root +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_line_consolidated +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Forecast" +msgstr "Plan de charge" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_lines_consolidated +msgid "Forecast (Consolidated)" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__forecast_hours +msgid "" +"Forecast (in hours). Forecast is positive for resources which add forecast, " +"such as employees, and negative for things which consume forecast, such as " +"holidays, sales, or tasks." +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__forecast_date_end +msgid "Forecast Date End" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__forecast_date_start +msgid "Forecast Date Start" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_company__forecast_line_granularity +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__forecast_line_granularity +msgid "Forecast Line Granularity" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_company__forecast_line_horizon +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__forecast_line_horizon +msgid "Forecast Line Horizon" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_project_stage__forecast_line_type +msgid "Forecast Line Type" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__group_forecast_line_on_quotation +msgid "Forecast Line on Quotations" +msgstr "" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Forecast Management" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_recomputation_trigger +msgid "Forecast Recomputation Trigger" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_product_product__forecast_role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_product_template__forecast_role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_role_id +msgid "Forecast Role" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_roles +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_role +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_employee_form +msgid "Forecast Roles" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.server,name:project_forecast_line.cron_forecast_lines_ir_actions_server +#: model:ir.cron,cron_name:project_forecast_line.cron_forecast_lines +#: model:ir.cron,name:project_forecast_line.cron_forecast_lines +msgid "Forecast recomputation" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__forecast_role_id +msgid "Forecast role" +msgstr "" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Group By" +msgstr "" + +#. module: project_forecast_line +#: model:product.product,uom_name:project_forecast_line.product_product_consultant +#: model:product.product,uom_name:project_forecast_line.product_product_dev +#: model:product.product,uom_name:project_forecast_line.product_product_pm +#: model:product.template,uom_name:project_forecast_line.product_product_consultant_product_template +#: model:product.template,uom_name:project_forecast_line.product_product_dev_product_template +#: model:product.template,uom_name:project_forecast_line.product_product_pm_product_template +msgid "Hours" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__id +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__id +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__id +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__id +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__id +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__id +msgid "ID" +msgstr "" + +#. module: project_forecast_line +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_normal:project_forecast_line.software_project_task_dev +#: model:project.task,legend_normal:project_forecast_line.software_project_task_pm +msgid "In Progress" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_job +msgid "Job Position" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__write_date +msgid "Last Updated On" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__write_date +#, fuzzy +msgid "Last Updated on" +msgstr "Dernière modification le" + +#. module: project_forecast_line +#: code:addons/project_forecast_line/models/hr_leave.py:0 +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__hr_leave_id +#, python-format +msgid "Leave" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee__main_role_id +msgid "Main Role" +msgstr "" + +#. module: project_forecast_line +#: model:res.groups,name:project_forecast_line.group_forecast_line_on_quotation +msgid "Manage Forecast Dates on Quotations" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__res_model +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Model" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__month +msgid "Month" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__name +msgid "Name" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_res_company__forecast_line_horizon +#: model:ir.model.fields,help:project_forecast_line.field_res_config_settings__forecast_line_horizon +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Number of month for the forecast planning" +msgstr "" + +#. module: project_forecast_line +#: model:project.project,name:project_forecast_line.software_project_1 +msgid "Odoo implementation" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_res_company__forecast_line_granularity +#: model:ir.model.fields,help:project_forecast_line.field_res_config_settings__forecast_line_granularity +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Periodicity of the forecast that will be generated" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_date_planned_end +msgid "Planned end date" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_date_planned_start +msgid "Planned start date" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_product_template +msgid "Product Template" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__project_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Project" +msgstr "" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_pm +#: model:product.template,name:project_forecast_line.product_product_pm_product_template +msgid "Project Manager" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_project_project_stage +msgid "Project Stage" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__rate +msgid "Rate" +msgstr "" + +#. module: project_forecast_line +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_done:project_forecast_line.software_project_task_dev +#: model:project.task,legend_done:project_forecast_line.software_project_task_pm +msgid "Ready" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__res_id +msgid "Record ID" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_resource_calendar_leaves +msgid "Resource Time Off Detail" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee__role_ids +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_job__role_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Role" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__sale_id +msgid "Sale" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__sale_line_id +#, fuzzy +msgid "Sale line" +msgstr "Bon de commande" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_sale_order +msgid "Sales Order" +msgstr "Bon de commande" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_sale_order_line +#, fuzzy +msgid "Sales Order Line" +msgstr "Bon de commande" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__sequence +msgid "Sequence" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.forecast_config_settings_action +#: model:ir.ui.menu,name:project_forecast_line.forecast_config_settings_menu_action +msgid "Settings" +msgstr "" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_dev +#: model:product.template,name:project_forecast_line.product_product_dev_product_template +msgid "Software Developer" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_project_task +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__task_id +msgid "Task" +msgstr "" + +#. module: project_forecast_line +#: model:project.project,label_tasks:project_forecast_line.software_project_1 +msgid "Tasks" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_project_task__forecast_recomputation_trigger +msgid "Technical field used to trigger the forecast recomputation" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_leave +msgid "Time Off" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__type +msgid "Type" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__week +msgid "Week" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_forecast_line_mixin +msgid "mixin for models which generate forecast lines" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__employee_resource_forecast_line_id +msgid "" +"technical field giving the name of the resource (model=hr.employee.forecast." +"role) line for that employee and that period" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_project_project_stage__forecast_line_type +msgid "type of forecast lines created by the tasks of projects in that stage" +msgstr "" + +#~ msgid "Indicate if a signed contract has been received for this SO" +#~ msgstr "Indique si un contrat signé a été reçu pour cette commande" + +#~ msgid "Log reception of signed contract" +#~ msgstr "Enregistrer la réception d'un contrat signé" + +#~ msgid "Missing contract" +#~ msgstr "Contrat manquant" + +#~ msgid "Record when the SO has (most recently) been marked as signed " +#~ msgstr "" +#~ "Enregistre quand la commande a été marquée comme signée (le plus " +#~ "récemment)" + +#~ msgid "Signed Date" +#~ msgstr "Date de signature" + +#~ msgid "Signed Status" +#~ msgstr "Statut signature" + +#~ msgid "" +#~ "You are not allowed to change the signed status of Sales Orders.\n" +#~ "Only members of the 'Log reception of signed contract' group can." +#~ msgstr "" +#~ "Vous n'êtes pas autorisé à changer le statut de signature des commandes " +#~ "de vente.\n" +#~ "Seuls les membres du goupe 'Enregistrer la réception d'un contrat signé' " +#~ "le peuvent." diff --git a/project_forecast_line/i18n/it.po b/project_forecast_line/i18n/it.po new file mode 100644 index 0000000000..75e7324ab2 --- /dev/null +++ b/project_forecast_line/i18n/it.po @@ -0,0 +1,611 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_forecast_line +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-20 17:33+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Allow to see forecast dates on quotations" +msgstr "Consente di vedere date previsionali nei preventivi" + +#. module: project_forecast_line +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_dev +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_pm +msgid "Blocked" +msgstr "Bloccata" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_res_company +msgid "Companies" +msgstr "Aziende" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__company_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__company_id +msgid "Company" +msgstr "Azienda" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni configurazione" + +#. module: project_forecast_line +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_config +msgid "Configuration" +msgstr "Configurazione" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__forecast_line__type__confirmed +#: model:ir.model.fields.selection,name:project_forecast_line.selection__project_project_stage__forecast_line_type__confirmed +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Confirmed" +msgstr "Confermata" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__confirmed_consolidated_forecast +msgid "Confirmed lines consolidated forecast" +msgstr "Righe confermate previsionale consolidato" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__consolidated_forecast +msgid "Consolidated Forecast" +msgstr "Previsionale consolidato" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__consolidated_forecast +msgid "Consolidated forecast for lines of all types consumed" +msgstr "Previsionale consolidato per le righe consumate di ogni tipo" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__confirmed_consolidated_forecast +msgid "Consolidated forecast for lines of type confirmed" +msgstr "Previsionale consolidato per le righe confermate di ogni tipo" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_consultant +#: model:product.template,name:project_forecast_line.product_product_consultant_product_template +msgid "Consultant" +msgstr "Consulente" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__cost +msgid "Cost" +msgstr "Costo" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__cost +msgid "" +"Cost, in company currency. Cost is positive for things which add forecast, " +"such as employees and negative for things which consume forecast such as " +"holidays, sales, or tasks. " +msgstr "" +"Costo nella valuta dell'azienda. Il costo è positivo per oggetti che " +"aggiungono previsionale, come i dipendenti, e negativo per oggetti che " +"consumano il previsionale come le vacanze, le vendite o i lavori. " + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__create_date +msgid "Created On" +msgstr "Creato il" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__currency_id +msgid "Currency" +msgstr "Valuta" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__date_end +msgid "Date End" +msgstr "Data fine" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__date_from +msgid "Date From" +msgstr "Dalla data" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__date_start +msgid "Date Start" +msgstr "Data inizio" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__date_to +msgid "Date To" +msgstr "Alla data" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Date from" +msgstr "Dalla data" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__date_from +msgid "Date of the period start for this line" +msgstr "La data del periodo inizia da questa riga" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__day +msgid "Day" +msgstr "Giorno" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order__default_forecast_date_end +msgid "Default Forecast Date End" +msgstr "Data fine previsionale predefinita" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order__default_forecast_date_start +msgid "Default Forecast Date Start" +msgstr "Data inizio previsionale predefinita" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__description +msgid "Description" +msgstr "Descrizione" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_employee +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__employee_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Employee" +msgstr "Dipendente" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_forecast_role_id +msgid "Employee Forecast Role" +msgstr "Ruolo previsionale del dipendente" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_resource_consumption_ids +msgid "Employee Resource Consumption" +msgstr "Consumo risorsa dipendente" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_resource_forecast_line_id +msgid "Employee Resource Forecast Line" +msgstr "Riga previsionale risorsa dipendente" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_employee_forecast_role +msgid "Employee forecast role" +msgstr "Ruolo previsionale del dipendente" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_forecast_role +msgid "Employee role for task matching" +msgstr "Ruolo del dipendente per corrispondenza attività" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_lines +#: model:ir.model,name:project_forecast_line.model_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__forecast_hours +#: model:ir.model.fields.selection,name:project_forecast_line.selection__forecast_line__type__forecast +#: model:ir.model.fields.selection,name:project_forecast_line.selection__project_project_stage__forecast_line_type__forecast +#: model:ir.ui.menu,name:project_forecast_line.forecast_menu_root +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_line_consolidated +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Forecast" +msgstr "Previsione" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_lines_consolidated +msgid "Forecast (Consolidated)" +msgstr "Previsione (consolidata)" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__forecast_hours +msgid "" +"Forecast (in hours). Forecast is positive for resources which add forecast, " +"such as employees, and negative for things which consume forecast, such as " +"holidays, sales, or tasks." +msgstr "" +"Previsione (in ore). La previsione è positiva per risorse che incrementano " +"il previsionale, come i dipendenti, e negativa per oggetti che consumano il " +"previsionale, come le vacanze, le vendite o i lavori." + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__forecast_date_end +msgid "Forecast Date End" +msgstr "Data fine previsionale" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__forecast_date_start +msgid "Forecast Date Start" +msgstr "Data inizio previsionale" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_company__forecast_line_granularity +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__forecast_line_granularity +msgid "Forecast Line Granularity" +msgstr "Granularità riga previsionale" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_company__forecast_line_horizon +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__forecast_line_horizon +msgid "Forecast Line Horizon" +msgstr "Orizzonte riga previsionale" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_project_stage__forecast_line_type +msgid "Forecast Line Type" +msgstr "Tipo riga previsionale" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__group_forecast_line_on_quotation +msgid "Forecast Line on Quotations" +msgstr "Riga previsionale nelle quotazioni" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Forecast Management" +msgstr "Gestione previsionale" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_recomputation_trigger +msgid "Forecast Recomputation Trigger" +msgstr "Automatismo ricalcolo previsionale" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_product_product__forecast_role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_product_template__forecast_role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_role_id +msgid "Forecast Role" +msgstr "Ruolo previsionale" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_roles +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_role +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_employee_form +msgid "Forecast Roles" +msgstr "Ruoli previsionali" + +#. module: project_forecast_line +#: model:ir.actions.server,name:project_forecast_line.cron_forecast_lines_ir_actions_server +#: model:ir.cron,cron_name:project_forecast_line.cron_forecast_lines +#: model:ir.cron,name:project_forecast_line.cron_forecast_lines +msgid "Forecast recomputation" +msgstr "Ricalcolo previsionale" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__forecast_role_id +msgid "Forecast role" +msgstr "Ruolo previsionale" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Group By" +msgstr "Raggruppa per" + +#. module: project_forecast_line +#: model:product.product,uom_name:project_forecast_line.product_product_consultant +#: model:product.product,uom_name:project_forecast_line.product_product_dev +#: model:product.product,uom_name:project_forecast_line.product_product_pm +#: model:product.template,uom_name:project_forecast_line.product_product_consultant_product_template +#: model:product.template,uom_name:project_forecast_line.product_product_dev_product_template +#: model:product.template,uom_name:project_forecast_line.product_product_pm_product_template +msgid "Hours" +msgstr "Ore" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__id +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__id +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__id +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__id +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__id +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__id +msgid "ID" +msgstr "ID" + +#. module: project_forecast_line +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_normal:project_forecast_line.software_project_task_dev +#: model:project.task,legend_normal:project_forecast_line.software_project_task_pm +msgid "In Progress" +msgstr "In corso" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_job +msgid "Job Position" +msgstr "Posizione lavorativa" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__write_date +msgid "Last Updated On" +msgstr "Ultimo aggiornamento il" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: project_forecast_line +#: code:addons/project_forecast_line/models/hr_leave.py:0 +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__hr_leave_id +#, python-format +msgid "Leave" +msgstr "Permesso" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee__main_role_id +msgid "Main Role" +msgstr "Ruolo principale" + +#. module: project_forecast_line +#: model:res.groups,name:project_forecast_line.group_forecast_line_on_quotation +msgid "Manage Forecast Dates on Quotations" +msgstr "Gestione date previsionale nelle quotazioni" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__res_model +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Model" +msgstr "Modello" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__month +msgid "Month" +msgstr "Mese" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__name +msgid "Name" +msgstr "Nome" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_res_company__forecast_line_horizon +#: model:ir.model.fields,help:project_forecast_line.field_res_config_settings__forecast_line_horizon +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Number of month for the forecast planning" +msgstr "Numero di mesi per la pianificazione del previsionale" + +#. module: project_forecast_line +#: model:project.project,name:project_forecast_line.software_project_1 +msgid "Odoo implementation" +msgstr "Implementazione Odoo" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_res_company__forecast_line_granularity +#: model:ir.model.fields,help:project_forecast_line.field_res_config_settings__forecast_line_granularity +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Periodicity of the forecast that will be generated" +msgstr "Periodicità del previsionale che verrà generata" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_date_planned_end +msgid "Planned end date" +msgstr "Data fine pianificata" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_date_planned_start +msgid "Planned start date" +msgstr "Data inizio pianificata" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_product_template +msgid "Product Template" +msgstr "Modello prodotto" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__project_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Project" +msgstr "Progetto" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_pm +#: model:product.template,name:project_forecast_line.product_product_pm_product_template +msgid "Project Manager" +msgstr "Responsabile progetto" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_project_project_stage +msgid "Project Stage" +msgstr "Fase progetto" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__rate +msgid "Rate" +msgstr "Valutazione" + +#. module: project_forecast_line +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_done:project_forecast_line.software_project_task_dev +#: model:project.task,legend_done:project_forecast_line.software_project_task_pm +msgid "Ready" +msgstr "Pronto" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__res_id +msgid "Record ID" +msgstr "ID record" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_resource_calendar_leaves +msgid "Resource Time Off Detail" +msgstr "Dettaglio ferie risorsa" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee__role_ids +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_job__role_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Role" +msgstr "Ruolo" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__sale_id +msgid "Sale" +msgstr "Vendita" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__sale_line_id +msgid "Sale line" +msgstr "Riga di vendita" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_sale_order +msgid "Sales Order" +msgstr "Ordine di vendita" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_sale_order_line +msgid "Sales Order Line" +msgstr "Riga ordine di vendita" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.forecast_config_settings_action +#: model:ir.ui.menu,name:project_forecast_line.forecast_config_settings_menu_action +msgid "Settings" +msgstr "Impostazioni" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_dev +#: model:product.template,name:project_forecast_line.product_product_dev_product_template +msgid "Software Developer" +msgstr "Sviluppatore software" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_project_task +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__task_id +msgid "Task" +msgstr "Lavoro" + +#. module: project_forecast_line +#: model:project.project,label_tasks:project_forecast_line.software_project_1 +msgid "Tasks" +msgstr "Lavori" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_project_task__forecast_recomputation_trigger +msgid "Technical field used to trigger the forecast recomputation" +msgstr "Campo tecnico per attivare il ricalcolo del previsionale" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_leave +msgid "Time Off" +msgstr "Ferie" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__type +msgid "Type" +msgstr "Tipo" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__week +msgid "Week" +msgstr "Settimana" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_forecast_line_mixin +msgid "mixin for models which generate forecast lines" +msgstr "mixin per modelli che generano righe previsionali" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__employee_resource_forecast_line_id +msgid "" +"technical field giving the name of the resource (model=hr.employee.forecast." +"role) line for that employee and that period" +msgstr "" +"campo tecnico che restituisce il nome della riga risorsa (modello=hr." +"employee.forecast.role) per quel dipendente e quel periodo" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_project_project_stage__forecast_line_type +msgid "type of forecast lines created by the tasks of projects in that stage" +msgstr "tipo righe previsionali create dai lavori dei progetti in quella fase" diff --git a/project_forecast_line/i18n/project_forecast_line.pot b/project_forecast_line/i18n/project_forecast_line.pot new file mode 100644 index 0000000000..a1af074bcb --- /dev/null +++ b/project_forecast_line/i18n/project_forecast_line.pot @@ -0,0 +1,600 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_forecast_line +# +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_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Allow to see forecast dates on quotations" +msgstr "" + +#. module: project_forecast_line +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_dev +#: model:project.task,legend_blocked:project_forecast_line.software_project_task_pm +msgid "Blocked" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_res_company +msgid "Companies" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__company_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__company_id +msgid "Company" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: project_forecast_line +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_config +msgid "Configuration" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__forecast_line__type__confirmed +#: model:ir.model.fields.selection,name:project_forecast_line.selection__project_project_stage__forecast_line_type__confirmed +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Confirmed" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__confirmed_consolidated_forecast +msgid "Confirmed lines consolidated forecast" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__consolidated_forecast +msgid "Consolidated Forecast" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__consolidated_forecast +msgid "Consolidated forecast for lines of all types consumed" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__confirmed_consolidated_forecast +msgid "Consolidated forecast for lines of type confirmed" +msgstr "" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_consultant +#: model:product.template,name:project_forecast_line.product_product_consultant_product_template +msgid "Consultant" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__cost +msgid "Cost" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__cost +msgid "" +"Cost, in company currency. Cost is positive for things which add forecast, " +"such as employees and negative for things which consume forecast such as " +"holidays, sales, or tasks. " +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__create_date +msgid "Created On" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__create_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__create_uid +msgid "Created by" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__create_date +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__create_date +msgid "Created on" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__currency_id +msgid "Currency" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__date_end +msgid "Date End" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__date_from +msgid "Date From" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__date_start +msgid "Date Start" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__date_to +msgid "Date To" +msgstr "" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Date from" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__date_from +msgid "Date of the period start for this line" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__day +msgid "Day" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order__default_forecast_date_end +msgid "Default Forecast Date End" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order__default_forecast_date_start +msgid "Default Forecast Date Start" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__description +msgid "Description" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__display_name +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__display_name +msgid "Display Name" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_employee +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__employee_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Employee" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_forecast_role_id +msgid "Employee Forecast Role" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_resource_consumption_ids +msgid "Employee Resource Consumption" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__employee_resource_forecast_line_id +msgid "Employee Resource Forecast Line" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_employee_forecast_role +msgid "Employee forecast role" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_forecast_role +msgid "Employee role for task matching" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_lines +#: model:ir.model,name:project_forecast_line.model_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__forecast_hours +#: model:ir.model.fields.selection,name:project_forecast_line.selection__forecast_line__type__forecast +#: model:ir.model.fields.selection,name:project_forecast_line.selection__project_project_stage__forecast_line_type__forecast +#: model:ir.ui.menu,name:project_forecast_line.forecast_menu_root +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_line_consolidated +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Forecast" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_lines_consolidated +msgid "Forecast (Consolidated)" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__forecast_hours +msgid "" +"Forecast (in hours). Forecast is positive for resources which add forecast, " +"such as employees, and negative for things which consume forecast, such as " +"holidays, sales, or tasks." +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__forecast_date_end +msgid "Forecast Date End" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__forecast_date_start +msgid "Forecast Date Start" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_company__forecast_line_granularity +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__forecast_line_granularity +msgid "Forecast Line Granularity" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_company__forecast_line_horizon +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__forecast_line_horizon +msgid "Forecast Line Horizon" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_project_stage__forecast_line_type +msgid "Forecast Line Type" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_res_config_settings__group_forecast_line_on_quotation +msgid "Forecast Line on Quotations" +msgstr "" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Forecast Management" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_recomputation_trigger +msgid "Forecast Recomputation Trigger" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_product_product__forecast_role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_product_template__forecast_role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_role_id +msgid "Forecast Role" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.action_forecast_roles +#: model:ir.ui.menu,name:project_forecast_line.menu_forecast_role +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_employee_form +msgid "Forecast Roles" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.server,name:project_forecast_line.cron_forecast_lines_ir_actions_server +#: model:ir.cron,cron_name:project_forecast_line.cron_forecast_lines +#: model:ir.cron,name:project_forecast_line.cron_forecast_lines +msgid "Forecast recomputation" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__forecast_role_id +msgid "Forecast role" +msgstr "" + +#. module: project_forecast_line +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Group By" +msgstr "" + +#. module: project_forecast_line +#: model:product.product,uom_name:project_forecast_line.product_product_consultant +#: model:product.product,uom_name:project_forecast_line.product_product_dev +#: model:product.product,uom_name:project_forecast_line.product_product_pm +#: model:product.template,uom_name:project_forecast_line.product_product_consultant_product_template +#: model:product.template,uom_name:project_forecast_line.product_product_dev_product_template +#: model:product.template,uom_name:project_forecast_line.product_product_pm_product_template +msgid "Hours" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__id +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__id +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__id +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__id +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__id +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__id +msgid "ID" +msgstr "" + +#. module: project_forecast_line +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_normal:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_normal:project_forecast_line.software_project_task_dev +#: model:project.task,legend_normal:project_forecast_line.software_project_task_pm +msgid "In Progress" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_job +msgid "Job Position" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves____last_update +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__write_date +msgid "Last Updated On" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__write_uid +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line_mixin__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_leave__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_resource_calendar_leaves__write_date +#: model:ir.model.fields,field_description:project_forecast_line.field_sale_order_line__write_date +msgid "Last Updated on" +msgstr "" + +#. module: project_forecast_line +#: code:addons/project_forecast_line/models/hr_leave.py:0 +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__hr_leave_id +#, python-format +msgid "Leave" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee__main_role_id +msgid "Main Role" +msgstr "" + +#. module: project_forecast_line +#: model:res.groups,name:project_forecast_line.group_forecast_line_on_quotation +msgid "Manage Forecast Dates on Quotations" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__res_model +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Model" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__month +msgid "Month" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__name +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_role__name +msgid "Name" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_res_company__forecast_line_horizon +#: model:ir.model.fields,help:project_forecast_line.field_res_config_settings__forecast_line_horizon +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Number of month for the forecast planning" +msgstr "" + +#. module: project_forecast_line +#: model:project.project,name:project_forecast_line.software_project_1 +msgid "Odoo implementation" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_res_company__forecast_line_granularity +#: model:ir.model.fields,help:project_forecast_line.field_res_config_settings__forecast_line_granularity +#: model_terms:ir.ui.view,arch_db:project_forecast_line.res_config_settings_view_form +msgid "Periodicity of the forecast that will be generated" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_date_planned_end +msgid "Planned end date" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_project_task__forecast_date_planned_start +msgid "Planned start date" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_product_template +msgid "Product Template" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__project_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Project" +msgstr "" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_pm +#: model:product.template,name:project_forecast_line.product_product_pm_product_template +msgid "Project Manager" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_project_project_stage +msgid "Project Stage" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__rate +msgid "Rate" +msgstr "" + +#. module: project_forecast_line +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant_cust_test +#: model:project.task,legend_done:project_forecast_line.software_project_task_consultant_internal_test +#: model:project.task,legend_done:project_forecast_line.software_project_task_dev +#: model:project.task,legend_done:project_forecast_line.software_project_task_pm +msgid "Ready" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__res_id +msgid "Record ID" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_resource_calendar_leaves +msgid "Resource Time Off Detail" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee__role_ids +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__role_id +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_job__role_id +#: model_terms:ir.ui.view,arch_db:project_forecast_line.view_forecast_line_search +msgid "Role" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__sale_id +msgid "Sale" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__sale_line_id +msgid "Sale line" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_hr_employee_forecast_role__sequence +msgid "Sequence" +msgstr "" + +#. module: project_forecast_line +#: model:ir.actions.act_window,name:project_forecast_line.forecast_config_settings_action +#: model:ir.ui.menu,name:project_forecast_line.forecast_config_settings_menu_action +msgid "Settings" +msgstr "" + +#. module: project_forecast_line +#: model:product.product,name:project_forecast_line.product_product_dev +#: model:product.template,name:project_forecast_line.product_product_dev_product_template +msgid "Software Developer" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_project_task +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__task_id +msgid "Task" +msgstr "" + +#. module: project_forecast_line +#: model:project.project,label_tasks:project_forecast_line.software_project_1 +msgid "Tasks" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_project_task__forecast_recomputation_trigger +msgid "Technical field used to trigger the forecast recomputation" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_hr_leave +msgid "Time Off" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,field_description:project_forecast_line.field_forecast_line__type +msgid "Type" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields.selection,name:project_forecast_line.selection__res_company__forecast_line_granularity__week +msgid "Week" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model,name:project_forecast_line.model_forecast_line_mixin +msgid "mixin for models which generate forecast lines" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_forecast_line__employee_resource_forecast_line_id +msgid "" +"technical field giving the name of the resource " +"(model=hr.employee.forecast.role) line for that employee and that period" +msgstr "" + +#. module: project_forecast_line +#: model:ir.model.fields,help:project_forecast_line.field_project_project_stage__forecast_line_type +msgid "type of forecast lines created by the tasks of projects in that stage" +msgstr "" diff --git a/project_forecast_line/models/__init__.py b/project_forecast_line/models/__init__.py new file mode 100644 index 0000000000..8cbfb95dfb --- /dev/null +++ b/project_forecast_line/models/__init__.py @@ -0,0 +1,13 @@ +from . import forecast_line_mixin +from . import forecast_line +from . import forecast_role +from . import hr_employee +from . import product_template +from . import sale_order +from . import sale_order_line +from . import res_company +from . import hr_leave +from . import project_task +from . import res_config_settings +from . import resource_calendar_leaves +from . import project_project_stage diff --git a/project_forecast_line/models/forecast_line.py b/project_forecast_line/models/forecast_line.py new file mode 100644 index 0000000000..debaebe098 --- /dev/null +++ b/project_forecast_line/models/forecast_line.py @@ -0,0 +1,493 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +from datetime import datetime, time + +import pytz +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.tools import date_utils, mute_logger + +_logger = logging.getLogger(__name__) + + +class ForecastLine(models.Model): + """ + we generate 1 forecast line per period defined on the current company (day, week, month). + """ + + _name = "forecast.line" + _order = "date_from, employee_id, project_id" + _description = "Forecast" + + name = fields.Char(required=True) + date_from = fields.Date( + required=True, help="Date of the period start for this line" + ) + date_to = fields.Date(required=True) + forecast_role_id = fields.Many2one( + "forecast.role", + string="Forecast role", + required=True, + index=True, + ondelete="restrict", + ) + employee_id = fields.Many2one("hr.employee", string="Employee", ondelete="cascade") + employee_forecast_role_id = fields.Many2one( + "hr.employee.forecast.role", string="Employee Forecast Role", ondelete="cascade" + ) + project_id = fields.Many2one( + "project.project", index=True, string="Project", ondelete="cascade" + ) + task_id = fields.Many2one( + "project.task", index=True, string="Task", ondelete="cascade" + ) + sale_id = fields.Many2one( + "sale.order", + related="sale_line_id.order_id", + store=True, + index=True, + string="Sale", + ) + sale_line_id = fields.Many2one( + "sale.order.line", index=True, string="Sale line", ondelete="cascade" + ) + hr_leave_id = fields.Many2one( + "hr.leave", index=True, string="Leave", ondelete="cascade" + ) + forecast_hours = fields.Float( + "Forecast", + help="Forecast (in hours). Forecast is positive for resources which add forecast, " + "such as employees, and negative for things which consume forecast, such as " + "holidays, sales, or tasks.", + ) + cost = fields.Monetary( + help="Cost, in company currency. Cost is positive for things which add forecast, " + "such as employees and negative for things which consume forecast such as " + "holidays, sales, or tasks. ", + ) + consolidated_forecast = fields.Float( + help="Consolidated forecast for lines of all types consumed", + digits=(12, 5), + store=True, + compute="_compute_consolidated_forecast", + ) + confirmed_consolidated_forecast = fields.Float( + string="Confirmed lines consolidated forecast", + help="Consolidated forecast for lines of type confirmed", + digits=(12, 5), + store=True, + compute="_compute_consolidated_forecast", + ) + currency_id = fields.Many2one(related="company_id.currency_id", store=True) + company_id = fields.Many2one( + "res.company", required=True, default=lambda s: s.env.company + ) + type = fields.Selection( + [("forecast", "Forecast"), ("confirmed", "Confirmed")], + required=True, + default="forecast", + ) + res_model = fields.Char(string="Model", index=True) + res_id = fields.Integer(string="Record ID", index=True) + employee_resource_forecast_line_id = fields.Many2one( + "forecast.line", + store=True, + index=True, + compute="_compute_employee_forecast_line_id", + ondelete="set null", + help="technical field giving the name of the resource " + "(model=hr.employee.forecast.role) line for that employee and that period", + ) + employee_resource_consumption_ids = fields.One2many( + "forecast.line", "employee_resource_forecast_line_id" + ) + + def write(self, vals): + # avoid retriggering the costly recomputation of + # employee_forecast_line_id when updating the lines during + # recomputation if the values have not changed for the trigger fields + if len(self) == 1: + for key in ("date_from", "type", "res_model"): + if key in vals and self[key] == vals[key]: + del vals[key] + if "employee_id" in vals and self["employee_id"].id == vals["employee_id"]: + del vals["employee_id"] + if vals: + return super().write(vals) + else: + return True + + @api.depends("employee_id", "date_from", "type", "res_model") + def _compute_employee_forecast_line_id(self): + employees = self.mapped("employee_id") + main_roles = employees.mapped("main_role_id") + date_froms = self.mapped("date_from") + date_tos = self.mapped("date_to") + forecast_roles = self.mapped("forecast_role_id") | main_roles + if employees: + lines = self.search( + [ + ("employee_id", "in", employees.ids), + ("forecast_role_id", "in", forecast_roles.ids), + ("res_model", "=", "hr.employee.forecast.role"), + ("date_from", ">=", min(date_froms)), + ("date_to", "<=", max(date_tos)), + ("type", "=", "confirmed"), + ] + ) + else: + lines = self.env["forecast.line"] + capacities = {} + for line in lines: + employee_id = line.employee_id + date_from = line.date_from + forecast_role_id = line.forecast_role_id + capacities[(employee_id.id, date_from, forecast_role_id.id)] = line.id + for rec in self: + if ( + rec.type in ("forecast", "confirmed") + and rec.res_model != "hr.employee.forecast.role" + ): + resource_forecast_line = capacities.get( + (rec.employee_id.id, rec.date_from, rec.forecast_role_id.id), False + ) + if resource_forecast_line: + rec.employee_resource_forecast_line_id = resource_forecast_line + else: + # if we didn't find a forecast line with a matching role + # we get forecast line with the main role of the employee + main_role_id = rec.employee_id.main_role_id + rec.employee_resource_forecast_line_id = capacities.get( + (rec.employee_id.id, rec.date_from, main_role_id.id), False + ) + else: + rec.employee_resource_forecast_line_id = False + + def _get_grouped_line_values(self): + data = {} + grouped_line_result = self.env["forecast.line"].read_group( + [("employee_resource_forecast_line_id", "in", self.ids)], + fields=["forecast_hours"], + groupby=["employee_resource_forecast_line_id", "type"], + lazy=False, + ) + for d in grouped_line_result: + line_id = d["employee_resource_forecast_line_id"][0] + if line_id not in data: + data[line_id] = {"confirmed": 0, "forecast": 0} + data[line_id][d["type"]] += d["forecast_hours"] + return data + + @api.model + def _get_consolidation_uom(self): + """ + Returns the unit of measure used for the consolidated forecast. + The default is days. + """ + return self.env.ref("uom.product_uom_day") + + def _convert_hours_to_days(self, hours): + to_convert_uom = self._get_consolidation_uom() + project_time_mode_id = self.company_id.project_time_mode_id + return project_time_mode_id._compute_quantity( + hours, to_convert_uom, round=False + ) + + @api.depends("employee_resource_consumption_ids.forecast_hours", "forecast_hours") + def _compute_consolidated_forecast(self): + grouped_lines_values = self._get_grouped_line_values() + for rec in self: + if rec.res_model != "hr.employee.forecast.role": + rec.consolidated_forecast = ( + self._convert_hours_to_days(rec.forecast_hours) * -1 + ) + if rec.type == "confirmed": + rec.confirmed_consolidated_forecast = rec.consolidated_forecast + else: + rec.confirmed_consolidated_forecast = 0.0 + else: + resource_forecast = grouped_lines_values.get(rec.id, 0) + confirmed = ( + resource_forecast.get("confirmed", 0) if resource_forecast else 0 + ) + unconfirmed = ( + confirmed + resource_forecast.get("forecast", 0) + if resource_forecast + else 0 + ) + rec.consolidated_forecast = self._convert_hours_to_days( + rec.forecast_hours + unconfirmed + ) + rec.confirmed_consolidated_forecast = self._convert_hours_to_days( + rec.forecast_hours + confirmed + ) + + def _update_forecast_lines( + self, + name, + date_from, + date_to, + ttype, + forecast_hours, + unit_cost, + res_model, + res_id=0, + **kwargs + ): + """this method is called on a recordset, it will update it so that all the + lines in the set are correct, removing the ones which need removing and + creating the missing ones. Updates lines, and return a list of dict to pass to + create""" + values = self._prepare_forecast_lines( + name, + date_from, + date_to, + ttype, + forecast_hours, + unit_cost, + res_model=res_model, + res_id=res_id, + **kwargs + ) + to_create = [] + self_by_start_date = {r.date_from: r for r in self} + updated = [] + for vals in values: + start_date = vals["date_from"] + rec = self_by_start_date.pop(start_date, None) + if rec is None: + to_create.append(vals) + else: + rec.write(vals) + updated.append(rec.id) + _logger.debug("updated lines %s", updated) + to_remove = self.browse([r.id for r in self_by_start_date.values()]) + to_remove.unlink() + _logger.debug("removed lines %s", to_remove.ids) + _logger.debug("%d records to create", len(to_create)) + return to_create + + def _prepare_forecast_lines( + self, + name, + date_from, + date_to, + ttype, + forecast_hours, + unit_cost, + res_model="", + res_id=0, + **kwargs + ): + common_value_dict = { + "company_id": self.env.company.id, + "name": name, + "type": ttype, + "forecast_role_id": kwargs.get("forecast_role_id", False), + "employee_id": kwargs.get("employee_id", False), + "project_id": kwargs.get("project_id", False), + "task_id": kwargs.get("task_id", False), + "sale_line_id": kwargs.get("sale_line_id", False), + "hr_leave_id": kwargs.get("hr_leave_id", False), + "employee_forecast_role_id": kwargs.get("employee_forecast_role_id", False), + "res_model": res_model, + "res_id": res_id, + } + forecast_line_vals = [] + if common_value_dict["employee_id"]: + resource = ( + self.env["hr.employee"] + .browse(common_value_dict["employee_id"]) + .resource_id + ) + calendar = resource.calendar_id + else: + resource = self.env["resource.resource"] + calendar = self.env.company.resource_calendar_id + for updates in self._split_per_period( + date_from, date_to, forecast_hours, unit_cost, resource, calendar + ): + values = common_value_dict.copy() + values.update(updates) + forecast_line_vals.append(values) + return forecast_line_vals + + def _company_horizon_end(self): + company = self.env.company + today = fields.Date.context_today(self) + horizon_end = today + relativedelta(months=company.forecast_line_horizon) + return horizon_end + + def _compute_horizon(self, date_from, date_to): + today = fields.Date.context_today(self) + horizon_end = self._company_horizon_end() + # the date_to passed as argument is "included". We want to be able to + # reason with this date "excluded" when doing substractions to compute + # a number of days -> add 1d + date_to += relativedelta(days=1) + horiz_date_from = max(date_from, today) + horiz_date_to = min(date_to, horizon_end) + return horiz_date_from, horiz_date_to, date_to + + def _split_per_period( + self, date_from, date_to, forecast_hours, unit_cost, resource, calendar + ): + company = self.env.company + granularity = company.forecast_line_granularity + delta = date_utils.get_timedelta(1, granularity) + horiz_date_from, horiz_date_to, date_to = self._compute_horizon( + date_from, date_to + ) + curr_date = date_utils.start_of(horiz_date_from, granularity) + if horiz_date_to <= horiz_date_from: + return + whole_period_forecast = self._number_of_hours( + horiz_date_from, horiz_date_to, resource, calendar + ) + if whole_period_forecast == 0: + # the resource if completely off during the period -> we cannot + # plan the forecast in the period. We put the whole forecast on the + # day after the period. + # TODO future improvement: dump this on the + # first day when the employee is not on holiday + _logger.warning( + "resource %s has 0 forecast on period %s -> %s", + resource, + horiz_date_from, + horiz_date_to, + ) + yield { + "date_from": horiz_date_to, + "date_to": horiz_date_to + delta - relativedelta(days=1), + "forecast_hours": forecast_hours, + "cost": forecast_hours * unit_cost, + } + return + daily_forecast = forecast_hours / whole_period_forecast + if daily_forecast == 0: + return + while curr_date < horiz_date_to: + next_date = curr_date + delta + # XXX fix periods which are not entirely in the horizon + # (min max trick on the numerator of the division) + period_forecast = self._number_of_hours( + max(curr_date, date_from), + min(next_date, date_to), + resource, + calendar, + ) + # note we do create lines even if the period_forecast is 0, as this + # ensures that consolidated capacity can be computed: if there is + # no line for a day when the employee does not work, but for some + # reason there is a need on that day, we need the 0 capacity line + # to compute the negative consolidated capacity. + period_forecast *= daily_forecast + period_cost = period_forecast * unit_cost + updates = { + "date_from": curr_date, + "date_to": next_date - relativedelta(days=1), + "forecast_hours": period_forecast, + "cost": period_cost, + } + yield updates + curr_date = next_date + + @api.model + def _cron_recompute_all(self, force_company_id=None, force_delete=False): + today = fields.Date.context_today(self) + ForecastLine = self.env["forecast.line"].sudo() + if force_company_id: + companies = self.env["res.company"].browse(force_company_id) + else: + companies = self.env["res.company"].search([]) + for company in companies: + ForecastLine = ForecastLine.with_company(company) + limit_date = date_utils.start_of(today, company.forecast_line_granularity) + if force_delete: + stale_forecast_lines = ForecastLine.search( + [ + ("company_id", "=", company.id), + ] + ) + else: + stale_forecast_lines = ForecastLine.search( + [ + ("date_from", "<", limit_date), + ("company_id", "=", company.id), + ] + ) + stale_forecast_lines.unlink() + + # always start with forecast role to ensure we can compute the + # employee_resource_forecast_line_id field + self.env["hr.employee.forecast.role"]._recompute_forecast_lines( + force_company_id=force_company_id + ) + self.env["sale.order.line"]._recompute_forecast_lines( + force_company_id=force_company_id + ) + self.env["hr.leave"]._recompute_forecast_lines( + force_company_id=force_company_id + ) + self.env["project.task"]._recompute_forecast_lines( + force_company_id=force_company_id + ) + # fix weird issue where the employee_resource_forecast_line_id seems to + # not be always computed + ForecastLine.search([])._compute_employee_forecast_line_id() + + @api.model + def convert_days_to_hours(self, days): + uom_day = self.env.ref("uom.product_uom_day") + uom_hour = self.env.ref("uom.product_uom_hour") + return uom_day._compute_quantity(days, uom_hour) + + @api.model + def _number_of_hours( + self, date_from, date_to, resource, calendar, force_granularity=False + ): + if force_granularity: + company = self.env.company + granularity = company.forecast_line_granularity + date_from = date_utils.start_of(date_from, granularity) + date_to = date_utils.end_of(date_to, granularity) + relativedelta(days=1) + tzinfo = pytz.timezone(calendar.tz) + start_dt = tzinfo.localize(datetime.combine(date_from, time(0))) + end_dt = tzinfo.localize(datetime.combine(date_to, time(0))) + intervals = calendar._work_intervals_batch( + start_dt, end_dt, resources=resource + )[resource.id] + nb_hours = sum( + (stop - start).total_seconds() / 3600 for start, stop, meta in intervals + ) + return nb_hours + + def unlink(self): + # we routinely unlink forecast lines, let's not fill the logs with this + with mute_logger("odoo.models.unlink"): + return super().unlink() + + @api.model_create_multi + @api.returns("self", lambda value: value.id) + def create(self, vals_list): + records = super().create(vals_list) + employee_role_lines = records.filtered( + lambda r: r.res_model == "hr.employee.forecast.role" + ) + if employee_role_lines: + # check for existing records which could have the new lines as + # employee_resource_forecast_line_id + other_lines = self.search( + [ + ("employee_resource_forecast_line_id", "=", False), + ( + "employee_id", + "in", + employee_role_lines.mapped("employee_id").ids, + ), + ] + ) + other_lines._compute_employee_forecast_line_id() + return records diff --git a/project_forecast_line/models/forecast_line_mixin.py b/project_forecast_line/models/forecast_line_mixin.py new file mode 100644 index 0000000000..b4da827c4f --- /dev/null +++ b/project_forecast_line/models/forecast_line_mixin.py @@ -0,0 +1,15 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class ForecastLineModelMixin(models.Model): + _name = "forecast.line.mixin" + _description = "mixin for models which generate forecast lines" + + def _get_forecast_lines(self, domain=None): + self.ensure_one() + base_domain = [("res_model", "=", self._name), ("res_id", "=", self.id)] + if domain is not None: + base_domain += domain + return self.env["forecast.line"].search(base_domain) diff --git a/project_forecast_line/models/forecast_role.py b/project_forecast_line/models/forecast_role.py new file mode 100644 index 0000000000..079b35a658 --- /dev/null +++ b/project_forecast_line/models/forecast_role.py @@ -0,0 +1,11 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ForecastRole(models.Model): + _name = "forecast.role" + _description = "Employee role for task matching" + + name = fields.Char(required=True) + description = fields.Text() diff --git a/project_forecast_line/models/hr_employee.py b/project_forecast_line/models/hr_employee.py new file mode 100644 index 0000000000..7518fabd69 --- /dev/null +++ b/project_forecast_line/models/hr_employee.py @@ -0,0 +1,185 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + + +class HrJob(models.Model): + _inherit = "hr.job" + + role_id = fields.Many2one("forecast.role", ondelete="restrict") + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + + # NB: these fields need to be marked as groups="hr.groups_hr_user", + # because otherwise if a user with less privileges tries to read + # hr.employee.public, they'll get an AccessError on these fields. + # More info at: https://github.com/odoo/odoo/blob/d21887008b1ac7 + # 69bd91d24e972323ffe936391a/addons/hr/models/hr_employee.py#L22 + role_ids = fields.One2many( + "hr.employee.forecast.role", + "employee_id", + groups="hr.group_hr_user", + ) + main_role_id = fields.Many2one( + "forecast.role", + compute="_compute_main_role_id", + ondelete="restrict", + groups="hr.group_hr_user", + ) + + def _compute_main_role_id(self): + # can"t store as it depends on current date + today = fields.Date.context_today(self) + for rec in self: + rec.main_role_id = rec.role_ids.filtered( + lambda r: r.date_start <= today and (r.date_end >= today) + if r.date_end + else True + )[:1].role_id + + def write(self, values): + values = self._check_job_role(values) + return super().write(values) + + @api.model_create_multi + @api.returns("self", lambda value: value.id) + def create(self, values): + values = [self._check_job_role(val) for val in values] + return super().create(values) + + def _check_job_role(self, values): + """helper method + ensures that you get a role when you set a job with a role""" + new_job_id = values.get("job_id") + if new_job_id: + job = self.env["hr.job"].browse(new_job_id) + if job.role_id and "role_ids" not in values: + values = values.copy() + values["role_ids"] = [ + fields.Command.clear(), + fields.Command.create({"role_id": job.role_id.id}), + ] + return values + + +class HrEmployeeForecastRole(models.Model): + _name = "hr.employee.forecast.role" + _inherit = "forecast.line.mixin" + _description = "Employee forecast role" + _order = "employee_id, date_start, sequence, rate DESC, id" + + employee_id = fields.Many2one("hr.employee", required=True, ondelete="cascade") + role_id = fields.Many2one("forecast.role", required=True) + date_start = fields.Date(required=True, default=fields.Date.today) + date_end = fields.Date() + rate = fields.Integer(default=100) + sequence = fields.Integer() + company_id = fields.Many2one(related="employee_id.company_id", store=True) + # TODO: + # ensure sum of rate = 100 + + @api.model_create_multi + def create(self, vals_list): + recs = super().create(vals_list) + recs._update_forecast_lines() + return recs + + def write(self, values): + res = super().write(values) + self._update_forecast_lines() + return res + + def _update_forecast_lines(self): + today = fields.Date.context_today(self) + leave_date_start = self.env.context.get("date_start") + leave_date_to = self.env.context.get("date_to") + ForecastLine = self.env["forecast.line"].sudo() + if not self: + return ForecastLine + leaves = self.env["hr.leave"].search( + [ + ("employee_id", "in", self.mapped("employee_id").ids), + ("state", "!=", "cancel"), + ("date_to", ">=", min(self.mapped("date_start"))), + ] + ) + leaves._update_forecast_lines() + forecast_vals = [] + ForecastLine.search( + [ + ("res_id", "in", self.ids), + ("res_model", "=", self._name), + ("date_from", "<", today), + ] + ).unlink() + horizon_end = ForecastLine._company_horizon_end() + for rec in self: + ForecastLine = ForecastLine.with_company(rec.company_id) + if rec.date_end: + date_end = rec.date_end + ForecastLine.search( + [ + ("res_id", "=", rec.id), + ("res_model", "=", self._name), + ("date_to", ">=", date_end), + ] + ).unlink() + else: + date_end = horizon_end - relativedelta(days=1) + if leave_date_to is not None: + date_end = min(leave_date_to, date_end) + date_start = max(rec.date_start, today) + if leave_date_start is not None: + date_start = max(date_start, leave_date_start) + resource = rec.employee_id.resource_id + calendar = resource.calendar_id + + forecast = ForecastLine._number_of_hours( + date_start, date_end, resource, calendar, force_granularity=True + ) + forecast_lines = ForecastLine.search( + [ + ("res_model", "=", self._name), + ("res_id", "in", rec.ids), + ("date_from", "<=", date_end), + ("date_to", ">=", date_start), + ] + ) + forecast_vals += forecast_lines._update_forecast_lines( + name="Employee %s as %s (%d%%)" + % (rec.employee_id.name, rec.role_id.name, rec.rate), + date_from=date_start, + date_to=date_end, + forecast_hours=forecast * rec.rate / 100.0, + unit_cost=rec.employee_id.hourly_cost, # XXX to check + ttype="confirmed", + forecast_role_id=rec.role_id.id, + employee_id=rec.employee_id.id, + employee_forecast_role_id=rec.id, + res_model=self._name, + res_id=rec.id, + ) + return ForecastLine.create(forecast_vals) + + @api.model + def _recompute_forecast_lines(self, force_company_id=None): + today = fields.Date.context_today(self) + if force_company_id: + companies = self.env["res.company"].browse(force_company_id) + else: + companies = self.env["res.company"].search([]) + for company in companies: + to_update = self.with_company(company).search( + [ + "|", + ("date_end", "=", False), + ("date_end", ">=", today), + ("company_id", "=", company.id), + ] + ) + to_update._update_forecast_lines() diff --git a/project_forecast_line/models/hr_leave.py b/project_forecast_line/models/hr_leave.py new file mode 100644 index 0000000000..6ab2afda36 --- /dev/null +++ b/project_forecast_line/models/hr_leave.py @@ -0,0 +1,87 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class HrLeave(models.Model): + _name = "hr.leave" + _inherit = ["hr.leave", "forecast.line.mixin"] + + @api.model_create_multi + def create(self, vals_list): + leaves = super().create(vals_list) + leaves._update_forecast_lines() + return leaves + + def write(self, values): + res = super().write(values) + self._update_forecast_lines() + return res + + def _update_forecast_lines(self): + forecast_vals = [] + ForecastLine = self.env["forecast.line"].sudo() + # XXX try to be smarter and only unlink those needing unlinking, update the others + ForecastLine.search( + [("res_id", "in", self.ids), ("res_model", "=", self._name)] + ).unlink() + leaves = self.filtered_domain([("state", "!=", "refuse")]) + # we need to use sudo here, because forecast line creation + # requires access to fields declared on hr.employee + # we don't want to restrict them with `groups="hr.group_hr_user"` + # as this will require giving access to employee app, + # which isn't wanted on some projects + # for more details see here: .../addons/hr/models/hr_employee.py#L22 + for leave in leaves.sudo(): + if not leave.employee_id.main_role_id: + _logger.warning( + "No forecast role for employee %s (%s)", + leave.employee_id.name, + leave.employee_id, + ) + continue + if leave.state == "validate": + # will be handled by the resource.calendar.leaves + continue + else: + forecast_type = "forecast" + ForecastLine = ForecastLine.with_company(leave.employee_company_id) + forecast_vals += ForecastLine._prepare_forecast_lines( + name=_("Leave"), + date_from=leave.date_from.date(), + date_to=leave.date_to.date(), + ttype=forecast_type, + forecast_hours=ForecastLine.convert_days_to_hours( + -1 * leave.number_of_days + ), + unit_cost=leave.employee_id.hourly_cost, + forecast_role_id=leave.employee_id.main_role_id.id, + hr_leave_id=leave.id, + employee_id=leave.employee_id.id, + res_model=self._name, + res_id=leave.id, + ) + return ForecastLine.create(forecast_vals) + + @api.model + def _recompute_forecast_lines(self, force_company_id=None): + today = fields.Date.context_today(self) + if force_company_id: + companies = self.env["res.company"].browse(force_company_id) + else: + companies = self.env["res.company"].search([]) + for company in companies: + to_update = self.with_company(company).search( + [ + ("date_to", ">=", today), + ("employee_company_id", "=", company.id), + ] + ) + to_update._update_forecast_lines() + + +# XXX: leave request should create forcast negative forecast? diff --git a/project_forecast_line/models/product_template.py b/project_forecast_line/models/product_template.py new file mode 100644 index 0000000000..fb9874266a --- /dev/null +++ b/project_forecast_line/models/product_template.py @@ -0,0 +1,9 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + forecast_role_id = fields.Many2one("forecast.role", ondelete="restrict") diff --git a/project_forecast_line/models/project_project_stage.py b/project_forecast_line/models/project_project_stage.py new file mode 100644 index 0000000000..09491c88b0 --- /dev/null +++ b/project_forecast_line/models/project_project_stage.py @@ -0,0 +1,12 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ProjectProjectStage(models.Model): + _inherit = "project.project.stage" + + forecast_line_type = fields.Selection( + [("forecast", "Forecast"), ("confirmed", "Confirmed")], + help="type of forecast lines created by the tasks of projects in that stage", + ) diff --git a/project_forecast_line/models/project_task.py b/project_forecast_line/models/project_task.py new file mode 100644 index 0000000000..16a9e7b05f --- /dev/null +++ b/project_forecast_line/models/project_task.py @@ -0,0 +1,249 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +import random + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class ProjectTask(models.Model): + _name = "project.task" + _inherit = ["project.task", "forecast.line.mixin"] + + forecast_role_id = fields.Many2one("forecast.role", ondelete="restrict") + forecast_date_planned_start = fields.Date("Planned start date") + forecast_date_planned_end = fields.Date("Planned end date") + forecast_recomputation_trigger = fields.Float( + compute="_compute_forecast_recomputation_trigger", + store=True, + help="Technical field used to trigger the forecast recomputation", + ) + + @api.model_create_multi + def create(self, vals_list): + # compatibility with fields from project_enterprise + for vals in vals_list: + if vals.get("planned_date_begin"): + vals["forecast_date_planned_start"] = vals["planned_date_begin"] + if vals.get("planned_date_end"): + vals["forecast_date_planned_end"] = vals["planned_date_end"] + tasks = super().create(vals_list) + # tasks._update_forecast_lines() + return tasks + + def _update_forecast_lines_trigger_fields(self): + return [ + # "sale_order_line_id", + "forecast_role_id", + "forecast_date_planned_start", + "forecast_date_planned_end", + # "remaining_hours", + "name", + # "planned_time", + "user_ids", + "project_id.stage_id", + "project_id.stage_id.forecast_line_type", + ] + + @api.depends(_update_forecast_lines_trigger_fields) + def _compute_forecast_recomputation_trigger(self): + value = random.random() + for rec in self: + rec.forecast_recomputation_trigger = value + + def write(self, values): + # compatibility with fields from project_enterprise + if "planned_date_begin" in values: + values["forecast_date_planned_start"] = values["planned_date_begin"] + if "planned_date_end" in values: + values["forecast_date_planned_end"] = values["planned_date_end"] + return super().write(values) + + def _write(self, values): + res = super()._write(values) + if "forecast_recomputation_trigger" in values: + self._update_forecast_lines() + elif "remaining_hours" in values: + self._quick_update_forecast_lines() + return res + + @api.onchange("user_ids") + def onchange_user_ids(self): + for task in self: + if not task.user_ids: + continue + if task.forecast_role_id: + continue + employees = task.mapped("user_ids.employee_id") + for employee in employees: + if employee.main_role_id: + task.forecast_role_id = employee.main_role_id + break + + def _get_task_employees(self): + return self.with_context(active_test=False).mapped("user_ids.employee_id") + + def _quick_update_forecast_lines(self): + # called when only the remaining hours have changed. In this case, we + # can only update the forecast lines by applying a ratio + ForecastLine = self.env["forecast.line"].sudo() + for task in self: + forecast_lines = ForecastLine.search( + [("res_model", "=", self._name), ("res_id", "=", task.id)] + ) + total_forecast = sum(forecast_lines.mapped("forecast_hours")) + if not forecast_lines or not total_forecast: + # no existing forecast lines, try to generate some using the + # normal flow + task._update_forecast_lines() + continue + # caution: total_forecast is negative -> make sure we have a + # positive ratio, so that the multiplication does not change the + # sign of the forecast + ratio = abs(task.remaining_hours / total_forecast) + for line in forecast_lines: + line.forecast_hours *= ratio + + def _should_have_forecast(self): + self.ensure_one() + if not self.forecast_role_id: + _logger.info("skip task %s: no forecast role", self) + return False + elif not self.project_id: + _logger.info("skip task %s: no project", self) + elif self.project_id.stage_id: + forecast_type = self.project_id.stage_id.forecast_line_type + if not forecast_type: + _logger.info("skip task %s: no forecast for project state", self) + return False + elif self.sale_line_id: + sale_state = self.sale_line_id.state + if sale_state == "cancel": + _logger.info("skip task %s: cancelled sale", self) + return False + elif sale_state == "sale": + return True + else: + # TODO have forecast quantity if the sale is in Draft and we + # are not generating forecast lines from SO + _logger.info("skip task %s: draft sale") + return False + + if not self.forecast_date_planned_start or not self.forecast_date_planned_end: + _logger.info("skip task %s: no planned dates", self) + return False + if not self.remaining_hours: + _logger.info("skip task %s: no remaining hours", self) + return False + if self.remaining_hours < 0: + _logger.info("skip task %s: negative remaining hours", self) + return False + return True + + def _update_forecast_lines(self): + _logger.debug("update forecast lines %s", self) + today = fields.Date.context_today(self) + forecast_vals = [] + ForecastLine = self.env["forecast.line"].sudo() + task_with_lines_to_clean = [] + for task in self: + task = task.with_company(task.company_id) + if not task._should_have_forecast(): + task_with_lines_to_clean.append(task.id) + continue + if task.project_id.stage_id: + forecast_type = task.project_id.stage_id.forecast_line_type + elif task.sale_line_id: + if task.sale_line_id.state == "sale": + forecast_type = "confirmed" + else: + forecast_type = "forecast" + else: + _logger.warn( + "strange case -> undefined forecast type for %s: skip", task + ) + continue + + date_start = max(today, task.forecast_date_planned_start) + date_end = max(today, task.forecast_date_planned_end) + employees = task._get_task_employees() + employee_ids = employees.ids + if not employees: + employees = [False] + employee_ids = [False] + _logger.debug( + "compute forecast for task %s: %s to %s %sh", + task, + date_start, + date_end, + task.remaining_hours, + ) + forecast_hours = task.remaining_hours / len(employees) + # remove lines for employees which are no longer assigned to the task + ForecastLine.search( + [ + ("res_model", "=", self._name), + ("res_id", "=", task.id), + ("employee_id", "not in", tuple(employee_ids)), + ] + ).unlink() + for employee in employees: + if employee: + employee_id = employee.id + company = employee.company_id + else: + employee_id = False + company = task.company_id + employee_lines = ForecastLine.search( + [ + ("res_model", "=", self._name), + ("res_id", "=", task.id), + ("employee_id", "=", employee_id), + ] + ) + ForecastLine = ForecastLine.with_company(company) + forecast_vals += employee_lines._update_forecast_lines( + name=task.name, + date_from=date_start, + date_to=date_end, + ttype=forecast_type, + forecast_hours=-1 * forecast_hours, + # XXX currency + unit conversion + unit_cost=task.sale_line_id.product_id.standard_price, + forecast_role_id=task.forecast_role_id.id, + sale_line_id=task.sale_line_id.id, + task_id=task.id, + project_id=task.project_id.id, + employee_id=employee_id, + res_model=self._name, + res_id=task.id, + ) + if task_with_lines_to_clean: + to_clean = ForecastLine.search( + [ + ("res_model", "=", self._name), + ("res_id", "in", tuple(task_with_lines_to_clean)), + ] + ) + if to_clean: + to_clean.unlink() + lines = ForecastLine.create(forecast_vals) + return lines + + @api.model + def _recompute_forecast_lines(self, force_company_id=None): + today = fields.Date.context_today(self) + if force_company_id: + companies = self.env["res.company"].browse(force_company_id) + else: + companies = self.env["res.company"].search([]) + for company in companies: + to_update = self.with_company(company).search( + [ + ("forecast_date_planned_end", ">=", today), + ("company_id", "=", company.id), + ] + ) + to_update._update_forecast_lines() diff --git a/project_forecast_line/models/res_company.py b/project_forecast_line/models/res_company.py new file mode 100644 index 0000000000..604ef8f9d7 --- /dev/null +++ b/project_forecast_line/models/res_company.py @@ -0,0 +1,25 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + forecast_line_granularity = fields.Selection( + [("day", "Day"), ("week", "Week"), ("month", "Month")], + default="month", + help="Periodicity of the forecast that will be generated", + ) + forecast_line_horizon = fields.Integer( + help="Number of month for the forecast planning", default=12 + ) + + def write(self, values): + res = super().write(values) + if "forecast_line_granularity" in values or "forecast_line_horizon" in values: + for company in self: + self.env["forecast.line"]._cron_recompute_all( + force_company_id=company.id, force_delete=True + ) + return res diff --git a/project_forecast_line/models/res_config_settings.py b/project_forecast_line/models/res_config_settings.py new file mode 100644 index 0000000000..4ac43649ec --- /dev/null +++ b/project_forecast_line/models/res_config_settings.py @@ -0,0 +1,19 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + forecast_line_granularity = fields.Selection( + related="company_id.forecast_line_granularity", readonly=False + ) + forecast_line_horizon = fields.Integer( + related="company_id.forecast_line_horizon", readonly=False + ) + + group_forecast_line_on_quotation = fields.Boolean( + "Forecast Line on Quotations", + implied_group="project_forecast_line.group_forecast_line_on_quotation", + ) diff --git a/project_forecast_line/models/resource_calendar_leaves.py b/project_forecast_line/models/resource_calendar_leaves.py new file mode 100644 index 0000000000..9a4ec63fe5 --- /dev/null +++ b/project_forecast_line/models/resource_calendar_leaves.py @@ -0,0 +1,51 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from dateutil.relativedelta import relativedelta + +from odoo import api, models + + +class ResourceCalendarLeaves(models.Model): + _name = "resource.calendar.leaves" + _inherit = ["resource.calendar.leaves", "forecast.line.mixin"] + + @api.model_create_multi + def create(self, vals_list): + recs = super().create(vals_list) + recs._update_forecast_lines() + return recs + + def write(self, values): + res = super().write(values) + self._update_forecast_lines() + return res + + def _get_resource_roles(self): + resources = self.mapped("resource_id") + if resources: + employees = self.env["hr.employee"].search([("id", "in", resources.ids)]) + else: + employees = self.env["hr.employee"].search( + [("company_id", "in", self.mapped("company_id").ids)] + ) + roles = self.env["hr.employee.forecast.role"].search( + [("employee_id", "in", employees.ids)] + ) + return roles + + def _update_forecast_lines(self): + roles = self._get_resource_roles() + if self: + date_start = min(self.mapped("date_from")).date() - relativedelta(days=1) + date_to = max(self.mapped("date_to")).date() + relativedelta(days=1) + else: + date_start = date_to = None + roles.with_context( + date_start=date_start, date_to=date_to + )._update_forecast_lines() + + def unlink(self): + roles = self._get_resource_roles() + res = super().unlink() + roles._update_forecast_lines() + return res diff --git a/project_forecast_line/models/sale_order.py b/project_forecast_line/models/sale_order.py new file mode 100644 index 0000000000..1ae499b33b --- /dev/null +++ b/project_forecast_line/models/sale_order.py @@ -0,0 +1,33 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + default_forecast_date_start = fields.Date() + default_forecast_date_end = fields.Date() + + # XXX rewrite this based on a trigger field on so lines + def action_cancel(self): + res = super().action_cancel() + self.filtered(lambda r: r.state == "cancel").mapped( + "order_line" + )._update_forecast_lines() + return res + + def action_confirm(self): + res = super().action_confirm() + self.filtered(lambda r: r.state == "sale").mapped( + "order_line" + )._update_forecast_lines() + return res + + def write(self, values): + res = super().write(values) + if self and "project_id" in values: + self.env["forecast.line"].sudo().search( + [("sale_id", "in", self.ids)] + ).write({"project_id": values["project_id"]}) + return res diff --git a/project_forecast_line/models/sale_order_line.py b/project_forecast_line/models/sale_order_line.py new file mode 100644 index 0000000000..be885aeec1 --- /dev/null +++ b/project_forecast_line/models/sale_order_line.py @@ -0,0 +1,138 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class SaleOrderLine(models.Model): + _name = "sale.order.line" + _inherit = ["sale.order.line", "forecast.line.mixin"] + + forecast_date_start = fields.Date() + forecast_date_end = fields.Date() + + @api.model_create_multi + def create(self, vals_list): + lines = super().create(vals_list) + lines._update_forecast_lines() + return lines + + def _update_forecast_lines(self): + forecast_vals = [] + ForecastLine = self.env["forecast.line"].sudo() + # XXX try to be smarter and only unlink those needing unlinking, update the others + ForecastLine.search( + [("res_id", "in", self.ids), ("res_model", "=", self._name)] + ).unlink() + for line in self: + ForecastLine = ForecastLine.with_company(line.company_id) + if not line.product_id.forecast_role_id: + continue + elif line.state in ("cancel", "sale"): + # no forecast line for confirmed sales -> this is handled by projects and tasks + continue + elif not (line.forecast_date_end and line.forecast_date_start): + _logger.info( + "sale line with forecast product but no dates -> ignoring %s", + line.id, + ) + continue + else: + forecast_type = "forecast" + uom = line.product_uom + quantity_hours = uom._compute_quantity( + line.product_uom_qty, self.env.ref("uom.product_uom_hour") + ) + forecast_vals += ForecastLine._prepare_forecast_lines( + name=line.name, + date_from=line.forecast_date_start, + date_to=line.forecast_date_end, + ttype=forecast_type, + forecast_hours=-1 * quantity_hours, + unit_cost=line.product_id.standard_price, # XXX currency + unit conversion + forecast_role_id=line.product_id.forecast_role_id.id, + sale_line_id=line.id, + project_id=line.project_id.id, + res_model="sale.order.line", + res_id=line.id, + ) + return ForecastLine.create(forecast_vals) + + @api.model + def _recompute_forecast_lines(self, force_company_id=None): + today = fields.Date.context_today(self) + if force_company_id: + companies = self.env["res.company"].browse(force_company_id) + else: + companies = self.env["res.company"].search([]) + for company in companies: + to_update = self.with_company(company).search( + [ + ("forecast_date_end", ">=", today), + ("company_id", "=", company.id), + ] + ) + to_update._update_forecast_lines() + + def _update_forecast_lines_trigger_fields(self): + return [ + "state", + "product_uom_qty", + "forecast_date_start", + "forecast_date_end", + "product_id", + "name", + ] + + def _write(self, values): + res = super()._write(values) + trigger_fields = self._update_forecast_lines_trigger_fields() + if any(field in values for field in trigger_fields): + self._update_forecast_lines() + return res + + @api.onchange("product_id") + def _onchange_product_id_warning(self): + res = super()._onchange_product_id_warning() + for line in self: + if not line.product_id.forecast_role_id: + line.forecast_date_start = False + line.forecast_date_end = False + else: + if ( + not line.forecast_date_start + and line.order_id.default_forecast_date_start + ): + line.forecast_date_start = line.order_id.default_forecast_date_start + if ( + not line.forecast_date_end + and line.order_id.default_forecast_date_end + ): + line.forecast_date_end = line.order_id.default_forecast_date_end + return res + + def _timesheet_create_task_prepare_values(self, project): + values = super()._timesheet_create_task_prepare_values(project) + values.update( + { + "forecast_role_id": self.product_id.forecast_role_id.id, + "forecast_date_planned_end": self.forecast_date_end, + "forecast_date_planned_start": self.forecast_date_start, + } + ) + return values + + def _timesheet_create_project(self): + project = super()._timesheet_create_project() + if self.product_id.project_template_id and self.product_id.forecast_role_id: + project.tasks.write( + { + "forecast_role_id": self.product_id.forecast_role_id.id, + "date_end": self.forecast_date_end, + "date_planned_start": self.forecast_date_start, + } + ) + return project diff --git a/project_forecast_line/readme/CONFIGURATION.rst b/project_forecast_line/readme/CONFIGURATION.rst new file mode 100644 index 0000000000..c30fb59df9 --- /dev/null +++ b/project_forecast_line/readme/CONFIGURATION.rst @@ -0,0 +1,20 @@ +In the settings, you'll find a Forecast Management section where you can configure + +* the granularity of the forecast (day / week / month) +* the horizon of the forecast (number of months) +* if we want to manage forecast from quotations + +Then you can configure in the Forecast application the forecast roles. These are +roles than can be linked to products and to employees, for instance "project +manager" or "consultant". + +If you want to use the forecast on sales -> forecast on tasks chain, you should +configure the service products that will be used on the sale order to give them +a forecast role. + +Finally, you need to set roles on employees. You can set this on the Jobs, and +when an employee is assigned a job, the job's role will be pushed to the +employee. Or you can just set the role on the employee. The roles of employees +have start and end dates, so you can manage people who will join the company in +the future or people's internal job changes. You can also set a rate and handle +people wearing multiple hats. diff --git a/project_forecast_line/readme/CONTRIBUTORS.rst b/project_forecast_line/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..ed6bc85941 --- /dev/null +++ b/project_forecast_line/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Alexandre Fayolle +* Maksym Yankin diff --git a/project_forecast_line/readme/DESCRIPTION.rst b/project_forecast_line/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..d86f5d98b8 --- /dev/null +++ b/project_forecast_line/readme/DESCRIPTION.rst @@ -0,0 +1,27 @@ +This module allows to plan your resources using forecast lines. + +For each employee of the company, the module will generate forecast line +records with a positive capacity based on their working time schedules. Then, +tasks assigned to employees will generate forecast lines with a negative +capacity which will "consume" the work time capacity of the employees. + +The idea is that you can then see the work capacity and scheduled work of +people by summing the "forecasts" per time period. If you have more resources +(positive forecast) than work (negative forecast) you will have a positive net +sum. Otherwise you are in trouble and need to recruit or reschedule your +work. Another way to use the report is checking when the work capacity of a +department becomes positive (or high enough) in order to provide you potential +customers with an estimate of when a project would be able to start. + +Forecast lines also come in two states "forecast" or "confirmed", depending on +whether the consumption is confirmed or not. For instance, holidays requests +and sales quotation lines create lines of type "forecast", whereas tasks for +project which are in a running state create lines with type "confirmed". + +To get the best experience using the Forecast application you may want to install: + +* project_forecast_line_holidays_public module which takes public holidays into + account during forecast lines creation + +* project_forecast_line_bokeh_chart module which improves the reports of + project_forecast_line module by using the bokeh widget available in OCA/web diff --git a/project_forecast_line/readme/USAGE.rst b/project_forecast_line/readme/USAGE.rst new file mode 100644 index 0000000000..da1496b3d8 --- /dev/null +++ b/project_forecast_line/readme/USAGE.rst @@ -0,0 +1,62 @@ +Forecast lines have the following data: + +* Forecast hours: it is positive for resources (employees) and negative for + things which consume time (project tasks, for instance) + +* From and To date which are the beginning and ending of the period of the + capacity + +* Consolidated forecast: this is a computed field, which is computed as follows: + + * for costs (project tasks for instance) we take the absolute value of the + forecast hours (so it is a positive number) + + * for resources (employee capacity for a period), we take the capacity and + substract all the costs for that employee on the same period. So it will be + positive if the employee still has some free time, and negative if he is + overloaded with work. + + * this consolidated forecast is currently converted to days to ease + readability of the forecast report + + +Objects creating forecast lines: + +* employees with a forecast role will create forecast line with a positive + capacity and type "confirmed" for each day on which they work. This + information comes from their work calendar, and the different roles that are + linked to the employee. + +* draft sale orders (if enabled in the settings) will create forecast lines of + type "forecast" for each sale order line having a product with a forecast + role and start and end dates. The forecast hours are negative + +* confirmed sale orders don't create forecast lines. This is handled by the + tasks created at the confirmation of the sale order + +* project tasks create forecast lines if they have a linked role and planned start/end + dates. The type of the line will depend on the related project's stage. The + `forecast_hours` field is based on the remaining time of the task, which is spread + on the work days of the planned start and end date of the task. If the + current date is in the middle of the planned duration of the task, it is used + as the start date. If the planned end date is in the past the task does not + generate forecast lines (and you need to fix your planning). In case multiple + employees are assigned to the task the forecast is split equally between + them. + +* holiday requests create negative forecast lines with type "forecast" when + they are pending manager validation. + +* Validated holiday requests do not generate forecast lines, as they alter the + work calendar of the employee: the employee will not have a positive line + associated to his leave days. + +The creation of forecast lines is done either in real time when some actions +are performed by the user (requesting leaves, updating the remaining time on a +project task, timesheeting) and also via a cron that runs on a daily basis. The +cron is required to cleanup lines related to dates in the past and to recompute +the lines related to project tasks by computing the ratio of remaing time on +the tasks on the remaining days, for tasks which are in progress. So, to start +using consolidated forecast report you first need to set everything mentioned +in Usage section. Then, probably run Forecast recomputation cron manually from +Scheduled Actions or wait till cron creates records. diff --git a/project_forecast_line/security/forecast_line_security.xml b/project_forecast_line/security/forecast_line_security.xml new file mode 100644 index 0000000000..bd57ee533d --- /dev/null +++ b/project_forecast_line/security/forecast_line_security.xml @@ -0,0 +1,13 @@ + + + + Manage Forecast Dates on Quotations + + + + + Forecast multi-company + + [('company_id', 'in', company_ids)] + + diff --git a/project_forecast_line/security/ir.model.access.csv b/project_forecast_line/security/ir.model.access.csv new file mode 100644 index 0000000000..bd24b3c536 --- /dev/null +++ b/project_forecast_line/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +project_forecast_line.access_forecast_line_mixin,access_forecast_line_mixin,project_forecast_line.model_forecast_line_mixin,base.group_user,1,0,0,0 +project_forecast_line.access_forecast_line,access_forecast_line,project_forecast_line.model_forecast_line,base.group_user,1,0,0,0 +project_forecast_line.access_forecast_role,access_forecast_role,project_forecast_line.model_forecast_role,base.group_user,1,0,0,0 +project_forecast_line.access_forecast_role_hr_user,access_forecast_role_hr_manager,project_forecast_line.model_forecast_role,hr.group_hr_user,1,1,1,1 +project_forecast_line.access_hr_employee_forecast_role,access_hr_employee_forecast_role,project_forecast_line.model_hr_employee_forecast_role,base.group_user,1,0,0,0 +project_forecast_line.access_hr_employee_forecast_role_hr_user,access_hr_employee_forecast_role_hr_user,project_forecast_line.model_hr_employee_forecast_role,hr.group_hr_user,1,1,1,1 diff --git a/project_forecast_line/static/description/icon.png b/project_forecast_line/static/description/icon.png new file mode 100644 index 0000000000..ebd3d6aada Binary files /dev/null and b/project_forecast_line/static/description/icon.png differ diff --git a/project_forecast_line/static/description/index.html b/project_forecast_line/static/description/index.html new file mode 100644 index 0000000000..76500140a2 --- /dev/null +++ b/project_forecast_line/static/description/index.html @@ -0,0 +1,507 @@ + + + + + + +Project Forecast Lines + + + +
+

Project Forecast Lines

+ + +

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

+

This module allows to plan your resources using forecast lines.

+

For each employee of the company, the module will generate forecast line +records with a positive capacity based on their working time schedules. Then, +tasks assigned to employees will generate forecast lines with a negative +capacity which will “consume” the work time capacity of the employees.

+

The idea is that you can then see the work capacity and scheduled work of +people by summing the “forecasts” per time period. If you have more resources +(positive forecast) than work (negative forecast) you will have a positive net +sum. Otherwise you are in trouble and need to recruit or reschedule your +work. Another way to use the report is checking when the work capacity of a +department becomes positive (or high enough) in order to provide you potential +customers with an estimate of when a project would be able to start.

+

Forecast lines also come in two states “forecast” or “confirmed”, depending on +whether the consumption is confirmed or not. For instance, holidays requests +and sales quotation lines create lines of type “forecast”, whereas tasks for +project which are in a running state create lines with type “confirmed”.

+

To get the best experience using the Forecast application you may want to install:

+
    +
  • project_forecast_line_holidays_public module which takes public holidays into +account during forecast lines creation
  • +
  • project_forecast_line_bokeh_chart module which improves the reports of +project_forecast_line module by using the bokeh widget available in OCA/web
  • +
+
+

Important

+

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

+
+

Table of contents

+ +
+

Usage

+

Forecast lines have the following data:

+
    +
  • Forecast hours: it is positive for resources (employees) and negative for +things which consume time (project tasks, for instance)
  • +
  • From and To date which are the beginning and ending of the period of the +capacity
  • +
  • Consolidated forecast: this is a computed field, which is computed as follows:
      +
    • for costs (project tasks for instance) we take the absolute value of the +forecast hours (so it is a positive number)
    • +
    • for resources (employee capacity for a period), we take the capacity and +substract all the costs for that employee on the same period. So it will be +positive if the employee still has some free time, and negative if he is +overloaded with work.
    • +
    • this consolidated forecast is currently converted to days to ease +readability of the forecast report
    • +
    +
  • +
+

Objects creating forecast lines:

+
    +
  • employees with a forecast role will create forecast line with a positive +capacity and type “confirmed” for each day on which they work. This +information comes from their work calendar, and the different roles that are +linked to the employee.
  • +
  • draft sale orders (if enabled in the settings) will create forecast lines of +type “forecast” for each sale order line having a product with a forecast +role and start and end dates. The forecast hours are negative
  • +
  • confirmed sale orders don’t create forecast lines. This is handled by the +tasks created at the confirmation of the sale order
  • +
  • project tasks create forecast lines if they have a linked role and planned start/end +dates. The type of the line will depend on the related project’s stage. The +forecast_hours field is based on the remaining time of the task, which is spread +on the work days of the planned start and end date of the task. If the +current date is in the middle of the planned duration of the task, it is used +as the start date. If the planned end date is in the past the task does not +generate forecast lines (and you need to fix your planning). In case multiple +employees are assigned to the task the forecast is split equally between +them.
  • +
  • holiday requests create negative forecast lines with type “forecast” when +they are pending manager validation.
  • +
  • Validated holiday requests do not generate forecast lines, as they alter the +work calendar of the employee: the employee will not have a positive line +associated to his leave days.
  • +
+

The creation of forecast lines is done either in real time when some actions +are performed by the user (requesting leaves, updating the remaining time on a +project task, timesheeting) and also via a cron that runs on a daily basis. The +cron is required to cleanup lines related to dates in the past and to recompute +the lines related to project tasks by computing the ratio of remaing time on +the tasks on the remaining days, for tasks which are in progress. So, to start +using consolidated forecast report you first need to set everything mentioned +in Usage section. Then, probably run Forecast recomputation cron manually from +Scheduled Actions or wait till cron creates records.

+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp SA
  • +
+
+
+

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_forecast_line/tests/__init__.py b/project_forecast_line/tests/__init__.py new file mode 100644 index 0000000000..93b26a9498 --- /dev/null +++ b/project_forecast_line/tests/__init__.py @@ -0,0 +1 @@ +from . import test_forecast_line diff --git a/project_forecast_line/tests/test_forecast_line.py b/project_forecast_line/tests/test_forecast_line.py new file mode 100644 index 0000000000..3e3e4fb1cc --- /dev/null +++ b/project_forecast_line/tests/test_forecast_line.py @@ -0,0 +1,1029 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from datetime import date + +from freezegun import freeze_time + +from odoo.tests.common import Form, TransactionCase, tagged + + +@tagged("-at_install", "post_install") +class BaseForecastLineTest(TransactionCase): + @classmethod + @freeze_time("2022-01-01") + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.ResUsers = cls.env["res.users"] + cls.ResPartner = cls.env["res.partner"] + cls.HrEmployee = cls.env["hr.employee"] + cls.HrEmployeeForecastRole = cls.env["hr.employee.forecast.role"] + cls.role_model = cls.HrEmployeeForecastRole._name + ProductProduct = cls.env["product.product"] + cls.env.company.write( + { + "forecast_line_granularity": "month", + "forecast_line_horizon": 6, # months + } + ) + cls.role_developer = cls.env["forecast.role"].create({"name": "developer"}) + cls.role_consultant = cls.env["forecast.role"].create({"name": "consultant"}) + cls.role_pm = cls.env["forecast.role"].create({"name": "project manager"}) + + cls.employee_dev = cls.HrEmployee.create({"name": "John Dev"}) + cls.user_consultant = cls.ResUsers.create( + {"name": "John Consultant", "login": "jc@example.com"} + ) + cls.employee_consultant = cls.HrEmployee.create( + {"name": "John Consultant", "user_id": cls.user_consultant.id} + ) + cls.user_pm = cls.ResUsers.create( + {"name": "John Peem", "login": "jp@example.com"} + ) + cls.employee_pm = cls.HrEmployee.create( + {"name": "John Peem", "user_id": cls.user_pm.id} + ) + cls.dev_employee_forecast_role = cls.HrEmployeeForecastRole.create( + { + "employee_id": cls.employee_dev.id, + "role_id": cls.role_developer.id, + "date_start": "2022-01-01", + "sequence": 1, + } + ) + cls.consult_employee_forecast_role = cls.HrEmployeeForecastRole.create( + { + "employee_id": cls.employee_consultant.id, + "role_id": cls.role_consultant.id, + "date_start": "2022-01-01", + "sequence": 1, + } + ) + cls.pm_employee_forecast_role = cls.HrEmployeeForecastRole.create( + { + "employee_id": cls.employee_pm.id, + "role_id": cls.role_pm.id, + "date_start": "2022-01-01", + "sequence": 1, + } + ) + + cls.product_dev_tm = ProductProduct.create( + { + "name": "development time and material", + "detailed_type": "service", + "service_tracking": "task_in_project", + "price_extra": 95, + "standard_price": 75, + "forecast_role_id": cls.role_developer.id, + "uom_id": cls.env.ref("uom.product_uom_hour").id, + "uom_po_id": cls.env.ref("uom.product_uom_hour").id, + } + ) + cls.product_consultant_tm = ProductProduct.create( + { + "name": "consultant time and material", + "detailed_type": "service", + "service_tracking": "task_in_project", + "price_extra": 100, + "standard_price": 80, + "forecast_role_id": cls.role_consultant.id, + "uom_id": cls.env.ref("uom.product_uom_hour").id, + "uom_po_id": cls.env.ref("uom.product_uom_hour").id, + } + ) + + cls.product_pm_tm = ProductProduct.create( + { + "name": "pm time and material", + "detailed_type": "service", + "service_tracking": "task_in_project", + "price_extra": 120, + "standard_price": 100, + "forecast_role_id": cls.role_consultant.id, + "uom_id": cls.env.ref("uom.product_uom_hour").id, + "uom_po_id": cls.env.ref("uom.product_uom_hour").id, + } + ) + cls.customer = cls.ResPartner.create({"name": "Some Customer"}) + + +class TestForecastLineEmployee(BaseForecastLineTest): + def test_employee_main_role(self): + self.HrEmployeeForecastRole.create( + { + "employee_id": self.employee_consultant.id, + "role_id": self.role_developer.id, + "date_start": "2021-01-01", + "date_end": "2021-12-31", + "sequence": 0, + } + ) + self.assertEqual(self.employee_consultant.main_role_id, self.role_consultant) + + def test_employee_job_role(self): + job = self.env["hr.job"].create( + {"name": "Developer", "role_id": self.role_developer.id} + ) + employee = self.env["hr.employee"].create( + {"name": "John Dev", "job_id": job.id} + ) + self.assertEqual(employee.main_role_id, self.role_developer) + self.assertEqual(len(employee.role_ids), 1) + self.assertEqual(employee.role_ids.rate, 100) + + def test_employee_job_role_change(self): + job1 = self.env["hr.job"].create( + {"name": "Consultant", "role_id": self.role_consultant.id} + ) + job2 = self.env["hr.job"].create( + {"name": "Developer", "role_id": self.role_developer.id} + ) + employee = self.env["hr.employee"].create( + {"name": "John Dev", "job_id": job2.id} + ) + employee.job_id = job1 + self.assertEqual(employee.main_role_id, self.role_consultant) + self.assertEqual(len(employee.role_ids), 1) + self.assertEqual(employee.role_ids.rate, 100) + + @freeze_time("2022-01-01") + def test_employee_forecast(self): + lines = self.env["forecast.line"].search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("forecast_role_id", "=", self.role_consultant.id), + ("res_model", "=", self.role_model), + ] + ) + self.assertEqual(len(lines), 6) # 6 months horizon + self.assertEqual( + lines.mapped("forecast_hours"), + # number of working days in the first 6 months of 2022, no vacations + [21.0 * 8, 20.0 * 8, 23.0 * 8, 21.0 * 8, 22.0 * 8, 22.0 * 8], + ) + res_ids = self.consult_employee_forecast_role.ids + self.consult_employee_forecast_role.unlink() + to_remove_lines = self.env["forecast.line"].search( + [("res_id", "in", res_ids), ("res_model", "=", "hr.employee.forecast.role")] + ) + self.assertFalse(to_remove_lines.exists()) + + @freeze_time("2022-01-01") + def test_employee_forecast_unlink(self): + roles = self.employee_consultant.role_ids + lines = self.env["forecast.line"].search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("forecast_role_id", "=", self.role_consultant.id), + ("res_model", "=", self.role_model), + ] + ) + roles.unlink() + self.assertFalse(lines.exists()) + + @freeze_time("2022-01-01") + def test_employee_forecast_change_roles(self): + # employee becomes 50% consultant, 50% PM on Feb 1st + roles = self.employee_consultant.role_ids + roles.write({"date_end": "2022-01-31"}) + self.env["base"].flush_model() + lines = self.env["forecast.line"].search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("forecast_role_id", "=", self.role_consultant.id), + ("res_model", "=", self.role_model), + ] + ) + self.assertEqual(len(lines), 1) # 100% consultant role now ends on 31/01 + self.assertEqual(lines.forecast_hours, 21.0 * 8) + self.HrEmployeeForecastRole.create( + [ + { + "employee_id": self.employee_consultant.id, + "role_id": self.role_consultant.id, + "date_start": "2022-02-01", + "sequence": 1, + "rate": 50, + }, + { + "employee_id": self.employee_consultant.id, + "role_id": self.role_pm.id, + "date_start": "2022-02-01", + "sequence": 2, + "rate": 50, + }, + ] + ) + self.env["base"].flush_model() + lines = self.env["forecast.line"].search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("forecast_role_id", "=", self.role_consultant.id), + ] + ) + self.assertEqual(len(lines), 6) # 6 months horizon + self.assertEqual( + lines.mapped("forecast_hours"), + # number of days in the first 6 months of 2022 + [ + 21.0 * 8, + 20.0 * 8 / 2, + 23.0 * 8 / 2, + 21.0 * 8 / 2, + 22.0 * 8 / 2, + 22.0 * 8 / 2, + ], + ) + res_ids = self.consult_employee_forecast_role.ids + self.consult_employee_forecast_role.unlink() + to_remove_lines = self.env["forecast.line"].search( + [("res_id", "in", res_ids), ("res_model", "=", "hr.employee.forecast.role")] + ) + self.assertFalse(to_remove_lines.exists()) + + @freeze_time("2022-01-01 12:00:00") + def test_forecast_with_calendar(self): + calendar = self.employee_dev.resource_calendar_id + self.env["resource.calendar.leaves"].create( + { + "name": "Easter monday", + "calendar_id": calendar.id, + "date_from": "2022-04-18 00:00:00", + "date_to": "2022-04-19 00:00:00", # Easter + "time_type": "leave", + } + ) + self.env["base"].flush_model() + lines = self.env["forecast.line"].search( + [ + ("employee_id", "=", self.employee_dev.id), + ("forecast_role_id", "=", self.role_developer.id), + ("res_model", "=", self.role_model), + ] + ) + self.assertEqual(len(lines), 6) # 6 months horizon + self.assertEqual( + lines.mapped("forecast_hours"), + # number of days in the first 6 months of 2022, minus easter in April + [21.0 * 8, 20.0 * 8, 23.0 * 8, (21.0 - 1) * 8, 22.0 * 8, 22.0 * 8], + ) + res_ids = self.dev_employee_forecast_role.ids + self.dev_employee_forecast_role.unlink() + to_remove_lines = self.env["forecast.line"].search( + [("res_id", "in", res_ids), ("res_model", "=", "hr.employee.forecast.role")] + ) + self.assertFalse(to_remove_lines.exists()) + + +class TestForecastLineSales(BaseForecastLineTest): + def _create_sale( + self, default_forecast_date_start, default_forecast_date_end, uom_qty=10 + ): + with Form(self.env["sale.order"]) as form: + form.partner_id = self.customer + with form.order_line.new() as line: + line.product_id = self.product_dev_tm + line.product_uom_qty = uom_qty # 1 FTE sold + line.product_uom = self.env.ref("uom.product_uom_day") + so = form.save() + so.date_order = "2022-01-10 08:00:00" + so.default_forecast_date_start = default_forecast_date_start + so.default_forecast_date_end = default_forecast_date_end + return so + + @freeze_time("2022-01-01") + def test_draft_sale_order_creates_negative_forecast_forecast(self): + so = self._create_sale("2022-02-07", "2022-02-20") + line = so.order_line[0] + line._onchange_product_id_warning() + self.assertEqual(line.forecast_date_start, date(2022, 2, 7)) + self.assertEqual(line.forecast_date_end, date(2022, 2, 20)) + forecast_lines = self.env["forecast.line"].search( + [ + ("sale_line_id", "=", line.id), + ("res_model", "=", "sale.order.line"), + ] + ) + self.assertEqual(len(forecast_lines), 1) # 10 days on 2022-02-01 to 2022-02-10 + self.assertEqual(forecast_lines.type, "forecast") + self.assertEqual( + forecast_lines.forecast_role_id, + self.product_dev_tm.forecast_role_id, + ) + self.assertEqual(forecast_lines.forecast_hours, -10 * 8) + self.assertEqual(forecast_lines.cost, -10 * 8 * 75) + self.assertEqual(forecast_lines.date_from, date(2022, 2, 1)) + self.assertEqual(forecast_lines.date_to, date(2022, 2, 28)) + + @freeze_time("2022-01-01") + def test_sale_line_unlink(self): + so = self._create_sale("2022-02-07", "2022-02-20") + line = so.order_line[0] + forecast_lines = self.env["forecast.line"].search( + [ + ("sale_line_id", "=", line.id), + ("res_model", "=", "sale.order.line"), + ] + ) + line.unlink() + self.assertFalse(forecast_lines.exists()) + + @freeze_time("2022-01-01") + def test_draft_sale_order_without_dates_no_forecast(self): + """a draft sale order with no dates on the line does not create forecast""" + so = self._create_sale("2022-02-07", False) + line = so.order_line[0] + line._onchange_product_id_warning() + self.assertEqual(line.forecast_date_start, date(2022, 2, 7)) + self.assertEqual(line.forecast_date_end, False) + forecast_lines = self.env["forecast.line"].search( + [ + ("sale_line_id", "=", line.id), + ("res_model", "=", "sale.order.line"), + ] + ) + self.assertFalse(forecast_lines) + + @freeze_time("2022-01-01") + def test_draft_sale_order_forecast_spread(self): + so = self._create_sale("2022-02-07", "2022-04-17", uom_qty=100) + + line = so.order_line[0] + line._onchange_product_id_warning() + self.assertEqual(line.forecast_date_start, date(2022, 2, 7)) + self.assertEqual(line.forecast_date_end, date(2022, 4, 17)) + forecast_lines = self.env["forecast.line"].search( + [ + ("sale_line_id", "=", line.id), + ("res_model", "=", "sale.order.line"), + ] + ) + self.assertEqual(len(forecast_lines), 3) + daily_ratio = 2 * 8 # 2 FTE * 8h days + self.assertAlmostEqual( + forecast_lines[0].forecast_hours, + -1 * daily_ratio * 16, # 16 worked days between 2022 Feb 7 and Feb 28 + ) + self.assertAlmostEqual( + forecast_lines[1].forecast_hours, + -1 * daily_ratio * 23, # 23 worked days in march 2022 + ) + self.assertAlmostEqual( + forecast_lines[2].forecast_hours, + -1 * daily_ratio * 11, # 11 worked day between april 1 and 17 2022 + ) + self.assertEqual( + forecast_lines.mapped("date_from"), + [date(2022, 2, 1), date(2022, 3, 1), date(2022, 4, 1)], + ) + self.assertEqual( + forecast_lines.mapped("date_to"), + [date(2022, 2, 28), date(2022, 3, 31), date(2022, 4, 30)], + ) + + @freeze_time("2022-01-01") + def test_confirm_order_sale_order_no_forecast_line(self): + so = self._create_sale("2022-02-14", "2022-04-14", uom_qty=60) + + so.action_confirm() + line = so.order_line[0] + forecast_lines = self.env["forecast.line"].search( + [ + ("sale_line_id", "=", line.id), + ("res_model", "=", "sale.order.line"), + ] + ) + self.assertFalse(forecast_lines) + + @freeze_time("2022-01-01") + def test_confirm_order_sale_order_create_project_task_with_forecast_line(self): + so = self._create_sale("2022-02-14", "2022-04-17", uom_qty=45 * 2) # 2 FTE + so.action_confirm() + line = so.order_line[0] + task = self.env["project.task"].search([("sale_line_id", "=", line.id)]) + forecast_lines = self.env["forecast.line"].search( + [("res_id", "=", task.id), ("res_model", "=", "project.task")] + ) + self.assertEqual(len(forecast_lines), 3) + self.assertEqual(forecast_lines.mapped("forecast_role_id"), self.role_developer) + daily_ratio = 8 * 2 # 2 FTE + self.assertAlmostEqual( + forecast_lines[0].forecast_hours, + -1 * daily_ratio * 11, # 11 working days on 2022-02-14 -> 2022-02-28 + ) + self.assertAlmostEqual( + forecast_lines[1].forecast_hours, + -1 * daily_ratio * 23, # 23 working days on 2022-03-01 -> 2022-03-31 + ) + self.assertAlmostEqual( + forecast_lines[2].forecast_hours, + -1 * daily_ratio * 11, # 11 working days on 2022-04-01 -> 2022-04-17 + ) + + +class TestForecastLineTimesheet(BaseForecastLineTest): + def test_timesheet_forecast_lines(self): + with freeze_time("2022-01-01"): + with Form(self.env["sale.order"]) as form: + form.partner_id = self.customer + with form.order_line.new() as line: + line.product_id = self.product_dev_tm + line.product_uom_qty = ( + 45 * 2 + ) # 45 working days in the period, sell 2 FTE + line.product_uom = self.env.ref("uom.product_uom_day") + so = form.save() + so.date_order = "2022-01-10 08:00:00" + so.default_forecast_date_start = "2022-02-14" + so.default_forecast_date_end = "2022-04-17" + so.action_confirm() + + with freeze_time("2022-02-14"): + line = so.order_line[0] + task = self.env["project.task"].search([("sale_line_id", "=", line.id)]) + # timesheet 1d + self.env["account.analytic.line"].create( + { + "employee_id": self.employee_dev.id, + "task_id": task.id, + "project_id": task.project_id.id, + "unit_amount": 8, + } + ) + task.flush_recordset() + forecast_lines = self.env["forecast.line"].search( + [("res_id", "=", task.id), ("res_model", "=", "project.task")] + ) + self.assertEqual(len(forecast_lines), 3) + daily_ratio = (45 * 2 - 1) * 8 / 45 + self.assertAlmostEqual( + forecast_lines[0].forecast_hours, -1 * daily_ratio * 11 + ) + self.assertAlmostEqual( + forecast_lines[1].forecast_hours, -1 * daily_ratio * 23 + ) + self.assertAlmostEqual( + forecast_lines[2].forecast_hours, -1 * daily_ratio * 11 + ) + self.assertEqual( + forecast_lines.mapped("date_from"), + [date(2022, 2, 1), date(2022, 3, 1), date(2022, 4, 1)], + ) + self.assertEqual( + forecast_lines.mapped("date_to"), + [date(2022, 2, 28), date(2022, 3, 31), date(2022, 4, 30)], + ) + + def test_timesheet_forecast_lines_cron(self): + """check recomputation of forecast lines of tasks even if we don"t TS""" + self.test_timesheet_forecast_lines() + with freeze_time("2022-03-10"): + self.env["forecast.line"]._cron_recompute_all() + forecast_lines = self.env["forecast.line"].search( + [("res_model", "=", "project.task")] + ) + self.assertEqual(len(forecast_lines), 2) + daily_ratio = ( + 8 + * (45 * 2 - 1) + / 27 # 27 worked days between 2022-03-10 and 2022-04-17 + ) + self.assertAlmostEqual( + forecast_lines[0].forecast_hours, + -1 + * daily_ratio + * 16, # 16 worked days between 2022-03-10 and 2022-03-31 + ) + self.assertAlmostEqual( + forecast_lines[1].forecast_hours, + -1 + * daily_ratio + * 11, # 11 worked days between 2022-04-01 and 2022-04-17 + ) + self.assertEqual( + forecast_lines.mapped("date_from"), + [date(2022, 3, 1), date(2022, 4, 1)], + ) + self.assertEqual( + forecast_lines.mapped("date_to"), + [date(2022, 3, 31), date(2022, 4, 30)], + ) + + +class TestForecastLineProjectReschedule(BaseForecastLineTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + # for this test, we use a daily granularity + cls.env.company.write( + { + "forecast_line_granularity": "day", + "forecast_line_horizon": 2, # months + } + ) + ProjectProject = cls.env["project.project"] + ProjectTask = cls.env["project.task"] + project = ProjectProject.create({"name": "TestProjectReschedule"}) + # set project in stage "in progress" to get confirmed forecast + project.stage_id = cls.env.ref("project.project_project_stage_1") + with freeze_time("2022-02-01 12:00:00"): + cls.task = ProjectTask.create( + { + "name": "TaskReschedule", + "project_id": project.id, + "forecast_role_id": cls.role_consultant.id, + "forecast_date_planned_start": "2022-02-14", + "forecast_date_planned_end": "2022-02-15", + "planned_hours": 16, + } + ) + # flush needed here to trigger the recomputation with the correct + # frozen time (otherwise it is called by the test runner before the + # tests, outside of the context manager. + cls.task.flush_recordset() + + @freeze_time("2022-02-01 12:00:00") + def test_task_unlink(self): + task_forecast = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.task.unlink() + self.assertFalse(task_forecast.exists()) + + @freeze_time("2022-02-01 12:00:00") + def test_task_forecast_line_reschedule_employee(self): + """changing the employee will create new lines""" + self.task.user_ids = self.user_consultant + task_forecast = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.assertEqual(task_forecast.mapped("employee_id"), self.employee_consultant) + self.task.user_ids = self.user_pm + self.task.flush_recordset() + task_forecast_after = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.assertNotEqual(task_forecast.ids, task_forecast_after.ids) + self.assertEqual(task_forecast_after.mapped("employee_id"), self.employee_pm) + + @freeze_time("2022-02-01 12:00:00") + def test_task_forecast_line_reschedule_dates(self): + """changing the dates will keep the lines which did not change dates""" + task_forecast = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.assertEqual(task_forecast[0].date_from.strftime("%Y-%m-%d"), "2022-02-14") + self.assertEqual(task_forecast[1].date_from.strftime("%Y-%m-%d"), "2022-02-15") + self.task.write( + { + "forecast_date_planned_start": "2022-02-15", + "forecast_date_planned_end": "2022-02-16", + } + ) + self.task.flush_recordset() + task_forecast_after = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.assertEqual( + task_forecast_after[0].date_from.strftime("%Y-%m-%d"), "2022-02-15" + ) + self.assertEqual( + task_forecast_after[1].date_from.strftime("%Y-%m-%d"), "2022-02-16" + ) + self.assertEqual(task_forecast.ids[1], task_forecast_after.ids[0]) + self.assertNotEqual(task_forecast.ids[0], task_forecast_after.ids[1]) + + @freeze_time("2022-02-01 12:00:00") + def test_task_forecast_line_reschedule_time(self): + """changing the remaining time will keep the forecast lines""" + self.task.user_ids = self.user_consultant + self.task.flush_recordset() + task_forecast = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.assertEqual(task_forecast.mapped("forecast_hours"), [-8, -8]) + self.task.write({"planned_hours": 24}) + self.task.flush_recordset() + task_forecast_after = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.assertEqual(task_forecast_after.mapped("forecast_hours"), [-12, -12]) + self.assertEqual(task_forecast.ids, task_forecast_after.ids) + + @freeze_time("2022-02-01 12:00:00") + def test_task_forecast_line_reschedule_time_no_employee(self): + """changing the remaining time will keep the forecast lines, even when no + employee assigned""" + self.task.flush_recordset() + task_forecast = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.assertEqual(task_forecast.mapped("forecast_hours"), [-8, -8]) + self.task.write({"planned_hours": 24}) + self.task.flush_recordset() + task_forecast_after = self.env["forecast.line"].search( + [("task_id", "=", self.task.id)] + ) + self.assertEqual(task_forecast_after.mapped("forecast_hours"), [-12, -12]) + self.assertEqual(task_forecast.ids, task_forecast_after.ids) + + +class TestForecastLineProject(BaseForecastLineTest): + @classmethod + @freeze_time("2022-01-01") + def setUpClass(cls): + super().setUpClass() + # for this test, we use a daily granularity + cls.env.company.write( + { + "forecast_line_granularity": "day", + "forecast_line_horizon": 2, # months + } + ) + + def _get_employee_forecast(self): + employee_forecast = self.env["forecast.line"].search( + [("employee_id", "=", self.employee_consultant.id)] + ) + # we can take first line to check as forecast values are equal + forecast_consultant = employee_forecast.filtered( + lambda l: l.res_model == self.role_model + and l.forecast_role_id == self.role_consultant + )[0] + forecast_pm = employee_forecast.filtered( + lambda l: l.res_model == self.role_model + and l.forecast_role_id == self.role_pm + )[0] + return forecast_consultant, forecast_pm + + @freeze_time("2022-02-14 12:00:00") + def test_task_forecast_lines_consolidated_forecast(self): + # set the consultant employee to 75% consultant and 25% PM + self.HrEmployeeForecastRole.create( + { + "employee_id": self.employee_consultant.id, + "role_id": self.role_pm.id, + "date_start": "2022-01-01", + "rate": 25, + "sequence": 1, + } + ) + consultant_role = self.HrEmployeeForecastRole.search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("role_id", "=", self.role_consultant.id), + ] + ) + consultant_role.rate = 75 + ProjectProject = self.env["project.project"] + ProjectTask = self.env["project.task"] + # Create 2 project and 2 tasks with role consultant with 8h planned on + # 1 day, assigned to the consultant + # + # Projet 1 is in TODO (not confirmed forecast) + project_1 = ProjectProject.create({"name": "TestProject1"}) + # set project in stage "to do" to get forecast + project_1.stage_id = self.env.ref("project.project_project_stage_0") + project_1.flush_recordset() + task_values = { + "project_id": project_1.id, + "forecast_role_id": self.role_consultant.id, + "forecast_date_planned_start": "2022-02-14", + "forecast_date_planned_end": "2022-02-14", + "planned_hours": 8, + } + task_values.update({"name": "Task1"}) + task_1 = ProjectTask.create(task_values) + task_1.user_ids = self.user_consultant + task_values.update({"name": "Task2"}) + task_2 = ProjectTask.create(task_values) + task_2.user_ids = self.user_consultant + # Project 2 is in stage "in rogress" to get forecast + project_2 = ProjectProject.create({"name": "TestProject2"}) + project_2.stage_id = self.env.ref("project.project_project_stage_1") + project_2.flush_recordset() + task_values.update({"project_id": project_2.id, "name": "Task3"}) + task_3 = ProjectTask.create(task_values) + task_3.user_ids = self.user_consultant + task_values.update({"name": "Task4"}) + task_4 = ProjectTask.create(task_values) + task_4.user_ids = self.user_consultant + + # check forecast lines + forecast = self.env["forecast.line"].search( + [("task_id", "in", (task_1.id, task_2.id, task_3.id, task_4.id))] + ) + self.assertEqual(len(forecast), 4) + self.assertEqual( + forecast.mapped("forecast_hours"), + [ + -8.0, + ] + * 4, + ) + # consolidated forecast is in days of 8 hours + self.assertEqual(forecast.mapped("consolidated_forecast"), [1.0] * 4) + self.assertEqual( + forecast.filtered(lambda r: r.type == "forecast").mapped( + "confirmed_consolidated_forecast" + ), + [0.0] * 2, + ) + self.assertEqual( + forecast.filtered(lambda r: r.type == "confirmed").mapped( + "confirmed_consolidated_forecast" + ), + [1.0] * 2, + ) + forecast_consultant, forecast_pm = self._get_employee_forecast() + self.assertEqual(forecast_consultant.forecast_hours, 6.0) + self.assertAlmostEqual( + forecast_consultant.consolidated_forecast, 1.0 * 75 / 100 - 4 + ) + self.assertAlmostEqual( + forecast_consultant.confirmed_consolidated_forecast, 1.0 * 75 / 100 - 2 + ) + self.assertEqual(forecast_pm.forecast_hours, 2.0) + self.assertAlmostEqual(forecast_pm.consolidated_forecast, 0.25) + self.assertAlmostEqual(forecast_pm.confirmed_consolidated_forecast, 0.25) + res_ids = project_1.task_ids.ids + project_1.task_ids.unlink() + to_remove_lines = self.env["forecast.line"].search( + [("res_id", "in", res_ids), ("res_model", "=", "project.task")] + ) + self.assertFalse(to_remove_lines.exists()) + + @freeze_time("2022-01-01 12:00:00") + def test_forecast_with_holidays(self): + self.test_task_forecast_lines_consolidated_forecast() + with Form(self.env["hr.leave"]) as form: + form.employee_id = self.employee_consultant + form.holiday_status_id = self.env.ref("hr_holidays.holiday_status_unpaid") + form.request_date_from = "2022-02-14" + form.request_date_to = "2022-02-15" + form.request_hour_from = "8" + form.request_hour_to = "18" + leave_request = form.save() + # validating the leave request will recompute the forecast lines for + # the employee capactities (actually delete the existing ones and + # create new ones -> we check that the project task lines are + # automatically related to the new newly created employee role lines. + leave_request.action_validate() + leave_request.flush_recordset() + forecast_lines = self.env["forecast.line"].search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("res_model", "=", self.role_model), + ("date_from", ">=", "2022-02-14"), + ("date_to", "<=", "2022-02-15"), + ] + ) + # 1 line per role per day -> 4 lines + self.assertEqual(len(forecast_lines), 2 * 2) + forecast_lines_consultant = forecast_lines.filtered( + lambda r: r.forecast_role_id == self.role_consultant + ) + # both new lines have now a capacity of 0 (employee is on holidays) + self.assertEqual(forecast_lines_consultant[0].forecast_hours, 0) + self.assertEqual(forecast_lines_consultant[1].forecast_hours, 0) + # first line has a negative consolidated forecast (because of the task) + self.assertEqual(forecast_lines_consultant[0].consolidated_forecast, 0 - 2) + self.assertEqual(forecast_lines_consultant[1].consolidated_forecast, -0) + + def test_task_forecast_lines_consolidated_forecast_overallocation(self): + ProjectProject = self.env["project.project"] + ProjectTask = self.env["project.task"] + with freeze_time("2022-01-01"): + employee_forecast = self.env["forecast.line"].search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("date_from", "=", "2022-02-14"), + ] + ) + self.assertEqual(len(employee_forecast), 1) + project = ProjectProject.create({"name": "TestProject"}) + # set project in stage "in progress" to get confirmed forecast + project.stage_id = self.env.ref("project.project_project_stage_1") + project.flush_recordset() + task = ProjectTask.create( + { + "name": "Task1", + "project_id": project.id, + "forecast_role_id": self.role_consultant.id, + "forecast_date_planned_start": "2022-02-14", + "forecast_date_planned_end": "2022-02-14", + "planned_hours": 8, + } + ) + task.remaining_hours = 10 + task.user_ids = self.user_consultant + forecast = self.env["forecast.line"].search([("task_id", "=", task.id)]) + self.assertEqual(len(forecast), 1) + # using assertEqual on purpose here + self.assertEqual(forecast.forecast_hours, -10.0) + self.assertEqual(forecast.consolidated_forecast, 1.25) + self.assertEqual(forecast.confirmed_consolidated_forecast, 1.25) + self.assertEqual( + forecast.employee_resource_forecast_line_id.consolidated_forecast, + -0.25, + ) + self.assertEqual( + forecast.employee_resource_forecast_line_id.confirmed_consolidated_forecast, + -0.25, + ) + + def test_task_forecast_lines_consolidated_forecast_overallocation_multiple_tasks( + self, + ): + ProjectProject = self.env["project.project"] + ProjectTask = self.env["project.task"] + with freeze_time("2022-01-01"): + employee_forecast = self.env["forecast.line"].search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("date_from", "=", "2022-02-14"), + ] + ) + self.assertEqual(len(employee_forecast), 1) + project = ProjectProject.create({"name": "TestProject"}) + # set project in stage "in progress" to get confirmed forecast + project.stage_id = self.env.ref("project.project_project_stage_1") + project.flush_recordset() + task1 = ProjectTask.create( + { + "name": "Task1", + "project_id": project.id, + "forecast_role_id": self.role_consultant.id, + "forecast_date_planned_start": "2022-02-14", + "forecast_date_planned_end": "2022-02-14", + "planned_hours": 8, + } + ) + task1.remaining_hours = 10 + task1.user_ids = self.user_consultant + forecast1 = self.env["forecast.line"].search([("task_id", "=", task1.id)]) + self.assertEqual(len(forecast1), 1) + task2 = ProjectTask.create( + { + "name": "Task2", + "project_id": project.id, + "forecast_role_id": self.role_consultant.id, + "forecast_date_planned_start": "2022-02-14", + "forecast_date_planned_end": "2022-02-14", + "planned_hours": 4, + } + ) + task2.remaining_hours = 4 + task2.user_ids = self.user_consultant + forecast2 = self.env["forecast.line"].search([("task_id", "=", task2.id)]) + # using assertEqual on purpose here + self.assertEqual( + forecast1.employee_resource_forecast_line_id, + forecast2.employee_resource_forecast_line_id, + ) + self.assertAlmostEqual( + forecast1.employee_resource_forecast_line_id.consolidated_forecast, + -0.75, + ) + self.assertAlmostEqual( + forecast1.employee_resource_forecast_line_id.confirmed_consolidated_forecast, + -0.75, + ) + + @freeze_time("2022-01-03 12:00:00") + def test_task_forecast_lines_employee_different_roles(self): + """ + Test forecast lines when employee has different roles. + + Employee has 2 forecast_role_id: consultant 75% and project manager 25%, + working 8h per day (standard calendar). + Create a task with forecast role consultant, with remaining time = 8h + and a scheduled period starting and ending on the same day (today for instance). + Assign this task to the user. + + Expected: for the user, on today, 3 forecast lines. + + res_model forecast_role_id forecast_hours consolidated_forecast + project.task consultant -8 1 (in days) + hr.employee.forecast.role consultant 6 -0.25 (in days) + hr.employee.forecast.role project manager 2 0.25 (in days) + + """ + ProjectProject = self.env["project.project"] + ProjectTask = self.env["project.task"] + self.HrEmployeeForecastRole.create( + { + "employee_id": self.employee_consultant.id, + "role_id": self.role_pm.id, + "date_start": "2022-01-01", + "rate": 25, + "sequence": 1, + } + ) + consultant_role = self.HrEmployeeForecastRole.search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("role_id", "=", self.role_consultant.id), + ] + ) + consultant_role.rate = 75 + project = ProjectProject.create({"name": "TestProjectDiffRoles"}) + # set project in stage "in progress" to get confirmed forecast + project.stage_id = self.env.ref("project.project_project_stage_1") + project.flush_recordset() + task = ProjectTask.create( + { + "name": "TaskDiffRoles", + "project_id": project.id, + "forecast_role_id": self.role_consultant.id, + "forecast_date_planned_start": date.today(), + "forecast_date_planned_end": date.today(), + "planned_hours": 8, + } + ) + task.user_ids = self.user_consultant + task_forecast = self.env["forecast.line"].search([("task_id", "=", task.id)]) + self.assertEqual(len(task_forecast), 1) + # using assertEqual on purpose here + self.assertEqual(task_forecast.forecast_hours, -8.0) + self.assertEqual(task_forecast.consolidated_forecast, 1.0) + self.assertEqual(task_forecast.confirmed_consolidated_forecast, 1.0) + forecast_consultant, forecast_pm = self._get_employee_forecast() + self.assertEqual(forecast_consultant.forecast_hours, 6.0) + self.assertAlmostEqual(forecast_consultant.consolidated_forecast, -0.25) + self.assertAlmostEqual( + forecast_consultant.confirmed_consolidated_forecast, -0.25 + ) + self.assertEqual(forecast_pm.forecast_hours, 2.0) + self.assertAlmostEqual(forecast_pm.consolidated_forecast, 0.25) + self.assertAlmostEqual(forecast_pm.confirmed_consolidated_forecast, 0.25) + + @freeze_time("2022-01-03 12:00:00") + def test_task_forecast_lines_employee_main_role(self): + """ + Test forecast lines when employee has different roles + and different from employee's role is assigned to the task. + + Employee has 2 forecast_role_id: consultant 75% and project manager 25%, + working 8h per day (standard calendar). + Create a task with forecast role developer, with remaining time = 8h + and a scheduled period starting and ending on the same day (today for instance). + Assign this task to the user. + + Expected: for the user, on today, 3 forecast lines. + + res_model forecast_role_id forecast_hours consolidated_forecast + project.task consultant -8 1 (in days) + hr.employee.forecast.role consultant 6 -0.25 (in days) + hr.employee.forecast.role project manager 2 0.25 (in days) + + """ + ProjectProject = self.env["project.project"] + ProjectTask = self.env["project.task"] + self.HrEmployeeForecastRole.create( + { + "employee_id": self.employee_consultant.id, + "role_id": self.role_pm.id, + "date_start": "2022-01-01", + "rate": 25, + "sequence": 1, + } + ) + consultant_role = self.HrEmployeeForecastRole.search( + [ + ("employee_id", "=", self.employee_consultant.id), + ("role_id", "=", self.role_consultant.id), + ] + ) + consultant_role.rate = 75 + project = ProjectProject.create({"name": "TestProjectDiffRoles"}) + # set project in stage "in progress" to get confirmed forecast + project.stage_id = self.env.ref("project.project_project_stage_1") + project.flush_recordset() + task = ProjectTask.create( + { + "name": "TaskDiffRoles", + "project_id": project.id, + "forecast_role_id": self.role_developer.id, + "forecast_date_planned_start": date.today(), + "forecast_date_planned_end": date.today(), + "planned_hours": 8, + } + ) + task.user_ids = self.user_consultant + task_forecast = self.env["forecast.line"].search([("task_id", "=", task.id)]) + self.assertEqual(len(task_forecast), 1) + # using assertEqual on purpose here + self.assertEqual(task_forecast.forecast_hours, -8.0) + self.assertEqual(task_forecast.consolidated_forecast, 1.0) + self.assertEqual(task_forecast.confirmed_consolidated_forecast, 1.0) + forecast_consultant, forecast_pm = self._get_employee_forecast() + self.assertEqual(forecast_consultant.forecast_hours, 6.0) + self.assertAlmostEqual(forecast_consultant.consolidated_forecast, -0.25) + self.assertAlmostEqual( + forecast_consultant.confirmed_consolidated_forecast, -0.25 + ) + self.assertEqual(forecast_pm.forecast_hours, 2.0) + self.assertAlmostEqual(forecast_pm.consolidated_forecast, 0.25) + self.assertAlmostEqual(forecast_pm.confirmed_consolidated_forecast, 0.25) diff --git a/project_forecast_line/views/forecast_line_views.xml b/project_forecast_line/views/forecast_line_views.xml new file mode 100644 index 0000000000..eef3008b84 --- /dev/null +++ b/project_forecast_line/views/forecast_line_views.xml @@ -0,0 +1,158 @@ + + + + forecast.line + + + + + + + + + + + + + + + + + + + + + + forecast.line + + + + + + + + + + + + + + + + forecast.line + + + + + + + + + + + + + + forecast.line + + + + + + + + + + + + + forecast.line + + + + + + + + + + + + + + + Forecast + ir.actions.act_window + forecast.line + tree,graph,pivot + + + Forecast (Consolidated) + ir.actions.act_window + forecast.line + graph,pivot,tree + + + [('employee_id', '!=', False)] + {'graph_groupbys': ['date_from:month', 'project_id']} + + + + + + + diff --git a/project_forecast_line/views/forecast_role_views.xml b/project_forecast_line/views/forecast_role_views.xml new file mode 100644 index 0000000000..a3b829945a --- /dev/null +++ b/project_forecast_line/views/forecast_role_views.xml @@ -0,0 +1,48 @@ + + + + forecast.role + + + + + + + + + forecast.role + + + + + + + + forecast.role + +
+ + + + + + +
+
+
+ + Forecast Roles + ir.actions.act_window + forecast.role + tree,form + + + + +
diff --git a/project_forecast_line/views/hr_employee_views.xml b/project_forecast_line/views/hr_employee_views.xml new file mode 100644 index 0000000000..afbad9594a --- /dev/null +++ b/project_forecast_line/views/hr_employee_views.xml @@ -0,0 +1,32 @@ + + + + hr.employee + + + + + + + + + + + + + + + + + + + + hr.job + + + + + + + + diff --git a/project_forecast_line/views/product_views.xml b/project_forecast_line/views/product_views.xml new file mode 100644 index 0000000000..66db9d076c --- /dev/null +++ b/project_forecast_line/views/product_views.xml @@ -0,0 +1,12 @@ + + + + product.template + + + + + + + + diff --git a/project_forecast_line/views/project_project_stage_views.xml b/project_forecast_line/views/project_project_stage_views.xml new file mode 100644 index 0000000000..c93ccc0b17 --- /dev/null +++ b/project_forecast_line/views/project_project_stage_views.xml @@ -0,0 +1,34 @@ + + + + project.project.stage + + + + + + + + + project.project.stage + + + + + + + + + + project.project.stage + + + + + + + + diff --git a/project_forecast_line/views/project_task_views.xml b/project_forecast_line/views/project_task_views.xml new file mode 100644 index 0000000000..3b45b166f4 --- /dev/null +++ b/project_forecast_line/views/project_task_views.xml @@ -0,0 +1,16 @@ + + + + project.task + + + + + + + + + + + + diff --git a/project_forecast_line/views/res_config_settings_views.xml b/project_forecast_line/views/res_config_settings_views.xml new file mode 100644 index 0000000000..e273587afb --- /dev/null +++ b/project_forecast_line/views/res_config_settings_views.xml @@ -0,0 +1,78 @@ + + + + res.config.settings.view.form.inherit.forecast + res.config.settings + + + +
+

Forecast Management

+
+
+
+ +
+
+
+
+
+
+
+
+
+ + Settings + ir.actions.act_window + res.config.settings + form + inline + {'module': 'forecast', 'bin_size': False} + + + +
diff --git a/project_forecast_line/views/sale_order_views.xml b/project_forecast_line/views/sale_order_views.xml new file mode 100644 index 0000000000..bb6b0149ca --- /dev/null +++ b/project_forecast_line/views/sale_order_views.xml @@ -0,0 +1,48 @@ + + + + sale.order + + + + + + + + + + + + + + + + + + diff --git a/setup/project_forecast_line/odoo/addons/project_forecast_line b/setup/project_forecast_line/odoo/addons/project_forecast_line new file mode 120000 index 0000000000..c46f8df746 --- /dev/null +++ b/setup/project_forecast_line/odoo/addons/project_forecast_line @@ -0,0 +1 @@ +../../../../project_forecast_line \ No newline at end of file diff --git a/setup/project_forecast_line/setup.py b/setup/project_forecast_line/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/project_forecast_line/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)