- Fix preparation status calculation by ignoring cancelled orders, preventing false green 'stock ready' icon - Update item prepared state when preparation order is marked as processed - Automatically cancel unprepared preparation orders when intervention is canceled - Send alert notification to stock managers when canceled intervention has already prepared orders - Add alert banner with direct restock/cancellation action on preparation order detail views - Prevent editing material switches or creating orders for canceled interventions - Add and compile translations for FR, EN and NL - Add comprehensive automated unit tests
170 lines
7.8 KiB
Python
170 lines
7.8 KiB
Python
# interventions/services/stock_sync_service.py
|
|
import logging
|
|
from django.contrib.auth import get_user_model
|
|
from django.db.models import Q
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext as _
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_stock_managers_for_notification():
|
|
"""
|
|
Retourne la liste des utilisateurs à notifier lors d'un événement stock (magasiniers) :
|
|
- Utilisateurs ayant le rôle 'stock_manager'
|
|
- Utilisateurs ayant le droit 'can_manage_preparation_orders' sur au moins un entrepôt
|
|
- En repli, superutilisateurs ou administrateurs.
|
|
"""
|
|
User = get_user_model()
|
|
managers = User.objects.filter(
|
|
Q(config__roles__name='stock_manager') |
|
|
Q(config__userwarehouses__can_manage_preparation_orders=True),
|
|
is_active=True,
|
|
).distinct()
|
|
|
|
if not managers.exists():
|
|
managers = User.objects.filter(
|
|
Q(is_superuser=True) | Q(config__roles__name='admin'),
|
|
is_active=True,
|
|
).distinct()
|
|
|
|
return list(managers)
|
|
|
|
|
|
def sync_preparation_orders_on_intervention_cancel(intervention, user=None):
|
|
"""
|
|
Synchronise les bons de préparation liés lors de l'annulation d'une intervention :
|
|
1. Si le bon n'est pas encore préparé (statut 'to_process' ou 'pending', sans sorties ni articles préparés) :
|
|
-> Le bon est automatiquement annulé et les quantités réservées libérées.
|
|
-> Un message est posté dans la discussion du bon et un événement timeline est créé.
|
|
2. Si le bon est déjà préparé (statut 'processed' ou sorties exécutées ou articles préparés) :
|
|
-> Le bon n'est pas annulé automatiquement.
|
|
-> Une alerte est envoyée au(x) magasinier(s) pour effectuer un retour en stock.
|
|
-> Un message d'avertissement est consigné dans la discussion du bon et sur la timeline.
|
|
"""
|
|
if not intervention or not hasattr(intervention, 'preparation_orders'):
|
|
return
|
|
|
|
orders = intervention.preparation_orders.exclude(status='cancelled')
|
|
if not orders.exists():
|
|
return
|
|
|
|
from notifications.services import (
|
|
notify_users,
|
|
get_or_create_preparation_order_discussion,
|
|
post_message,
|
|
)
|
|
from interventions.models import InterventionTimeLine
|
|
|
|
stock_managers = None
|
|
|
|
for order in orders:
|
|
try:
|
|
is_prepared = getattr(order, 'is_already_prepared', False)
|
|
if not is_prepared:
|
|
# Vérification complémentaire
|
|
if order.status == 'processed':
|
|
is_prepared = True
|
|
elif order.stock_batches.filter(movement_type='out').exists():
|
|
is_prepared = True
|
|
elif order.preparationorderitem_set.filter(is_prepared=True).exists():
|
|
is_prepared = True
|
|
elif order.other_items.filter(is_prepared=True).exists():
|
|
is_prepared = True
|
|
|
|
if is_prepared:
|
|
# Le bon est déjà préparé : NE PAS l'annuler automatiquement, avertir le magasinier pour retour en stock
|
|
if stock_managers is None:
|
|
stock_managers = get_stock_managers_for_notification()
|
|
|
|
if stock_managers:
|
|
title = _("Retour en stock requis : intervention %(code)s annulée") % {'code': intervention.code}
|
|
content = _(
|
|
"L'intervention %(code)s a été annulée alors que le bon de préparation %(order)s est déjà préparé. "
|
|
"Veuillez effectuer le retour en stock du matériel."
|
|
) % {
|
|
'code': intervention.code,
|
|
'order': order.order_number,
|
|
}
|
|
try:
|
|
action_url = reverse('stock:preparation_order_detail', args=[order.pk])
|
|
except Exception:
|
|
action_url = ''
|
|
|
|
notify_users(
|
|
recipients=stock_managers,
|
|
title=title,
|
|
content=content,
|
|
notification_type='alert',
|
|
action_url=action_url,
|
|
action_label=str(_("Voir le bon de préparation")),
|
|
source_app='stock',
|
|
sender=user if (user and user.is_authenticated) else None,
|
|
)
|
|
|
|
# Message dans la discussion du bon de préparation
|
|
try:
|
|
discussion, _created = get_or_create_preparation_order_discussion(order)
|
|
post_message(
|
|
discussion=discussion,
|
|
author=user if (user and user.is_authenticated) else None,
|
|
content=_(
|
|
"ATTENTION : L'intervention %(code)s a été annulée. "
|
|
"Ce bon de préparation étant déjà préparé, un retour en stock du matériel doit être effectué par le magasinier."
|
|
) % {'code': intervention.code},
|
|
)
|
|
except Exception:
|
|
logger.exception("Erreur lors de l'ajout du message de discussion sur le bon #%s", order.pk)
|
|
|
|
# Événement timeline sur l'intervention
|
|
try:
|
|
InterventionTimeLine.objects.create(
|
|
intervention=intervention,
|
|
event_user=user if (user and user.is_authenticated) else None,
|
|
event_time=timezone.now(),
|
|
event_type='note',
|
|
event_description=_(
|
|
"Bon de préparation %(order)s déjà préparé : notification envoyée au magasinier pour retour en stock."
|
|
) % {'order': order.order_number},
|
|
)
|
|
except Exception:
|
|
logger.exception("Erreur lors de la création de la timeline pour le bon #%s", order.pk)
|
|
|
|
else:
|
|
# Le bon n'est pas préparé : annulation automatique
|
|
order.cancel()
|
|
|
|
# Message dans la discussion du bon
|
|
try:
|
|
discussion, _created = get_or_create_preparation_order_discussion(order)
|
|
post_message(
|
|
discussion=discussion,
|
|
author=user if (user and user.is_authenticated) else None,
|
|
content=_(
|
|
"Bon de préparation annulé automatiquement suite à l'annulation de l'intervention %(code)s."
|
|
) % {'code': intervention.code},
|
|
)
|
|
except Exception:
|
|
logger.exception("Erreur lors de l'ajout du message de discussion sur le bon #%s", order.pk)
|
|
|
|
# Événement timeline sur l'intervention
|
|
try:
|
|
InterventionTimeLine.objects.create(
|
|
intervention=intervention,
|
|
event_user=user if (user and user.is_authenticated) else None,
|
|
event_time=timezone.now(),
|
|
event_type='note',
|
|
event_description=_(
|
|
"Bon de préparation %(order)s annulé automatiquement suite à l'annulation de l'intervention."
|
|
) % {'order': order.order_number},
|
|
)
|
|
except Exception:
|
|
logger.exception("Erreur lors de la création de la timeline pour le bon #%s", order.pk)
|
|
|
|
except Exception:
|
|
logger.exception(
|
|
"Erreur lors de la synchronisation du bon de préparation #%s suite à l'annulation de l'intervention #%s",
|
|
order.pk,
|
|
intervention.pk,
|
|
)
|