From 4369cd6a10a549e8f61fc54ce8f61b1ab004f31c Mon Sep 17 00:00:00 2001 From: Stefano Consolaro Date: Mon, 1 Jun 2020 23:50:00 +0200 Subject: [PATCH 1/6] [12][ADD] new module hr_duty_planner --- hr_duty_planner/README.rst | 149 +++++ hr_duty_planner/TODO.rst | 56 ++ hr_duty_planner/__init__.py | 5 + hr_duty_planner/__manifest__.py | 48 ++ hr_duty_planner/data/data_service_rule.xml | 39 ++ .../data/data_service_rulefield.xml | 32 ++ hr_duty_planner/data/demo.xml | 502 +++++++++++++++++ hr_duty_planner/models/__init__.py | 5 + hr_duty_planner/models/employee_profile.py | 17 + hr_duty_planner/models/expected_equipment.py | 27 + hr_duty_planner/models/expected_skill.py | 26 + hr_duty_planner/models/expected_vehicle.py | 26 + hr_duty_planner/models/service_aggregator.py | 24 + hr_duty_planner/models/service_allocate.py | 303 ++++++++++ hr_duty_planner/models/service_container.py | 20 + hr_duty_planner/models/service_profile.py | 22 + .../models/service_profile_parameter.py | 25 + hr_duty_planner/models/service_rule.py | 143 +++++ hr_duty_planner/models/service_rule_field.py | 34 ++ hr_duty_planner/models/service_state.py | 20 + hr_duty_planner/models/service_template.py | 87 +++ hr_duty_planner/readme/CONTRIBUTORS.rst | 1 + hr_duty_planner/readme/DESCRIPTION.rst | 3 + hr_duty_planner/readme/HISTORY.rst | 4 + hr_duty_planner/readme/USAGE.rst | 51 ++ hr_duty_planner/security/ir.model.access.csv | 13 + hr_duty_planner/static/description/icon.png | Bin 0 -> 37374 bytes hr_duty_planner/static/description/index.html | 523 ++++++++++++++++++ .../static/src/css/service_planner.css | 3 + hr_duty_planner/views/employee_profile.xml | 18 + .../views/service_aggregator_view.xml | 38 ++ .../views/service_allocate_view.xml | 166 ++++++ .../views/service_container_view.xml | 21 + .../views/service_expected_view.xml | 46 ++ hr_duty_planner/views/service_menu.xml | 207 +++++++ .../views/service_profile_view.xml | 73 +++ hr_duty_planner/views/service_rule_view.xml | 84 +++ .../views/service_template_view.xml | 95 ++++ hr_duty_planner/wizards/__init__.py | 1 + hr_duty_planner/wizards/service_generate.py | 103 ++++ hr_duty_planner/wizards/service_generate.xml | 50 ++ 41 files changed, 3110 insertions(+) create mode 100644 hr_duty_planner/README.rst create mode 100644 hr_duty_planner/TODO.rst create mode 100644 hr_duty_planner/__init__.py create mode 100644 hr_duty_planner/__manifest__.py create mode 100644 hr_duty_planner/data/data_service_rule.xml create mode 100644 hr_duty_planner/data/data_service_rulefield.xml create mode 100644 hr_duty_planner/data/demo.xml create mode 100644 hr_duty_planner/models/__init__.py create mode 100644 hr_duty_planner/models/employee_profile.py create mode 100644 hr_duty_planner/models/expected_equipment.py create mode 100644 hr_duty_planner/models/expected_skill.py create mode 100644 hr_duty_planner/models/expected_vehicle.py create mode 100644 hr_duty_planner/models/service_aggregator.py create mode 100644 hr_duty_planner/models/service_allocate.py create mode 100644 hr_duty_planner/models/service_container.py create mode 100644 hr_duty_planner/models/service_profile.py create mode 100644 hr_duty_planner/models/service_profile_parameter.py create mode 100644 hr_duty_planner/models/service_rule.py create mode 100644 hr_duty_planner/models/service_rule_field.py create mode 100644 hr_duty_planner/models/service_state.py create mode 100644 hr_duty_planner/models/service_template.py create mode 100644 hr_duty_planner/readme/CONTRIBUTORS.rst create mode 100644 hr_duty_planner/readme/DESCRIPTION.rst create mode 100644 hr_duty_planner/readme/HISTORY.rst create mode 100644 hr_duty_planner/readme/USAGE.rst create mode 100644 hr_duty_planner/security/ir.model.access.csv create mode 100644 hr_duty_planner/static/description/icon.png create mode 100644 hr_duty_planner/static/description/index.html create mode 100644 hr_duty_planner/static/src/css/service_planner.css create mode 100644 hr_duty_planner/views/employee_profile.xml create mode 100644 hr_duty_planner/views/service_aggregator_view.xml create mode 100644 hr_duty_planner/views/service_allocate_view.xml create mode 100644 hr_duty_planner/views/service_container_view.xml create mode 100644 hr_duty_planner/views/service_expected_view.xml create mode 100644 hr_duty_planner/views/service_menu.xml create mode 100644 hr_duty_planner/views/service_profile_view.xml create mode 100644 hr_duty_planner/views/service_rule_view.xml create mode 100644 hr_duty_planner/views/service_template_view.xml create mode 100644 hr_duty_planner/wizards/__init__.py create mode 100644 hr_duty_planner/wizards/service_generate.py create mode 100644 hr_duty_planner/wizards/service_generate.xml diff --git a/hr_duty_planner/README.rst b/hr_duty_planner/README.rst new file mode 100644 index 00000000000..e1592c7d0f5 --- /dev/null +++ b/hr_duty_planner/README.rst @@ -0,0 +1,149 @@ +============================ +Vertical NGO - Services Plan +============================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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%2Fvertical--ngo-lightgray.png?logo=github + :target: https://github.com/OCA/vertical-ngo/tree/12.0/service_planner + :alt: OCA/vertical-ngo +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/vertical-ngo-12-0/vertical-ngo-12-0-service_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/160/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +These module manages the Service Planner. + +_TODO_ + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +Install with standard method + +Configuration +============= + +This module doesn't need configuration + +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 + + **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 Service → 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 +* Associazione PNLUG - Gruppo Odoo + +Contributors +~~~~~~~~~~~~ + +* Stefano Consolaro + +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/vertical-ngo `_ 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..7d51d890928 --- /dev/null +++ b/hr_duty_planner/TODO.rst @@ -0,0 +1,56 @@ +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 + * demo data + * FIX content + + +**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 vertical-ngo --branch 12.0 --addon-dir ~/odoo-dev/odoo12/OCA/addons-custom-sp/service_planner/ diff --git a/hr_duty_planner/__init__.py b/hr_duty_planner/__init__.py new file mode 100644 index 00000000000..071448f431c --- /dev/null +++ b/hr_duty_planner/__init__.py @@ -0,0 +1,5 @@ + +from . import ( + models, + wizards, +) diff --git a/hr_duty_planner/__manifest__.py b/hr_duty_planner/__manifest__.py new file mode 100644 index 00000000000..cdc497128c3 --- /dev/null +++ b/hr_duty_planner/__manifest__.py @@ -0,0 +1,48 @@ +# 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, " + "Associazione PNLUG - Gruppo Odoo", + "website": "https://github.com/PNLUG/vertical-ngo/", + "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', + ], + "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', + ], + "css": [ + 'static/src/css/service_planner.css', + ], + 'installable': True, +} 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..667230124de --- /dev/null +++ b/hr_duty_planner/data/data_service_rulefield.xml @@ -0,0 +1,32 @@ + + + + + + + 1 + h_max + Maximum work hours per day + 12 + 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..430d6372628 --- /dev/null +++ b/hr_duty_planner/data/demo.xml @@ -0,0 +1,502 @@ + + + + + + + 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 + 1 + 8 + + + 3 + 1 + 40 + + + 1 + 1 + 16 + + + + + Nurse professional + + + + Nurse trainee + + + + Driver professional + + + + Controller + + + + + + Fabrizio Bianchi + + + + Roberto Testa + + + + Bianca Monti + + + + Moreno Rossi + + + + Luca Verdi + + + + Dina Lampa + + + + Dario Lampa + + + + Dino Como + + + + + + Nurse + + + Driver type 'B' + + + Driver type 'C' + + + Pilot + + + Controller + + + + + + 2 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Emergency support + Non-stop emergency + + + Call center + Non-stop call center + + + Off Duty + Rest/Maintenance shifts + + + + + Call center diurnal + + 16 + 6 + + + + Call center night + + 16 + 6 + + + + Rest + + 8 + 6 + True + + + + Emergency + + 8 + 6 + + + + + + + + + + + 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..224b2194949 --- /dev/null +++ b/hr_duty_planner/models/expected_vehicle.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 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('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..e6880f7857a --- /dev/null +++ b/hr_duty_planner/models/service_allocate.py @@ -0,0 +1,303 @@ +# 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 + + +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 key (eg. to select a list of aumatic generated services) + generation_key = fields.Char('Generation Key', + 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 + + @api.model + def create(self, values): + """ + Override create function to manage next service generation + """ + new_service = super(ServiceAllocate, self).create(values) + + # generation key can be set by the automatic flow + if not new_service.generation_key: + new_service.generation_key = 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_key" : new_service.generation_key, + } + 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..1ac95d1c788 --- /dev/null +++ b/hr_duty_planner/models/service_profile.py @@ -0,0 +1,22 @@ +# 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) + # rule + + # 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..b510c0b771b --- /dev/null +++ b/hr_duty_planner/models/service_rule.py @@ -0,0 +1,143 @@ +# 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') : + for employee in service.employee_ids: + 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'): + for equipment in service.equipment_ids: + 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'): + for vehicle in service.vehicle_ids: + 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): + """ + 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['rule_name'] + # Get the method from 'self'. Default to a lambda. + method = getattr(self, rule_name, lambda: "Invalid rule") + # Call the method as we return it + + result = method() + return result + + def _rule_method_template(self): + """ + Rules definition template + @param _todo_ + @return _todo_ + """ + return 0 + + def hour_active_week(self): + """ + 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 + """ + return 42 + + 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..31e0b4689e1 --- /dev/null +++ b/hr_duty_planner/models/service_template.py @@ -0,0 +1,87 @@ +# 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') + + @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..d091f396229 --- /dev/null +++ b/hr_duty_planner/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Stefano Consolaro 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..3af8a798dd3 --- /dev/null +++ b/hr_duty_planner/readme/USAGE.rst @@ -0,0 +1,51 @@ +**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 + + **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/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 0000000000000000000000000000000000000000..cdf07108df3e4f428098a8a07f8bddfdf049cf1e GIT binary patch literal 37374 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 V@16sh8O{a{w6U2{v7tl6zW@(`5iS4# literal 0 HcmV?d00001 diff --git a/hr_duty_planner/static/description/index.html b/hr_duty_planner/static/description/index.html new file mode 100644 index 00000000000..1e827afd86f --- /dev/null +++ b/hr_duty_planner/static/description/index.html @@ -0,0 +1,523 @@ + + + + + + +Vertical NGO - Services Plan + + + +
+

Vertical NGO - Services Plan

+ + +

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

+

These module manages the Service Planner.

+

_TODO_

+

Table of contents

+ +
+

Installation

+

Install with standard method

+
+
+

Configuration

+

This module doesn’t need configuration

+
+
+

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
  • +
+
+
+
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 Service → 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
  • +
  • Associazione PNLUG - Gruppo Odoo
  • +
+
+
+

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/vertical-ngo 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/service_planner.css b/hr_duty_planner/static/src/css/service_planner.css new file mode 100644 index 00000000000..22ae2775a94 --- /dev/null +++ b/hr_duty_planner/static/src/css/service_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..e2c3df3e30c --- /dev/null +++ b/hr_duty_planner/views/service_allocate_view.xml @@ -0,0 +1,166 @@ + + + + + + + + + Service Allocated + service.allocate + + + +