bemade-addons/portal_planning/controllers/planning_slot.py
2025-06-19 11:31:33 -04:00

652 lines
30 KiB
Python

# -*- coding: utf-8 -*-
from odoo import http, _, fields
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
from odoo.exceptions import AccessError, MissingError, UserError
from odoo.http import request
from odoo import fields
from datetime import datetime, timedelta, time, date
from odoo.tools import format_duration
import logging
import calendar
import dateutil.utils as date_utils
_logger = logging.getLogger(__name__)
def get_employee_from_user(user):
"""Helper method to find the employee associated with the current user.
This handles both internal users and portal users.
"""
employee = user.employee_id
_logger.info("Recherche d'employé pour l'utilisateur %s (ID: %s)", user.name, user.id)
_logger.info("Méthode 1 (employee_id): %s", employee.id if employee else None)
if not employee:
# Chercher un employé directement lié à cet utilisateur
employee = request.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1)
_logger.info("Méthode 2 (user_id): %s", employee.id if employee else None)
if not employee:
# Essayer de trouver via la relation inverse depuis res.users
if hasattr(user, 'employee_ids') and user.employee_ids:
employee = user.employee_ids[0]
_logger.info("Méthode 3 (employee_ids): %s", employee.id if employee else None)
# Dernière tentative: chercher dans tous les employés si l'un d'eux a le même email
if not employee and user.email:
employee = request.env['hr.employee'].sudo().search([('work_email', '=', user.email)], limit=1)
_logger.info("Méthode 4 (work_email): %s", employee.id if employee else None)
_logger.info("Employé final trouvé: %s (ID: %s)", employee.name if employee else None, employee.id if employee else None)
_logger.info(f"Employee found for user {user.name} (ID: {user.id}): {employee.name if employee else 'None'}")
return employee
class PlanningSlotPortal(CustomerPortal):
@http.route(['/my/planning/slots', '/my/planning/slots/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_planning_slots(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw):
"""Afficher la liste des créneaux de planning de l'utilisateur."""
values = self._prepare_portal_layout_values()
# Récupérer l'employé associé à l'utilisateur connecté
employee = get_employee_from_user(request.env.user)
if not employee:
values.update({
'planning_slots': [],
'page_name': 'planning_slots',
'default_url': '/my/planning/slots',
'pager': {'page_count': 1, 'page': {'url': '/my/planning/slots'}},
'searchbar_sortings': {},
'sortby': 'date',
'searchbar_filters': {},
'filterby': 'all',
'no_employee': True
})
return request.render("portal_planning.portal_my_planning_slots", values)
domain = [
('employee_id', '=', employee.id)
]
# Filtrer par date si spécifié
if date_begin and date_end:
domain += [
('start_datetime', '>=', date_begin),
('end_datetime', '<=', date_end)
]
# Filtrer par état
searchbar_filters = {
'all': {'label': _('Tous'), 'domain': []},
'draft': {'label': _('À confirmer'), 'domain': [('portal_status', '=', 'draft')]},
'confirmed': {'label': _('Confirmés'), 'domain': [('portal_status', '=', 'confirmed')]},
'modified': {'label': _('Modifiés'), 'domain': [('portal_status', '=', 'modified')]},
'pending': {'label': _('En attente'), 'domain': [('portal_status', '=', 'pending_approval')]},
}
# Filtre par défaut
if not filterby:
filterby = 'all'
domain += searchbar_filters[filterby]['domain']
# Options de tri
searchbar_sortings = {
'date': {'label': _('Date'), 'order': 'start_datetime desc'},
'role': {'label': _('Rôle'), 'order': 'role_id'},
'status': {'label': _('Statut'), 'order': 'portal_status'},
}
# Tri par défaut
if not sortby:
sortby = 'date'
order = searchbar_sortings[sortby]['order']
# Pagination
planning_slots_count = request.env['planning.slot'].sudo().search_count(domain)
pager = portal_pager(
url="/my/planning/slots",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby},
total=planning_slots_count,
page=page,
step=self._items_per_page
)
# Récupérer les créneaux de planning
planning_slots = request.env['planning.slot'].sudo().search(
domain,
order=order,
limit=self._items_per_page,
offset=pager['offset']
)
# Préparer les valeurs pour le template
values.update({
'date': date_begin,
'planning_slots': planning_slots,
'page_name': 'planning_slots',
'pager': pager,
'default_url': '/my/planning/slots',
'searchbar_sortings': searchbar_sortings,
'searchbar_filters': searchbar_filters,
'sortby': sortby,
'filterby': filterby,
'format_duration': format_duration,
})
return request.render("portal_planning.portal_my_planning_slots", values)
@http.route(['/my/planning/slot/create'], type='http', auth="user", website=True)
def portal_planning_slot_create(self, date=None, **kw):
"""Page de création d'un créneau de planning"""
values = self._prepare_portal_layout_values()
employee = get_employee_from_user(request.env.user)
if not employee:
values.update({
'page_name': 'planning_slot_create',
'no_employee': True
})
return request.render("portal_planning.portal_my_planning_slots", values)
# Préparer les valeurs par défaut
default_values = {}
if date:
try:
selected_date = datetime.strptime(date, '%Y-%m-%d').date()
# Heures par défaut (8h à 17h)
default_start = datetime.combine(selected_date, time(8, 0))
default_end = datetime.combine(selected_date, time(17, 0))
default_values.update({
'start_datetime': default_start,
'end_datetime': default_end,
})
except ValueError:
pass
# Récupérer les rôles disponibles
roles = request.env['planning.role'].sudo().search([])
values.update({
'page_name': 'planning_slot_create',
'employee': employee,
'roles': roles,
'default_values': default_values,
})
return request.render("portal_planning.portal_planning_slot_create", values)
@http.route(['/my/planning/slot/submit'], type='http', auth="user", website=True, methods=['POST'], csrf=True)
def portal_planning_slot_submit(self, **kw):
"""Traiter la soumission du formulaire de création de créneau"""
# Récupérer directement l'employé de l'utilisateur courant
employee = get_employee_from_user(request.env.user)
# Vérifier que l'employé existe
if not employee:
_logger.warning("Aucun employé trouvé pour l'utilisateur %s (ID: %s)",
request.env.user.name, request.env.user.id)
return request.redirect('/my/planning/slots')
employee_id = employee.id
_logger.info("Utilisation de l'employé %s (ID: %s) pour la création du créneau",
employee.name, employee_id)
# Convertir les dates
try:
start_datetime = datetime.strptime(kw.get('start_datetime'), '%Y-%m-%dT%H:%M')
end_datetime = datetime.strptime(kw.get('end_datetime'), '%Y-%m-%dT%H:%M')
except (ValueError, TypeError):
return request.redirect('/my/planning/slots')
# Récupérer la ressource associée à l'employé
resource = request.env['resource.resource'].sudo().search([('employee_id', '=', employee_id)], limit=1)
if not resource:
_logger.warning("Aucune ressource trouvée pour l'employé %s (ID: %s)", employee.name, employee_id)
return request.redirect('/my/planning/slots')
_logger.info("Ressource trouvée pour l'employé %s (ID: %s): %s (ID: %s)",
employee.name, employee_id, resource.name, resource.id)
# Créer le créneau de planning avec resource_id au lieu de employee_id
vals = {
'resource_id': resource.id, # Utiliser resource_id au lieu de employee_id
'start_datetime': start_datetime,
'end_datetime': end_datetime,
'allocated_hours': (end_datetime - start_datetime).total_seconds() / 3600,
'name': kw.get('name', ''),
# Champs spécifiques à la création via le portail
'portal_created': True,
'portal_creation_date': fields.Datetime.now(),
'portal_creation_user_id': request.env.user.id,
'portal_creation_approved': 'pending',
}
# Débogage des valeurs avant création
_logger.info("Valeurs pour la création du créneau: %s", vals)
# Ajouter le rôle si spécifié
role_id = kw.get('role_id')
if role_id and role_id.isdigit() and int(role_id) > 0:
vals['role_id'] = int(role_id)
# Créer le créneau
try:
slot = request.env['planning.slot'].sudo().create(vals)
_logger.info("Créneau de planning créé avec succès: ID=%s, start=%s, end=%s, employee_id=%s",
slot.id, slot.start_datetime, slot.end_datetime, slot.employee_id.id)
# Vérifier si le créneau est bien en base de données
created_slot = request.env['planning.slot'].sudo().browse(slot.id)
if created_slot.exists():
_logger.info("Créneau %s existe en base de données avec employee_id=%s",
created_slot.id, created_slot.employee_id.id)
else:
_logger.warning("Créneau %s n'existe pas en base de données après création", slot.id)
# Vérifier si le créneau est visible dans une recherche directe
domain = [
('id', '=', slot.id)
]
found_slots = request.env['planning.slot'].sudo().search(domain)
_logger.info("Recherche directe du créneau %s: trouvé %s créneaux", slot.id, len(found_slots))
except Exception as e:
_logger.error("Erreur lors de la création du créneau de planning: %s", str(e))
return request.redirect('/my/planning/slots')
# Rediriger vers la vue hebdomadaire
return request.redirect('/my/planning/week?date_start=%s' % start_datetime.date().strftime('%Y-%m-%d'))
@http.route(['/my/planning/slot/<int:slot_id>'], type='http', auth="user", website=True)
def portal_planning_slot_detail(self, slot_id, **kw):
"""Afficher le détail d'un créneau de planning."""
try:
slot_sudo = request.env['planning.slot'].sudo().browse(slot_id)
employee = get_employee_from_user(request.env.user)
if not slot_sudo.exists() or not employee or slot_sudo.employee_id.id != employee.id:
_logger.warning(f"Access denied to slot {slot_id} for user {request.env.user.id}. Employee: {employee.id if employee else 'None'}, Slot employee: {slot_sudo.employee_id.id if slot_sudo.exists() else 'None'}")
return request.redirect('/my/planning/slots')
except (AccessError, MissingError) as e:
_logger.error(f"Error accessing slot {slot_id}: {str(e)}")
return request.redirect('/my/planning/slots')
values = self._prepare_portal_layout_values()
values.update({
'slot': slot_sudo,
'page_name': 'planning_slot_detail',
'format_duration': format_duration,
})
return request.render("portal_planning.portal_planning_slot", values)
@http.route(['/my/planning/slot/<int:slot_id>/confirm'], type='http', auth="user", website=True)
def portal_planning_slot_confirm(self, slot_id, **kw):
"""Confirmer un créneau de planning."""
try:
slot_sudo = request.env['planning.slot'].sudo().browse(slot_id)
employee = get_employee_from_user(request.env.user)
if not slot_sudo.exists() or not employee or slot_sudo.employee_id.id != employee.id:
_logger.warning(f"Access denied to confirm slot {slot_id} for user {request.env.user.id}")
return request.redirect('/my/planning/slots')
# Vérifier si le créneau peut être confirmé
if not slot_sudo.portal_confirmed:
slot_sudo.action_confirm_portal()
except (AccessError, MissingError):
return request.redirect('/my/planning/slots')
except UserError as e:
return request.render('portal_planning.portal_planning_error', {
'error': str(e),
'back_url': '/my/planning/slot/%s' % slot_id,
})
return request.redirect('/my/planning/slot/%s' % slot_id)
@http.route(['/my/planning/slot/<int:slot_id>/modify'], type='http', auth="user", website=True, methods=['GET', 'POST'])
def portal_planning_slot_modify(self, slot_id, **kw):
"""Modifier un créneau de planning."""
try:
slot_sudo = request.env['planning.slot'].sudo().browse(slot_id)
employee = get_employee_from_user(request.env.user)
if not slot_sudo.exists() or not employee or slot_sudo.employee_id.id != employee.id:
_logger.warning(f"Access denied to modify slot {slot_id} for user {request.env.user.id}")
return request.redirect('/my/planning/slots')
# Vérifier si le créneau peut être modifié
if not slot_sudo.portal_can_modify:
return request.redirect('/my/planning/slot/%s' % slot_id)
except (AccessError, MissingError):
return request.redirect('/my/planning/slots')
if request.httprequest.method == 'POST':
# Validation des données du formulaire
start_datetime = kw.get('start_datetime')
end_datetime = kw.get('end_datetime')
role_id = kw.get('role_id')
notes = kw.get('notes')
if not start_datetime or not end_datetime:
return request.render('portal_planning.portal_planning_slot_modify', {
'error': _('Veuillez remplir tous les champs obligatoires.'),
'slot': slot_sudo,
'roles': request.env['planning.role'].sudo().search([]),
})
# Conversion des dates
try:
start_dt = datetime.strptime(start_datetime, '%Y-%m-%dT%H:%M')
end_dt = datetime.strptime(end_datetime, '%Y-%m-%dT%H:%M')
except ValueError:
return request.render('portal_planning.portal_planning_slot_modify', {
'error': _('Format de date invalide.'),
'slot': slot_sudo,
'roles': request.env['planning.role'].sudo().search([]),
})
# Vérification que la date de fin est après la date de début
if end_dt <= start_dt:
return request.render('portal_planning.portal_planning_slot_modify', {
'error': _('La date de fin doit être postérieure à la date de début.'),
'slot': slot_sudo,
'roles': request.env['planning.role'].sudo().search([]),
})
# Préparation des valeurs pour la modification
vals = {
'start_datetime': start_dt,
'end_datetime': end_dt,
'portal_modification_notes': notes,
}
if role_id and role_id != '':
vals['role_id'] = int(role_id)
try:
# Appliquer la modification
slot_sudo.action_modify_portal(vals)
return request.redirect('/my/planning/slot/%s' % slot_id)
except UserError as e:
return request.render('portal_planning.portal_planning_slot_modify', {
'error': str(e),
'slot': slot_sudo,
'roles': request.env['planning.role'].sudo().search([]),
})
# Affichage du formulaire de modification
values = self._prepare_portal_layout_values()
values.update({
'slot': slot_sudo,
'roles': request.env['planning.role'].sudo().search([]),
'page_name': 'planning_slot_modify',
})
return request.render("portal_planning.portal_planning_slot_modify", values)
@http.route(['/my/planning/slot/<int:slot_id>/exchange'], type='http', auth="user", website=True, methods=['GET', 'POST'])
def portal_planning_slot_exchange(self, slot_id, **kw):
"""Créer une demande d'échange pour un créneau de planning."""
try:
slot_sudo = request.env['planning.slot'].sudo().browse(slot_id)
employee = get_employee_from_user(request.env.user)
if not slot_sudo.exists() or not employee or slot_sudo.employee_id.id != employee.id:
_logger.warning(f"Access denied to exchange slot {slot_id} for user {request.env.user.id}")
return request.redirect('/my/planning/slots')
except (AccessError, MissingError):
return request.redirect('/my/planning/slots')
if request.httprequest.method == 'POST':
# Validation des données du formulaire
is_open_request = kw.get('is_open_request') == 'on'
target_slot_id = kw.get('target_slot_id')
preferred_start = kw.get('preferred_start')
preferred_end = kw.get('preferred_end')
notes = kw.get('notes')
# Vérification des champs requis selon le type de demande
if is_open_request:
if not preferred_start or not preferred_end:
return request.render('portal_planning.portal_planning_slot_exchange', {
'error': _('Veuillez spécifier les dates préférées pour votre demande ouverte.'),
'slot': slot_sudo,
'available_slots': request.env['planning.slot'].sudo().search([
('employee_id.user_id', '!=', request.env.user.id),
('start_datetime', '>=', datetime.now())
]),
})
# Conversion des dates
try:
start_dt = datetime.strptime(preferred_start, '%Y-%m-%dT%H:%M')
end_dt = datetime.strptime(preferred_end, '%Y-%m-%dT%H:%M')
except ValueError:
return request.render('portal_planning.portal_planning_slot_exchange', {
'error': _('Format de date invalide.'),
'slot': slot_sudo,
'available_slots': request.env['planning.slot'].sudo().search([
('employee_id.user_id', '!=', request.env.user.id),
('start_datetime', '>=', datetime.now())
]),
})
# Vérification que la date de fin est après la date de début
if end_dt <= start_dt:
return request.render('portal_planning.portal_planning_slot_exchange', {
'error': _('La date de fin doit être postérieure à la date de début.'),
'slot': slot_sudo,
'available_slots': request.env['planning.slot'].sudo().search([
('employee_id.user_id', '!=', request.env.user.id),
('start_datetime', '>=', datetime.now())
]),
})
else:
if not target_slot_id:
return request.render('portal_planning.portal_planning_slot_exchange', {
'error': _('Veuillez sélectionner un créneau à échanger.'),
'slot': slot_sudo,
'available_slots': request.env['planning.slot'].sudo().search([
('employee_id.user_id', '!=', request.env.user.id),
('start_datetime', '>=', datetime.now())
]),
})
# Préparation des valeurs pour la demande d'échange
vals = {
'source_slot_id': slot_id,
'is_open_request': is_open_request,
'notes': notes,
}
if is_open_request:
# Récupérer les dates préférées depuis le formulaire
preferred_start = request.httprequest.form.get('preferred_start')
preferred_end = request.httprequest.form.get('preferred_end')
vals.update({
'preferred_start': preferred_start,
'preferred_end': preferred_end,
})
else:
# S'assurer que target_slot_id est un entier valide
if target_slot_id:
vals['target_slot_id'] = int(target_slot_id)
try:
# Créer la demande d'échange
exchange = request.env['portal.planning.exchange'].sudo().create(vals)
# Soumettre la demande
exchange.action_submit()
return request.redirect('/my/planning/exchanges')
except UserError as e:
return request.render('portal_planning.portal_planning_slot_exchange', {
'error': str(e),
'slot': slot_sudo,
'available_slots': request.env['planning.slot'].sudo().search([
('employee_id.user_id', '!=', request.env.user.id),
('start_datetime', '>=', datetime.now())
]),
})
# Affichage du formulaire d'échange
values = self._prepare_portal_layout_values()
values.update({
'slot': slot_sudo,
'available_slots': request.env['planning.slot'].sudo().search([
('employee_id.user_id', '!=', request.env.user.id),
('start_datetime', '>=', datetime.now())
]),
'page_name': 'planning_slot_exchange',
})
return request.render("portal_planning.portal_planning_slot_exchange", values)
@http.route(['/my/planning/week'], type='http', auth="user", website=True)
def portal_my_planning_week(self, date_start=None, **kw):
"""Afficher la vue hebdomadaire du planning."""
values = self._prepare_portal_layout_values()
# Récupérer l'employé associé à l'utilisateur connecté
employee = get_employee_from_user(request.env.user)
if not employee:
values.update({
'planning_slots': [],
'no_employee': True,
'page_name': 'planning_week',
'default_url': '/my/planning/week',
})
return request.render("portal_planning.portal_my_planning_slots", values)
# Déterminer la date de début de semaine (lundi)
today = fields.Date.today()
start_date = today
if date_start:
try:
start_date = fields.Date.from_string(date_start)
except ValueError:
start_date = today
# S'assurer que start_date n'est pas None
if not start_date:
start_date = today
# Si pas de date spécifiée, prendre le lundi de la semaine courante
weekday = start_date.weekday()
start_date = start_date - timedelta(days=weekday)
# Calculer la date de fin de semaine (dimanche)
end_date = start_date + timedelta(days=6)
# Convertir en datetime pour la recherche
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
# Récupérer les créneaux de planning pour la semaine
domain = [
('employee_id', '=', employee.id),
'|', '|',
'&', ('start_datetime', '>=', start_datetime), ('start_datetime', '<=', end_datetime),
'&', ('end_datetime', '>=', start_datetime), ('end_datetime', '<=', end_datetime),
'&', ('start_datetime', '<=', start_datetime), ('end_datetime', '>=', end_datetime),
]
# Ajouter des logs pour débogage
_logger.info("Recherche de créneaux pour la semaine du %s au %s", start_date, end_date)
_logger.info("Domaine de recherche: %s", domain)
slots = request.env['planning.slot'].sudo().search(domain, order='start_datetime')
# Ajouter des logs pour débogage
_logger.info("Créneaux trouvés pour la semaine: %s", slots.ids)
# Organiser les créneaux par jour
days = []
current_date = start_date
while current_date <= end_date:
day_slots = slots.filtered(lambda s: s.start_datetime.date() <= current_date and s.end_datetime.date() >= current_date)
days.append({
'date': current_date,
'name': current_date.strftime('%A'),
'day_slots': day_slots,
'is_today': current_date == today,
})
current_date += timedelta(days=1)
# Préparer les valeurs pour le template
values.update({
'employee': employee,
'days': days,
'start_date': start_date,
'end_date': end_date,
'prev_week': (start_date - timedelta(days=7)).strftime('%Y-%m-%d'),
'next_week': (start_date + timedelta(days=7)).strftime('%Y-%m-%d'),
'today': today.strftime('%Y-%m-%d'),
'page_name': 'planning_week',
'default_url': '/my/planning/week',
'format_duration': format_duration,
})
return request.render("portal_planning.portal_my_planning_slots", values)
@http.route(['/my/planning/day'], type='http', auth="user", website=True)
def portal_my_planning_day(self, date=None, **kw):
"""Afficher la vue quotidienne du planning."""
values = self._prepare_portal_layout_values()
# Récupérer l'employé associé à l'utilisateur connecté
employee = get_employee_from_user(request.env.user)
if not employee:
values.update({
'planning_slots': [],
'no_employee': True,
'page_name': 'planning_day',
'default_url': '/my/planning/day',
})
return request.render("portal_planning.portal_my_planning_slots", values)
# Déterminer la date à afficher
today = fields.Date.today()
selected_date = today
if date:
try:
selected_date = fields.Date.from_string(date)
except ValueError:
selected_date = today
# Convertir en datetime pour la recherche
start_datetime = datetime.combine(selected_date, datetime.min.time())
end_datetime = datetime.combine(selected_date, datetime.max.time())
# Récupérer les créneaux de planning pour le jour
domain = [
('employee_id', '=', employee.id),
'|',
'&', ('start_datetime', '>=', start_datetime), ('start_datetime', '<=', end_datetime),
'&', ('end_datetime', '>=', start_datetime), ('end_datetime', '<=', end_datetime),
]
slots = request.env['planning.slot'].sudo().search(domain, order='start_datetime')
# Préparer les valeurs pour le template
values.update({
'employee': employee,
'planning_slots': slots, # Utiliser la même clé que dans la vue standard
'selected_date': selected_date,
'prev_day': (selected_date - timedelta(days=1)).strftime('%Y-%m-%d'),
'next_day': (selected_date + timedelta(days=1)).strftime('%Y-%m-%d'),
'today': today.strftime('%Y-%m-%d'),
'is_today': selected_date == today,
'page_name': 'planning_day',
'default_url': '/my/planning/day',
'format_duration': format_duration,
'view_mode': 'day', # Indiquer le mode d'affichage
})
return request.render("portal_planning.portal_my_planning_slots", values)