# -*- coding: utf-8 -*- from datetime import datetime, timedelta import pytz from openerp import api, fields, models, _ from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DTF from openerp.exceptions import ValidationError class ShiftLeave(models.Model): _inherit = 'shift.leave' show_alert_proposed_date = fields.Boolean( string='Show Alert Proposed Date', default=False, compute='_compute_proposed_date', store=True) alert_message = fields.Html(string='Alert Message', compute='_compute_proposed_date', store=True) return_date = fields.Date( string='Return Date', compute='_compute_proposed_date', help='Stop Date of the Temporary Leave plus 1 day') proposed_date = fields.Date(string='Proposed Date', compute='_compute_proposed_date', store=True) shift_date_after_return = fields.Date( string="Shift Time after return", compute="_compute_shift_time_after_return") show_proceed_message = fields.Boolean( string='Show Message Proceed', default=False, compute='_compute_proceed_message', store=True) proceed_message = fields.Html(string='Message Proceed', compute='_compute_proceed_message', store=True) non_defined_type = fields.Boolean(related="type_id.is_non_defined", string="Has Non Defined", store=True) non_defined_leave = fields.Boolean(string="Undefined Leave") is_send_reminder = fields.Boolean("Send Reminder", default=False) event_id = fields.Many2one('shift.counter.event', string="Event Counter") @api.multi @api.depends('partner_id', 'type_id', 'stop_date', 'non_defined_leave') def _compute_proposed_date(self): """ ABCD Member: Proposed Date = Date of the ABCD shift after End Date entered - 1 Volant / FTOP Member: Case 1: End Date entered + 7 >= Date of FTOP shift right after End Date entered => Proposed Date = Date of FTOP shift right after End Date entered + 1 Case 2: Otherwise, do not propose anything """ for leave in self: if leave.type_id.is_temp_leave and leave.non_defined_leave: leave.reset_propose_info() continue if not (leave.partner_id and leave.type_id and leave.stop_date) \ or not leave.type_id.is_temp_leave: leave.reset_propose_info() continue stop_date = fields.Date.from_string(leave.stop_date) leave.return_date = stop_date + timedelta(days=1) partner = leave.partner_id leave_stop_time_utc = \ self.convert_date_to_utc_datetime( leave.stop_date, start_of_day=False) next_shift = partner.get_next_shift( start_date=leave_stop_time_utc) _next_shift_time, next_shift_date = \ partner.get_next_shift_date(start_date=leave_stop_time_utc) if not next_shift_date or not next_shift: future_date_shift = leave.guess_future_date_shift( leave.stop_date, is_all_team=True) if len(future_date_shift) > 0: next_shift_date = future_date_shift[0] if next_shift_date: next_shift_date = fields.Date.to_string( next_shift_date) else: leave.reset_propose_info() continue next_shift = next_shift and next_shift.shift_id or '' next_shift_date = fields.Date.from_string(next_shift_date) in_ftop_team = partner.in_ftop_team if not in_ftop_team and \ (next_shift_date + timedelta(days=-1) > stop_date): proposed_date = next_shift_date + timedelta(days=-1) alert_message = _("You have inputted <b><i>{end_date}</i></b> " "as the end date of the temporary leave for " "<b><i>{partner}</i></b>. But a better date" " would be <b><i>{proposed_date}</i></b> " "which is one day before the shift " "<b><i>{shift}</i></b>. Do you accept the " "proposed date?") elif in_ftop_team and \ (next_shift_date + timedelta(days=1) < stop_date + timedelta(days=7)): proposed_date = next_shift_date + timedelta(days=1) alert_message = _("You have inputted <b><i>{end_date}</i>" "</b> as the end date of the temporary leave" " for <b><i>{partner}</i></b>. But a better " "date would be <b><i>{proposed_date}</i></b>" " which is one day after the shift " "<b><i>{shift}</i></b>. Do you accept the " "proposed date?") else: leave.reset_propose_info() continue leave.proposed_date = proposed_date if leave.proposed_date and leave.proposed_date == leave.stop_date: continue leave.show_alert_proposed_date = True if next_shift: shift_name = next_shift.name + \ (next_shift.begin_date_string and (' ' + next_shift.begin_date_string) or '') else: shift_name = ' begin on ' +\ fields.Date.to_string(next_shift_date) leave.alert_message = alert_message.format( end_date=stop_date, partner=leave.partner_id.name, proposed_date=proposed_date, shift=shift_name) @api.multi def _compute_shift_time_after_return(self): ''' @Function to get shift time after propose date ''' for leave in self: if not leave.partner_id or not leave.return_date: leave.shift_date_after_return = False continue leave_return_time_utc = \ self.convert_date_to_utc_datetime(leave.return_date) # Look for date next shift after return date _next_shift_time, next_shift_date = \ leave.partner_id.get_next_shift_date( start_date=leave_return_time_utc) leave.shift_date_after_return = next_shift_date @api.multi def btn_confirm_propose(self): for leave in self: leave.show_alert_proposed_date = False leave.stop_date = leave.proposed_date @api.multi def btn_cancel_propose(self): for leave in self: leave.reset_propose_info() def reset_propose_info(self): self.proposed_date = False self.show_alert_proposed_date = False self.alert_message = '' def convert_date_to_utc_datetime(self, input_date, start_of_day=True): ''' @Function to convert the date (at 00:00:00) to UTC datetime ''' if start_of_day: input_date = input_date + ' 00:00:00' else: input_date = input_date + ' 23:59:59' # Convert input date from string to datetime input_date = datetime.strptime(input_date, DTF) # Convert to UTC time tz_name = self._context.get('tz') or self.env.user.tz context_tz = pytz.timezone(tz_name) local_dt = context_tz.localize(input_date, is_dst=None) return_datetime = local_dt.astimezone(pytz.utc) return return_datetime.strftime(DTF) @api.multi @api.depends('type_id', 'stop_date', 'start_date') def _compute_proceed_message(self): for leave in self: if leave.stop_date and leave.start_date: days_leave =\ (datetime.strptime(leave.stop_date, "%Y-%m-%d") - datetime.strptime(leave.start_date, "%Y-%m-%d")).days + 1 if leave.type_id.is_temp_leave and days_leave < 56: leave.show_proceed_message = True leave.proceed_message = (_( "Leave duration is under 8 weeks, do you want to proceed?" )) @api.multi @api.constrains('type_id', 'partner_id', 'start_date', 'stop_date') def _check_leave_for_ABCD_member(self): for record in self: today = fields.Date.context_today(self) abcd_lines_in_leave = record.partner_id.registration_ids.filtered( lambda l: l.date_begin >= record.start_date and l.date_end <= record.stop_date and l.date_begin >= today and l.state != 'cancel' and l.shift_ticket_id.shift_type == 'standard') if record.type_id.is_anticipated: num_line_guess = record.calculate_number_shift_future_in_leave() total_line = len(abcd_lines_in_leave) + num_line_guess if record.partner_id.in_ftop_team: raise ValidationError( _("This member is not part of an ABCD team.")) elif record.partner_id.final_standard_point != 0: raise ValidationError(_( "Normally, this member is not eligible for early" + " leave because he has to catch up")) elif len(abcd_lines_in_leave) < 2: raise ValidationError(_( "The period of leave must include TWO" + " minimum missed services.")) elif record.partner_id.final_ftop_point < total_line: raise ValidationError(_( "The member does not have enough" + " credits to cover the proposed period.")) @api.multi def update_info_anticipated_leave(self): self.ensure_one() today = fields.Date.context_today(self) lines = self.partner_id.registration_ids abcd_lines_in_leave = lines.filtered( lambda l: l.date_begin >= self.start_date and l.date_end <= self.stop_date and l.date_begin >= today and l.state != 'cancel' and l.shift_ticket_id.shift_type == 'standard') num_shift_guess = self.calculate_number_shift_future_in_leave() # Update FTOP points count_point_remove = len(abcd_lines_in_leave) + num_shift_guess point_counter_env = self.env['shift.counter.event'] event = point_counter_env.sudo().with_context( {'automatic': True}).create({ 'name': _('Anticipated Leave'), 'type': 'ftop', 'partner_id': self.partner_id.id, 'point_qty': -count_point_remove, 'notes': _('This event was created to remove point base' + ' on anticipated leave.') }) self.event_id = event.id @api.multi def calculate_number_shift_future_in_leave(self): self.ensure_one() # Find shift template include current partner templates = self.partner_id.tmpl_reg_line_ids.filtered( lambda l: (l.is_current or l.is_future) and l.date_begin >= self.start_date and l.shift_ticket_id.shift_type == 'standard').mapped('shift_template_id') # Get number of shifts in period that is from end date of past ABCD line to # the end of leave to guess line partner might register num_shift_guess = 0 for template in templates: last_shift_date = (fields.Datetime.from_string( template.last_shift_date) + timedelta(days=1)).strftime('%Y-%m-%d') rec_dates = template.get_recurrent_dates( last_shift_date, self.stop_date) for rec in rec_dates: if fields.Datetime.to_string(rec) < self.stop_date: num_shift_guess += 1 return num_shift_guess @api.multi def button_cancel(self): ''' Add point counter event base on cancelling leave anticipated ''' super(ShiftLeave, self).button_cancel() for leave in self: if leave.type_id.is_anticipated and leave.event_id: last_notes = leave.event_id.notes last_points = leave.event_id.point_qty leave.event_id.point_qty = 0 leave.event_id.notes =\ last_notes + '\nLast point quantity: %s' % last_points @api.multi def guess_future_date_shift(self, stop_date, is_all_team=False): for leave in self: templates = leave.partner_id.tmpl_reg_line_ids.filtered( lambda l: (l.is_current or l.is_future) and l.shift_ticket_id.shift_type == 'standard').mapped('shift_template_id') if is_all_team and not templates: templates = leave.partner_id.tmpl_reg_line_ids.filtered( lambda l: (l.is_current or l.is_future) and l.shift_ticket_id.shift_type == 'ftop').mapped('shift_template_id') shift_after_leave = [] for template in templates: # Get the day after end leave 30 days to guess shift after leave next_shift_mounth = (fields.Datetime.from_string( stop_date) + timedelta(days=30)).strftime('%Y-%m-%d') rec_dates = template.get_recurrent_dates( template.last_shift_date, next_shift_mounth) for rec in rec_dates: if fields.Datetime.to_string(rec) > stop_date: shift_after_leave.append(rec) if shift_after_leave: shift_after_leave = sorted(shift_after_leave) return shift_after_leave @api.multi def update_date_end_anticipated_leave(self, vals): for leave in self: partner_id = vals.get('partner_id', leave.partner_id.id) stop_date = vals.get('stop_date', False) partner = self.env['res.partner'].browse(partner_id) future_lines = partner.registration_ids.filtered( lambda l: l.date_begin >= stop_date) # Suggest date end of leave before the registration after leave a day date_shift_guess = leave.guess_future_date_shift(stop_date) if date_shift_guess: vals.update({ 'stop_date': fields.Datetime.to_string( date_shift_guess[0] - timedelta(days=1)) }) elif future_lines: date_suggest = fields.Date.from_string( future_lines[0].date_begin) - timedelta(days=1) if stop_date != date_suggest: vals.update({ 'stop_date': date_suggest }) @api.model def create(self, vals): type_id = vals.get('type_id', False) type_leave = self.env['shift.leave.type'].browse(type_id) if type_leave.is_anticipated: partner_id = vals.get('partner_id', self.partner_id.id) partner = self.env['res.partner'].browse(partner_id) stop_date = vals.get('stop_date', False) future_lines = partner.registration_ids.filtered( lambda l: l.date_begin >= stop_date) date_shift_guess = self.guess_future_date_shift(stop_date) if date_shift_guess: vals.update({ 'stop_date': fields.Datetime.to_string( date_shift_guess[0] - timedelta(days=1)) }) elif future_lines: date_suggest = fields.Date.from_string( future_lines[0].date_begin) - timedelta(days=1) if stop_date != date_suggest: vals.update({ 'stop_date': date_suggest }) return super(ShiftLeave, self).create(vals) @api.multi def write(self, vals): type_id = vals.get('type_id', self.type_id.id) type_leave = self.env['shift.leave.type'].browse(type_id) if type_leave.is_anticipated and 'stop_date' in vals: self.update_date_end_anticipated_leave(vals) return super(ShiftLeave, self).write(vals) @api.multi def update_registration_template_based_non_define_leave(self): ''' This method is remove such members from their teams immediately by putting the end day for the templates closest the leave of current partner. Apply for non-define leave ''' for leave in self: if leave.non_defined_type and leave.non_defined_leave: # get all template on partner template_registration = leave.partner_id.tmpl_reg_line_ids # get templates before the stop date leave last_templates =\ template_registration.filtered( lambda l: l.is_current).sorted( key=lambda l: l.date_begin, reverse=True) # Must mark leave as done before setting end_date # for tmpl_reg_line to make _compute_is_unsubscribed # work as expected # => (Dont set unsubscribed for this partner in # period of non-defined leave) leave.state = 'done' # put end date to template which has closest begin day and # update state leave if last_templates: last_templates[0].date_end = fields.Date.from_string( leave.start_date) - timedelta(days=1) return True @api.model def send_mail_reminder_non_defined_leaves(self): leave_env = self.env['shift.leave'] leave_to_send = leave_env.search([ ('is_send_reminder', '=', False), ('non_defined_leave', '=', True), ('stop_date', '<=', (datetime.now() + timedelta(days=10)).strftime('%Y-%m-%d')) ]) # get mail template and send mail_template = self.env.ref( 'coop_membership.reminder_end_leave_email') if mail_template: for leave in leave_to_send: mail_template.send_mail(leave.id) # update sent reminder leave_to_send.write({ 'is_send_reminder': True })