# -*- coding: utf-8 -*- ############################################################################## # # Purchase - Computed Purchase Order Module for Odoo # Copyright (C) 2016-Today: La Louve (<http://www.lalouve.net/>) # @author Julien WESTE # @author Sylvain LE GAL (https://twitter.com/legalsylvain) # # 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 <http://www.gnu.org/licenses/>. # ############################################################################## import pytz from openerp import models, fields, api, _ from openerp.exceptions import UserError from datetime import datetime, timedelta from openerp.osv import expression # this variable is used for shift confirmation. It tells how many days before # its date_begin a shift is confirmed SHIFT_CONFIRMATION_DAYS = 5 WEEK_NUMBERS = [ (1, 'A'), (2, 'B'), (3, 'C'), (4, 'D') ] class ShiftShift(models.Model): _inherit = 'event.event' _name = 'shift.shift' _description = 'Shift Template' @api.model def _default_shift_mail_ids(self): return [(0, 0, { 'interval_unit': 'now', 'interval_type': 'after_sub', 'template_id': self.env.ref('coop_shift.shift_subscription') })] name = fields.Char(string="Shift Name") event_mail_ids = fields.One2many(default=None) shift_mail_ids = fields.One2many( 'shift.mail', 'shift_id', string='Mail Schedule', default=lambda self: self._default_shift_mail_ids()) shift_type_id = fields.Many2one( 'shift.type', string='Category', required=False, readonly=False, states={'done': [('readonly', True)]}) week_number = fields.Selection( WEEK_NUMBERS, string='Week', compute="_compute_week_number", store=True) week_list = fields.Selection([ ('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday') registration_ids = fields.One2many( 'shift.registration', 'shift_id', string='Attendees', readonly=False, states={'done': [('readonly', True)]}) shift_template_id = fields.Many2one( 'shift.template', string='Template', ondelete='restrict') seats_reserved = fields.Integer(compute='_compute_seats_shift') seats_available = fields.Integer(compute='_compute_seats_shift') seats_unconfirmed = fields.Integer(compute='_compute_seats_shift') seats_used = fields.Integer(compute='_compute_seats_shift') seats_expected = fields.Integer(compute='_compute_seats_shift') auto_confirm = fields.Boolean( string='Confirmation not required', compute='_compute_auto_confirm') event_ticket_ids = fields.One2many( default=lambda rec: rec._default_tickets()) shift_ticket_ids = fields.One2many( 'shift.ticket', 'shift_id', string='Shift Ticket', default=lambda rec: rec._default_shift_tickets(), copy=True) date_tz = fields.Selection('_tz_get', string='Timezone', default=False) date_begin = fields.Datetime( compute="_compute_date_begin", store=True, required=False) date_begin_tz = fields.Datetime(string='Begin Date Time') date_end = fields.Datetime( compute="_compute_date_end", store=True, required=False) date_end_tz = fields.Datetime(string='End Date Time') date_without_time = fields.Date( string='Date', compute='_compute_begin_date_fields', store=True, multi="begin_date") begin_date_string = fields.Char( string='Begin Date', compute='_compute_begin_date_fields', store=True, multi="begin_date") end_date_for_mail = fields.Char( string='End Date for mail', compute='_compute_end_date_fields', store=True, multi="end_date") begin_date_without_time_string = fields.Char( string='Begin Date for mail without time', multi="begin_date", compute='_compute_begin_date_fields') begin_time = fields.Float( string='Start Time', compute='_compute_begin_date_fields', store=True, multi="begin_date") begin_time_string = fields.Char( string='Start Time', compute='_compute_begin_date_fields', multi="begin_date") end_time = fields.Float( string='Start Time', compute='_compute_end_date_fields', store=True, multi="end_date") end_time_for_mail = fields.Char( string='End Time', compute='_compute_end_date_fields', multi="end_date") user_ids = fields.Many2many( 'res.partner', 'res_partner_shift_shift_rel', 'shift_template_id', 'partner_id', string='Shift Leaders') user_id = fields.Many2one(comodel_name='res.partner', default=False) seats_max = fields.Integer() _sql_constraints = [( 'template_date_uniq', 'unique (shift_template_id, date_begin, company_id)', 'The same template cannot be planned several time at the same date !'), ] @api.multi @api.depends('date_without_time') def _compute_week_number(self): for shift in self: if not shift.date_without_time: shift.week_number = False else: weekA_date = fields.Date.from_string( self.env.ref('coop_shift.config_parameter_weekA').value) start_date = fields.Date.from_string(shift.date_without_time) shift.week_number =\ 1 + (((start_date - weekA_date).days // 7) % 4) @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] domain = [] if name: domain = [ '|', ('begin_date_string', operator, name), ('name', operator, name) ] if operator in expression.NEGATIVE_TERM_OPERATORS: domain = ['&', '!'] + domain[1:] shifts = self.search(domain + args, limit=limit) return shifts.name_get() @api.multi @api.depends('name', 'date_begin') def name_get(self): result = [] for shift in self: name = shift.name + (shift.begin_date_string and (' ' + shift.begin_date_string) or '') result.append((shift.id, name)) return result @api.model def _default_tickets(self): return None @api.model def _default_shift_tickets(self): try: product = self.env.ref('coop_shift.product_product_shift_standard') product2 = self.env.ref('coop_shift.product_product_shift_ftop') return [ { 'name': _('Standard'), 'product_id': product.id, 'price': 0, }, { 'name': _('FTOP'), 'product_id': product2.id, 'price': 0, }] except ValueError: return self.env['shift.ticket'] @api.multi def _compute_auto_confirm(self): for shift in self: shift.auto_confirm = False @api.model def _default_event_mail_ids(self): return None @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats_shift(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 for shift in self: shift.seats_unconfirmed = shift.seats_reserved =\ shift.seats_used = shift.seats_available = 0 # aggregate registrations by shift and by state if self.ids: state_field = { 'draft': 'seats_reserved', 'open': 'seats_reserved', 'replacing': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT shift_id, state, count(shift_id) FROM shift_registration WHERE shift_id IN %s AND state IN ('draft', 'open', 'done', 'replacing') GROUP BY shift_id, state """ self._cr.execute(query, (tuple(self.ids),)) for shift_id, state, num in self._cr.fetchall(): shift = self.browse(shift_id) shift[state_field[state]] += num # compute seats_available for shift in self: if shift.seats_max > 0: shift.seats_available = shift.seats_max - ( shift.seats_reserved + shift.seats_used) shift.seats_expected = shift.seats_unconfirmed +\ shift.seats_reserved + shift.seats_used @api.multi def write(self, vals): special = self._context.get('special', False) if any(shift.state == "done" for shift in self): raise UserError(_( 'You can only repercute changes on draft shifts.')) res = super(ShiftShift, self).write(vals) if special: for field in special: if field == 'shift_ticket_ids': for shift in self: template = shift.shift_template_id ftop_ticket = template.shift_ticket_ids.filtered( lambda t: t.shift_type == 'ftop') standard_ticket = template.shift_ticket_ids.filtered( lambda t: t.shift_type == 'standard') ftop_seats_max = ftop_ticket and\ ftop_ticket[0].seats_max or False standard_seats_max = standard_ticket and\ standard_ticket[0].seats_max or False for ticket in shift.shift_ticket_ids: if ticket.shift_type == 'ftop': ticket.seats_max = ftop_seats_max if ticket.shift_type == 'standard': ticket.seats_max = standard_seats_max return res @api.onchange('shift_template_id') def _onchange_template_id(self): if self.shift_template_id: self.name = self.shift_template_id.name self.user_ids = self.shift_template_id.user_ids self.shift_type_id = self.shift_template_id.shift_type_id self.week_number = self.shift_template_id.week_number cur_date = self.date_begin and datetime.strptime( self.date_begin, "%Y-%m-%d %H:%M:%S").date() or\ datetime.strptime( self.shift_template_id.start_date, "%Y-%m-%d") self.date_begin = datetime.strftime( cur_date + timedelta( hours=self.shift_template_id.start_time), "%Y-%m-%d %H:%M:%S") self.date_end = datetime.strftime( cur_date + timedelta( hours=self.shift_template_id.end_time), "%Y-%m-%d %H:%M:%S") cur_attendees = [r.partner_id.id for r in self.registration_ids] vals = [] for attendee in self.shift_template_id.registration_ids: if attendee.id not in cur_attendees: vals.append((0, 0, { 'partner_id': attendee.id, 'state': 'draft', 'email': attendee.email, 'phone': attendee.phone, 'name': attendee.name, 'shift_id': self.id, })) self.registration_ids = vals @api.depends('date_begin_tz') @api.multi def _compute_date_begin(self): tz_name = self._context.get('tz') or self.env.user.tz if not tz_name: raise UserError(_( "You can not create Shift if your timezone is not defined.")) context_tz = pytz.timezone(tz_name) for shift in self: if shift.date_begin_tz: start_date_object_tz = fields.Datetime.from_string( shift.date_begin_tz) utc_timestamp = pytz.utc.localize( start_date_object_tz, is_dst=False) start_date_object = utc_timestamp.astimezone(context_tz) start_date = start_date_object_tz + timedelta( hours=start_date_object_tz.hour - start_date_object.hour) shift.date_begin = "%s-%02d-%02d %02d:%02d:%02d" % ( start_date.year, start_date.month, start_date.day, start_date.hour, start_date.minute, start_date.second, ) @api.depends('date_end_tz') @api.multi def _compute_date_end(self): tz_name = self._context.get('tz') or self.env.user.tz if not tz_name: raise UserError(_( "You can not create Shift if your timezone is not defined.")) context_tz = pytz.timezone(tz_name) for shift in self: if shift.date_end_tz: end_date_object_tz = fields.Datetime.from_string( shift.date_end_tz) utc_timestamp = pytz.utc.localize( end_date_object_tz, is_dst=False) end_date_object = utc_timestamp.astimezone(context_tz) end_date = end_date_object_tz + timedelta( hours=end_date_object_tz.hour - end_date_object.hour) shift.date_end = "%s-%02d-%02d %02d:%02d:%02d" % ( end_date.year, end_date.month, end_date.day, end_date.hour, end_date.minute, end_date.second, ) @api.multi @api.depends('date_begin_tz') def _compute_begin_date_fields(self): for shift in self: if shift.date_begin_tz: start_date_object_tz = datetime.strptime( shift.date_begin_tz, '%Y-%m-%d %H:%M:%S') shift.begin_time = ( start_date_object_tz.hour + (start_date_object_tz.minute / 60.0)) shift.begin_time_string = "%02d:%02d" % ( start_date_object_tz.hour, start_date_object_tz.minute, ) shift.begin_date_string = "%02d/%02d/%s %02d:%02d" % ( start_date_object_tz.day, start_date_object_tz.month, start_date_object_tz.year, start_date_object_tz.hour, start_date_object_tz.minute, ) shift.begin_date_without_time_string = "%02d/%02d/%s" % ( start_date_object_tz.day, start_date_object_tz.month, start_date_object_tz.year, ) shift.date_without_time = "%s-%02d-%02d" % ( start_date_object_tz.year, start_date_object_tz.month, start_date_object_tz.day, ) @api.multi @api.depends('date_end') def _compute_end_date_fields(self): tz_name = self._context.get('tz') or self.env.user.tz if not tz_name: raise UserError(_( "You can not create Shift if your timezone is not defined.")) for shift in self: if shift.date_end: shift.end_date_for_mail = datetime.strftime( fields.Datetime.from_string(shift.date_end), "%d/%m/%Y %H:%M") shift.end_time_for_mail = datetime.strftime( fields.Datetime.from_string(shift.date_end), "%H:%M") start_date_object = datetime.strptime( shift.date_end, '%Y-%m-%d %H:%M:%S') utc_timestamp = pytz.utc.localize( start_date_object, is_dst=False) context_tz = pytz.timezone(tz_name) start_date_object_tz = utc_timestamp.astimezone(context_tz) shift.end_time = ( start_date_object_tz.hour + (start_date_object_tz.minute / 60.0)) @api.multi def button_confirm(self): super(ShiftShift, self).button_confirm() @api.model def run_shift_confirmation(self): # This method is called by the cron task compare_date = fields.Date.to_string( datetime.today() + timedelta(days=SHIFT_CONFIRMATION_DAYS)) shifts = self.env['shift.shift'].search([ ('state', '=', 'draft'), ('date_begin', '<=', compare_date)]) shifts.button_confirm() @api.one @api.constrains('seats_max', 'seats_available') def _check_seats_limit(self): return True