# -*- coding: utf-8 -*- from odoo import api, fields, models, _ class PlanningExchange(models.Model): """Modèle pour les demandes d'échange de créneaux de planning via le portail.""" _name = 'portal.planning.exchange' _description = 'Échange de Planning via Portail' _rec_name = 'name' _order = 'date desc' _inherit = ['mail.thread', 'mail.activity.mixin'] name = fields.Char( string='Référence', required=True, copy=False, readonly=True, default=lambda self: _('Nouvel échange') ) date = fields.Datetime( string='Date de demande', required=True, default=fields.Datetime.now, tracking=True ) # Créneau source (celui que l'utilisateur souhaite échanger) source_slot_id = fields.Many2one( 'planning.slot', string='Créneau à échanger', required=True, ondelete='cascade', tracking=True ) source_employee_id = fields.Many2one( 'hr.employee', string='Employé source', related='source_slot_id.employee_id', store=True, readonly=True ) source_user_id = fields.Many2one( 'res.users', string='Demandeur', required=True, default=lambda self: self.env.user.id, tracking=True ) source_start = fields.Datetime( string='Début source', related='source_slot_id.start_datetime', store=True, readonly=True ) source_end = fields.Datetime( string='Fin source', related='source_slot_id.end_datetime', store=True, readonly=True ) source_role_id = fields.Many2one( 'planning.role', string='Rôle source', related='source_slot_id.role_id', store=True, readonly=True ) # Créneau cible (celui avec lequel l'utilisateur souhaite échanger) target_slot_id = fields.Many2one( 'planning.slot', string='Créneau souhaité', ondelete='cascade', tracking=True ) target_employee_id = fields.Many2one( 'hr.employee', string='Employé cible', related='target_slot_id.employee_id', store=True, readonly=True ) target_user_id = fields.Many2one( 'res.users', string='Destinataire', related='target_employee_id.user_id', store=True, readonly=True ) target_start = fields.Datetime( string='Début cible', related='target_slot_id.start_datetime', store=True, readonly=True ) target_end = fields.Datetime( string='Fin cible', related='target_slot_id.end_datetime', store=True, readonly=True ) target_role_id = fields.Many2one( 'planning.role', string='Rôle cible', related='target_slot_id.role_id', store=True, readonly=True ) # Dates libres (si l'utilisateur souhaite échanger avec n'importe qui) is_open_request = fields.Boolean( string='Demande ouverte', default=False, help="Si activé, la demande est ouverte à tous les employés disponibles" ) preferred_start = fields.Datetime( string='Début préféré', help="Date de début préférée pour l'échange (si demande ouverte)" ) preferred_end = fields.Datetime( string='Fin préférée', help="Date de fin préférée pour l'échange (si demande ouverte)" ) # Suivi de la demande state = fields.Selection([ ('draft', 'Brouillon'), ('pending', 'En attente'), ('accepted', 'Accepté par l\'employé'), ('approved', 'Approuvé'), ('rejected', 'Refusé'), ('cancelled', 'Annulé') ], string='État', default='draft', tracking=True, copy=False) approver_id = fields.Many2one( 'res.users', string='Approuvé/Refusé par', copy=False, tracking=True ) approval_date = fields.Datetime( string='Date d\'approbation/refus', copy=False, tracking=True ) notes = fields.Text( string='Notes', tracking=True ) company_id = fields.Many2one( 'res.company', string='Société', default=lambda self: self.env.company ) # Champs calculés can_submit = fields.Boolean( compute='_compute_can_submit', string='Peut soumettre', help="Indique si l'utilisateur actuel peut soumettre cette demande" ) can_accept = fields.Boolean( compute='_compute_can_accept', string='Peut accepter', help="Indique si l'utilisateur actuel peut accepter cette demande" ) can_approve = fields.Boolean( compute='_compute_can_approve', string='Peut approuver', help="Indique si l'utilisateur actuel peut approuver cette demande" ) can_reject = fields.Boolean( compute='_compute_can_reject', string='Peut refuser', help="Indique si l'utilisateur actuel peut refuser cette demande" ) can_cancel = fields.Boolean( compute='_compute_can_cancel', string='Peut annuler', help="Indique si l'utilisateur actuel peut annuler cette demande" ) @api.model_create_multi def create(self, vals_list): """Génère une séquence unique pour chaque nouvelle demande d'échange.""" for vals in vals_list: if vals.get('name', _('Nouvel échange')) == _('Nouvel échange'): vals['name'] = self.env['ir.sequence'].next_by_code('portal.planning.exchange') or _('Nouvel échange') return super(PlanningExchange, self).create(vals_list) @api.depends('state', 'source_user_id') def _compute_can_submit(self): """Détermine si l'utilisateur actuel peut soumettre cette demande.""" for exchange in self: exchange.can_submit = ( exchange.state == 'draft' and exchange.source_user_id.id == self.env.user.id ) @api.depends('state', 'target_user_id') def _compute_can_accept(self): """Détermine si l'utilisateur actuel peut accepter cette demande.""" for exchange in self: exchange.can_accept = ( exchange.state == 'pending' and exchange.target_user_id.id == self.env.user.id ) @api.depends('state') def _compute_can_approve(self): """Détermine si l'utilisateur actuel peut approuver cette demande.""" is_manager = self.env.user.has_group('planning.group_planning_manager') for exchange in self: exchange.can_approve = exchange.state == 'accepted' and is_manager @api.depends('state') def _compute_can_reject(self): """Détermine si l'utilisateur actuel peut refuser cette demande.""" is_manager = self.env.user.has_group('planning.group_planning_manager') for exchange in self: exchange.can_reject = exchange.state in ['pending', 'accepted'] and ( is_manager or exchange.target_user_id.id == self.env.user.id ) @api.depends('state', 'source_user_id') def _compute_can_cancel(self): """Détermine si l'utilisateur actuel peut annuler cette demande.""" is_manager = self.env.user.has_group('planning.group_planning_manager') for exchange in self: exchange.can_cancel = ( exchange.state in ['draft', 'pending', 'accepted'] and (exchange.source_user_id.id == self.env.user.id or is_manager) ) def action_submit(self): """Soumet la demande d'échange.""" self.ensure_one() if not self.can_submit: return False # Vérifier que le créneau source appartient bien à l'utilisateur if self.source_slot_id.employee_id.user_id.id != self.env.user.id: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Erreur'), 'message': _('Vous ne pouvez échanger que vos propres créneaux de planning.'), 'type': 'danger', 'sticky': False, } } # Vérifier que le créneau cible existe si ce n'est pas une demande ouverte if not self.is_open_request and not self.target_slot_id: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Erreur'), 'message': _('Veuillez sélectionner un créneau cible ou activer la demande ouverte.'), 'type': 'danger', 'sticky': False, } } # Vérifier que les dates préférées sont renseignées si c'est une demande ouverte if self.is_open_request and (not self.preferred_start or not self.preferred_end): return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Erreur'), 'message': _('Veuillez renseigner les dates préférées pour une demande ouverte.'), 'type': 'danger', 'sticky': False, } } # Mettre à jour l'état de la demande self.write({ 'state': 'pending', 'date': fields.Datetime.now() }) # Envoyer une notification au destinataire si ce n'est pas une demande ouverte if not self.is_open_request and self.target_user_id: self.message_post( body=_('%s souhaite échanger son créneau du %s avec votre créneau du %s') % ( self.source_employee_id.name, self.source_start.strftime('%d/%m/%Y %H:%M'), self.target_start.strftime('%d/%m/%Y %H:%M') ), message_type='notification', subtype_xmlid='mail.mt_comment', partner_ids=[self.target_user_id.partner_id.id] ) return True def action_accept(self): """Accepte la demande d'échange (par l'employé cible).""" self.ensure_one() if not self.can_accept: return False # Mettre à jour l'état de la demande self.write({ 'state': 'accepted', }) # Envoyer une notification au demandeur self.message_post( body=_('%s a accepté votre demande d\'échange. En attente d\'approbation par un responsable.') % self.target_employee_id.name, message_type='notification', subtype_xmlid='mail.mt_comment', partner_ids=[self.source_user_id.partner_id.id] ) return True def action_approve(self): """Approuve la demande d'échange (par un responsable).""" self.ensure_one() if not self.can_approve: return False # Échanger les employés des créneaux source_employee = self.source_slot_id.employee_id target_employee = self.target_slot_id.employee_id self.source_slot_id.write({ 'employee_id': target_employee.id, 'portal_confirmed': False, # Nécessite une nouvelle confirmation }) self.target_slot_id.write({ 'employee_id': source_employee.id, 'portal_confirmed': False, # Nécessite une nouvelle confirmation }) # Mettre à jour l'état de la demande self.write({ 'state': 'approved', 'approver_id': self.env.user.id, 'approval_date': fields.Datetime.now() }) # Envoyer des notifications aux employés self.message_post( body=_('Votre demande d\'échange a été approuvée par %s. Les créneaux ont été échangés.') % self.env.user.name, message_type='notification', subtype_xmlid='mail.mt_comment', partner_ids=[self.source_user_id.partner_id.id, self.target_user_id.partner_id.id] ) return True def action_reject(self): """Refuse la demande d'échange.""" self.ensure_one() if not self.can_reject: return False # Mettre à jour l'état de la demande self.write({ 'state': 'rejected', 'approver_id': self.env.user.id if self.env.user.has_group('planning.group_planning_manager') else False, 'approval_date': fields.Datetime.now() if self.env.user.has_group('planning.group_planning_manager') else False }) # Envoyer une notification au demandeur if self.env.user.id == self.target_user_id.id: message = _('%s a refusé votre demande d\'échange.') % self.target_employee_id.name else: message = _('Votre demande d\'échange a été refusée par %s.') % self.env.user.name self.message_post( body=message, message_type='notification', subtype_xmlid='mail.mt_comment', partner_ids=[self.source_user_id.partner_id.id] ) return True def action_cancel(self): """Annule la demande d'échange.""" self.ensure_one() if not self.can_cancel: return False # Mettre à jour l'état de la demande self.write({ 'state': 'cancelled', }) # Envoyer une notification if self.env.user.id == self.source_user_id.id: message = _('Vous avez annulé votre demande d\'échange.') partners = [] if self.target_user_id: partners.append(self.target_user_id.partner_id.id) else: message = _('Votre demande d\'échange a été annulée par %s.') % self.env.user.name partners = [self.source_user_id.partner_id.id] if partners: self.message_post( body=message, message_type='notification', subtype_xmlid='mail.mt_comment', partner_ids=partners ) return True