import json from datetime import datetime, time, timedelta from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError class Task(models.Model): _name = 'beesdoo.shift.shift' _inherit = ['mail.thread'] _order = "start_time asc" ################################## # Method to override # # to have different state # # on the shift # ################################## def _get_selection_status(self): return [ ("open","Confirmed"), ("done","Attended"), ("absent","Absent"), ("excused","Excused"), ("cancel","Cancelled") ] def _get_color_mapping(self, state): return { "draft": 0, "open": 1, "done": 5, "absent": 2, "excused": 3, "cancel": 9, }[state] def _get_final_state(self): return ["done", "absent", "excused"] name = fields.Char(track_visibility='always') task_template_id = fields.Many2one('beesdoo.shift.template') planning_id = fields.Many2one(related='task_template_id.planning_id', store=True) task_type_id = fields.Many2one('beesdoo.shift.type', string="Task Type") worker_id = fields.Many2one('res.partner', track_visibility='onchange', domain=[ ('is_worker', '=', True), ('working_mode', 'in', ('regular', 'irregular')), ('state', 'not in', ('unsubscribed', 'resigning')), ]) start_time = fields.Datetime(track_visibility='always', index=True, required=True) end_time = fields.Datetime(track_visibility='always', required=True) state = fields.Selection(selection=_get_selection_status, default="open", required=True, track_visibility='onchange', group_expand='_expand_states' ) color = fields.Integer(compute="_compute_color") super_coop_id = fields.Many2one('res.users', string="Super Cooperative", domain=[('partner_id.super', '=', True)], track_visibility='onchange') is_regular = fields.Boolean(default=False, string="Regular shift") is_compensation = fields.Boolean(default=False, string="Compensation shift") replaced_id = fields.Many2one('res.partner', track_visibility='onchange', domain=[ ('eater', '=', 'worker_eater'), ('working_mode', '=', 'regular'), ('state', 'not in', ('unsubscribed', 'resigning')), ]) revert_info = fields.Text(copy=False) working_mode = fields.Selection(related='worker_id.working_mode') def _expand_states(self, states, domain, order): return [key for key, val in self._fields['state'].selection] @api.depends("state") def _compute_color(self): for rec in self: rec.color = self._get_color_mapping(rec.state) def _compensation_validation(self, task): """ Raise a validation error if the fields is_regular and is_compensation are not properly set. """ if (task.is_regular == task.is_compensation or not (task.is_regular or task.is_compensation)): raise ValidationError( "You must choose between Regular Shift or " "Compensation Shift." ) @api.constrains("state") def _lock_future_task(self): if datetime.now() < self.start_time: if self.state in self._get_final_state(): raise UserError(_( "Shift state of a future shift " "can't be set to 'present' or 'absent'." )) @api.constrains('is_regular', 'is_compensation') def _check_compensation(self): for task in self: if task.working_mode == 'regular': self._compensation_validation(task) @api.constrains('worker_id') def _check_worker_id(self): """ When worker_id changes we need to check whether is_regular and is_compensation are set correctly. When worker_id is set to a worker that doesn't need field is_regular and is_compensation, these two fields are set to False. """ for task in self: if task.working_mode == 'regular': self._compensation_validation(task) else: task.write({ 'is_regular': False, 'is_compensation': False, }) if task.worker_id: if task.worker_id == task.replaced_id: raise UserError("A worker cannot replace himself.") def message_auto_subscribe(self, updated_fields, values=None): self._add_follower(values) return super(Task, self).message_auto_subscribe(updated_fields, values=values) def _add_follower(self, vals): if vals.get('worker_id'): worker = self.env['res.partner'].browse(vals['worker_id']) self.message_subscribe(partner_ids=worker.ids) #TODO button to replaced someone @api.model def unsubscribe_from_today(self, worker_ids, today=None, end_date=None, now=None): """ Unsubscribe workers from *worker_ids* from all shift that start *today* and later. If *end_date* is given, unsubscribe workers from shift between *today* and *end_date*. If *now* is given workers are unsubscribed from all shifts starting *now* and later. If *now* is given, *end_date* is not taken into account. :type today: date :type end_date: date :type now: datetime """ if now: if not isinstance(now, datetime): raise UserError (_("'Now' must be a datetime.")) date_domain = [('start_time', '>', now)] else: today = today or fields.Date.today() today = datetime.combine(today, time()) date_domain = [('start_time', '>', today)] if end_date: end_date = datetime.combine(end_date,time(hour=23, minute=59, second=59)) date_domain.append(('end_time', '<=', end_date)) to_unsubscribe = self.search([('worker_id', 'in', worker_ids)] + date_domain) to_unsubscribe.write({'worker_id': False}) # Remove worker, replaced_id and regular to_unsubscribe_replace = self.search([('replaced_id', 'in', worker_ids)] + date_domain) to_unsubscribe_replace.write({'worker_id': False, 'replaced_id': False}) # If worker is Super cooperator, remove it from planning super_coop_ids = self.env['res.users'].search( [('partner_id', 'in', worker_ids), ('super', '=', True)]).ids if super_coop_ids: to_unsubscribe_super_coop = self.search( [('super_coop_id', 'in', super_coop_ids)] + date_domain) to_unsubscribe_super_coop.write({'super_coop_id': False}) @api.multi def write(self, vals): """ Overwrite write to track state change If worker is changer: Revert for the current worker Change the worker info Compute state change for the new worker """ if 'worker_id' in vals: for rec in self: if rec.worker_id.id != vals['worker_id']: rec._revert() # To satisfy the constrains on worker_id, it must be # accompanied by the change in is_regular and # is_compensation field. super(Task, rec).write({ 'worker_id': vals['worker_id'], 'is_regular': vals.get('is_regular', rec.is_regular), 'is_compensation': vals.get('is_compensation', rec.is_compensation), }) rec._update_state(rec.state) if 'state' in vals: for rec in self: if vals['state'] != rec.state: rec._update_state(vals['state']) return super(Task, self).write(vals) def _set_revert_info(self, data, status): data_new = { 'status_id': status.id, 'data' : {k: data.get(k, 0) * -1 for k in ['sr', 'sc', 'irregular_absence_counter']} } if data.get('irregular_absence_date'): data_new['data']['irregular_absence_date'] = False self.write({'revert_info': json.dumps(data_new)}) def _revert(self): if not self.revert_info: return data = json.loads(self.revert_info) self.env['cooperative.status'].browse(data['status_id']).sudo()._change_counter(data['data']) self.revert_info = False def _update_state(self, new_state): self.ensure_one() self._revert() if not (self.worker_id or self.replaced_id) and new_state in self._get_final_state(): raise UserError(_("You cannot change to the status %s if no worker is defined for the shift") % new_state) always_update = int(self.env['ir.config_parameter'].sudo().get_param('always_update', False)) if always_update or not (self.worker_id or self.replaced_id): return if not (self.worker_id.working_mode in ['regular', 'irregular']): raise UserError(_("Working mode is not properly defined. Please check if the worker is subscribed")) data, status = self._get_counter_date_state_change(new_state) if status: status.sudo()._change_counter(data) self._set_revert_info(data, status) @api.model def _cron_send_weekly_emails(self): """ Send a summary email for all workers if they have a shift planned during the week. """ tasks = self.env["beesdoo.shift.shift"] shift_summary_mail_template = self.env.ref( "beesdoo_shift.email_template_shift_summary", False ) start_time = datetime.now() + timedelta(days=1) end_time = datetime.now() + timedelta(days=7) confirmed_tasks = tasks.search( [ ("start_time", ">", start_time), ("start_time", "<", end_time), ("worker_id", "!=", False), ("state", "=", "open"), ] ) for rec in confirmed_tasks: shift_summary_mail_template.send_mail(rec.id, True) ######################################################## # Method to override # # To define the behavior of the status # # # # By default: everyone is always up to date # ######################################################## def _get_counter_date_state_change(self, new_state): """ Return the cooperator_status of the cooperator that need to be change and data that need to be change. It does not perform the change directly. The cooperator_status will be changed by the _change_counter function. Check has been done to ensure that worker is legitimate. """ data = {} status = None return data, status