diff --git a/hr_duty_planner/.gitignore b/hr_duty_planner/.gitignore new file mode 100644 index 00000000000..68abf6f41b6 --- /dev/null +++ b/hr_duty_planner/.gitignore @@ -0,0 +1 @@ +models/__pycache__ \ No newline at end of file diff --git a/hr_duty_planner/README.rst b/hr_duty_planner/README.rst new file mode 100644 index 00000000000..dcfaf3e5e11 --- /dev/null +++ b/hr_duty_planner/README.rst @@ -0,0 +1,141 @@ +================= +HR - Duty Planner +================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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-lightgray.png?logo=github + :target: https://github.com/OCA/hr/tree/12.0/hr_duty_planner + :alt: OCA/hr +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-12-0/hr-12-0-hr_duty_planner + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/116/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +These module manages the Service Planner. + +_TODO_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +**Steps to follow to use Service Planner** + +**Set up** + + **Rules** + * Rules and Rules Fields can not be modified by Users. Read what they do and how to use fields. + + **Select Rules → Profile** + * Create a Profile + * Add rule fields with values + * Add profile to Employees: Employee → get one → fill Service Profile + + **Employee** + * Create desired Skills: get an Employee → Skill tab → add row → create + + **Maintenance → Configuration → Categories** + * Create desired Categories + + **Fleet** + * Create desired Types: get a Vehicle → Vehicle Type → create + + **Select Template → Container Service** + * Create a list of Container Services to collect single services + + **Select Template → Service Template** + * Create a Template Service + + * Name, Container Service and Duration are mandatory + * Check if it is an Off Duty Service (normally used to track some informations, ie. rest shifts) + * Add requested Features to be sure to not forget to assign Resources. + (Minimum quantity if zero will not be considered) + * Optional, associate a Next Service: this will be automatically generated starting form the end of this one. + * Optional, set resources of parent service locked by the next service + + **Select Template → Service Template → your template** + * Clik on *Generate Services* button + * Select a Template + * Select a Container + * Select a start date-time + * Select a stop date-time + * Set repetition interval (hours). If interval is inferior to the template duration, this one will be used + * Set available days of week + * Set to use work Calendar + * Set to use holiday + * Click on *Generate* button + + **Planning** + * Select Services → List view* + * Select a Service + * Fill resources (eg. Employee, Equipments, Vehicles) + * Check requirements (Template requestested Resources) fulfillment + * Calendar/Timeline: overview / move services + +Changelog +========= + +12.0.1.0.0 (2020-05-01) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [INI] Initial development + +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 +~~~~~~~ + +* Stefano Consolaro +* Marcelo Frare Associazione PNLUG - Gruppo Odoo + +Contributors +~~~~~~~~~~~~ + +* Stefano Consolaro +* Marcelo Frare + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_duty_planner/TODO.rst b/hr_duty_planner/TODO.rst new file mode 100644 index 00000000000..6745cd6fab8 --- /dev/null +++ b/hr_duty_planner/TODO.rst @@ -0,0 +1,53 @@ +ToDo list + + +**Models** + * service template + * DEV limit to 1 Container Service for off_duty type service + * DEV management of working/holiday calendar + * service rule + * DEV complete rule method management + * calendar + * DEV check module + +**Views** + * service template form + * FIX custom css loading + * FIX eliminate self.id from next list + * FIX on deploy off_duty set to readonly + * FIX auto select template on generation form + * FIX layout + * service allocate tree + * FIX check rule button position + * FIX direct call of rule method for overlap check + * service allocate form + * FIX direct call of rule method for overlap check + * service allocate calendar + * DEV text format + * DEV lock action on empty cells + * DEV check/alert template expected fulfillment + * FIX employee name display (newline separated) + * FIX element dedicated color (web_calendar) + * service allocate timeline + * DEV try add another level of group (ie. locality) + * DEV check UIX + * DEV check/alert template expected fulfillment + * FIX show computed field (employee_names) + * FIX element dedicated color (web_timeline) + * service rule + * DEV add profile reference to employee, equipment, vehicle + * FIX on deploy lock edit option + * FIX optimize double_assign method + * service profile + * FIX filter available fields for selected rule + * DEV create check for rule-field assignment (association, mandatory) + * reporting + * DEV all + + +**Security** + * fix model authorizations + + +**Readme** +oca-gen-addon-readme --repo-name hr --branch 12.0 --addon-dir ~/odoo-dev/odoo12/OCA/addons-custom-sp/hr_duty_planner/ diff --git a/hr_duty_planner/__init__.py b/hr_duty_planner/__init__.py new file mode 100644 index 00000000000..7c10c8dbe8c --- /dev/null +++ b/hr_duty_planner/__init__.py @@ -0,0 +1,5 @@ +from . import ( + models, + wizards, + reports, +) diff --git a/hr_duty_planner/__manifest__.py b/hr_duty_planner/__manifest__.py new file mode 100644 index 00000000000..de15542e5f6 --- /dev/null +++ b/hr_duty_planner/__manifest__.py @@ -0,0 +1,51 @@ +# Copyright 2019 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "HR - Duty Planner", + "summary": "Management of duty planning.", + "version": "12.0.1.0.0", + + "author": "Stefano Consolaro , " + "Marcelo Frare " + "Associazione PNLUG - Gruppo Odoo, " + "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/hr/", + "license": "AGPL-3", + + "category": "Human Resources", + + "depends": [ + 'fleet', + 'maintenance', + 'hr', + 'product', + # OCA modules + 'web_timeline', + 'web_widget_color', + 'hr_skill', + 'hr_employee_partner', + 'fleet_vehicle_category', + 'report_xlsx', + ], + "data": [ + 'data/data_service_rule.xml', + 'data/data_service_rulefield.xml', + 'data/demo.xml', + 'security/ir.model.access.csv', + 'wizards/service_generate.xml', + 'views/service_menu.xml', + 'views/service_allocate_view.xml', + 'views/service_rule_view.xml', + 'views/service_profile_view.xml', + 'views/service_aggregator_view.xml', + 'views/service_container_view.xml', + 'views/service_expected_view.xml', + 'views/service_template_view.xml', + 'views/employee_profile.xml', + 'reports/xlsx_allocated_service.xml', + ], + "css": [ + 'static/src/css/hr_duty_planner.css', + ], +} diff --git a/hr_duty_planner/data/data_service_rule.xml b/hr_duty_planner/data/data_service_rule.xml new file mode 100644 index 00000000000..595f90f5da8 --- /dev/null +++ b/hr_duty_planner/data/data_service_rule.xml @@ -0,0 +1,39 @@ + + + + + + hour_active_day + Hours active per day + + + hour_active_week + Rest hours per week + + + hour_active_month + Rest hours per month + + + hour_rest_day + Rest hours per day + + + hour_rest_week + Rest hours per week + + + hour_rest_month + Rest hours per month + + + end_of_shift_rest + End of shift rest + + + end_of_night_shift_rest + End of night shift rest + + + + diff --git a/hr_duty_planner/data/data_service_rulefield.xml b/hr_duty_planner/data/data_service_rulefield.xml new file mode 100644 index 00000000000..dee64aec4ca --- /dev/null +++ b/hr_duty_planner/data/data_service_rulefield.xml @@ -0,0 +1,39 @@ + + + + + + + 1 + h_max + Maximum work hours per day + 12 + True + + + 1 + h_min + Minimum work hours per day + 4 + True + + + + 2 + h_min + Maximum rest hours per week + 24 + True + + + + 7 + is_request + Reguire a rest shift after a work shift + True + True + + + + + diff --git a/hr_duty_planner/data/demo.xml b/hr_duty_planner/data/demo.xml new file mode 100644 index 00000000000..041979be3ba --- /dev/null +++ b/hr_duty_planner/data/demo.xml @@ -0,0 +1,524 @@ + + + + + + + Ambulance + + + Doctor's car + + + Pickup + + + + + + 1 + 1 + + + + 1 + 1 + + + + + 1 + + PN-2020-LUG + + + 1 + + OD-2020-OO + + + + + Electrical continuity + + + Pulmonary support + + + Heart support + + + Hardware + + + + + + + + + + + + + + + + Generator set 1 + + + + Oxygen tank 1 + + + + Defibrillator 1 + + + + Generator set 2 + + + + Oxygen tank 2 + + + + Defibrillator 2 + + + + + + 1 + 1 + 10 + + + 1 + 2 + 5 + + + 1 + 1 + 8 + + + 3 + 1 + 40 + + + 3 + 1 + 16 + + + + + Nurse professional + + + + Nurse trainee + + + + Driver professional + + + + Controller + + + + + + AA Fabrizio Bianchi + + + + AA Roberto Testa + + + + AA Bianca Monti + + + + AA Moreno Rossi + + + + AA Luca Verdi + + + + AA Dina Lampa + + + + AA Dario Lampa + + + + AA Dino Como + + + + + + Nurse + + + Driver type 'B' + + + Driver type 'C' + + + Pilot + + + Controller + + + + + + 2 + + + + 1 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Emergency support + Non-stop emergency + + + Call center + Non-stop call center + + + Off Duty + Rest/Maintenance shifts + + + + + Call center diurnal + + + 16 + 6 + #6AFF6A + + + + Call center night + + + 16 + 6 + #6A6AFF + + + + Rest + + 8 + 6 + True + #BEBEBE + + + + Emergency + + 8 + 6 + + + + + #FF6A6A + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + + + + planned + + + + + + + planned + + + + + + + planned + + + + + + + planned + + + + + + + planned + + + + + + + planned + + + + + + + planned + + + + + + diff --git a/hr_duty_planner/models/__init__.py b/hr_duty_planner/models/__init__.py new file mode 100644 index 00000000000..de289ae50ba --- /dev/null +++ b/hr_duty_planner/models/__init__.py @@ -0,0 +1,5 @@ + +from . import expected_equipment, expected_vehicle, expected_skill, \ + service_container, service_template, service_allocate, service_state, \ + service_rule, service_profile, service_rule_field, \ + service_aggregator, employee_profile, service_profile_parameter diff --git a/hr_duty_planner/models/employee_profile.py b/hr_duty_planner/models/employee_profile.py new file mode 100644 index 00000000000..897ceeab0b4 --- /dev/null +++ b/hr_duty_planner/models/employee_profile.py @@ -0,0 +1,17 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class EmployeeProfile(models.Model): + """ + Add Profile reference to Employee + """ + + _inherit = 'hr.employee' + + # Profile reference + profile_id = fields.Many2one('service.profile', + 'Service profile', + help='Employee service profile') diff --git a/hr_duty_planner/models/expected_equipment.py b/hr_duty_planner/models/expected_equipment.py new file mode 100644 index 00000000000..e4d51b05fb1 --- /dev/null +++ b/hr_duty_planner/models/expected_equipment.py @@ -0,0 +1,27 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ExpectedEquipmentCategory(models.Model): + """ + Equipment category list expected on a service + """ + + # model + _name = 'expected.eqpmnt_cat' + _description = 'Service expected equipment' + + # fields + # minimum quantity + min_qty = fields.Integer('Minimum quantity', required=True, default=1) + # maximum quantity: 0 for no limit + max_qty = fields.Integer('Maximum quantity', help="Value 0 means no limit") + # equipment category required + eqp_cat_id = fields.Many2one('maintenance.equipment.category', + string='Equipment category', + required=True) + + # define record name to display in form view + _rec_name = 'eqp_cat_id' diff --git a/hr_duty_planner/models/expected_skill.py b/hr_duty_planner/models/expected_skill.py new file mode 100644 index 00000000000..f8f7cacf96c --- /dev/null +++ b/hr_duty_planner/models/expected_skill.py @@ -0,0 +1,26 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ExpectedSkill(models.Model): + """ + Employee skills expected on a service + """ + + # model + _name = 'expected.skill' + _description = 'Service expected skills' + + # fields + # minimum quantity + min_qty = fields.Integer('Minimum quantity', required=True, default=1) + # maximum quantity: 0 for no limit + max_qty = fields.Integer('Maximum quantity', help="Value 0 means no limit") + # skill required + skill_id = fields.Many2one('hr.skill', string='Skill', + required=True) + + # define record name to display in form view + _rec_name = 'skill_id' diff --git a/hr_duty_planner/models/expected_vehicle.py b/hr_duty_planner/models/expected_vehicle.py new file mode 100644 index 00000000000..b524f8ef744 --- /dev/null +++ b/hr_duty_planner/models/expected_vehicle.py @@ -0,0 +1,28 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ExpectedVehicle(models.Model): + """ + Vehicle element expected on a service + """ + + # model + _name = 'expected.vhcl_category' + _description = 'Service expected vehicle' + + # fields + # minimum quantity + min_qty = fields.Integer('Minimum quantity', required=True, default=1) + # maximum quantity: 0 for no limit + max_qty = fields.Integer('Maximum quantity', help="Value 0 means no limit") + # vehicle + vehicle_category_id = fields.Many2one('fleet.vehicle.category', + string='Vehicle Category', + required=True, + ) + + # define record name to display in form view + _rec_name = 'vehicle_category_id' diff --git a/hr_duty_planner/models/service_aggregator.py b/hr_duty_planner/models/service_aggregator.py new file mode 100644 index 00000000000..475f030d3b6 --- /dev/null +++ b/hr_duty_planner/models/service_aggregator.py @@ -0,0 +1,24 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ServiceAggregator(models.Model): + """ + Aggregator of service templates to calculate final values + eg. amount of hours worked, number of rest shift in a month + """ + + # model + _name = 'service.aggregator' + _description = 'Aggregator of service templates' + + # fields + # name + name = fields.Char('Name') + # description + description = fields.Char('Description') + # service template to collect + service_template_ids = fields.Many2many('service.template', + string='Collected Services') diff --git a/hr_duty_planner/models/service_allocate.py b/hr_duty_planner/models/service_allocate.py new file mode 100644 index 00000000000..014cd3e9d28 --- /dev/null +++ b/hr_duty_planner/models/service_allocate.py @@ -0,0 +1,340 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models, api, _ +import datetime +import json +import logging + + +_logger = logging.getLogger(__name__) + +class ServiceAllocate(models.Model): + """ + Allocated service with definition of all the template components + """ + + # model + _name = 'service.allocate' + _description = 'Allocate service' + + # fields + # template service reference + service_template_id = fields.Many2one('service.template', + string='Template service', + required=True, + ) + # off duty service + off_duty = fields.Boolean('Off Duty', + related='service_template_id.off_duty') + + # container service reference + service_container_id = fields.Many2one('service.container', + string='Container service', + required=True, + ) + # generation id (eg. to select a list of aumatic generated services) + generation_id = fields.Char('Generation ID', + help='Group services generated automatically') + # dedicated color + service_color = fields.Char('Color', + related='service_template_id.base_color') + + # assigned vehicles + vehicle_ids = fields.Many2many('fleet.vehicle', string='Vehicles') + # message for vehicles check + vehicle_check = fields.Text('Vehicles coverage', store=True) + # assigned employee + employee_ids = fields.Many2many('hr.employee', string='Team') + # employee names + employee_names = fields.Text('Employees', compute='_compute_emply_name', store=True) + # message for skills check + employee_check = fields.Text('Skills coverage', store=True) + # assigned equipment + equipment_ids = fields.Many2many('maintenance.equipment', string='Equipment') + # message for equipments check + equipment_check = fields.Text('Equipments coverage', store=True) + + # locality reference + locality = fields.Char('Locality') + + # scheduled start time + scheduled_start = fields.Datetime('Start scheduled', required=True) + # scheduled start time + scheduled_stop = fields.Datetime('Stop scheduled', + compute='_compute_scheduled_stop', store=True) + # effective start time + start_real = fields.Datetime('Start real') + # effective stop time + stop_real = fields.Datetime('Stop real') + + # state of the service + state = fields.Selection([('planned', 'Planned'), + ('confirmed', 'Confirmed'), + ('closed', 'Closed') + ], string='State', required=True, default='planned') + + # parent service that generate this as next service + parent_service_id = fields.Many2one('service.allocate', + 'Parent Service', + help='Service that generated this one by \ + template next option' + ) + # next service generate from this one + next_service_id = fields.Many2one('service.allocate', + 'Next Service', + help='Service generated by this one by \ + template next option' + ) + + # define record name to display in form view + _rec_name = 'id' + + @api.depends('scheduled_start') + def _compute_scheduled_stop(self): + for service in self: + if service.scheduled_start: + slot = service.service_template_id.duration + # avoid empty value of duration + slot = slot if slot > 0 else 1 + service.scheduled_stop = (service.scheduled_start + + datetime.timedelta(hours=slot)) + + return + + # main changes monitor methods + @api.multi + @api.onchange('employee_ids') + def _employee_ids_change(self): + """ + Call methods on employees changes + """ + self._compute_emply_name() + self._check_skill_request() + + @api.multi + @api.onchange('equipment_ids') + def _equipment_ids_change(self): + """ + Call methods on equipments changes + """ + self._check_equipment_request() + + @api.multi + @api.onchange('vehicle_ids') + def _vehicle_ids_change(self): + """ + Call methods on vehicles changes + """ + self._check_vehicle_request() + + # changes dedicated methods + def _compute_emply_name(self): + """ + Update field with name list + """ + for service in self: + service.employee_names = '' + for employee in service.employee_ids: + service.employee_names += employee.name + '\n' + return + + def _check_skill_request(self): + """ + Check if all required skills are covered by employees + """ + for service in self: + # clear error message + self.employee_check = '' + # get requested skills by template + skill_request = service.service_template_id.exp_skill_ids + # for each request counts available employees + for request in skill_request: + available_qty = 0 + # chek requested skill in each employee + for employee in service.employee_ids: + if self.env['hr.employee.skill'].search( + [('employee_id', '=', employee.id), + ('skill_id', '=', request.skill_id.id), + ]): + available_qty += 1 + if available_qty < request.min_qty: + self.employee_check += (_('Missing %s\n') + % (request.skill_id.name)) + if request.max_qty > 0 and available_qty > request.max_qty: + self.employee_check += (_('Too many %s\n') + % (request.skill_id.name)) + if self.employee_check == '': + self.employee_check = 'All covered' + return + + def _check_equipment_request(self): + """ + Check if all required categories are covered by equipments + """ + for service in self: + # clear error message + self.equipment_check = '' + # get requested categoryies by template + category_request = service.service_template_id.exp_eqp_cat_ids + # for each request counts available categories + for request in category_request: + available_qty = 0 + # chek requested categories in each equipment + for equipment in service.equipment_ids: + if self.env['maintenance.equipment'].search( + [('id', '=', equipment.id), + ('category_id', '=', request.eqp_cat_id.id), + ]): + available_qty += 1 + if available_qty < request.min_qty: + self.equipment_check += (_('Missing %s\n') + % (request.eqp_cat_id.name)) + if request.max_qty > 0 and available_qty > request.max_qty: + self.equipment_check += (_('Too many %s\n') + % (request.eqp_cat_id.name)) + if self.equipment_check == '': + self.equipment_check = 'All covered' + return + + def _check_vehicle_request(self): + """ + Check if all required type are covered by vehicles + """ + for service in self: + # clear error message + self.vehicle_check = '' + # get requested categoryies by template + vehicle_request = service.service_template_id.exp_vehicle_ids + # for each request counts available types + for request in vehicle_request: + available_qty = 0 + # chek requested types in each vehicle + for vehicle in service.vehicle_ids: + if self.env['fleet.vehicle'].search( + [('id', '=', vehicle.id), + ('vehicle_category_id', '=', request.vehicle_category_id.id), + ]): + available_qty += 1 + if available_qty < request.min_qty: + self.vehicle_check += (_('Missing %s\n') + % (request.vehicle_category_id.name)) + if request.max_qty > 0 and available_qty > request.max_qty: + self.vehicle_check += (_('Too many %s\n') + % (request.vehicle_category_id.name)) + if self.vehicle_check == '': + self.vehicle_check = 'All covered' + return + + # utility to filter container services to template's container services + @api.onchange('service_template_id') + def _get_template_container(self): + """ + Extract list of container services associated to the template service + """ + container_services = [] + # reset value to avoid errors + self.service_container_id = [(5)] + for glob_srv in self.service_template_id.service_container_ids: + container_services.append(glob_srv.id) + + return {'domain': {'service_container_id': [('id', 'in', container_services)]}} + + def double_assign(self, parameters): + """ + _TODO_ _FIX_ direct call to service.rule.double_assign on the button + """ + result = self.env['service.rule'].double_assign(parameters['resource_type'], + parameters['srv_id']) + return result + + def rule_call(self, parameters): + """ + _TODO_ _FIX_ direct call to service.rule.rule_call on the button + """ + result = self.env['service.rule'].rule_call(parameters['rule_name'], + parameters['srv_id']) + return result + + def check_resource_rule(self, parameters): + """ + Check rules for each resource associated to the service + @param srv_id int: id of the service + """ + + # get employee of the service + for employee in self.env['service.allocate'] \ + .search([('id', '=', parameters['srv_id'])]).employee_ids: + # memorixe a dictionary of rules and fields + rule_method = {} + # get rules of the profile associated to the employee + for rule in employee.profile_id.parameter_ids: + # create rule element if not exists + try: + rule_method[rule.rule_id.method] + except: + rule_method[rule.rule_id.method] = {} + # save rile/field value + rule_method[rule.rule_id.method][rule.rule_field_id.field_name] = \ + rule.field_value + _logger.info(employee.name+' '+json.dumps(rule_method)) + # print(employee.name+' '+json.dumps(rule_method)) + return + + @api.model + def create(self, values): + """ + Override create function to manage next service generation + """ + new_service = super(ServiceAllocate, self).create(values) + + # generation id can be set by the automatic flow + if not new_service.generation_id: + new_service.generation_id = datetime.datetime.now(). \ + strftime("M %Y-%m-%d-%H-%M-%S") + + # generate next service if present on template + if new_service.service_template_id.next_template_id.id: + # get end of the original service + next_strt = new_service.scheduled_stop + # get next service template + next_serv = new_service.service_template_id.next_template_id.id + # get first container of the next service template + next_cont = new_service.service_template_id.service_container_ids[0].id + + new_service_data = { + "service_template_id" : next_serv, + "service_container_id" : next_cont, + "scheduled_start" : next_strt, + "parent_service_id" : new_service.id, + "generation_id" : new_service.generation_id, + } + new_service_nxt = super(ServiceAllocate, self).create(new_service_data) + # save child reference + new_service.next_service_id = new_service_nxt + + return new_service + + @api.multi + def write(self, values): + """ + Override with check elements for double assigns before save + """ + ServiceAllocate_write = super(ServiceAllocate, self).write(values) + # call double assignment + self.double_assign({'resource_type': 'all', 'srv_id': self.id}) + + return ServiceAllocate_write + + @api.multi + def unlink(self): + """ + Override with unlink for next services + """ + # scan services for next element + for service in self: + # when next service is defined and not yet present in self list + if service.next_service_id.id and service.next_service_id not in self: + service.next_service_id.unlink() + + return super(ServiceAllocate, self).unlink() diff --git a/hr_duty_planner/models/service_container.py b/hr_duty_planner/models/service_container.py new file mode 100644 index 00000000000..7c49de0439d --- /dev/null +++ b/hr_duty_planner/models/service_container.py @@ -0,0 +1,20 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ServiceContainer(models.Model): + """ + General service definition that includes several time managed services + """ + + # model + _name = 'service.container' + _description = 'General service definition' + + # fields + # name + name = fields.Char('Name') + # description + description = fields.Char('Description') diff --git a/hr_duty_planner/models/service_profile.py b/hr_duty_planner/models/service_profile.py new file mode 100644 index 00000000000..83e0ce11078 --- /dev/null +++ b/hr_duty_planner/models/service_profile.py @@ -0,0 +1,23 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ServiceProfile(models.Model): + """ + Collection of rules to assign to a resource + """ + + # model + _name = 'service.profile' + _description = 'Profile to group rules' + + # fields + # name + name = fields.Char('Name', required=True) + # set rule ignore + rule_ignore = fields.Boolean('Ignore Rule', default=False, + help='If active all the Rules will not be checked') + # parameter values + parameter_ids = fields.Many2many('service.profileparameter', string='Field') diff --git a/hr_duty_planner/models/service_profile_parameter.py b/hr_duty_planner/models/service_profile_parameter.py new file mode 100644 index 00000000000..8cd48c850c3 --- /dev/null +++ b/hr_duty_planner/models/service_profile_parameter.py @@ -0,0 +1,25 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ServiceProfileParameter(models.Model): + """ + Assign values to the rules fields + """ + + # model + _name = 'service.profileparameter' + _description = 'Define value of fields used by rules in profile' + + # fields + # rule reference + rule_id = fields.Many2one('service.rule', 'Rule') + # rule field + rule_field_id = fields.Many2one('service.rulefield', 'Field') + # Value to use in method + field_value = fields.Char('Value') + + # define record name to display in form view + _rec_name = 'rule_id' diff --git a/hr_duty_planner/models/service_rule.py b/hr_duty_planner/models/service_rule.py new file mode 100644 index 00000000000..530acc556e8 --- /dev/null +++ b/hr_duty_planner/models/service_rule.py @@ -0,0 +1,199 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models, _ +from odoo.exceptions import UserError + + +class ServiceRule(models.Model): + """ + Definition of the rules available as specific method. + This model if filled through xls data file. + """ + + # model + _name = 'service.rule' + _description = 'Rules to manage Services' + + # fields + # method of the rule + method = fields.Char('Method') + # rule description + description = fields.Char('Description') + # method's fields + field_ids = fields.Many2many('service.rulefield', string='Fields') + # define if rule is used + is_active = fields.Boolean('Active', default=True, + help='Set if the rule had to be evaluated') + + # define record name to display in form view + _rec_name = 'description' + + def double_assign(self, resource_type, obj_id): + """ + Check if a resource has more than one shift assigned at same time + @param resource_type string: type of the resource + [all, employee, vehicle, equipment] + @param obj_id int: id of the service; -1 to check all services + """ + # _TODO_ optimize + + rule_result = True + rule_msg = '' + + # select service to check + if obj_id > 0: + allocate_ids = self.env['service.allocate'].search([('id', '=', obj_id)]) + else: + allocate_ids = self.env['service.allocate'].search([]) + + # get the service data + for service in allocate_ids: + date_ini = service.scheduled_start + date_fin = service.scheduled_stop + + if resource_type in ('employee', 'all') : + # if it is a next service uses the parent employees for check + # if lock is activated + lock = service.parent_service_id.service_template_id.next_lock_employee + employee_list = ( + service.employee_ids + if not service.parent_service_id or not lock + else self.env['service.allocate'] + .search([('id', '=', service.parent_service_id.id), + ]).employee_ids + ) + for employee in employee_list: + all_services = self.env['service.allocate'] \ + .search([('id', '!=', service.id), + ('scheduled_start', '<', date_fin), + ('scheduled_stop', '>', date_ini), + ('state', '!=', 'closed') + ]) + for service_double in all_services: + if employee in service_double.employee_ids: + rule_result = False + rule_msg += (('Shift %s/%s: %s\n') % (service.id, + service_double.id, + employee.name)) + + if resource_type in ('equipment', 'all'): + # if it is a next service uses the parent equipments for check + # if lock is activated + lock = service.parent_service_id.service_template_id.next_lock_equipment + equipment_list = ( + service.equipment_ids + if not service.parent_service_id or not lock + else self.env['service.allocate'] + .search([('id', '=', service.parent_service_id.id), + ]).equipment_ids + ) + for equipment in equipment_list: + all_services = self.env['service.allocate'] \ + .search([('id', '!=', service.id), + ('scheduled_start', '<', date_fin), + ('scheduled_stop', '>', date_ini), + ('state', '!=', 'closed') + ]) + for service_double in all_services: + if equipment in service_double.equipment_ids: + rule_result = False + rule_msg += (('Shift %s/%s: %s\n') % (service.id, + service_double.id, + equipment.name)) + + if resource_type in ('vehicle', 'all'): + # if it is a next service uses the parent vehicles for check + # if lock is activated + lock = service.parent_service_id.service_template_id.next_lock_vehicle + vehicle_list = ( + service.vehicle_ids + if not service.parent_service_id or not lock + else self.env['service.allocate'].search( + [('id', '=', service.parent_service_id.id), + ]).vehicle_ids + ) + for vehicle in vehicle_list: + all_services = self.env['service.allocate'] \ + .search([('id', '!=', service.id), + ('scheduled_start', '<', date_fin), + ('scheduled_stop', '>', date_ini), + ('state', '!=', 'closed'), + ]) + for service_double in all_services: + if vehicle in service_double.vehicle_ids: + rule_result = False + rule_msg += (('Shift %s/%s: %s\n') % (service.id, + service_double.id, + vehicle.name)) + + if not rule_result: + raise UserError(_('Elements with overlapped shift:')+'\n'+rule_msg) + return rule_result + + def rule_call(self, rule, obj_id): + """ + Call requested rule + @param rule obj: form select element with name of the rule to call: + has to be in rule_id selection + @return rule elaboration + """ + + # _TODO_ check if in rule_id + rule_name = rule + # Get the method from 'self'. Default to a lambda. + method = getattr(self, rule_name, "_invalid_rule") + # Call the method as we return it + if method == '_invalid_rule': + self._invalid_rule(rule_name) + else: + result = method(obj_id) + return result + + def _invalid_rule(self, rule_name): + """ + Management of method non defined + """ + raise UserError(_('Method %s not defined') % (rule_name)) + + def _rule_method_template(self): + """ + Rules definition template + @param _todo_ + @return _todo_ + """ + return 0 + + def hour_active_week(self, obj_id): + """ + Calculate the total of active hours of a resource in a week. + By active hours is meant work+on call + + _todo_ define/set active shift + """ + total_time = 0 + + # extract employees of the service + for employee in (self.env['service.allocate'] + .search([('id', '=', obj_id)]).employee_ids): + # get services where employee is assigned + sql = ('SELECT service_allocate_id ' + 'FROM hr_employee_service_allocate_rel ' + 'WHERE hr_employee_id= %s') + self.env.cr.execute(sql, str(employee.id)) + # get duration of each service + # _todo_ calculate as end-start + for srv_id in self.env.cr.fetchall(): + total_time += self.env['service.allocate'] \ + .search([('id', '=', srv_id)]) \ + .service_template_id.duration + raise UserError(_('Totale ore uomo %s') % (total_time)) + #return total_time + + def hour_rest_week(self): + """ + Calculate the total of rest hours of a resource in a week. + By active hours is meant not work or on call + _todo_ define/set active shift + """ + return 8 diff --git a/hr_duty_planner/models/service_rule_field.py b/hr_duty_planner/models/service_rule_field.py new file mode 100644 index 00000000000..516cbe50e21 --- /dev/null +++ b/hr_duty_planner/models/service_rule_field.py @@ -0,0 +1,34 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ServiceRuleField(models.Model): + """ + Definition of the fields used by rule's methods. + Field value type will be ever saved as string then converted inside methods + """ + + # model + _name = 'service.rulefield' + _description = 'fields available to rule\'s methods' + + # fields + # reference rule + rule_id = fields.Many2one('service.rule', string='Rule', required=True) + # field name used in the method + field_name = fields.Char('Field', required=True, + help='Name of the field used inside method') + # field description + field_desc = fields.Char('Description', required=True, + help='Describe what this field is used for') + # defualt value + field_default = fields.Char('Default value', + help='Value used if the field is not filled by user') + # set field mandatory + field_mandatory = fields.Boolean('Mandatory', default=False, + help='Indicate that the field has to be set') + + # define record name to display in form view + _rec_name = 'field_name' diff --git a/hr_duty_planner/models/service_state.py b/hr_duty_planner/models/service_state.py new file mode 100644 index 00000000000..9bb0ab2b50f --- /dev/null +++ b/hr_duty_planner/models/service_state.py @@ -0,0 +1,20 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ServiceState(models.Model): + """ + State of the allocated services + """ + + # model + _name = 'service.state' + _description = 'States of allocated services' + + # fields + # name + name = fields.Char('Name') + # description + description = fields.Char('Description') diff --git a/hr_duty_planner/models/service_template.py b/hr_duty_planner/models/service_template.py new file mode 100644 index 00000000000..51df5af7a55 --- /dev/null +++ b/hr_duty_planner/models/service_template.py @@ -0,0 +1,103 @@ +# Copyright 2020 Stefano Consolaro (Ass. PNLUG - Gruppo Odoo ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models, api, _ +from odoo.exceptions import ValidationError + + +class ServiceTemplate(models.Model): + """ + Model of a service with definition of the required components + """ + + # model + _name = 'service.template' + _description = 'Model of a service' + + # fields + # name + name = fields.Char('Name', required=True) + # container service reference + service_container_ids = fields.Many2many('service.container', + string='Container service', + required=True) + + # standard duration + duration = fields.Integer('Duration', required=True, default=1) + # duration uom + duration_uom_id = fields.Many2one('uom.uom', + string='Unit of Measure') + + # off-duty services identification to manage rest/maintenance conditions + off_duty = fields.Boolean('Off Duty', default=False, + help='Check if it is a technical ' + 'service to manage rest/maintenance conditions') + + # expected vehicles + exp_vehicle_ids = fields.Many2many('expected.vhcl_category', + string='Vehicles') + # expected skills + exp_skill_ids = fields.Many2many('expected.skill', + string='Operator Skills') + # expected equipment category + exp_eqp_cat_ids = fields.Many2many('expected.eqpmnt_cat', + string='Equipment Category') + + # product reference used to valorize + product_id = fields.Many2one('product.product', + string='Product reference') + + # identification color + base_color = fields.Char('Color') + + # Service Template to generate on Service completion + next_template_id = fields.Many2one( + 'service.template', + string='Next Service', + help='Service Template to insert after the end of this one', + ) + # Next service lock employees + next_lock_employee = fields.Boolean( + 'Lock Employees', default=True, + help='Next service will allocate Employees of the parent service', + ) + # Next service lock equipments + next_lock_equipment = fields.Boolean( + 'Lock Equipments', default=False, + help='Next service will allocate Equipments of the parent service', + ) + # Next service lock vehicles + next_lock_vehicle = fields.Boolean( + 'Lock Vehicles', default=False, + help='Next service will allocate Vehicles of the parent service', + ) + + @api.multi + def write(self, values): + """ + Add control for fields value on record changes + """ + if ('duration' in values + and values['duration'] <= 0): + raise ValidationError(_('Enter a value for Duration')) + + if ('service_container_ids' in values + and len(values['service_container_ids'][0][2]) == 0): + raise ValidationError(_('Set a Container Service')) + + ServiceTemplate_write = super(ServiceTemplate, self).write(values) + return ServiceTemplate_write + + @api.model + def create(self, values): + """ + Add control for fields value on new records + """ + if ('duration' in values + and values['duration'] <= 0): + raise ValidationError(_('Enter a value for Duration')) + if ('service_container_ids' in values + and len(values['service_container_ids'][0][2]) == 0): + raise ValidationError(_('Set a Container Service')) + ServiceTemplate_create = super(ServiceTemplate, self).create(values) + return ServiceTemplate_create diff --git a/hr_duty_planner/readme/CONTRIBUTORS.rst b/hr_duty_planner/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..31839b5202e --- /dev/null +++ b/hr_duty_planner/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Stefano Consolaro +* Marcelo Frare diff --git a/hr_duty_planner/readme/DESCRIPTION.rst b/hr_duty_planner/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..2b601adcca2 --- /dev/null +++ b/hr_duty_planner/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +These module manages the Service Planner. + +_TODO_ diff --git a/hr_duty_planner/readme/HISTORY.rst b/hr_duty_planner/readme/HISTORY.rst new file mode 100644 index 00000000000..4abe91a55d9 --- /dev/null +++ b/hr_duty_planner/readme/HISTORY.rst @@ -0,0 +1,4 @@ +12.0.1.0.0 (2020-05-01) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [INI] Initial development diff --git a/hr_duty_planner/readme/USAGE.rst b/hr_duty_planner/readme/USAGE.rst new file mode 100644 index 00000000000..24e9c0663b3 --- /dev/null +++ b/hr_duty_planner/readme/USAGE.rst @@ -0,0 +1,52 @@ +**Steps to follow to use Service Planner** + +**Set up** + + **Rules** + * Rules and Rules Fields can not be modified by Users. Read what they do and how to use fields. + + **Select Rules → Profile** + * Create a Profile + * Add rule fields with values + * Add profile to Employees: Employee → get one → fill Service Profile + + **Employee** + * Create desired Skills: get an Employee → Skill tab → add row → create + + **Maintenance → Configuration → Categories** + * Create desired Categories + + **Fleet** + * Create desired Types: get a Vehicle → Vehicle Type → create + + **Select Template → Container Service** + * Create a list of Container Services to collect single services + + **Select Template → Service Template** + * Create a Template Service + + * Name, Container Service and Duration are mandatory + * Check if it is an Off Duty Service (normally used to track some informations, ie. rest shifts) + * Add requested Features to be sure to not forget to assign Resources. + (Minimum quantity if zero will not be considered) + * Optional, associate a Next Service: this will be automatically generated starting form the end of this one. + * Optional, set resources of parent service locked by the next service + + **Select Template → Service Template → your template** + * Clik on *Generate Services* button + * Select a Template + * Select a Container + * Select a start date-time + * Select a stop date-time + * Set repetition interval (hours). If interval is inferior to the template duration, this one will be used + * Set available days of week + * Set to use work Calendar + * Set to use holiday + * Click on *Generate* button + + **Planning** + * Select Services → List view* + * Select a Service + * Fill resources (eg. Employee, Equipments, Vehicles) + * Check requirements (Template requestested Resources) fulfillment + * Calendar/Timeline: overview / move services diff --git a/hr_duty_planner/reports/__init__.py b/hr_duty_planner/reports/__init__.py new file mode 100644 index 00000000000..abf08ae2638 --- /dev/null +++ b/hr_duty_planner/reports/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import xlsx_allocated_service diff --git a/hr_duty_planner/reports/xlsx_allocated_service.py b/hr_duty_planner/reports/xlsx_allocated_service.py new file mode 100644 index 00000000000..b7db96cbed2 --- /dev/null +++ b/hr_duty_planner/reports/xlsx_allocated_service.py @@ -0,0 +1,279 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models +from odoo.tools.translate import _ +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT + + +class XlsxAllocatedService(models.AbstractModel): + _name = 'report.hr_duty_planner.xlsx_allocated_service' + _inherit = 'report.report_xlsx.abstract' + + def generate_xlsx_report(self, workbook, data, extra): + + layout_report = data['form']['layout_report'] + + if layout_report == 'pivot': + self._export_pivot(workbook, data) + else: + self._export_list(workbook, data) + + def _export_list(self, workbook, data): + + ids = data['ids'] + services = self.env['service.allocate'].browse(ids) + date_from = data['form']['date_from'] + date_to = data['form']['date_to'] + + sheet = workbook.add_worksheet(_('Service allocate')) + sheet.set_landscape() + sheet.fit_to_pages(1, 0) + sheet.set_column(0, 1, 20) + sheet.set_column(2, 3, 35) + sheet.set_column(4, 7, 15) + sheet.set_column(8, 11, 35) + + bold_style = workbook.add_format({'bold': True, 'font_size': 14, 'bottom': 1}) + title_style = workbook.add_format({'bold': False, 'align': 'center', + 'bg_color': '#C0C0C0', 'bottom': 1}) + center_style = workbook.add_format({'bold': False, + 'align': 'center', 'valign': 'top'}) + left_style = workbook.add_format({'bold': False, + 'align': 'left', 'valign': 'top'}) + right_style = workbook.add_format({'bold': False, + 'align': 'right', 'valign': 'top'}) + + # rows + row = 0 + header = False + for service in services: + # header + if not header: + header = True + sheet.write(row, 0, _('Allocated service'), bold_style) + sheet.write(row, 3, _('From:'), bold_style) + from_date = date_from[8:10] + '/' + date_from[5:7] + '/' + date_from[:4] + sheet.write(row, 4, from_date, bold_style) + sheet.write(row, 5, _('To:'), bold_style) + to_date = date_to[8:10] + '/' + date_to[5:7] + '/' + date_to[:4] + sheet.write(row, 6, to_date, bold_style) + + row += 2 + self.header_ = [_('Scheduled start'), + _('Scheduled stop'), + _('Container'), + _('Name'), + _('State'), + _('Duration'), + _('Uom'), + _('Employees'), + _('Vehicles'), + _('Equipments'), + ] + sheet_title = self.header_ + sheet.write_row(row, 0, sheet_title, title_style) + row += 1 + + s_start = service.scheduled_start.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + s_stop = service.scheduled_stop.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + s_container = service.service_container_id.name + s_name = service.service_template_id.name + s_state = service.state + s_duration = service.service_template_id.duration + s_duration_uom = service.service_template_id.duration_uom_id.name + + # used to set row height + max_rows = 1 + + s_employees = '' + for employee in service.employee_ids: + s_employees += employee.name + ';\n' + if s_employees: + s_employees = s_employees[:-2] + if len(service.employee_ids) > max_rows: + max_rows = len(service.employee_ids) + + s_vehicles = '' + for vehicle in service.vehicle_ids: + s_vehicles += vehicle.name + ';\n' + if s_vehicles: + s_vehicles = s_vehicles[:-2] + if len(service.vehicle_ids) > max_rows: + max_rows = len(service.vehicle_ids) + + s_equipments = '' + for equipment in service.equipment_ids: + s_equipments += equipment.name + ';\n' + if s_equipments: + s_equipments = s_equipments[:-2] + if len(service.equipment_ids) > max_rows: + max_rows = len(service.equipment_ids) + + # row height + if max_rows > 1: + sheet.set_row(row, 25 * (max_rows - 1)) + + # row details + sheet.write(row, 0, s_start or '', center_style) + sheet.write(row, 1, s_stop or '', center_style) + sheet.write(row, 2, s_container or '', left_style) + sheet.write(row, 3, s_name or '', left_style) + sheet.write(row, 4, s_state or '', left_style) + sheet.write(row, 5, s_duration or '', right_style) + sheet.write(row, 6, s_duration_uom or '', left_style) + sheet.write(row, 7, s_employees or '', left_style) + sheet.write(row, 8, s_vehicles or '', left_style) + sheet.write(row, 9, s_equipments or '', left_style) + + row += 1 + return row + + def _export_pivot(self, workbook, data): + + ids = data['ids'] + services = self.env['service.allocate'].browse(ids) + date_from = data['form']['date_from'] + date_to = data['form']['date_to'] + + # mapped scheduled_start + datestime_start = sorted(set(services.mapped('scheduled_start'))) + + # _todo_ if possible optimized mapped on only date? + dates_start = [] + for datetime_start in datestime_start: + date_start = datetime_start.strftime(DEFAULT_SERVER_DATE_FORMAT) + if date_start not in dates_start: + dates_start.append(date_start) + + sheet = workbook.add_worksheet(_('Service allocate')) + sheet.set_landscape() + sheet.fit_to_pages(1, 0) + sheet.set_column(0, 1, 20) + sheet.set_column(2, 2, 15) + sheet.set_column(3, 30, 25) + + bold_style = workbook.add_format({'bold': True, 'font_size': 14, 'bottom': 1}) + title_style = workbook.add_format({'bold': False, 'align': 'center', + 'bg_color': '#C0C0C0', 'bottom': 1}) + left_style = workbook.add_format({'bold': False, 'align': 'left', + 'valign': 'top'}) + + # rows + row = 0 + + # used to write only one time each record + already_read = [] + + # used to add header + header = False + + for service in services: + # header + if not header: + header = True + sheet.write(0, 0, _('Allocated service'), bold_style) + sheet.write(1, 0, _('From:'), bold_style) + from_date = date_from[8:10] + '/' + date_from[5:7] + '/' + date_from[:4] + sheet.write(1, 1, from_date, bold_style) + sheet.write(2, 0, _('To:'), bold_style) + to_date = date_to[8:10] + '/' + date_to[5:7] + '/' + date_to[:4] + sheet.write(2, 1, to_date, bold_style) + + # dates + col = 3 + for date_start in dates_start: + sheet.write(2, col, date_start, title_style) + col += 1 + + if service.id not in already_read: + + # service in focus + focus_container = service.service_container_id + focus_template = service.service_template_id + focus_start = service.scheduled_start.strftime('%H:%M:%S') + focus_stop = service.scheduled_stop.strftime('%H:%M:%S') + + # row identification + row += 3 + s_container = service.service_container_id.name + s_name = (service.service_template_id.name + '\n' + + focus_start + '-' + focus_stop + '\n' + + str(service.service_template_id.duration) + ) + + sheet.write(row, 0, s_container or '', left_style) + sheet.write(row, 1, s_name or '', left_style) + sheet.write(row, 2, _('Employees'), left_style) + sheet.write(row + 1, 2, _('Vehicles'), left_style) + sheet.write(row + 2, 2, _('Equipments'), left_style) + + # write service + self._write_service(service, row, sheet, workbook, dates_start) + + # identify record + already_read.append(service.id) + + for find in services: + if focus_container == find.service_container_id and \ + focus_template == find.service_template_id and \ + focus_start == find.scheduled_start.strftime('%H:%M:%S') and \ + focus_stop == find.scheduled_stop.strftime('%H:%M:%S'): + + if find.id not in already_read: + # write service + self._write_service(find, row, sheet, workbook, dates_start) + + # identify record + already_read.append(find.id) + return row + + def _write_service(self, service, row, sheet, workbook, dates_start): + + left_style = workbook.add_format({'bold': False, 'align': 'left', + 'valign': 'top'}) + + # locate column + col = 3 + for date_start in dates_start: + if (date_start == service.scheduled_start + .strftime(DEFAULT_SERVER_DATE_FORMAT)): + break + col += 1 + + # used to set row height + max_rows = 3 + + s_employees = '' + for employee in service.employee_ids: + s_employees += employee.name + ';\n' + if s_employees: + s_employees = s_employees[:-2] + if len(service.employee_ids) > max_rows: + max_rows = len(service.employee_ids) + + s_vehicles = '' + for vehicle in service.vehicle_ids: + s_vehicles += vehicle.name + ';\n' + if s_vehicles: + s_vehicles = s_vehicles[:-2] + if len(service.vehicle_ids) > max_rows: + max_rows = len(service.vehicle_ids) + + s_equipments = '' + for equipment in service.equipment_ids: + s_equipments += equipment.name + ';\n' + if s_equipments: + s_equipments = s_equipments[:-2] + if len(service.equipment_ids) > max_rows: + max_rows = len(service.equipment_ids) + + # row height + sheet.set_row(row, 20 * max_rows) + sheet.set_row(row+1, 20 * max_rows) + sheet.set_row(row+2, 20 * max_rows) + + # row details + sheet.write(row, col, s_employees or '', left_style) + sheet.write(row+1, col, s_vehicles or '', left_style) + sheet.write(row+2, col, s_equipments or '', left_style) diff --git a/hr_duty_planner/reports/xlsx_allocated_service.xml b/hr_duty_planner/reports/xlsx_allocated_service.xml new file mode 100644 index 00000000000..2a34a093973 --- /dev/null +++ b/hr_duty_planner/reports/xlsx_allocated_service.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/hr_duty_planner/security/ir.model.access.csv b/hr_duty_planner/security/ir.model.access.csv new file mode 100644 index 00000000000..1374773b710 --- /dev/null +++ b/hr_duty_planner/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_expected_vhcl_category,access_expected_vhcl_category,model_expected_vhcl_category,base.group_user,1,1,1,1 +access_expected_eqpmnt_cat,access_expected_eqpmnt_cat,model_expected_eqpmnt_cat,base.group_user,1,1,1,1 +access_expected_skill,access_expected_skill,model_expected_skill,base.group_user,1,1,1,1 +access_service_container,access_service_container,model_service_container,base.group_user,1,1,1,1 +access_service_state,access_service_state,model_service_state,base.group_user,1,1,1,1 +access_service_template,access_service_template,model_service_template,base.group_user,1,1,1,1 +access_service_allocate,access_service_allocate,model_service_allocate,base.group_user,1,1,1,1 +access_service_rule,access_service_rule,model_service_rule,base.group_user,1,1,0,0 +access_service_profile,access_service_profile,model_service_profile,base.group_user,1,1,1,1 +access_service_profileparameter,access_service_profileparameter,model_service_profileparameter,base.group_user,1,1,1,1 +access_service_rulefield,access_service_rulefield,model_service_rulefield,base.group_user,1,1,0,0 +access_service_aggregator,access_service_aggregator,model_service_aggregator,base.group_user,1,1,1,1 diff --git a/hr_duty_planner/static/description/icon.png b/hr_duty_planner/static/description/icon.png new file mode 100644 index 00000000000..6d6ceca9b92 Binary files /dev/null and b/hr_duty_planner/static/description/icon.png differ diff --git a/hr_duty_planner/static/description/index.html b/hr_duty_planner/static/description/index.html new file mode 100644 index 00000000000..6b3c7677c95 --- /dev/null +++ b/hr_duty_planner/static/description/index.html @@ -0,0 +1,515 @@ + + + + + + +HR - Duty Planner + + + +
+

HR - Duty Planner

+ + +

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

+

These module manages the Service Planner.

+

_TODO_

+

Table of contents

+ +
+

Usage

+

Steps to follow to use Service Planner

+

Set up

+
+
+
Rules
+
    +
  • Rules and Rules Fields can not be modified by Users. Read what they do and how to use fields.
  • +
+
+
Select Rules → Profile
+
    +
  • Create a Profile
  • +
  • Add rule fields with values
  • +
  • Add profile to Employees: Employee → get one → fill Service Profile
  • +
+
+
Employee
+
    +
  • Create desired Skills: get an Employee → Skill tab → add row → create
  • +
+
+
Maintenance → Configuration → Categories
+
    +
  • Create desired Categories
  • +
+
+
Fleet
+
    +
  • Create desired Types: get a Vehicle → Vehicle Type → create
  • +
+
+
Select Template → Container Service
+
    +
  • Create a list of Container Services to collect single services
  • +
+
+
Select Template → Service Template
+
    +
  • Create a Template Service
  • +
+
+
    +
  • Name, Container Service and Duration are mandatory
  • +
  • Check if it is an Off Duty Service (normally used to track some informations, ie. rest shifts)
  • +
  • Add requested Features to be sure to not forget to assign Resources. +(Minimum quantity if zero will not be considered)
  • +
  • Optional, associate a Next Service: this will be automatically generated starting form the end of this one.
  • +
  • Optional, set resources of parent service locked by the next service
  • +
+
+
+
Select Template → Service Template → your template
+
    +
  • Clik on Generate Services button
  • +
  • Select a Template
  • +
  • Select a Container
  • +
  • Select a start date-time
  • +
  • Select a stop date-time
  • +
  • Set repetition interval (hours). If interval is inferior to the template duration, this one will be used
  • +
  • Set available days of week
  • +
  • Set to use work Calendar
  • +
  • Set to use holiday
  • +
  • Click on Generate button
  • +
+
+
Planning
+
    +
  • Select Services → List view*
  • +
  • Select a Service
  • +
  • Fill resources (eg. Employee, Equipments, Vehicles)
  • +
  • Check requirements (Template requestested Resources) fulfillment
  • +
  • Calendar/Timeline: overview / move services
  • +
+
+
+
+
+
+

Changelog

+
+

12.0.1.0.0 (2020-05-01)

+
    +
  • [INI] Initial development
  • +
+
+
+
+

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

+ +
+
+

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

+

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

+
+
+
+ + diff --git a/hr_duty_planner/static/src/css/hr_duty_planner.css b/hr_duty_planner/static/src/css/hr_duty_planner.css new file mode 100644 index 00000000000..22ae2775a94 --- /dev/null +++ b/hr_duty_planner/static/src/css/hr_duty_planner.css @@ -0,0 +1,3 @@ +.sp_td_label .o_td_label { + text-align: right !important; + } \ No newline at end of file diff --git a/hr_duty_planner/views/employee_profile.xml b/hr_duty_planner/views/employee_profile.xml new file mode 100644 index 00000000000..7788467108a --- /dev/null +++ b/hr_duty_planner/views/employee_profile.xml @@ -0,0 +1,18 @@ + + + + + + + + hr.employee + hr.employee + + + + + + + + diff --git a/hr_duty_planner/views/service_aggregator_view.xml b/hr_duty_planner/views/service_aggregator_view.xml new file mode 100644 index 00000000000..ef66e055426 --- /dev/null +++ b/hr_duty_planner/views/service_aggregator_view.xml @@ -0,0 +1,38 @@ + + + + + + + + + Service Aggregator + service.aggregator + + + + + + + + + + + Service Aggregator + service.aggregator + +
+ +

+
+ + + + +
+
+
+
+ +
diff --git a/hr_duty_planner/views/service_allocate_view.xml b/hr_duty_planner/views/service_allocate_view.xml new file mode 100644 index 00000000000..96a88f78613 --- /dev/null +++ b/hr_duty_planner/views/service_allocate_view.xml @@ -0,0 +1,175 @@ + + + + + + + + + Service Allocated + service.allocate + + + +