diff --git a/hr_utilization_analysis/README.rst b/hr_utilization_analysis/README.rst new file mode 100644 index 000000000..9494ca1de --- /dev/null +++ b/hr_utilization_analysis/README.rst @@ -0,0 +1,106 @@ +============================== +Task Logs Utilization Analysis +============================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-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%2Fhr--timesheet-lightgray.png?logo=github + :target: https://github.com/OCA/hr-timesheet/tree/12.0/hr_utilization_analysis + :alt: OCA/hr-timesheet +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-timesheet-12-0/hr-timesheet-12-0-hr_utilization_analysis + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/117/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to view Utilization Analysis from Task Logs. + +Features: + * Configure source data set + * Select time interval + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To view analysis using Utilization Analysis Wizard: +#. Go to *Timesheets > Reporting > Utilization Analysis Wizard*. +#. Configure the data set and click "View". + +To view analysis using Utilization Analysis Wizard on a specific set of Employees: +#. Go to *Employees > Employees*. +#. Select employees that should be used in the analysis +#. Press the *Action > View Utilization Analysis* button +#. Configure the data set and click "View". + +To view analysis using Utilization Analysis Wizard on a specific set of Departments: +#. Go to *Employees > Departments*. +#. Select departments that should be used in the analysis +#. Press the *Action > View Utilization Analysis* button +#. Configure the data set and click "View". + +With `project_timesheet_holidays` module installed, leaves are not taken into +account: for a single 4-hour entry on specific day with 8 working hours and +4 hours of leaves, capacity would be calculated as 8 hours and utilization +would be calculated as 100%. + +Without `project_timesheet_holidays` module installed, leaves are taken into +account: for a single 4-hour entry on specific day with 8 working hours and +4 hours of leaves, capacity would be calculated as 4 hours and utilization +would be calculated as 100%. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Brainbean Apps + +Contributors +~~~~~~~~~~~~ + +* Alexey Pelykh + +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/hr-timesheet `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_utilization_analysis/__init__.py b/hr_utilization_analysis/__init__.py new file mode 100644 index 000000000..4c067fdc2 --- /dev/null +++ b/hr_utilization_analysis/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import wizards +from . import report diff --git a/hr_utilization_analysis/__manifest__.py b/hr_utilization_analysis/__manifest__.py new file mode 100644 index 000000000..866fc57cb --- /dev/null +++ b/hr_utilization_analysis/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2018 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'Task Logs Utilization Analysis', + 'version': '12.0.1.0.0', + 'category': 'Human Resources', + 'website': 'https://github.com/OCA/hr-timesheet', + 'author': + 'Brainbean Apps, ' + 'Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'installable': True, + 'application': False, + 'summary': 'View Utilization Analysis from Task Logs.', + 'depends': [ + 'hr_timesheet', + ], + 'data': [ + 'views/hr_department.xml', + 'views/hr_employee.xml', + 'report/hr_utilization_analysis.xml', + 'wizards/hr_utilization_analysis_wizard.xml', + ], +} diff --git a/hr_utilization_analysis/i18n/hr_utilization_analysis.pot b/hr_utilization_analysis/i18n/hr_utilization_analysis.pot new file mode 100644 index 000000000..ea45363b4 --- /dev/null +++ b/hr_utilization_analysis/i18n/hr_utilization_analysis.pot @@ -0,0 +1,246 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_utilization_analysis +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.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: hr_utilization_analysis +#: sql_constraint:hr.utilization.analysis.entry:0 +msgid "An analysis entry for employee/date pair has to be unique!" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__analysis_id +msgid "Analysis" +msgstr "" + +#. module: hr_utilization_analysis +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_form +msgid "Analysis Entry" +msgstr "" + +#. module: hr_utilization_analysis +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_wizard_form +msgid "Cancel" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__capacity +msgid "Capacity" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__create_uid +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__create_uid +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__create_date +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__create_date +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__date +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_search +msgid "Date" +msgstr "" + +#. module: hr_utilization_analysis +#: code:addons/hr_utilization_analysis/report/hr_utilization_analysis.py:52 +#, python-format +msgid "Date-To can not be earlier than Date-From" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__department_id +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_search +msgid "Department" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__department_ids +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__department_ids +msgid "Departments" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__difference +msgid "Difference" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__display_name +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__display_name +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__employee_id +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_search +msgid "Employee" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__employee_category_ids +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__employee_category_ids +msgid "Employee Tags" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__employee_ids +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__employee_ids +msgid "Employees" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__date_to +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__date_to +msgid "End Date" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__entry_ids +msgid "Entries" +msgstr "" + +#. module: hr_utilization_analysis +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_search +msgid "Group By" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model,name:hr_utilization_analysis.model_hr_utilization_analysis +msgid "HR Utilization Analysis" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model,name:hr_utilization_analysis.model_hr_utilization_analysis_wizard +msgid "HR Utilization Analysis Wizard" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model,name:hr_utilization_analysis.model_hr_utilization_analysis_entry +msgid "HR Utilization Analysis entry" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__id +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__id +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__id +msgid "ID" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis____last_update +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry____last_update +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__write_uid +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__write_uid +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__write_date +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__write_date +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__manager_id +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_search +msgid "Manager" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__name +msgid "Name" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__only_active_employees +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__only_active_employees +msgid "Only Active Employees" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__amount +msgid "Quantity" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis__date_from +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_wizard__date_from +msgid "Start Date" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.model.fields,field_description:hr_utilization_analysis.field_hr_utilization_analysis_entry__line_ids +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_form +msgid "Timesheet Lines" +msgstr "" + +#. module: hr_utilization_analysis +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_tree +msgid "Total Amount" +msgstr "" + +#. module: hr_utilization_analysis +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_tree +msgid "Total Capacity" +msgstr "" + +#. module: hr_utilization_analysis +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_tree +msgid "Total Difference" +msgstr "" + +#. module: hr_utilization_analysis +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_graph +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_pivot +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_entry_search +msgid "Utilization" +msgstr "" + +#. module: hr_utilization_analysis +#: code:addons/hr_utilization_analysis/wizards/hr_utilization_analysis_wizard.py:55 +#, python-format +msgid "Utilization Analysis" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.actions.act_window,name:hr_utilization_analysis.action_utilization_analysis_wizard +#: model:ir.ui.menu,name:hr_utilization_analysis.menu_timesheet_analysis_wizard +msgid "Utilization Analysis Wizard" +msgstr "" + +#. module: hr_utilization_analysis +#: model_terms:ir.ui.view,arch_db:hr_utilization_analysis.hr_utilization_analysis_wizard_form +msgid "View" +msgstr "" + +#. module: hr_utilization_analysis +#: model:ir.actions.server,name:hr_utilization_analysis.action_utilization_analysis_wizard_from_departments +#: model:ir.actions.server,name:hr_utilization_analysis.action_utilization_analysis_wizard_from_employees +msgid "View Utilization Analysis" +msgstr "" + diff --git a/hr_utilization_analysis/readme/CONTRIBUTORS.rst b/hr_utilization_analysis/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..1c6a35a1e --- /dev/null +++ b/hr_utilization_analysis/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Alexey Pelykh diff --git a/hr_utilization_analysis/readme/DESCRIPTION.rst b/hr_utilization_analysis/readme/DESCRIPTION.rst new file mode 100644 index 000000000..af9ece9f6 --- /dev/null +++ b/hr_utilization_analysis/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module allows to view Utilization Analysis from Task Logs. + +Features: + * Configure source data set + * Select time interval diff --git a/hr_utilization_analysis/readme/USAGE.rst b/hr_utilization_analysis/readme/USAGE.rst new file mode 100644 index 000000000..8c1de4732 --- /dev/null +++ b/hr_utilization_analysis/readme/USAGE.rst @@ -0,0 +1,25 @@ +To view analysis using Utilization Analysis Wizard: +#. Go to *Timesheets > Reporting > Utilization Analysis Wizard*. +#. Configure the data set and click "View". + +To view analysis using Utilization Analysis Wizard on a specific set of Employees: +#. Go to *Employees > Employees*. +#. Select employees that should be used in the analysis +#. Press the *Action > View Utilization Analysis* button +#. Configure the data set and click "View". + +To view analysis using Utilization Analysis Wizard on a specific set of Departments: +#. Go to *Employees > Departments*. +#. Select departments that should be used in the analysis +#. Press the *Action > View Utilization Analysis* button +#. Configure the data set and click "View". + +With `project_timesheet_holidays` module installed, leaves are not taken into +account: for a single 4-hour entry on specific day with 8 working hours and +4 hours of leaves, capacity would be calculated as 8 hours and utilization +would be calculated as 100%. + +Without `project_timesheet_holidays` module installed, leaves are taken into +account: for a single 4-hour entry on specific day with 8 working hours and +4 hours of leaves, capacity would be calculated as 4 hours and utilization +would be calculated as 100%. diff --git a/hr_utilization_analysis/report/__init__.py b/hr_utilization_analysis/report/__init__.py new file mode 100644 index 000000000..b5384a9fe --- /dev/null +++ b/hr_utilization_analysis/report/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import hr_utilization_analysis diff --git a/hr_utilization_analysis/report/hr_utilization_analysis.py b/hr_utilization_analysis/report/hr_utilization_analysis.py new file mode 100644 index 000000000..8e83f2a70 --- /dev/null +++ b/hr_utilization_analysis/report/hr_utilization_analysis.py @@ -0,0 +1,240 @@ +# Copyright 2018 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models, api, _ +from odoo.exceptions import ValidationError + +import pytz +from datetime import datetime, time, timedelta + + +class HrUtilizationAnalysis(models.TransientModel): + _name = 'hr.utilization.analysis' + _description = 'HR Utilization Analysis' + + date_from = fields.Date( + string='Start Date', + required=True, + ) + date_to = fields.Date( + string='End Date', + required=True, + ) + only_active_employees = fields.Boolean( + string='Only Active Employees', + default=True, + ) + employee_ids = fields.Many2many( + string='Employees', + comodel_name='hr.employee', + ) + employee_category_ids = fields.Many2many( + string='Employee Tags', + comodel_name='hr.employee.category', + ) + department_ids = fields.Many2many( + string='Departments', + comodel_name='hr.department', + ) + entry_ids = fields.One2many( + string='Entries', + comodel_name='hr.utilization.analysis.entry', + inverse_name='analysis_id', + compute='_compute_entry_ids', + store=True, + ) + + @api.multi + @api.constrains('date_from', 'date_to') + def _check_dates(self): + for analysis in self: + if analysis.date_from > analysis.date_to: + raise ValidationError(_( + 'Date-To can not be earlier than Date-From' + )) + + @api.multi + @api.depends( + 'only_active_employees', + 'employee_ids', + 'employee_category_ids', + 'department_ids', + ) + def _compute_entry_ids(self): + HrEmployee = self.env['hr.employee'] + + for analysis in self: + employee_ids = HrEmployee.search( + analysis._get_employees_domain() + ) + + entry_ids = [(5, False, False)] + for employee_id in employee_ids: + date = analysis.date_from + one_day = timedelta(days=1) + while date <= analysis.date_to: + entry_ids.append((0, False, { + 'employee_id': employee_id.id, + 'date': date, + })) + date += one_day + + analysis.entry_ids = entry_ids + + @api.multi + def _get_employees_domain(self): + self.ensure_one() + + query = [] + + if self.only_active_employees: + query.append(('active', '=', True)) + employee_ids = ( + self.employee_ids + | self.employee_category_ids.mapped('employee_ids') + ) + if employee_ids: + query.append(('id', 'in', employee_ids.ids)) + if self.department_ids: + query.append(('department_id', 'in', self.department_ids.ids)) + + return query + + +class HrUtilizationAnalysisEntry(models.TransientModel): + _name = 'hr.utilization.analysis.entry' + _description = 'HR Utilization Analysis entry' + + analysis_id = fields.Many2one( + string='Analysis', + comodel_name='hr.utilization.analysis', + required=True, + ondelete='cascade', + ) + name = fields.Char( + related='employee_id.name', + store=True, + ) + employee_id = fields.Many2one( + string='Employee', + comodel_name='hr.employee', + required=True, + ) + department_id = fields.Many2one( + string='Department', + comodel_name='hr.department', + related='employee_id.department_id', + store=True, + ) + manager_id = fields.Many2one( + string='Manager', + comodel_name='hr.employee', + related='employee_id.parent_id', + store=True, + ) + date = fields.Date( + string='Date', + required=True, + ) + line_ids = fields.Many2many( + string='Timesheet Lines', + comodel_name='account.analytic.line', + compute='_compute_line_ids', + store=True, + ) + capacity = fields.Float( + string='Capacity', + compute='_compute_capacity', + store=True, + ) + amount = fields.Float( + string='Quantity', + compute='_compute_amount', + store=True, + ) + difference = fields.Float( + string='Difference', + compute='_compute_difference', + store=True, + ) + + _sql_constraints = [ + ( + 'entry_uniq', + 'UNIQUE(analysis_id, employee_id, date)', + 'An analysis entry for employee/date pair has to be unique!' + ), + ] + + @api.multi + @api.depends('employee_id', 'date') + def _compute_line_ids(self): + AccountAnalyticLine = self.env['account.analytic.line'] + + for entry in self: + entry.line_ids = AccountAnalyticLine.search([ + ('project_id', '!=', False), + ('employee_id', '=', entry.employee_id.id), + ('date', '=', entry.date), + ]) + + @api.multi + @api.depends('employee_id', 'date') + def _compute_capacity(self): + HrEmployee = self.env['hr.employee'] + Module = self.env['ir.module.module'] + + project_timesheet_holidays = Module.sudo().search([ + ('name', '=', 'project_timesheet_holidays'), + ('state', '=', 'installed'), + ], limit=1) + + for entry in self: + tz = pytz.timezone( + entry.employee_id.resource_calendar_id.tz + ) + from_datetime = datetime.combine( + entry.date, + time.min + ).replace(tzinfo=tz) + to_datetime = datetime.combine( + entry.date, + time.max + ).replace(tzinfo=tz) + + capacity = entry.employee_id.get_work_days_data( + from_datetime, + to_datetime, + compute_leaves=not project_timesheet_holidays, + )['hours'] + + if project_timesheet_holidays: + capacity -= HrEmployee.get_leave_days_data( + from_datetime, + to_datetime, + calendar=entry.employee_id.resource_calendar_id + )['hours'] + + entry.capacity = max(capacity, 0) + + @api.multi + @api.depends('line_ids') + def _compute_amount(self): + uom_hour = self.env.ref('uom.product_uom_hour') + + for entry in self: + amount = 0.0 + for line_id in entry.line_ids: + amount += ( + line_id.product_uom_id._compute_quantity( + line_id.unit_amount, + uom_hour + ) + ) + entry.amount = amount + + @api.multi + @api.depends('amount', 'capacity') + def _compute_difference(self): + for entry in self: + entry.difference = entry.capacity - entry.amount diff --git a/hr_utilization_analysis/report/hr_utilization_analysis.xml b/hr_utilization_analysis/report/hr_utilization_analysis.xml new file mode 100644 index 000000000..0c03a383b --- /dev/null +++ b/hr_utilization_analysis/report/hr_utilization_analysis.xml @@ -0,0 +1,93 @@ + + + + + + hr.utilization.analysis.entry.form + hr.utilization.analysis.entry + +
+ + + + + + + + + + + + + + +
+
+
+ + + hr.utilization.analysis.entry.tree + hr.utilization.analysis.entry + + + + + + + + + + + + + hr.utilization.analysis.entry.pivot + hr.utilization.analysis.entry + + + + + + + + + + + + + hr.utilization.analysis.entry.graph + hr.utilization.analysis.entry + + + + + + + + + + + + + hr.utilization.analysis.entry.search + hr.utilization.analysis.entry + + + + + + + + + + + + + + + + + +
diff --git a/hr_utilization_analysis/static/description/index.html b/hr_utilization_analysis/static/description/index.html new file mode 100644 index 000000000..d5bf7160c --- /dev/null +++ b/hr_utilization_analysis/static/description/index.html @@ -0,0 +1,452 @@ + + + + + + +Task Logs Utilization Analysis + + + +
+

Task Logs Utilization Analysis

+ + +

Beta License: AGPL-3 OCA/hr-timesheet Translate me on Weblate Try me on Runbot

+

This module allows to view Utilization Analysis from Task Logs.

+
+
Features:
+
    +
  • Configure source data set
  • +
  • Select time interval
  • +
+
+
+

Table of contents

+ +
+

Usage

+

To view analysis using Utilization Analysis Wizard: +#. Go to Timesheets > Reporting > Utilization Analysis Wizard. +#. Configure the data set and click “View”.

+

To view analysis using Utilization Analysis Wizard on a specific set of Employees: +#. Go to Employees > Employees. +#. Select employees that should be used in the analysis +#. Press the Action > View Utilization Analysis button +#. Configure the data set and click “View”.

+

To view analysis using Utilization Analysis Wizard on a specific set of Departments: +#. Go to Employees > Departments. +#. Select departments that should be used in the analysis +#. Press the Action > View Utilization Analysis button +#. Configure the data set and click “View”.

+

With project_timesheet_holidays module installed, leaves are not taken into +account: for a single 4-hour entry on specific day with 8 working hours and +4 hours of leaves, capacity would be calculated as 8 hours and utilization +would be calculated as 100%.

+

Without project_timesheet_holidays module installed, leaves are taken into +account: for a single 4-hour entry on specific day with 8 working hours and +4 hours of leaves, capacity would be calculated as 4 hours and utilization +would be calculated as 100%.

+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Brainbean Apps
  • +
+
+ +
+

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

+

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

+
+
+
+ + diff --git a/hr_utilization_analysis/tests/__init__.py b/hr_utilization_analysis/tests/__init__.py new file mode 100644 index 000000000..c18e66076 --- /dev/null +++ b/hr_utilization_analysis/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_hr_utilization_analysis diff --git a/hr_utilization_analysis/tests/test_hr_utilization_analysis.py b/hr_utilization_analysis/tests/test_hr_utilization_analysis.py new file mode 100644 index 000000000..905a228bc --- /dev/null +++ b/hr_utilization_analysis/tests/test_hr_utilization_analysis.py @@ -0,0 +1,68 @@ +# Copyright 2018 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields +from odoo.tests import common + +from dateutil.relativedelta import relativedelta + + +class TestHrUtilizationAnalysis(common.TransactionCase): + + def setUp(self): + super().setUp() + + self.today = fields.Date.today() + self.Project = self.env['project.project'] + self.SudoProject = self.Project.sudo() + self.HrEmployee = self.env['hr.employee'] + self.SudoHrEmployee = self.HrEmployee.sudo() + self.AccountAnalyticLine = self.env['account.analytic.line'] + self.SudoAccountAnalyticLine = self.AccountAnalyticLine.sudo() + self.Wizard = self.env['hr.utilization.analysis.wizard'] + self.Analysis = self.env['hr.utilization.analysis'] + + def test_1(self): + project = self.SudoProject.create({ + 'name': 'Project #1', + }) + employee_1 = self.SudoHrEmployee.create({ + 'name': 'Employee #1-1', + }) + employee_2 = self.SudoHrEmployee.create({ + 'name': 'Employee #1-2', + 'active': False, + }) + employee_3 = self.SudoHrEmployee.create({ + 'name': 'Employee #1-3', + }) + self.SudoAccountAnalyticLine.create({ + 'project_id': project.id, + 'name': 'Time Entry #1-1', + 'employee_id': employee_1.id, + 'date': self.today, + 'unit_amount': 4, + }) + self.SudoAccountAnalyticLine.create({ + 'project_id': project.id, + 'name': 'Time Entry #1-2', + 'employee_id': employee_1.id, + 'date': self.today - relativedelta(days=1), + 'unit_amount': 4, + }) + + wizard = self.Wizard.create({ + 'date_from': self.today, + 'date_to': self.today, + 'employee_ids': [(6, False, [ + employee_1.id, + employee_2.id, + employee_3.id, + ])], + }) + wizard.action_view() + + analysis = self.Analysis.create( + wizard._collect_analysis_values() + ) + self.assertEqual(len(analysis.entry_ids), 2) diff --git a/hr_utilization_analysis/views/hr_department.xml b/hr_utilization_analysis/views/hr_department.xml new file mode 100644 index 000000000..7b1b88469 --- /dev/null +++ b/hr_utilization_analysis/views/hr_department.xml @@ -0,0 +1,28 @@ + + + + + + View Utilization Analysis + ir.actions.server + + + code + + if records: + action = { + 'type': 'ir.actions.act_window', + 'res_model': 'hr.utilization.analysis.wizard', + 'views': [[False, 'form']], + 'target': 'new', + 'context': { + 'default_department_ids': [(6, False, records.ids)], + }, + } + + + + diff --git a/hr_utilization_analysis/views/hr_employee.xml b/hr_utilization_analysis/views/hr_employee.xml new file mode 100644 index 000000000..db148774e --- /dev/null +++ b/hr_utilization_analysis/views/hr_employee.xml @@ -0,0 +1,28 @@ + + + + + + View Utilization Analysis + ir.actions.server + + + code + + if records: + action = { + 'type': 'ir.actions.act_window', + 'res_model': 'hr.utilization.analysis.wizard', + 'views': [[False, 'form']], + 'target': 'new', + 'context': { + 'default_employee_ids': [(6, False, records.ids)], + }, + } + + + + diff --git a/hr_utilization_analysis/wizards/__init__.py b/hr_utilization_analysis/wizards/__init__.py new file mode 100644 index 000000000..27ba9ce0b --- /dev/null +++ b/hr_utilization_analysis/wizards/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import hr_utilization_analysis_wizard diff --git a/hr_utilization_analysis/wizards/hr_utilization_analysis_wizard.py b/hr_utilization_analysis/wizards/hr_utilization_analysis_wizard.py new file mode 100644 index 000000000..918572905 --- /dev/null +++ b/hr_utilization_analysis/wizards/hr_utilization_analysis_wizard.py @@ -0,0 +1,71 @@ +# Copyright 2018 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models, api, _ + + +class HrUtilizationAnalysisWizard(models.TransientModel): + _name = 'hr.utilization.analysis.wizard' + _description = 'HR Utilization Analysis Wizard' + + date_from = fields.Date( + string='Start Date', + required=True, + ) + date_to = fields.Date( + string='End Date', + required=True, + ) + only_active_employees = fields.Boolean( + string='Only Active Employees', + default=True, + ) + employee_ids = fields.Many2many( + string='Employees', + comodel_name='hr.employee', + ) + employee_category_ids = fields.Many2many( + string='Employee Tags', + comodel_name='hr.employee.category', + ) + department_ids = fields.Many2many( + string='Departments', + comodel_name='hr.department', + ) + + @api.multi + def action_view(self): + self.ensure_one() + + analysis = self.env['hr.utilization.analysis'].create( + self._collect_analysis_values() + ) + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'hr.utilization.analysis.entry', + 'views': [ + [False, 'graph'], + [False, 'pivot'], + ], + 'target': 'main', + 'domain': [ + ('analysis_id', '=', analysis.id), + ], + 'name': _('Utilization Analysis'), + } + + @api.multi + def _collect_analysis_values(self): + self.ensure_one() + + return { + 'date_from': self.date_from, + 'date_to': self.date_to, + 'only_active_employees': self.only_active_employees, + 'employee_ids': [(6, False, self.employee_ids.ids)], + 'employee_category_ids': [ + (6, False, self.employee_category_ids.ids) + ], + 'department_ids': [(6, False, self.department_ids.ids)], + } diff --git a/hr_utilization_analysis/wizards/hr_utilization_analysis_wizard.xml b/hr_utilization_analysis/wizards/hr_utilization_analysis_wizard.xml new file mode 100644 index 000000000..4fff4fce2 --- /dev/null +++ b/hr_utilization_analysis/wizards/hr_utilization_analysis_wizard.xml @@ -0,0 +1,58 @@ + + + + + + hr.utilization.analysis.wizard.form + hr.utilization.analysis.wizard + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Utilization Analysis Wizard + ir.actions.act_window + hr.utilization.analysis.wizard + form + new + + + + +