From f63d2d5135c0a6e3d032082ec8bfce16c40add69 Mon Sep 17 00:00:00 2001 From: Jitendra Prajapati Date: Tue, 20 May 2014 14:59:48 +0530 Subject: [PATCH] [ADD] hr_equipment: track employees' equipment and manage its allocation --- addons/hr_equipment/__init__.py | 22 + addons/hr_equipment/__openerp__.py | 20 + .../hr_equipment/data/hr_equipment_data.xml | 87 +++ .../hr_equipment/data/hr_equipment_demo.xml | 143 +++++ addons/hr_equipment/models/__init__.py | 4 + addons/hr_equipment/models/hr_equipment.py | 350 +++++++++++ addons/hr_equipment/models/res_config.py | 47 ++ addons/hr_equipment/security/hr_equipment.xml | 48 ++ .../hr_equipment/security/ir.model.access.csv | 10 + .../hr_equipment/static/description/icon.png | Bin 0 -> 17765 bytes addons/hr_equipment/tests/__init__.py | 3 + .../hr_equipment/tests/test_hr_equipment.py | 75 +++ .../hr_equipment/views/hr_equipment_views.xml | 566 ++++++++++++++++++ .../hr_equipment/views/res_config_views.xml | 36 ++ 14 files changed, 1411 insertions(+) create mode 100644 addons/hr_equipment/__init__.py create mode 100644 addons/hr_equipment/__openerp__.py create mode 100644 addons/hr_equipment/data/hr_equipment_data.xml create mode 100644 addons/hr_equipment/data/hr_equipment_demo.xml create mode 100644 addons/hr_equipment/models/__init__.py create mode 100644 addons/hr_equipment/models/hr_equipment.py create mode 100644 addons/hr_equipment/models/res_config.py create mode 100644 addons/hr_equipment/security/hr_equipment.xml create mode 100644 addons/hr_equipment/security/ir.model.access.csv create mode 100644 addons/hr_equipment/static/description/icon.png create mode 100644 addons/hr_equipment/tests/__init__.py create mode 100644 addons/hr_equipment/tests/test_hr_equipment.py create mode 100644 addons/hr_equipment/views/hr_equipment_views.xml create mode 100644 addons/hr_equipment/views/res_config_views.xml diff --git a/addons/hr_equipment/__init__.py b/addons/hr_equipment/__init__.py new file mode 100644 index 00000000000..b0813b00fc7 --- /dev/null +++ b/addons/hr_equipment/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo (formerly OpenERP), Open Source Management Solution +# Copyright (C) 2014-Today Odoo S.A (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import models diff --git a/addons/hr_equipment/__openerp__.py b/addons/hr_equipment/__openerp__.py new file mode 100644 index 00000000000..a57a191b65e --- /dev/null +++ b/addons/hr_equipment/__openerp__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Track Employees Equipment', + 'version': '1.0', + 'description': """ + Track employees' equipment and manage its allocation """, + 'author': 'Odoo S.A.', + 'depends': ['hr'], + 'data': [ + 'security/hr_equipment.xml', + 'security/ir.model.access.csv', + 'data/hr_equipment_data.xml', + 'views/res_config_views.xml', + 'views/hr_equipment_views.xml', + ], + 'demo': ['data/hr_equipment_demo.xml'], + 'installable': True, + 'application': True, +} diff --git a/addons/hr_equipment/data/hr_equipment_data.xml b/addons/hr_equipment/data/hr_equipment_data.xml new file mode 100644 index 00000000000..9935b5731de --- /dev/null +++ b/addons/hr_equipment/data/hr_equipment_data.xml @@ -0,0 +1,87 @@ + + + + + + + New Request + + + + + In Progress + + + + + Repaired + + + + + Scrap + + + + + Cancelled + + + + + + + helpdesk + + + + + + + Request Created + hr.equipment.request + + Maintenance Request created + + + Status Changed + hr.equipment.request + + Status changed + + + + + Equipment Assigned + hr.equipment + Equipment Assigned + + + + + Maintenance Request Created + hr.equipment.category + + + category_id + + + Equipment Assigned + hr.equipment.category + + + category_id + + + + + Open HR Menu + reload + + + + + open + + + diff --git a/addons/hr_equipment/data/hr_equipment_demo.xml b/addons/hr_equipment/data/hr_equipment_demo.xml new file mode 100644 index 00000000000..f2c277933f0 --- /dev/null +++ b/addons/hr_equipment/data/hr_equipment_demo.xml @@ -0,0 +1,143 @@ + + + + + + + Computers + + + + Monitors + + 3 + + + Softwares + + + + Printers + + + + Phones + + + + + + Samsung Monitor 15" + + + + + MT/122/11112222 + NP300E5X + + + Samsung Monitor 15" + + + + + MT/125/22778837 + NP355E5X + + + Samsung Monitor 15" + + + + + MT/127/18291018 + NP355E5X + 3 + + + Acer Laptop + + + + + LP/203/19281928 + NE56R + + + Acer Laptop + + + + + LP/205/12928291 + V5131 + + + HP Laptop + + + + + LP/303/28292090 + 17-j059nr + + + HP Laptop + + + + + LP/305/17281718 + + + HP Inkjet printer + + + PR/011/2928191889 + + + + + + Resolution is bad + + + + + 7 + + + + Some keys are not working + + + + + + + + Motherboard failed + + + + + + + + Battery drains fast + + + + + + + + Touchpad not working + + + + + + + + diff --git a/addons/hr_equipment/models/__init__.py b/addons/hr_equipment/models/__init__.py new file mode 100644 index 00000000000..96f730a43a9 --- /dev/null +++ b/addons/hr_equipment/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +import hr_equipment +import res_config diff --git a/addons/hr_equipment/models/hr_equipment.py b/addons/hr_equipment/models/hr_equipment.py new file mode 100644 index 00000000000..929a35532f0 --- /dev/null +++ b/addons/hr_equipment/models/hr_equipment.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- + +from openerp import api, fields, models, fields +from openerp import _, tools +from openerp.exceptions import UserError + + +class HrEquipmentStage(models.Model): + """ Model for case stages. This models the main stages of a Maintenance Request management flow. """ + + _name = 'hr.equipment.stage' + _description = 'Equipment Stage' + _order = 'sequence, id' + + name = fields.Char('Name', required=True, translate=True) + sequence = fields.Integer('Sequence', default=20) + fold = fields.Boolean('Folded in Kanban View') + + +class HrEquipmentCategory(models.Model): + _name = 'hr.equipment.category' + _inherits = {"mail.alias": "alias_id"} + _inherit = ['mail.thread'] + _description = 'Equipment Category' + + @api.one + @api.depends('equipment_ids') + def _compute_fold(self): + self.fold = False if self.equipment_count else True + + name = fields.Char('Category Name', required=True, translate=True) + user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.uid) + color = fields.Integer('Color Index') + note = fields.Text('Comments', translate=True) + equipment_ids = fields.One2many('hr.equipment', 'category_id', string='Equipments', copy=False) + equipment_count = fields.Integer(string="Equipment", compute='_compute_equipment_count') + maintenance_ids = fields.One2many('hr.equipment.request', 'category_id', copy=False, domain=[('stage_id.fold', '=', False)]) + maintenance_count = fields.Integer(string="Maintenance", compute='_compute_maintenance_count') + alias_id = fields.Many2one( + 'mail.alias', 'Alias', ondelete='cascade', required=True, + help="Email alias for this equipment category. New emails will automatically " + "create new maintenance request for this equipment category.") + fold = fields.Boolean(string='Folded in Kanban View', compute='_compute_fold', store=True) + + @api.multi + def _compute_equipment_count(self): + equipment_data = self.env['hr.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) + mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data]) + for category in self: + category.equipment_count = mapped_data.get(category.id, 0) + + @api.multi + def _compute_maintenance_count(self): + maintenance_data = self.env['hr.equipment.request'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) + mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in maintenance_data]) + for category in self: + category.maintenance_count = mapped_data.get(category.id, 0) + + @api.model + def create(self, vals): + self = self.with_context(alias_model_name='hr.equipment.request', alias_parent_model_name=self._name) + category_id = super(HrEquipmentCategory, self).create(vals) + category_id.alias_id.write({'alias_parent_thread_id': category_id.id, 'alias_defaults': {'category_id': category_id.id}}) + return category_id + + @api.multi + def unlink(self): + for category in self: + if category.equipment_ids or category.maintenance_ids: + raise UserError(_("You cannot delete an equipment category containing equipments or maintenance requests.")) + res = super(HrEquipmentCategory, self).unlink() + return res + + +class HrEquipment(models.Model): + _name = 'hr.equipment' + _inherit = ['mail.thread'] + _description = 'Equipment' + + @api.multi + def _track_subtype(self, init_values): + self.ensure_one() + if ('employee_id' in init_values and self.employee_id) or ('department_id' in init_values and self.department_id): + return 'hr_equipment.mt_mat_assign' + return super(HrEquipment, self)._track_subtype(init_values) + + @api.multi + def name_get(self): + result = [] + for record in self: + if record.name and record.serial_no: + result.append((record.id, record.name + '/' + record.serial_no)) + if record.name and not record.serial_no: + result.append((record.id, record.name)) + return result + + @api.model + def name_search(self, name, args=None, operator='ilike', limit=100): + args = args or [] + recs = self.browse() + if name: + recs = self.search([('name', '=', name)] + args, limit=limit) + if not recs: + recs = self.search([('name', operator, name)] + args, limit=limit) + return recs.name_get() + + name = fields.Char('Name', required=True, translate=True) + user_id = fields.Many2one('res.users', string='Technician', track_visibility='onchange') + employee_id = fields.Many2one('hr.employee', string='Assigned to Employee', track_visibility='onchange') + department_id = fields.Many2one('hr.department', string='Assigned to Department', track_visibility='onchange') + category_id = fields.Many2one('hr.equipment.category', string='Equipment Category', track_visibility='onchange') + partner_id = fields.Many2one('res.partner', string='Supplier', domain="[('supplier', '=', 1)]") + model = fields.Char('Model') + serial_no = fields.Char('Serial Number', copy=False) + assign_date = fields.Date('Assigned Date', track_visibility='onchange') + cost = fields.Float('Cost') + note = fields.Text('Note') + color = fields.Integer('Color Index') + scrap_date = fields.Date('Scrap Date') + equipment_assign_to = fields.Selection( + [('department', 'By Department'), ('employee', 'By Employee')], + string='Allocate To', + help='By Employee: equipment assigned to individual Employee, By Department: equipment assigned to group of employees in department', + required=True, + default='employee') + maintenance_ids = fields.One2many('hr.equipment.request', 'equipment_id', domain=[('stage_id.fold', '=', False)]) + maintenance_count = fields.Integer(compute='_compute_maintenance_count', string="Maintenance") + + @api.one + @api.depends('maintenance_ids') + def _compute_maintenance_count(self): + self.maintenance_count = len(self.maintenance_ids) + + @api.onchange('equipment_assign_to') + def _onchange_equipment_assign_to(self): + if self.equipment_assign_to == 'employee': + self.department_id = False + if self.equipment_assign_to == 'department': + self.employee_id = False + self.assign_date = fields.Date.context_today(self) + + @api.onchange('category_id') + def _onchange_category_id(self): + self.user_id = self.category_id.user_id + + _sql_constraints = [ + ('serial_no', 'unique(serial_no)', "The serial number of this equipment must be unique !"), + ] + + @api.model + def create(self, vals): + equipment = super(HrEquipment, self).create(vals) + # subscribe employee or department manager when equipment assign to him. + user_ids = [] + if equipment.employee_id and equipment.employee_id.user_id: + user_ids.append(equipment.employee_id.user_id.id) + if equipment.department_id and equipment.department_id.manager_id and equipment.department_id.manager_id.user_id: + user_ids.append(equipment.department_id.manager_id.user_id.id) + if user_ids: + equipment.message_subscribe_users(user_ids=user_ids) + return equipment + + @api.multi + def write(self, vals): + user_ids = [] + # subscribe employee or department manager when equipment assign to employee or department. + if vals.get('employee_id'): + user_id = self.env['hr.employee'].browse(vals['employee_id'])['user_id'] + if user_id: + user_ids.append(user_id.id) + if vals.get('department_id'): + department = self.env['hr.department'].browse(vals['department_id']) + if department and department.manager_id and department.manager_id.user_id: + user_ids.append(department.manager_id.user_id.id) + if user_ids: + self.message_subscribe_users(user_ids=user_ids) + return super(HrEquipment, self).write(vals) + + @api.multi + def _read_group_category_ids(self, domain, read_group_order=None, access_rights_uid=None): + """ Read group customization in order to display all the category in the + kanban view, even if they are empty + """ + category_obj = self.env['hr.equipment.category'] + order = category_obj._order + access_rights_uid = access_rights_uid or self._uid + if read_group_order == 'category_id desc': + order = '%s desc' % order + + category_ids = category_obj._search([], order=order, access_rights_uid=access_rights_uid) + result = [category.name_get()[0] for category in category_obj.browse(category_ids)] + # restore order of the search + result.sort(lambda x, y: cmp(category_ids.index(x[0]), category_ids.index(y[0]))) + + fold = {} + for category in category_obj.browse(category_ids): + fold[category.id] = category.fold + return result, fold + + _group_by_full = { + 'category_id': _read_group_category_ids + } + + +class HrEquipmentRequest(models.Model): + _name = 'hr.equipment.request' + _inherit = ['mail.thread'] + _description = 'Equipment Request' + + @api.returns('self') + def _default_employee_get(self): + return self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + + @api.returns('self') + def _default_stage(self): + return self.env['hr.equipment.stage'].search([], limit=1) + + @api.multi + def _track_subtype(self, init_values): + self.ensure_one() + if 'stage_id' in init_values and self.stage_id.sequence <= 1: + return 'hr_equipment.mt_req_created' + elif 'stage_id' in init_values and self.stage_id.sequence > 1: + return 'hr_equipment.mt_req_status' + return super(HrEquipmentRequest, self)._track_subtype(init_values) + + name = fields.Char('Subjects', required=True) + description = fields.Text('Description') + request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today) + employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee_get) + department_id = fields.Many2one('hr.department', string='Department') + category_id = fields.Many2one('hr.equipment.category', string='Category') + equipment_id = fields.Many2one('hr.equipment', string='Equipment', select=True) + user_id = fields.Many2one('res.users', string='Assigned to', track_visibility='onchange') + stage_id = fields.Many2one('hr.equipment.stage', string='Stage', track_visibility='onchange', default=_default_stage) + priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority') + color = fields.Integer('Color Index') + close_date = fields.Date('Close Date') + kanban_state = fields.Selection([('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], + string='Kanban State', required=True, default='normal', track_visibility='onchange') + + @api.onchange('employee_id', 'department_id') + def onchange_department_or_employee_id(self): + domain = [] + if self.department_id: + domain = [('department_id', '=', self.department_id.id)] + if self.employee_id and self.department_id: + domain = ['|'] + domain + if self.employee_id: + domain = domain + ['|', ('employee_id', '=', self.employee_id.id), ('employee_id', '=', None)] + equipment = self.env['hr.equipment'].search(domain, limit=2) + if len(equipment) == 1: + self.equipment_id = equipment + return {'domain': {'equipment_id': domain}} + + @api.onchange('equipment_id') + def onchange_equipment_id(self): + self.user_id = self.equipment_id.user_id if self.equipment_id.user_id else self.equipment_id.category_id.user_id + self.category_id = self.equipment_id.category_id + + @api.onchange('category_id') + def onchange_category_id(self): + if not self.user_id or not self.equipment_id or (self.user_id and not self.equipment_id.user_id): + self.user_id = self.category_id.user_id + + @api.model + def create(self, vals): + # context: no_log, because subtype already handle this + self = self.with_context(mail_create_nolog=True) + result = super(HrEquipmentRequest, self).create(vals) + if result.employee_id.user_id: + result.message_subscribe_users(user_ids=[result.employee_id.user_id.id]) + return result + + @api.multi + def write(self, vals): + # Overridden to reset the kanban_state to normal whenever + # the stage (stage_id) of the Maintenance Request changes. + if vals and 'kanban_state' not in vals and 'stage_id' in vals: + new_stage = vals.get('stage_id') + vals_reset_kstate = dict(vals, kanban_state='normal') + for equipment_request in self: + write_vals = vals_reset_kstate if equipment_request.stage_id.id != new_stage else vals + equipment_request.write(write_vals) + if vals.get('employee_id'): + user = False + employee = self.env['hr.employee'].browse(vals['employee_id']) + if employee: + user = employee.user + if user: + self.message_subscribe_users(user_ids=[user.id]) + return super(HrEquipmentRequest, self).write(vals) + + @api.multi + def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None): + """ Read group customization in order to display all the stages in the + kanban view, even if they are empty + """ + stage_obj = self.env['hr.equipment.stage'] + order = stage_obj._order + access_rights_uid = access_rights_uid or self._uid + + if read_group_order == 'stage_id desc': + order = '%s desc' % order + + stage_ids = stage_obj._search([], order=order, access_rights_uid=access_rights_uid) + result = [stage.name_get()[0] for stage in stage_obj.browse(stage_ids)] + + # restore order of the search + result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0]))) + + fold = {} + for stage in stage_obj.browse(stage_ids): + fold[stage.id] = stage.fold or False + return result, fold + + _group_by_full = { + 'stage_id': _read_group_stage_ids + } + + @api.model + def get_empty_list_help(self, help): + res = super(HrEquipmentRequest, self).get_empty_list_help(help) + alias_record = self.env.ref('hr_equipment.mail_alias_equipment') + check_alias_manage = self.env['ir.values'].get_default('hr.config.settings', 'alias_manage') + if alias_record and alias_record.alias_domain and alias_record.alias_name and res and check_alias_manage: + return _("""

+ Click to add a new maintenance request or send an email to: %(alias_name)s +

+

+ Follow the process of the request and communicate with the collaborator. +

""" + ) % {'alias_name': alias_record.alias_name + '@' + alias_record.alias_domain} + return res + + @api.model + def message_new(self, msg, custom_values=None): + """ Overrides mail_thread message_new that is called by the mailgateway + through message_process. + This override updates the document according to the email. + """ + if custom_values is None: + custom_values = {} + email = tools.email_split(msg.get('from')) and tools.email_split(msg.get('from'))[0] or False + user = self.env['res.users'].search([('login', '=', email)], limit=1) + if user: + employee = self.env['hr.employee'].search([('user_id', '=', user.id)], limit=1) + if employee: + custom_values['employee_id'] = employee and employee[0].id + return super(HrEquipmentRequest, self).message_new(msg, custom_values=custom_values) diff --git a/addons/hr_equipment/models/res_config.py b/addons/hr_equipment/models/res_config.py new file mode 100644 index 00000000000..cc7869124ee --- /dev/null +++ b/addons/hr_equipment/models/res_config.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +from urlparse import urlsplit +from openerp import api, fields, models + + +class HrEquipmentConfigSettings(models.TransientModel): + _name = 'hr.equipment.config.settings' + _inherit = 'res.config.settings' + + alias_manage = fields.Boolean('Alias Manage') + equipment_alias_prefix = fields.Char('Use the following alias to report internal equipment issue') + alias_domain = fields.Char("Alias Domain") + + @api.multi + def get_default_alias_equipment(self): + alias_name = False + alias_id = self.env.ref('hr_equipment.mail_alias_equipment') + if alias_id: + alias_name = alias_id.alias_name + return {'equipment_alias_prefix': alias_name} + + @api.multi + def set_default_alias_equipment(self): + for record in self: + default_equipment_alias_prefix = record.get_default_alias_equipment()['equipment_alias_prefix'] + if record.equipment_alias_prefix != default_equipment_alias_prefix: + alias_id = self.env.ref('hr_equipment.mail_alias_equipment') + if alias_id: + alias_id.write({'alias_name': record.equipment_alias_prefix}) + return True + + @api.multi + def get_default_alias_domain(self): + alias_domain = self.env['ir.config_parameter'].get_param("mail.catchall.domain") + if not alias_domain: + domain = self.env["ir.config_parameter"].get_param("web.base.url") + try: + alias_domain = urlsplit(domain).netloc.split(':')[0] + except Exception: + pass + return {'alias_domain': alias_domain} + + @api.multi + def set_default_alias_manage(self): + config_value = self.alias_manage + self.env['ir.values'].set_default('hr.config.settings', 'alias_manage', config_value) diff --git a/addons/hr_equipment/security/hr_equipment.xml b/addons/hr_equipment/security/hr_equipment.xml new file mode 100644 index 00000000000..dc0d86a2c82 --- /dev/null +++ b/addons/hr_equipment/security/hr_equipment.xml @@ -0,0 +1,48 @@ + + + + + Equipment Manager + + + The user will be able to manage equipments. + + + + + + + + + + Employee is allowed to see its own maintenance requests + + ['|', ('user_id.id', '=', user.id), ('employee_id.user_id.id', '=', user.id)] + + + + + Employee is allowed to see its own equipments + + [('employee_id.user_id.id', '=', user.id)] + + + + + Administrator of maintenance requests + + [(1, '=', 1)] + + + + + Equipments administrator + + [(1, '=', 1)] + + + + diff --git a/addons/hr_equipment/security/ir.model.access.csv b/addons/hr_equipment/security/ir.model.access.csv new file mode 100644 index 00000000000..3748c9f79bb --- /dev/null +++ b/addons/hr_equipment/security/ir.model.access.csv @@ -0,0 +1,10 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_equipment_user,hr.equipment.user,model_hr_equipment,base.group_hr_user,1,1,1,1 +access_hr_equipment_emp,hr.equipment.emp,model_hr_equipment,base.group_user,1,0,0,0 +access_hr_equipment_admin_user,hr.equipment.admin.user,model_hr_equipment,base.group_equipment_manager,1,1,1,1 +access_hr_maintenance_user,hr.equipment.request user,model_hr_equipment_request,base.group_hr_user,1,1,1,1 +access_hr_maintenance_system_user,hr.equipment.request system user,model_hr_equipment_request,base.group_user,1,1,1,1 +access_hr_equipment_category_user,hr.equipment.category.user,model_hr_equipment_category,base.group_hr_user,1,1,1,1 +access_maintenance_request_stage_user,hr.equipment.stage.user,model_hr_equipment_stage,base.group_hr_user,1,1,1,1 +access_hr_equipment_category_employee,hr.equipment.category.employee,model_hr_equipment_category,base.group_user,1,0,0,0 +access_maintenance_request_stage_employee,hr.equipment.stage.employee,model_hr_equipment_stage,base.group_user,1,0,0,0 diff --git a/addons/hr_equipment/static/description/icon.png b/addons/hr_equipment/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..53055cf085c21fcfa8908db58d7d9d73f9d93516 GIT binary patch literal 17765 zcmV)HK)t_-P)6UbZiiXZ zgWGmHwA=16(>Bn!4Yt9=U<^nmC{{_Os*)&r zwO0GA_1DSg-P(T+``@Is#y@WV{eqjnP#zeA=hx9~EyF3*;8%vAWsE3nBknX%8{LB_ zY$0qdqB^=4ad#bYXAQz}02RS?OK^Q3o?nJ=TrAC<13>235e^*JhvRwhTpwPkg7wBS z*4LKcdLD#u&2z5n!S#H2ehEPkpxthPh)~$H1_`&e{npohUv*^n#M;bLBi-iWrOxW* z|B5YaS}O$2d9zQq>gXQ$)gi22dJ&pqHVGwTq+8H# zVBej0-a0-pj!vV4&hipme?SauzwKw{kAL<7;rg3Hri!-s|I)wvVDr&avqcbj^N0BE2w zdlBXO04hVHP%0q;O%fWhXv|+gr@4j=t_QTlz_vqgUOD%T7liAJPmpjoM*v&@VHn(g zXj25BwSwzcZ@A;-KmCEF%g5h4{nV#_XJz)uKO;@;>y7}%_aFA}y#JlQ^2(q5g`a~4 z9{r22pt(8^e_--WOVi(Yx02!Cezye}S@FL#{S3zT-H7tg2(*-d($G>v8F36~E1KO> ztvbB(&Ualo{+a&;-x5KV!nU||8}kS@pfosT#C!jZAma#10Eb`r)|+=8xc5IkbM$Y1 z^qFt|*}IespAn9C{Sd%Q-~Kaucir;hmqbk+`4n)%wDj z-^O>{c_4$Ql?&4t*-I!73_)w1A}=}C_E^Rk+;RI4uUtCv?^PU}ClY}!a#zcWC#+DH?aNCzt<{0usU<>m$Xvd?=E z!mA8T4DY)0osG*+{8}$tfJh+htfqvsW#~2+v2yXKu~W3xeTmHRO4IGu(t51oI>8VG z_TG5kr0dr{{Ebh%?*}`p7rzMMTzdl0T56EeBmfergjCRm17m}L!R@#H3&*eB*jc;$ z6I!d;@4Ph72`CM65^i6=GK13aAOJ*KYiNxuB9+3x~k0&9cPSm{e^w&E#t=(NMkEE|cX^GtjUN*J- z!ngg_@vr>;_ZiVZbnOU0h7wvTuvTO4(kh15G9iRGgyU5XkM6zai;bD5eyZD;`8YwQ zht90xh$AzgNSM_kUqY(L7*|?GP;u81f&x@LQK}4lNOmiKO^~s0{9(uS z2881dIIdgYw(~%l$f*iPR4E|_hsba=1PO>?2ae+=oJJ0WW8w}5hy;iT!u24?UK8e; z89*6!Q6Csb)Q!{Vg&l2xtT9*}-u0r|_<=VzW}o^fO99r+S5*yK;@BHbX zIsSz||Kklpn-z}JQ@_`ScEAxXx{XDa7RBe#yz
    uZma(ekwDE70C5;WsSq-3K?kcKXoIM#Cjf+?vRfP7ck7RNo)6C} z!7G(bDA#pdIId@qRA5$ZM6`^$D3xjm+m;x0`Udzp z5*Xe0(*HRQS|7E@VFR$eilDu0jv4m>>JvBK{+jpv+)tO?$=dkd+wYq>@s*FLqKY%d zW~n-YeRsSGmrs5PgJTEKYR*Ckua_rqzO{VuNT<7Y-(YnTI=!v{MG})c@3>`f;)Yju z8dp9c9QRrg06}ZYNHQ6RP;m#UyKImTk;Low9k>;KschbHTsHy7F+e1W1HdPMwg>{j z%5D~)U}S3KyetAg16d#p5Seq*)^n60sv~<|?N^6i8#L#?z=0uv90#?reHjAiHXFG6 zhkoKclY1uz=AHt!-}s8}yK?&BkNB0r0zzHC3epN%#zy?_zv~CF;oGC#xMGMtzeU}3 zy*RbM_6bA-oa> zwjKVN*3!8zq*8#rfH1?+ZmBwY_dj{#|Il6pWDHCly!Rz4`pt46gKkPFO2sJG#xSwx zMOeJ>h(VwMrQrEB1kDBX+V&oN)&6>Q!bqP)YU$LKHBxe9>d?LCpZuEv88n+9x)u^Z zTPoo3_KaQ@AW?<;>|hSmFfuxfYPAaChy=)ra2|WcH}NqM82O9`*4>$V!W;-BEKLwu z9Ulvndo&i)0 zjPHKIP4)2`ZjOWI(UhxY+{MnDUjwf^WQYbtoSh^Gj$5|b%M`TnoL+SQ`~JhtFL}pL zhV5nZUh=!s@lMoQtv0#cEf4L|GCDydzWW5A;z(+#qLc;*ggELN<8N$y1bcREGl+Lw z0V0cdK_F1Uv?l5!0Emw%d!xs@1cJ_ys(=vLC?NH)Xk4%25AJxuyRRJk+lNzQkVu%c zuU5*6|26Et=VdQC3O2`Pv_s zD#IIk{;i4FSMgO{KC-m}u2(SxKm?3T9sYOQZ+PV|MnNmdW@?!rK8;{%YX?982|JT&Pm;aKC+a|DNcuOg^lTtNc zKqV?m%23M)AaQMsKrlcnjglu&DVNach5)RtCm!qq;&k?!X3IZCDjzz-u3h`kE?E)jwMMtZI>y5|)?)>^#qyIO^F5H56U2~m5^+!L-Y3C}MZ=2n1G zi7ME(?{_)o`{?;}Y7tpQgl7pP^A4ZujLS#N$OSt? zr@B-#;t9ltrcqN41i41E5|!a^DE0c-;U8H(|K;BnZq+>R_!TIr;CN*hB-}U#q~b_; zZFJ0eb&yeFiq2l1NwfSd`xTX3@8F!cC?z%y08lM>3anG;rx%K|{ zsW?Dv42l?9NegLMT_$@23Lruh1@ZdgnF}iJg077O$cOE<5&2ORfrJ84DgkoBwg>2p z38&eVV}YFu`e-%~*{9?0nCO|ZcZuzebT1rZpgF<^-~rkN%&`y0DM2_jjP85YPi_#v zpTtpfok$o1SI1q%3MvX>rH24zkq=>2HXM`(M~BMw(R;@C+-1f%*UCIzL4pO8izgt}JIksctqSINMYt9`% zqh)jr2!MGoO9)_hB@NGl$n0~LH;eV0Do{rECB}|Qk);%*V&>#~|-1wke2F8Tm@AmKv@-v(yL==$2|4Yv(U-TCIFb6@%(k%#*D zKGeqcBI*RI%a<4CM|Wbotv4|s0Vr5PkSZY<6+?I>Q61T}vo^AO=hO|~^V+bphFynW z8#r#+S2Bc*667mG0I>ywbZaL_K=V0-k5*&$gkK(7P9lMH?IZy5$hX|r>9!GcyAZ2qgpd>QVr$;Iq=fcHwN1feocEqm(*mP=AbP?kt^PV`$sHw-}r@et z1T@I0Liv-3y(y444ie==LX<|JI~O4$4+8@My4|jIn^IeaF-5|IfNyc0Q&5%|Rop>s z?3P!I?fbs_yNwHo?9UPr84s{$eRT0~L9B%;NSB$~7E-QFq^)Mo*f>2kmTOP-u@)by3hExPVfnP`F zh(PwEc2C5tZ0!(0!ttsY-u2S=Tt0I0lfp?=1aSVyS3Wg&dggtV`VKE_rS3X^LK!3) zuw2iaZN$Bf?KNrCMXCaz*#_V-v*KUIaI4#}a_)f>?WJ=M2;uSoT}v+j3Y=xLu2c{N zSY2I3x6^@e2rL?z8vzs%4GqRiv0y}#0O>?}#3*4N6Hu0o1LYgem0Cc9SQPUC*pbwn zTmt2iP-*OmjGH#N?M3%5)<665&g%3(xaA>~hqfUOx{sgy`@j48OWyvDS88xTh=Gx( zSe!GW?OvyPU&qNilIDB0Zr$y{z#08i4eSMzxd z&MPosCE}w3m}lFd=A*Tt+&i zMY?7+z~^cMQrm+e^#Vv~i24*3&V6Zl`NAU~)G|P<;%i3$*2A+ZAi*L>s|2DbMi_?X znv6AK89}C^E)jDENm9m$<2X51=G@+kA$1*Xdq;{d)F`zUBQ67h)l9`gWZ_f_w zof<+MtU{|W8zR*zCr$V_vtL3snM7$oAt@+{8mNvRe2ZHdy)9}j9D@*0B(Zq@n;&`j zum96e-}}SAe4lUu*){mGoUJYAGr|>T1I<2_1e{dUsr$zg%iN3M)%Kv#ynvZwAOGDr zXr2^a={lJj#t}L;+JrV1coaouF5smLXL0<=hmC;;800fj5`&Oq1|;AJ0pYmjT|vTl z0YV7t5qR(%2aYG!#nT#$+?rC{$HVBQpcj?tMdboR*zcAqqX5yhCID^4K3HK>I;aF0#|B6U43CWC z_PbsR*CN%HdD--^O&A$)5)S~JKq3Rkq)gKdY1I%#2GMcULL7uCk~!B;n(dFAwXYf3 z*^sr3)yuenI9vlEz~J`7KeBr1q2HEqcu_b>1ygJJ!WWMJ-S2-e7TbPoa_?Ozi4=h` zl;oHOTlO5@Wm-(!2hGyJ<}j5Z_-?Rj7~4a@U0%~Ga-}1#l@^*CcclN4Z)TQzi&l$}VKwgjQV; z1#mYKu!Gz0ct_*PvEL-&CZ%suVe#D8|E2rj@7%t=^!7U@_P!W{0~Kq@NE-*x?B7E{ zW%41c2FQ+&ksfx=CH$J1e>r>Ni$B|5I{zs3C;_{6p!*WlkfJ(DxQ?{ooB@X$8QAViT|djA5>f_GvIQ->AQ1tG0Zph+-tbS?W{&)! zii1^vtXdOujoHWFPKZC-U7g-Lu1 zNkJv5B!I?~)v^5`x=v;Wu@MDJy&Q2P6Zqk%U+PxzYZ}BS%!P#_5ax6?=(4Y|{mJ#d z#L|grBQNqfEzD9!sidk|1yHhUINuZ^Ls?>%@aq#dRY&%`t#Re}ADenSo&I3=WOM%1 z_eJgHj|3~vyl8FU0IDMgQK=83QYphXHIM)Yf?~i_PDjSoRHm3F1i?l`1p(xwy$!rl zeR5Pro$H7IG}mNm%|({BXp{X|MJyfIk>fl76A{}KU_Y8jaGpRZHh%Kw0AeYvWFU0I z0QJFP0HuVpc~%f34U`~W#!x!8+!h!_XvAl&KxYeCFaPL2X7qroyn}&)*APw)@oH7Yvie-gc0Ze{F zCtg!hSuHBVyT+7B{LuA508&b&v{ngT730N*e5I0@Uz~?;-4)G-SxO^j08-1J5l#Td z6*wdeo!+-ZllskMu5s0{h9F_wMqzaXOCZG&O9Du^&?-VKJEmv^Dl(i&Lc?>3J(kSG zQ5Dx2O>j4-9nn#lR$>fRzkw9{d)weR|LBJn^@JABlMXNy6fY2d2 zofecP)>JHY*y<-23Q;2NW!pUKxQTZHZ8CCMzk@WiCPQ*M4jogCV~ICvufiR!0U$$I zN=6U2RxZ7@DTDp4SAE09hd%SmdtdrvJDutz%ASOV;3OE35;dzSkjcB@y8~tH@*y zwT`iT{_7uJKJ&F-9NPKfZSm?l9ILjN{kcAZ4C09?P$`saBt6cNseg1g#}g4sPoqlfYEeHe3$i@`Z=~>@RP5<4gYq z6v1^87g8$$fIF8U>z{xi41@W#@on{qL#B%O`XB(VR>{TI6R9pE6T3GTq_T!u`W){- zhB%lg3j9}y4ag*}pcRDfVrOs?%0|-0oXgN~QtLWFa|Z94zi9FfZ3v11C@v6XqU-^j zDaz2SttgIzb-3kWGci_%&16EW>zjLvr@r{X#WN57%)r37>v~;iTQ#L}0Sy9IQYGJ& zs8mOH#O;EeqLB(yjK8%Pijw!#G4NCO`GSNFBLug_91+dkQ zIBb4%_KCmvg1YHVZ=gy9uUrQ?eiok^Oi<|!)6b|r*_b;qtm4ibk#ikI0=3E*fn@z` zrL^HxZNw`_^zuM!^)2VcOVh7;UaYB3J|U)@puv(0D^_| zlPUrzPNi87+-8HulqQUbZ!$3V#@)4|p0Lx=?X zUO9cbB=+Cn>JD_L38z$r=T|rO6*z7Q!uCN(K`RB1`d>!2pO@EV=z9VypJF|34k+dl)9Nv02~mCDgavBNvei`xf&p2 z{N>&wo&O=!x4`|XMkqnaV70w`?5_ug_y3ZNB9khXaR?c7-~<)8r48^aaBKigBvy~P zrsMA3#ZzB~aQ*8*1t43rP~SIB7?H8n9bA{GMeZ@*bnOH^#(k~mS z;CQ|XdZj)=xjuQ292bsPxgKl)Vgzeh7^qbIYHg9A>@8BZ4utZ0pLM)N9u>K$Xgk>V z1qh1x8-fH&YGQ;kDCTU|YRH}tKcAhX(w`(qz$*>)EZWd2e5l=+eqgYE=c_6rhO`W1 zs+}w$I8g_VS2jS73*~iCAKkZqXy;uV47zp%z{uk1OFDy`C=AfnB8hiX)>BM8-$b_& z<;5^>AQ>=jzbVS-42FEMnRqj#yevORDYg`e;xilcvqbifB z5`wVaT7~N-?cX(Y8uvGQgA0{j<6sI%*hs3jF$Dr3ASl}tK)5VLm=JgtQmI;o@0YAC z0#;K{oDn1rNa!A_MfcVL)W55^J+ODpx#*oZ?mTup{}*@#vV5J}@& zh8l4F>Ymo}xd|DzE)!kbK7qEk9v;J}Aluc8P~0f%@@2A#)(`Fjm{1jr&VMz2kU>(lME(_e~)Cg18hQED7#I$vv^8izP& z!!HdEZ}c|aA9Yu+2Lg~#T;GP=Y<^qvEI@)dD_2A!ZyYON!@D1fH1B$M2?Ge6UCT~p zw46`)&``ZAl8cFrOFD2S5Pycmy>Yle&%;-`^DiJ6T#t%oPZGl_Gr@pw`JsrDjmN}m^Lc9YOA zU_*|JaQ)JkI<1BIf%4R(BNQ}z2Y24))yCoawQC&*PyiDFz-8AxiGcE{ zV19-X%7P#l{_9aH#(VordWAJGWs%Px;DKgj7afo6dz%jo_ZJdGP;qCr(>VL3cy!0x zU6RnLWD-;uKQbA|4(cQOZZ)0(ySdl8O`NuQYrd8lSS|~ME+Q%eR7?@_Ic3hTsvh_m zzE6~!xm46I4AcYZMR#!1OWE{S6F__Q)F->qOWs~~DBI$QicF-U0F~*g60xhA-^2;h;#t;`&srcw-GS_U_970i9`4lzLg0^X zN(B)0xlagNGhgns78eIg+r|ZPSRBQmxrq;W-tgqep1W^~x~tcg0HUZJD;Y*QRRaW2 z>d$jMUvf+YVua_}gi%X*4X^w35lLx)iYg?_M4T1O#4B<`-IHhvM&XvE0D=14bfDwz z<#yxz1M$fAx0FQ8VR3$sy#Yu#!gETc!Mm2ue)Vh4cd$5wgoFMnKuWEZk{k-oO&2Gw z7G@^p(rn7-K-tgFB9wK1QC098eH}Hr>P!~e6f%jcp7WU^+Kd5N3yW5!ld=TC9 zmXf2>97ax3Fyp8=Kz(HYJ$`-Lcc40m*L~=&FPMFA6#!u;vnUR(WV(227Q8F*!W;3YX%f?5&_YD6Pgud0^tE z+StJxqR#45B%J3<=0gVSW}(k5Rt0OW39z_hw(eya6N~j8oX?Q!71qNj>`5w`$t8Yb7xw zU1YS?>ES%{m64Q5^%fCC<{3*P#3)U#+be}b)+@;_xdk$znwu2!uIS}x2(92h5kNlVC#}3fwuFV=e zZm}ecyI1oN@;{nbh+CDP6%Ga=lwN1eM*!oSfP(#n{DuBpN|b97m9YaLnq>yHCzyC#V`{Bd-M5KKzSgE@v&)x7DX=ai&X9%oGT~J*%-8i0gk$B zh=V2^$A|7u0O+n?dLWGNerw6GV@fuL!eTR>sSH=RUTt{KD`u}e_2{=(Ad-}^N1e5A zw;q6J*FwwuFc(>wi6U$KaYTo=O&VoI9HUqqLaeS}_x3dlOoYYIge6?AEQY|QbHboB zZ4L>qGHMDw0gK#XTo^J~`DVAhzWsA>Dch(;aAO`{LJqrE*ouG^sBIA zh~V3v7y_kO6=Vj!h(ao^2~-rbnI&BE!5AKdVjz)|MDD(J49lY_N`E-C0@2%1~)bQ>5L-uE)MRNpS+ zV0KG(0FkLflu`S6sR}O43{Fq`N^Krg9&C&0ycbYBr95>MpXG=nXMnlqKFHo<3IHEx z3{cHB#`fO( z^2Xe;j|#`%lJ4)U%|5qWfFJ=PVUe%0IL)Ao`|#@?x&8cDb~*usAo!jK5+cJ1R?cKT z&u#;AzeYY1$wE^7|XZ$ zPCjd?i#-%&h=GxPuV1|IwU2Jz9V9?WMD6A0UKXyoClEx&jL&)h8QRr3L{f%n;yA{e z-~9d9zI_58{Lo*a(+*5sA%LrkXv%WC25}uAQE_IlJtxW;ij=A8XCl*?qEU9s?IB%t zTon3jw)zSiMQd4fA;`(@7ak{&z!YA z{{(-NXCboKMG6D%759A(SPBlZS3e4Kv2KXSLQs`!juQJniGlH)xU1kE{B zuTBY4tlo-tu+ib=!KwWN<2SynG56GGNVuDdO~|12{4|ROa}-yzWCd)YCs5DkZ?;W* zr_;s%`==km`ExU-)2OoT(*y^IGJdhJh))-bW18ZqBor4|@(iF}1*+@~fQXGxE;;8g zWYOr`QW?4F3Z@nO$_R)=8d+V4I!h;|o|+QHx+acmkbv36Ax5TN@W$1dqn{B@>07&{ zL4yhIIoSZ3n?dD8GfPq=b@zQzdW=dy2!Tekh0lEMLDXtxG#X7PNpL7FvLst3&PZeg zX(+QNrb`ytX`8kTAri5nhg^LDrzb3dQ#Ob3T#(Pei&_b<444+VUEOWX9EsFz-|IWs z+(}BLEcyhZ_6kO~zuGPO@ek1|vm#~Q0E ztz;b^k$M{hvfOMHKO+gOr&+RSP(EsD?km2NC0gb#hzh>4Ie+4BdrASKq!pc#&kF%?UqIUqu8<0% z9x2;g)lQKFc;Sfje!6EuV)uyerA8sAc{Do#oGenwMMB1<%a^1V=|k&$u!xEO`RUnj zRDp0kKz5%B!gj3771xHbxpJCEjy;;FgORB_-a3Es^M7mJ2cfW$;4Uc7Pn)<{X(=Vo z1ZKP!Z2?AOR>Csqo@d;mMHlAm5g!$t%8a4NJ$w0A(pk<7**6OkoQYg4M{A!Eo~ z5O!A5@1Kdn=A6>Coi3>!U*c-vsJ(=-?RUQ3tBp^|aQ#ZYR*)#EC)6^29^1slT`6T4 zrSk(cL>Aq>s^Rn|1HDWnS9i8FfiY8XK?^}4D69W-#sT$cPs4eO>^V=}Qq-qlfUvcV ztWl^PuL9Svu0-9H(^Bu)=|ZIvE>}#G#xTfu0~NovZFJ8)Z>(y6UWA>cFeszIyqw4sA#9PG&vP(ekqo0{&qShY}|*ni`n{v45Cd`7XQ z9#hz9=yJGxJeCS@EkTOnCs;BFMD0ZkkKcTsbkD zG*RJ><{O2ikQV9B??VWbgI;P*aS?_AmX>C*v^axEA?dZJBM2R$QmLCIRm*-2 zez^kSIPA}}o;C+OK6wcMCV&+GJ#vy%xTCX`PJ$Qzwr5TcE~hOJfXx1R6D2$wN8Pqk z^{VS==$?{?L&Q<@3Z@Re>VKSl`cwaR+-SuFO=7IZN3v9L6Q zOP9_d=yc#!2e9wpO_&%TN7?X1Wbyz6fo8Lf)zt<{7wrNWwq~F81f(9^??AG^p~4TghXPS|6}8d*Hw;W1 zzJKxb-+vgJ*^1>)dUJswt+P|Z_0~$yg>W!?r zRtjOzHMxM1A&D!i-^A>ti`cjGX1Jx2L1r2OghDYA$*Rd3ftg7x1z}A^B4D%><(Chj z#ADM_^b6h>mfei`IlgW^f@U19UX*Ireir|_kJF(I$o3L;9{PbF?@s_slsyPQ8Q6po z$gN@vGg+=~$)}w{I^8BNUOWXID~R$aZoc_;9Nf1TQEV4W(tz){NyEuFNjfcemT-CI zEQA)=yZ2@@=|VB_WkfqmdJV z6?a$9NlmZJ;)aS%wTRU~(458S)a|dSPab;Z`uxeS0&wzj&4Ob=7|8xEnL%>zPN8gF z)8^bHJIPU^v9^G6$ww4PP<0#TSJus4*L93U@ckrLF6~-73u}v*U77_XaN+z3l*(1? z*lw7k7<9^ds_ac50Z~*@`H!&sSJbw!c5VegSR5NhXdf<9_PvOofzE-9g5`5kA)Cr3 zebuRJJmtI#5d~uV;U9R{se}Mr9ug)>d)+sCCV&V)VD_w@dWC}1?jWgX<@Qhg+N=;c zXm!?%Vdpq5oZ1-56(8p=&EnXpX}s{ZLkPnNN+|=b;~)g#=&8q`x?Swqe*;Qx1+#P0 z7#bWywKiy;D>~JLo>fegf0~NczW$c=OcjejC+7`lKJH-vGN(Dkx#vs(GHhIk&Dz2Q zA(-h3^C~$63qcNowTqZKaNnEGJ@SFOqt?=itewKhx8T{|AuaL+BOj`#1W-Bunz&xC z50D|6ZntU9+T9Ri6O))209paz@ncV5d}I)NckV=|+cm`@0O9zVV`#LRcPx*;xsYvsHAiD3p<}(|i25IU$~9ln?+TvA-Kw+$2kY z%zecGsIYli#xax@aI^x*>5*5Z%wgPJMcvt`8b5UZPh5QT&;PAeRqVa6@ya#~|CX3l z1E{wIVBKT6O4uB=?1z6O|Dj|xP;z{X4fw`~9VojPFIDi&Gf$yb_Kn_NSy@86-9iwD zc;}mb8dDS75yp|J6L{erFTujXGJ>cJ&np=ugOfp=CeD;;X{b#6kNtscAd$n&Tp5-( z0(BWcvttbIR?waI1AfwidlHEY*T1neS2qO2p?HT7T41{avvFq>~-+KAzUwlHv z;T%gFI5_}wI;*Trm4({?1w)UlFLRtos)(#%R`y*)K^sJb;gK;of=r9~v(u+==;qt7 zXU{&IJAVc@)DEItDq(455y~z_SF2UkAkdy>Z*b z4X=6Y+{w>;2tc)uDvDBIBe1_0VAr)*8E>FqYY^qF0E0lJOQS&|jw4J=P8p+5X^GuC z_h4jb)I)Y%I1Sp`^bV~+Qjpv*l-$*`f~*0R!*lRT_CJb{~ zS|e&)!tPt%^aFEGfA%qGJ;(YUvDNn-yKe(%@i}5vMZL0tTIH&2QB24beZ#a&+XQ&L z4X9MAs8wo!Ane?>8$qU`ShVRlf|?$% zD@gg)a$Nn%Q^|R#^PYpp1G=S6=UgvY(7C)+QCG#S1*ya#Xq^drD-CuGN!Yyu*DaYb zmRcE?aB%X__Wk$%)W2Cg`@q-ROVf|YsC$ly4qX820GgHvx;X;So5%%laD^GZ_w?$t zaM5)=l>H4Tmrbwyfq_Bn*typb#i?`WLFx*2O>M`e^B3{WryqihB@V6MjGf~I}r6ejWD9)0D6>dhpxE7o(WCl6V!n#-Vc|X!|bDl^; z@mtVDcEvRZo?C*jE0@78ybTZ>a*Yd(P6NC5-?yVaIQiDb%K7_O&Oh|I=G>{j)LPA2 zj&E2Z=voORE$DCEDpoTY5@nO8#Ra(u+H$Ppcu*=fVqXXsp66l5j-Bva564cO#TUQ! zbqqOO?B6kksi_@!!OeHzfrq|`N1lEdC!Vh1z|I3$SzAT76M{sQ#Xsp{i9cI%J-d^F z^}nqY()qwBO9HYr$LvNqTj&+1GcH!~n#|op7__bsS+NdbM4jt+pj0i%E@GC#h%*(> z4m!=t@ck+#hW2B4U{_^y;?SEHXC8my{HZU$KWHxg4XXl#_H`z;z66kqx$$)t^{k62 z(|pMHE9iFD4Ci~Ei+a6|P9X8XWB-6BpFROs$C%tTVcMmaDmC1>{Z5>{dR$F}VUaKnKYrUqNK4Gj^8wxx=Dx`BN>hGy=;cyIhRmcUcn*Pdlz256kBkaUGqjFP6;QVmfVvJ`zmCy? zy%@grhCL%Y?t1^ZM?N&rxN`jc{JWsGlndbas)c+FfRze)^;a$rpc^#Jy+(5bp{Fij zX6XbPjb#udh6gGpN~o1WtKG)f*d*@0^&VWlbRH->Kq=wqk&!7AR#r;emhsH-$h3}U z>z|#x9))L`uUNyD!4hzHed&tUDz*ebTO@!O0Oc(Iz;a-e>SiI_;feh({g7WDzcZ2= z-5Bs)1&4CoP>A}?j^^rVXvAR?QMis$c?eUJcld*^yZJYs{`wybES&oLf4%jRu&@&= z8AX}^%@zS0|CzZJ_Sjmdo>zhElubbB@|9&YT4&+c$KZReiRzVokt{eRBs!fo+MNz= z+Itg*M#iwVvI3C8*uYMV3~jSG)p^8oC-Lk*Tv?kp>QXn63K`C9>RCouzWBt~ECT(l zaz7*Aay}bg0lQ?-qq|=4+kSoftCg-NbrBxmIpFj*t6Z}$8mY*P4UY#Q>^2b1pG0l+ z0B(QH|MA}ASNe5e#oSS(`izy?zwiK36UVo zH6A#7y)cU%+72p_{kc3K!u8RZJ3ZZ8oIVbqyjk&|BLd`*-_1#YusSsLmg?C4A6J+_ z$sfgFS(-6X%DB&#!PqSYv2jS5#yJ_G%uHYry}ooBRk;thfA6pSE61xe=8u2sH)Yh_ zbdMkorLp$0kSnR?UzjE@;Cp3EPVU9(+8Tne4Tk{PSq7;AG`quaMHgj*xDbuiMXWVP zu-<7vxK$jSxCI0CQA_=d{37Cc&Om-BaMpe8VEFyjkwg>3x+! ze%AF0gm8vNw%_`H!r2Dj9Yeho7^KSt`HWtjF;_B^Wr*_wxjy{)CHe*0E7Pa~IQ;To z{Eha?`Ex7NU;nTW?iNbIXah2>SMqy>g>}`SmZlf;C_`uerPT4mLUL+z^Ur+_WQlT-7kO`N5u=_2pHrGk*lXCZbeIs zQBcnC*5GTp*mcvm<(-vt7$^_nw%7i~?>_S1-gzu)Ej~f6zsZFoGfT#>07`*~ldEYX z2G$13)cYN}`Gr_qn#J;CLGsTnbx% zNtOtT#K4$l>8y)#8d|t$&K<+Zfme+mc*W2C>N8*X)we*#fF-a#HGzdBEF(SF_KgZY zIIr8|2!W|>yD>ht9bwcl>*T~TflXA(e;H6A;kBX6Zl!4LbjPr zn}AEVx!791{N$t7@Dp3?6VJ$J&Tr*|FJ7s3z^`rp9!DvZ{E(Y&3FcgA&e;eSw|tuo zzD!pm&F73QV!KUgV14#+?7HdxH_trr(KoGJc39z>9H19c&BvXy>B$x7%;7mpl~VP_FQX$zvCIfV6o z3qX^(pDS>@%Fv-wX=t$QCi8i(q3r%?@f}#?t-%uLV9dCqjs1#naXj_PzM0 zem-cI*qaIcf3M))qB?GpMI#H5tz#avX(&}@dWhl z3Ts7j27|2bG%>L;@_Qxzh|gICunj_6TG_*LzmMh1Pn`%hEI7AAS<9P-;1`OSRj z)dF0SbSM65@SW$Bv7ym}w+xLRJm{4N zJwnnEOmSK?7p=AKqezam(uf*YFtPXLuR8aQKk)&CeI-B^!vY$Tr(UHZhM(ie6wm5_ z?#tE16f+w=#h0iDwH0Q0>S7f!sxZKm6P&hQH?*RR@rmyyrh zFN6z0jvygYddOPpl#OJvBqg)@_`>C*$Id+Q7ymJix~o^teeKJBX>jN8+7`jMI1SpC5>4zS%)_Z9SK_BE?z7-2?dyNUet~M^b5=c0u z-UFsBgRyKQ5~kDG%=80aoj(4FKh;WV0HXjX3>uf`FFt%>12%|ZU~+ik=0n4ihYk;o z-LTKE43~_!m!Zw$P^MY8iaPMU@E|) z$k+29F7`0*Sv{r`|Cfn$gk&0zdZml44RkbFFo?PXCC|D9~}AWZ@vGSNB{g|8-#H} z#i0Zljg{6+8c-^N(AueN5CGCBc+W^MTl-Kt0T;Yd*Ht5Jy$r5i3qeqib;5eBE>6g* z9)91P+EBu`bdTnzAAZboZ3&y+INE3R{-PECom{?OX9!Zph{E+VN~sLHU9HKMco zsnbXQ;zJ9UAN!Q8_bcT}%v5nG5k*1s!ouZ8&u+lyrw-oxo}C9@^FrSrwzyJUKWU_c zO1&QHWdmf~EvYo(4RAnd{e8Mpcm|n;z)_BU_#DM*9!6|7zn|wrDoP;YKvkhLI1g{a z{q42c)zz8fPqF3SN8~fa23YUU16VAGe%_4nRv+7E5C9?0NZAF6a!=*uxFtkkt8@D3 zM?SE&c|M+~!g_ScxQU33-tKic2+-Fx=k_ zVpGs*vUK*NTC+{Swqy2vF5t^q1G=ESna+tDI&=MIMnDvM1KK^KztADXhJ7DBs${g% zhx^&kW8_C%W`$V`YdNc*acY-E1I5H87M9w2UG9YS)X4yedHs5 z)N0N=1)!F4HvfGFRyRB$gaH(^mQRMknv^nd9H*RMLMJ#=GK7rUT}v#ghXmlk6O_cu zSDS4N4%AI#i4j?}=5^Lh?6q3n)!8o73XYJur1K6rnVUMp?D_8@U+~dcx^U!3AGc34 zKrz=FJ-=)bKM!CvYxVbUEExssr`Ojm{zJJk{(9&rqXdNGqp^H$W`py8KkTlZxA};W z)eWLzY8f#T031fXdDoA^&Y2A&n2CowyMQ^!o%`~Bgf;Qwa`QS_dvWs z_*noa0i4T;e!1B`_^S&iKdrUOUpCpu#>$z~8_1uwNNig*vto&E0l=bx^q66(cpYod zdbZu1KM}{ROewN(N@%ZNxg54vFQqKwZpH&5#|KDYZLNuJ5Mr=iL9JSX=d!;^+|8pu zKDk(<=+;r;SvKnOi4M)3NeJ1@q%JhH7Kpm*YVrJIkM-w#F6gi#eh$ElML>(KeJBH4 zSY@QSH2s-&Yi_1qpV}d{WCL4C42~VRk%($q>t)U>pp&aAiU5Q~L(uAfmH;sTKUiNr z5{8Y}RjRSMtpPY*8I6l4o`BX1={K&@B`lOmNtunC3uwoOiq;VXg!v$X*Kjd8T8HZh z0O*y$%k&BAzm#0D)T>y3$oEo%#+3?i3gJ(d0Jh?1qr1v8-Sx$bxmmTy=z5Op&jYw* z382wqQpIMGPXIg_2h(#GzV?CIEkF1xloBx|Mp>n{ZPKp{j|AEh02zKpU=gd{9CdqXVdl)=ZP6QBJfE8YCm zlW=>60W|0R%asxYs?e$g6>fkGK%@|AK*kDMM!I!n`izpXbR4%rLimD&M?{`*9FK(K z637uY0Zzg(-b8ZhfG9l{34lO?n3#wlgk!Y@$Iq3FP~QNbGED}#kKnVQ6c*2#uy5D8 ze#$~*Ic*ledAn}_2z%@JtwS!BP--`3KY8)=XKvkp_$~iD`9CQW#~&EpdzggdYbEFM z?%o#uhGdr&&ieAj53Vmyzh!*tuG?m&|L(6=W{&+ARuxdaOTtKNaE%>&-v{YR(mc}l zjo|_f2-m!>=BFR|{CZ>QL5nfL)0N33zz4wv30D9<5tT_eB_i^l1&}BcQAr3<0^mNo zC!!5-%0iF_A}=8(;X!O{uoPm5FTilvM2*rv$&)G5Q0)5B`T4cEXHKSW4~u&&Sj1nn z)^_l`aeNUJBM0uv%#;72R2kj1>%eQ?l=S?QC{@OGmTTiXJI%SLS)@a6@ox}!0f7sx z&n#U!`u_}!9r*S0$Nu)c_MX$XLx@4L2Bjfit^u^Z9}yhKGa^6guD7o|^WewZYgdl4 zamAw3e!==?5|S;=f=6ffA`e950nY+F666xdPY6VmWgNKM+KszOh`ns|XZ8Q$`G+5e z)(rrjl>#dk>C*t_SxS3!#8J?hB3J?-PO^~WpwpOdu3kQN)IPsv z_54}8UuOgFc|?4F#1MdvJ&&$TKlt-9?#}MM>Gi)*_UleyK z`9As*0F)wPpN+F}MpqLYw}Q2~Q`0jizw!~=p+Z@4x@tw~GV9VJ0c1Iy31Sm1S@|7) zPYU6zr23zUMVmbQ(uIw%Zya6(uxb&1-b#S*yMy>*GAY+gN-tb|EpPh0 z`q1t>KvaU(!Sh6LTW=KUR|l0=N}2{u&jMhlEW1YMFPwY)n{#Kr@j+-cXE>d+VryIy z)sup$?{R;iqRK!ZTyLR}^)rh$uSP2uj(iTlF}Ci}xdw>u#i24K{MOp+r%yiofk!tA zJ>KRxenrMXfbY&$B!IBJ)>@f8adc$-7Q65;nHH~MXB9Iizxw|zUwZOCa~|A4t`cSo zMfuNV;~;;``E{c+jE%B1^YB-qZsRX)FsB6|z7|ODKcR$)S;oQK(#(^;$JN%?hydcS zgDcbj@SmfwT`!e~4|#sIP991NmyUj{xiI~a9?b@}Jh0Cl3B~DgWI!MeyL#o~3qZH>j$p=Z|dm=Dj78vE>y^#woNJ5E{JBz5Aj9s| wvqh4z>YQ)Ro&F^LaoZG-*8e>-$UkoXKaqbfb%uQ|<^TWy07*qoM6N<$f@QG`_y7O^ literal 0 HcmV?d00001 diff --git a/addons/hr_equipment/tests/__init__.py b/addons/hr_equipment/tests/__init__.py new file mode 100644 index 00000000000..a43ab866ebc --- /dev/null +++ b/addons/hr_equipment/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import test_hr_equipment diff --git a/addons/hr_equipment/tests/test_hr_equipment.py b/addons/hr_equipment/tests/test_hr_equipment.py new file mode 100644 index 00000000000..f1c9e923692 --- /dev/null +++ b/addons/hr_equipment/tests/test_hr_equipment.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +from openerp.tests.common import TransactionCase +import time + + +class TestHrEquipment(TransactionCase): + """ Test used to check that when doing equipment/maintenance_request/equipment_category creation.""" + + def setUp(self): + super(TestHrEquipment, self).setUp() + self.equipment = self.env['hr.equipment'] + self.maintenance_request = self.env['hr.equipment.request'] + self.user = self.env['res.users'] + self.main_company = self.env.ref('base.main_company') + res_hr_user = self.env.ref('base.group_user') + res_hr_manager = self.env.ref('base.group_hr_manager') + + self.hr_user = self.user.create(dict( + name="Normal User/Employee", + company_id=self.main_company.id, + login="emp", + password="emp", + email="empuser@yourcompany.example.com", + groups_id=[(6, 0, [res_hr_user.id])] + )) + + self.hr_manager = self.user.create(dict( + name="HR Manager", + company_id=self.main_company.id, + login="hm", + password="hm", + email="hrmanager@yourcompany.example.com", + groups_id=[(6, 0, [res_hr_manager.id])] + )) + + def test_hr_equipment_request_category(self): + + # Create a new equipment + equipment_01 = self.equipment.sudo(self.hr_manager).create({ + 'name': 'Samsung Monitor "15', + 'category_id': self.ref('hr_equipment.hr_equipment_monitor'), + 'employee_id': self.ref('hr.employee_al'), + 'user_id': self.ref('base.user_root'), + 'assign_date': time.strftime('%Y-%m-%d'), + 'serial_no': 'MT/127/18291015', + 'model': 'NP355E5X', + 'color': 3, + }) + + # Check that equipment is created or not + assert equipment_01, "Equipment not created" + + # Create new maintenance request + maintenance_request_01 = self.maintenance_request.sudo(self.hr_user).create({ + 'name': 'Resolution is bad', + 'user_id': self.hr_user.id, + 'employee_id': self.ref('hr.employee_qdp'), + 'equipment_id': equipment_01.id, + 'category_id': self.ref('hr_equipment.hr_equipment_monitor'), + 'color': 7, + 'stage_id': self.ref('hr_equipment.stage_0') + }) + + # I check that maintenance_request is created or not + assert maintenance_request_01, "Maintenance Request not created" + + # I check that Initially maintenance request is in the "New Request" stage + self.assertEquals(maintenance_request_01.stage_id.id, self.ref('hr_equipment.stage_0')) + + # I check that change the maintenance_request stage on click statusbar + maintenance_request_01.sudo(self.hr_user).write({'stage_id': self.ref('hr_equipment.stage_1')}) + + # I check that maintenance request is in the "In Progress" stage + self.assertEquals(maintenance_request_01.stage_id.id, self.ref('hr_equipment.stage_1')) diff --git a/addons/hr_equipment/views/hr_equipment_views.xml b/addons/hr_equipment/views/hr_equipment_views.xml new file mode 100644 index 00000000000..64022a125a8 --- /dev/null +++ b/addons/hr_equipment/views/hr_equipment_views.xml @@ -0,0 +1,566 @@ + + + + + + hr.equipment.request.search + hr.equipment.request + + + + + + + + + + + + + + + + + + + + + + + + hr.equipment.request.form + hr.equipment.request + +
    +
    + +
    + +
    + +
    +
    +
    + + + + + + + + + + + + + + +
    +
    + + +
    +
    +
    +
    + + + hr.equipment.request.kanban + hr.equipment.request + + + + + + + + + + + +
      +
    • Category:
    • +
    • Request to:
    • +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + hr.equipment.request.tree + hr.equipment.request + + + + + + + + + + + + + + + + Maintenance Requests + hr.equipment.request + kanban,tree,form + + +

    + Click to add a new maintenance request. +

    + Follow the process of the request and communicate with the collaborator. +

    +
    +
    + + + Maintenance Requests + hr.equipment.request + kanban,tree,form + + + { + 'search_default_category_id': [active_id], + 'default_category_id': active_id, + } + +

    + Click to add a new maintenance request. +

    + Follow the process of the request and communicate with the collaborator. +

    +
    +
    + + + Maintenance Requests + hr.equipment.request + hr.equipment + kanban,tree,form + { + 'default_equipment_id': active_id, + } + [('equipment_id', '=', active_id)] + + + + + hr.equipment.form + hr.equipment + +
    + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    +
    +
    + + + hr.equipment.kanban + hr.equipment + + + + + + + + + + + + +
      +
    • Serial Number:
    • +
    • Model Number:
    • +
    +
    + +
    +
    + +
    +
    +
    ()
    +
    +
    +
    + + Maintenance Request + + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + + hr.equipment.tree + hr.equipment + + + + + + + + + + + + + + + + + hr.equipment.search + hr.equipment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Equipments + hr.equipment + kanban,tree,form + + +

    + Click to add a new equipment. +

    + This application helps you to track all equipment allocated to employees. + Create some laptop, printer or phone and link each equipment to an employee or departement. + You will manage allocations, issues and maintenance of equipment. +

    +
    +
    + + + + Equipments + hr.equipment + kanban,tree,form + + + { + 'search_default_category_id': [active_id], + 'default_category_id': active_id, + } + +

    + Click to add a new Engine. +

    + This application helps you to track all equipment allocated to employees. + Create some laptop, printer or phone and link each equipment to an employee or departement. + You will manage allocations, issues and mainteannce of equipment. +

    +
    +
    + + + + hr.equipment.category.form + hr.equipment.category + +
    + +
    +
    +
    + + +
    + + + + + + + + +
    +
    + + +
    +
    +
    +
    + + + hr.equipment.category.tree + hr.equipment.category + + + + + + + + + + hr.equipment.category.search + hr.equipment.category + + + + + + + + + + + + + Equipment Categories + hr.equipment.category + tree,form + + +

    + Click to add a new equipment category. +

    +
    +
    + + + + hr.equipment.stage.search + hr.equipment.stage + + + + + + + + + hr.equipment.stage.tree + hr.equipment.stage + + + + + + + + + + + + Stages + hr.equipment.stage + tree + form + + +

    + Click to add a stage in the maintenance request. +

    +
    +
    + + + + + + + + + + + + + + + + + +
    diff --git a/addons/hr_equipment/views/res_config_views.xml b/addons/hr_equipment/views/res_config_views.xml new file mode 100644 index 00000000000..ec31667b1ab --- /dev/null +++ b/addons/hr_equipment/views/res_config_views.xml @@ -0,0 +1,36 @@ + + + + hr.equipment.config.settings + hr.equipment.config.settings + +
    +
    +
    + + + + +
    +
    + + + Configure Equipments + hr.equipment.config.settings + + form + inline + +