feat: add stock management module, common models, and mobile views
This commit is contained in:
parent
ca4d1b3c1b
commit
f429838a39
9 changed files with 654 additions and 98 deletions
|
|
@ -411,7 +411,7 @@ class UserConfig(models.Model):
|
||||||
|
|
||||||
def can_manage_preparation_orders_any(self):
|
def can_manage_preparation_orders_any(self):
|
||||||
"""Vérifie si l'utilisateur peut gérer des bons de préparation sur au moins un entrepôt."""
|
"""Vérifie si l'utilisateur peut gérer des bons de préparation sur au moins un entrepôt."""
|
||||||
if self.user.is_superuser or self.has_role('admin'):
|
if self.user.is_superuser or self.has_role('admin') or self.has_role('stock_manager'):
|
||||||
return True
|
return True
|
||||||
if not self.userwarehouses.exists():
|
if not self.userwarehouses.exists():
|
||||||
if self.userlocations.exists():
|
if self.userlocations.exists():
|
||||||
|
|
@ -421,7 +421,7 @@ class UserConfig(models.Model):
|
||||||
|
|
||||||
def can_manage_preparation_orders_for(self, warehouse):
|
def can_manage_preparation_orders_for(self, warehouse):
|
||||||
"""Vérifie si l'utilisateur peut gérer des bons de préparation pour un entrepôt spécifique."""
|
"""Vérifie si l'utilisateur peut gérer des bons de préparation pour un entrepôt spécifique."""
|
||||||
if self.user.is_superuser or self.has_role('admin'):
|
if self.user.is_superuser or self.has_role('admin') or self.has_role('stock_manager'):
|
||||||
return True
|
return True
|
||||||
if not self.userwarehouses.exists():
|
if not self.userwarehouses.exists():
|
||||||
if self.userlocations.exists():
|
if self.userlocations.exists():
|
||||||
|
|
|
||||||
|
|
@ -988,11 +988,17 @@ def stock_preparation_order_detail_mobile(request, pk):
|
||||||
if request.method == 'POST' and request.FILES.getlist('documents'):
|
if request.method == 'POST' and request.FILES.getlist('documents'):
|
||||||
from stock.models import PreparationOrderDocument
|
from stock.models import PreparationOrderDocument
|
||||||
for f in request.FILES.getlist('documents'):
|
for f in request.FILES.getlist('documents'):
|
||||||
PreparationOrderDocument.objects.create(
|
doc = PreparationOrderDocument.objects.create(
|
||||||
preparation_order=preparation_order,
|
preparation_order=preparation_order,
|
||||||
file=f,
|
file=f,
|
||||||
name=f.name,
|
name=f.name,
|
||||||
)
|
)
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="document_added",
|
||||||
|
description=_("Ajout du document joint : {name}").format(name=doc.name or doc.filename()),
|
||||||
|
user=request.user,
|
||||||
|
details={'document_id': doc.pk, 'filename': doc.filename()},
|
||||||
|
)
|
||||||
messages.success(request, _('Document(s) ajouté(s) avec succès.'))
|
messages.success(request, _('Document(s) ajouté(s) avec succès.'))
|
||||||
return redirect('mobile:stock_preparation_order_detail_mobile', pk=pk)
|
return redirect('mobile:stock_preparation_order_detail_mobile', pk=pk)
|
||||||
|
|
||||||
|
|
@ -1056,10 +1062,12 @@ def stock_preparation_order_detail_mobile(request, pk):
|
||||||
'location_info': ", ".join(loc_names),
|
'location_info': ", ".join(loc_names),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
from stock.permissions import user_can_manage_preparation_orders, user_can_edit_preparation_order
|
||||||
try:
|
try:
|
||||||
user_config = request.user.config
|
user_config = request.user.config
|
||||||
can_manage = user_config.has_role('stock_manager') or user_config.has_role('admin') or user_config.can_manage_preparation_orders_any()
|
can_manage = user_can_manage_preparation_orders(request.user)
|
||||||
can_cancel = user_config.can_manage_preparation_orders_any()
|
can_edit = user_can_edit_preparation_order(request.user, preparation_order)
|
||||||
|
can_cancel = can_manage and preparation_order.status != 'cancelled'
|
||||||
can_manage_purchase = user_config.can_manage_purchase_orders_any()
|
can_manage_purchase = user_config.can_manage_purchase_orders_any()
|
||||||
can_manage_stock = (
|
can_manage_stock = (
|
||||||
request.user.is_superuser
|
request.user.is_superuser
|
||||||
|
|
@ -1069,10 +1077,14 @@ def stock_preparation_order_detail_mobile(request, pk):
|
||||||
)
|
)
|
||||||
except UserConfig.DoesNotExist:
|
except UserConfig.DoesNotExist:
|
||||||
can_manage = False
|
can_manage = False
|
||||||
|
can_edit = False
|
||||||
can_cancel = False
|
can_cancel = False
|
||||||
can_manage_purchase = False
|
can_manage_purchase = False
|
||||||
can_manage_stock = False
|
can_manage_stock = False
|
||||||
|
|
||||||
|
history_entries = preparation_order.history_entries.select_related('user').order_by('-created_at')
|
||||||
|
has_pending_stock_adjustments = any(it['delta'] != 0 for it in items_with_stock) or bool(deleted_items_to_return)
|
||||||
|
|
||||||
other_items_with_info = []
|
other_items_with_info = []
|
||||||
for oi in preparation_order.other_items.all():
|
for oi in preparation_order.other_items.all():
|
||||||
other_items_with_info.append({
|
other_items_with_info.append({
|
||||||
|
|
@ -1109,6 +1121,7 @@ def stock_preparation_order_detail_mobile(request, pk):
|
||||||
'items_with_stock': items_with_stock,
|
'items_with_stock': items_with_stock,
|
||||||
'other_items': other_items_with_info,
|
'other_items': other_items_with_info,
|
||||||
'can_manage': can_manage,
|
'can_manage': can_manage,
|
||||||
|
'can_edit': can_edit,
|
||||||
'can_cancel': can_cancel,
|
'can_cancel': can_cancel,
|
||||||
'can_manage_purchase': can_manage_purchase,
|
'can_manage_purchase': can_manage_purchase,
|
||||||
'can_manage_stock': can_manage_stock,
|
'can_manage_stock': can_manage_stock,
|
||||||
|
|
@ -1117,6 +1130,8 @@ def stock_preparation_order_detail_mobile(request, pk):
|
||||||
'has_executed_outputs': has_executed_outputs,
|
'has_executed_outputs': has_executed_outputs,
|
||||||
'is_reopened': is_reopened,
|
'is_reopened': is_reopened,
|
||||||
'deleted_items_to_return': deleted_items_to_return,
|
'deleted_items_to_return': deleted_items_to_return,
|
||||||
|
'history_entries': history_entries,
|
||||||
|
'has_pending_stock_adjustments': has_pending_stock_adjustments,
|
||||||
'discussion': _discussion,
|
'discussion': _discussion,
|
||||||
'discussion_messages': discussion_messages_mobile,
|
'discussion_messages': discussion_messages_mobile,
|
||||||
'can_post_discussion_message': can_post_discussion_message_mobile,
|
'can_post_discussion_message': can_post_discussion_message_mobile,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from django.utils.html import format_html
|
||||||
from .models import (
|
from .models import (
|
||||||
Product, ProductCategory, Warehouse, WarehouseLocation, Stock, StockMovement,
|
Product, ProductCategory, Warehouse, WarehouseLocation, Stock, StockMovement,
|
||||||
PurchaseOrder, PurchaseOrderItem, PreparationOrder, PreparationOrderItem,
|
PurchaseOrder, PurchaseOrderItem, PreparationOrder, PreparationOrderItem,
|
||||||
PreparationOrderOtherItem,
|
PreparationOrderOtherItem, PreparationOrderHistory,
|
||||||
ProductionOrder,
|
ProductionOrder,
|
||||||
SerializedItem, SerializedItemEvent,
|
SerializedItem, SerializedItemEvent,
|
||||||
)
|
)
|
||||||
|
|
@ -91,12 +91,23 @@ class PreparationOrderOtherItemInline(admin.TabularInline):
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
|
class PreparationOrderHistoryInline(admin.TabularInline):
|
||||||
|
model = PreparationOrderHistory
|
||||||
|
extra = 0
|
||||||
|
readonly_fields = ('created_at', 'author_short_name', 'action', 'description', 'details')
|
||||||
|
can_delete = False
|
||||||
|
|
||||||
|
def author_short_name(self, obj):
|
||||||
|
return obj.author_short_name
|
||||||
|
author_short_name.short_description = 'Auteur'
|
||||||
|
|
||||||
|
|
||||||
@admin.register(PreparationOrder)
|
@admin.register(PreparationOrder)
|
||||||
class PreparationOrderAdmin(admin.ModelAdmin):
|
class PreparationOrderAdmin(admin.ModelAdmin):
|
||||||
list_display = ('order_number', 'date', 'status', 'priority')
|
list_display = ('order_number', 'date', 'status', 'priority')
|
||||||
search_fields = ('order_number',)
|
search_fields = ('order_number',)
|
||||||
list_filter = ('status', 'date', 'priority')
|
list_filter = ('status', 'date', 'priority')
|
||||||
inlines = [PreparationOrderItemInline, PreparationOrderOtherItemInline]
|
inlines = [PreparationOrderItemInline, PreparationOrderOtherItemInline, PreparationOrderHistoryInline]
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
if obj: # editing an existing object
|
if obj: # editing an existing object
|
||||||
|
|
@ -104,6 +115,14 @@ class PreparationOrderAdmin(admin.ModelAdmin):
|
||||||
return self.readonly_fields
|
return self.readonly_fields
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PreparationOrderHistory)
|
||||||
|
class PreparationOrderHistoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('preparation_order', 'created_at', 'author_short_name', 'action', 'description')
|
||||||
|
list_filter = ('action', 'created_at')
|
||||||
|
search_fields = ('preparation_order__order_number', 'user_short_name', 'description')
|
||||||
|
readonly_fields = ('preparation_order', 'user', 'user_short_name', 'created_at', 'action', 'description', 'details')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(PreparationOrderItem)
|
@admin.register(PreparationOrderItem)
|
||||||
class PreparationOrderItemAdmin(admin.ModelAdmin):
|
class PreparationOrderItemAdmin(admin.ModelAdmin):
|
||||||
list_display = ('preparation_order', 'product', 'quantity', 'is_prepared', 'is_used')
|
list_display = ('preparation_order', 'product', 'quantity', 'is_prepared', 'is_used')
|
||||||
|
|
|
||||||
34
loko/stock/migrations/0003_preparationorderhistory.py
Normal file
34
loko/stock/migrations/0003_preparationorderhistory.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-29 11:03
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0002_preparationorderitem_is_used_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PreparationOrderHistory',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('user_short_name', models.CharField(blank=True, max_length=150, verbose_name='Nom court')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Date et heure')),
|
||||||
|
('action', models.CharField(choices=[('created', 'Création'), ('item_added', 'Ajout de produit'), ('quantity_changed', 'Modification de quantité'), ('item_removed', 'Suppression de produit'), ('other_item_added', "Ajout d'article personnalisé"), ('other_item_updated', "Modification d'article personnalisé"), ('other_item_removed', "Suppression d'article personnalisé"), ('status_changed', 'Changement de statut'), ('reopened', 'Réouverture'), ('processed', 'Traitement'), ('cancelled', 'Annulation'), ('loan_updated', 'Modification du prêt'), ('document_added', 'Ajout de document'), ('document_removed', 'Suppression de document'), ('other', 'Autre modification')], default='other', max_length=50, verbose_name='Action')),
|
||||||
|
('description', models.TextField(verbose_name='Description')),
|
||||||
|
('details', models.JSONField(blank=True, default=dict, verbose_name='Détails')),
|
||||||
|
('preparation_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history_entries', to='stock.preparationorder', verbose_name='Bon de préparation')),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='preparation_order_history_entries', to=settings.AUTH_USER_MODEL, verbose_name='Utilisateur')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Historique de bon de préparation',
|
||||||
|
'verbose_name_plural': 'Historiques de bons de préparation',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Sum, F
|
from django.db.models import Sum, F
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
@ -1673,6 +1674,21 @@ class PreparationOrder(models.Model):
|
||||||
def total_items_count(self):
|
def total_items_count(self):
|
||||||
return self.preparationorderitem_set.count() + self.other_items.count()
|
return self.preparationorderitem_set.count() + self.other_items.count()
|
||||||
|
|
||||||
|
def log_history(self, action, description, user=None, details=None):
|
||||||
|
"""Enregistre un événement dans l'historique du bon de préparation."""
|
||||||
|
from common.utils import get_short_name
|
||||||
|
user_short = ""
|
||||||
|
if user and getattr(user, "is_authenticated", False):
|
||||||
|
user_short = get_short_name(user)
|
||||||
|
return self.history_entries.create(
|
||||||
|
user=user if (user and getattr(user, "is_authenticated", False)) else None,
|
||||||
|
user_short_name=user_short,
|
||||||
|
action=action,
|
||||||
|
description=description,
|
||||||
|
details=details or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PreparationOrderItem(models.Model):
|
class PreparationOrderItem(models.Model):
|
||||||
|
|
@ -1866,6 +1882,134 @@ class PreparationOrderOtherItem(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PreparationOrderHistory(models.Model):
|
||||||
|
"""
|
||||||
|
Historique des modifications apportées à un bon de préparation.
|
||||||
|
Trace les ajouts, modifications de quantité, suppressions et changements de statut avec utilisateur (shortname) et date.
|
||||||
|
"""
|
||||||
|
ACTION_CHOICES = [
|
||||||
|
("created", _("Création")),
|
||||||
|
("item_added", _("Ajout de produit")),
|
||||||
|
("quantity_changed", _("Modification de quantité")),
|
||||||
|
("item_removed", _("Suppression de produit")),
|
||||||
|
("other_item_added", _("Ajout d'article personnalisé")),
|
||||||
|
("other_item_updated", _("Modification d'article personnalisé")),
|
||||||
|
("other_item_removed", _("Suppression d'article personnalisé")),
|
||||||
|
("status_changed", _("Changement de statut")),
|
||||||
|
("reopened", _("Réouverture")),
|
||||||
|
("processed", _("Traitement")),
|
||||||
|
("cancelled", _("Annulation")),
|
||||||
|
("loan_updated", _("Modification du prêt")),
|
||||||
|
("document_added", _("Ajout de document")),
|
||||||
|
("document_removed", _("Suppression de document")),
|
||||||
|
("other", _("Autre modification")),
|
||||||
|
]
|
||||||
|
|
||||||
|
preparation_order = models.ForeignKey(
|
||||||
|
PreparationOrder,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="history_entries",
|
||||||
|
verbose_name=_("Bon de préparation"),
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="preparation_order_history_entries",
|
||||||
|
verbose_name=_("Utilisateur"),
|
||||||
|
)
|
||||||
|
user_short_name = models.CharField(
|
||||||
|
max_length=150,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Nom court"),
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name=_("Date et heure"),
|
||||||
|
)
|
||||||
|
action = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=ACTION_CHOICES,
|
||||||
|
default="other",
|
||||||
|
verbose_name=_("Action"),
|
||||||
|
)
|
||||||
|
description = models.TextField(
|
||||||
|
verbose_name=_("Description"),
|
||||||
|
)
|
||||||
|
details = models.JSONField(
|
||||||
|
default=dict,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Détails"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Historique de bon de préparation")
|
||||||
|
verbose_name_plural = _("Historiques de bons de préparation")
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"[{self.created_at.strftime('%d/%m/%Y %H:%M')}] {self.author_short_name}: {self.description}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.user_short_name and self.user:
|
||||||
|
from common.utils import get_short_name
|
||||||
|
self.user_short_name = get_short_name(self.user)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def author_short_name(self):
|
||||||
|
if self.user:
|
||||||
|
from common.utils import get_short_name
|
||||||
|
name = get_short_name(self.user)
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
return self.user_short_name or _("Système")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def badge_class(self):
|
||||||
|
badges = {
|
||||||
|
"created": "bg-primary",
|
||||||
|
"item_added": "bg-success",
|
||||||
|
"quantity_changed": "bg-warning text-dark",
|
||||||
|
"item_removed": "bg-danger",
|
||||||
|
"other_item_added": "bg-info text-dark",
|
||||||
|
"other_item_updated": "bg-warning text-dark",
|
||||||
|
"other_item_removed": "bg-danger",
|
||||||
|
"status_changed": "bg-info text-dark",
|
||||||
|
"reopened": "bg-warning text-dark",
|
||||||
|
"processed": "bg-success",
|
||||||
|
"cancelled": "bg-secondary",
|
||||||
|
"loan_updated": "bg-primary",
|
||||||
|
"document_added": "bg-secondary",
|
||||||
|
"document_removed": "bg-secondary",
|
||||||
|
}
|
||||||
|
return badges.get(self.action, "bg-secondary")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon_class(self):
|
||||||
|
icons = {
|
||||||
|
"created": "bi-plus-circle-fill",
|
||||||
|
"item_added": "bi-box-seam-fill",
|
||||||
|
"quantity_changed": "bi-pencil-fill",
|
||||||
|
"item_removed": "bi-trash-fill",
|
||||||
|
"other_item_added": "bi-tag-fill",
|
||||||
|
"other_item_updated": "bi-pencil-square",
|
||||||
|
"other_item_removed": "bi-trash-fill",
|
||||||
|
"status_changed": "bi-arrow-repeat",
|
||||||
|
"reopened": "bi-arrow-counterclockwise",
|
||||||
|
"processed": "bi-check2-circle",
|
||||||
|
"cancelled": "bi-x-circle-fill",
|
||||||
|
"loan_updated": "bi-calendar-event-fill",
|
||||||
|
"document_added": "bi-paperclip",
|
||||||
|
"document_removed": "bi-trash",
|
||||||
|
}
|
||||||
|
return icons.get(self.action, "bi-clock-history")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# class ProductDesign(models.Model):
|
# class ProductDesign(models.Model):
|
||||||
# """
|
# """
|
||||||
# Conception de produit : une fiche/recette décrivant comment assembler
|
# Conception de produit : une fiche/recette décrivant comment assembler
|
||||||
|
|
|
||||||
|
|
@ -303,3 +303,36 @@ def user_can_manage_products(user):
|
||||||
config.has_role('admin')
|
config.has_role('admin')
|
||||||
or config.can_administer_any_warehouse()
|
or config.can_administer_any_warehouse()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def user_can_manage_preparation_orders(user, warehouse=None):
|
||||||
|
"""
|
||||||
|
Vérifie si l'utilisateur peut gérer des bons de préparation (création, traitement, annulation).
|
||||||
|
Autorisé pour les superutilisateurs, le rôle admin, le rôle stock_manager (magasinier),
|
||||||
|
ou les utilisateurs ayant la permission can_manage_preparation_orders.
|
||||||
|
"""
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
config = get_user_config(user)
|
||||||
|
if not config:
|
||||||
|
return False
|
||||||
|
if config.has_role('admin') or config.has_role('stock_manager'):
|
||||||
|
return True
|
||||||
|
if warehouse:
|
||||||
|
return config.can_manage_preparation_orders_for(warehouse)
|
||||||
|
return config.can_manage_preparation_orders_any()
|
||||||
|
|
||||||
|
|
||||||
|
def user_can_edit_preparation_order(user, preparation_order=None):
|
||||||
|
"""
|
||||||
|
Vérifie si l'utilisateur peut modifier un bon de préparation.
|
||||||
|
La modification est possible pour les gestionnaires et magasiniers,
|
||||||
|
même lorsque le bon est au statut 'traité' (processed).
|
||||||
|
Seul le statut 'annulé' (cancelled) est non modifiable.
|
||||||
|
"""
|
||||||
|
if preparation_order and preparation_order.status == 'cancelled':
|
||||||
|
return False
|
||||||
|
return user_can_manage_preparation_orders(user)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,21 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if preparation_order.status == 'processed' and has_pending_stock_adjustments %}
|
||||||
|
<div class="alert alert-warning border-warning shadow-sm mb-3" role="alert">
|
||||||
|
<div class="d-flex align-items-center mb-1">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill text-warning me-2 fs-4"></i>
|
||||||
|
<h6 class="alert-heading mb-0 fw-bold">{% translate "Ajustements de stock en attente !" %}</h6>
|
||||||
|
</div>
|
||||||
|
<p class="mb-2 small" style="font-size:0.82rem;">{% translate "Ce bon a été modifié après son traitement. Une régularisation du stock est nécessaire." %}</p>
|
||||||
|
{% if can_manage %}
|
||||||
|
<a href="{% url 'stock:preparation_order_process' preparation_order.pk %}" class="btn btn-warning btn-sm w-100">
|
||||||
|
<i class="bi bi-check-circle me-1"></i> {% translate "Régulariser les stocks maintenant" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Intervention liée -->
|
<!-- Intervention liée -->
|
||||||
{% if preparation_order.intervention %}
|
{% if preparation_order.intervention %}
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
|
|
@ -465,6 +480,11 @@
|
||||||
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#confirmProcessModalMobile">
|
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#confirmProcessModalMobile">
|
||||||
<i class="bi bi-check2-circle me-1"></i>{% translate "Signaler comme traité" %}
|
<i class="bi bi-check2-circle me-1"></i>{% translate "Signaler comme traité" %}
|
||||||
</button>
|
</button>
|
||||||
|
{% if can_edit %}
|
||||||
|
<a href="{% url 'stock:preparation_order_detail' preparation_order.pk %}?open_edit=1" class="btn btn-outline-warning">
|
||||||
|
<i class="bi bi-pencil-square me-1"></i>{% translate "Modifier les articles" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if can_manage_purchase %}
|
{% if can_manage_purchase %}
|
||||||
<a href="{% url 'stock:preparation_order_create_purchase' preparation_order.pk %}" class="btn btn-primary">
|
<a href="{% url 'stock:preparation_order_create_purchase' preparation_order.pk %}" class="btn btn-primary">
|
||||||
<i class="bi bi-cart-plus me-1"></i>{% translate "Créer un bon de commande" %}
|
<i class="bi bi-cart-plus me-1"></i>{% translate "Créer un bon de commande" %}
|
||||||
|
|
@ -480,6 +500,16 @@
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif preparation_order.status == 'processed' %}
|
{% elif preparation_order.status == 'processed' %}
|
||||||
|
{% if can_edit %}
|
||||||
|
<a href="{% url 'stock:preparation_order_detail' preparation_order.pk %}?open_edit=1" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil-square me-1"></i>{% translate "Modifier le bon" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_pending_stock_adjustments %}
|
||||||
|
<a href="{% url 'stock:preparation_order_process' preparation_order.pk %}" class="btn btn-warning">
|
||||||
|
<i class="bi bi-arrow-repeat me-1"></i>{% translate "Régulariser les stocks" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if can_manage_stock %}
|
{% if can_manage_stock %}
|
||||||
<a href="{% url 'mobile:new_stock_entry_mobile' %}?preparation_order={{ preparation_order.pk }}" class="btn btn-info text-white">
|
<a href="{% url 'mobile:new_stock_entry_mobile' %}?preparation_order={{ preparation_order.pk }}" class="btn btn-info text-white">
|
||||||
<i class="bi bi-arrow-return-left me-1"></i>{% translate "Créer un retour de stock" %}
|
<i class="bi bi-arrow-return-left me-1"></i>{% translate "Créer un retour de stock" %}
|
||||||
|
|
@ -564,6 +594,38 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Historique des modifications -->
|
||||||
|
<div class="section-title mt-3">
|
||||||
|
<i class="bi bi-clock-history me-1"></i>{% translate "Historique des modifications" %}
|
||||||
|
<span class="badge bg-secondary ms-1">{{ history_entries|length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
{% if history_entries %}
|
||||||
|
{% for entry in history_entries %}
|
||||||
|
<div class="py-2 border-bottom {% if forloop.last %}border-bottom-0{% endif %}">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span class="badge {{ entry.badge_class }}" style="font-size:0.75rem;">
|
||||||
|
<i class="bi {{ entry.icon_class }} me-1"></i>{{ entry.get_action_display }}
|
||||||
|
</span>
|
||||||
|
<small class="text-muted" style="font-size:0.75rem;">
|
||||||
|
{{ entry.created_at|date:"d/m/Y H:i" }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-baseline">
|
||||||
|
<span class="small fw-semibold text-secondary">
|
||||||
|
<i class="bi bi-person me-1"></i>{{ entry.author_short_name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="small text-dark mt-1" style="font-size:0.83rem;">
|
||||||
|
{{ entry.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted fst-italic mb-0 small text-center py-2">{% translate "Aucun historique disponible." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
const dropZone = document.getElementById('mob-drop-zone');
|
const dropZone = document.getElementById('mob-drop-zone');
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,23 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if preparation_order.status == 'processed' and has_pending_stock_adjustments %}
|
||||||
|
<div class="alert alert-warning border-warning shadow-sm mb-3" role="alert">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill text-warning me-3 fs-1"></i>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="alert-heading mb-1 fw-bold">{% translate "Ajustements de stock en attente !" %}</h5>
|
||||||
|
<p class="mb-2">{% translate "Ce bon de préparation a été modifié après son traitement initial. Les quantités demandées ont changé et nécessitent une régularisation du stock (sortie de stock additionnelle ou réintégration)." %}</p>
|
||||||
|
{% if can_manage %}
|
||||||
|
<a href="{% url 'stock:preparation_order_process' preparation_order.pk %}" class="btn btn-warning btn-sm">
|
||||||
|
<i class="bi bi-check-circle me-1"></i> {% translate "Régulariser les stocks maintenant" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Intervention liée -->
|
<!-- Intervention liée -->
|
||||||
{% if preparation_order.intervention %}
|
{% if preparation_order.intervention %}
|
||||||
<div class="card mb-3 shadow-sm">
|
<div class="card mb-3 shadow-sm">
|
||||||
|
|
@ -267,9 +284,16 @@
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="bi bi-box-seam"></i> {% translate "Produits demandés" %}
|
<i class="bi bi-box-seam"></i> {% translate "Produits demandés" %}
|
||||||
</h5>
|
</h5>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
<span class="badge bg-secondary">
|
<span class="badge bg-secondary">
|
||||||
{{ items_with_stock|length }} {% translate "article(s)" %}
|
{{ items_with_stock|length }} {% translate "article(s)" %}
|
||||||
</span>
|
</span>
|
||||||
|
{% if can_edit %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editPreparationOrderModal">
|
||||||
|
<i class="bi bi-pencil me-1"></i> {% translate "Modifier les articles" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
|
|
@ -323,7 +347,17 @@
|
||||||
{% if has_executed_outputs %}
|
{% if has_executed_outputs %}
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if preparation_order.status == 'processed' %}
|
{% if preparation_order.status == 'processed' %}
|
||||||
{% if item_data.gross_in > 0 and item_data.executed_qty == 0 %}
|
{% if item_data.delta > 0 %}
|
||||||
|
<span class="badge bg-warning text-dark fs-6" title="{% translate 'Complément à prélever' %}">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>+{{ item_data.delta }} {% translate "à prélever" %}
|
||||||
|
</span>
|
||||||
|
<br><small class="text-muted">{% translate "Sorti net" %}: {{ item_data.executed_qty }}</small>
|
||||||
|
{% elif item_data.delta < 0 %}
|
||||||
|
<span class="badge bg-info text-dark fs-6" title="{% translate 'Surplus à restituer en stock' %}">
|
||||||
|
<i class="bi bi-arrow-return-left me-1"></i>{{ item_data.delta }} {% translate "à restituer" %}
|
||||||
|
</span>
|
||||||
|
<br><small class="text-muted">{% translate "Sorti net" %}: {{ item_data.executed_qty }}</small>
|
||||||
|
{% elif item_data.gross_in > 0 and item_data.executed_qty == 0 %}
|
||||||
<span class="badge bg-info text-dark fs-6" title="{% translate 'Tous les articles ont été restitués au stock' %}">
|
<span class="badge bg-info text-dark fs-6" title="{% translate 'Tous les articles ont été restitués au stock' %}">
|
||||||
<i class="bi bi-arrow-return-left me-1"></i>{% translate "Restitué au stock" %} ({{ item_data.gross_in }})
|
<i class="bi bi-arrow-return-left me-1"></i>{% translate "Restitué au stock" %} ({{ item_data.gross_in }})
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -597,6 +631,60 @@
|
||||||
</div>
|
</div>
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
<!-- Historique des modifications -->
|
||||||
|
<div class="card mb-3 shadow-sm">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-clock-history me-1"></i> {% translate "Historique des modifications" %}
|
||||||
|
</h5>
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
{{ history_entries|length }} {% translate "événement(s)" %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if history_entries %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 170px;">{% translate "Date / Heure" %}</th>
|
||||||
|
<th style="width: 160px;">{% translate "Utilisateur" %}</th>
|
||||||
|
<th style="width: 160px;">{% translate "Action" %}</th>
|
||||||
|
<th>{% translate "Détails" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in history_entries %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap small text-muted">
|
||||||
|
<i class="bi bi-calendar3 me-1"></i>{{ entry.created_at|date:"d/m/Y H:i:s" }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="fw-semibold">
|
||||||
|
<i class="bi bi-person me-1 text-secondary"></i>{{ entry.author_short_name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {{ entry.badge_class }}">
|
||||||
|
<i class="bi {{ entry.icon_class }} me-1"></i>{{ entry.get_action_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ entry.description }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="p-3 text-muted text-center">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>{% translate "Aucun historique disponible pour ce bon." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
{% if can_manage %}
|
{% if can_manage %}
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
|
|
@ -640,7 +728,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if can_cancel %}
|
{% if can_edit %}
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="card h-100 border-warning">
|
<div class="card h-100 border-warning">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
|
|
@ -653,7 +741,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if can_cancel %}
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="card h-100 border-danger">
|
<div class="card h-100 border-danger">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
|
|
@ -672,6 +762,36 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif preparation_order.status == 'processed' %}
|
{% elif preparation_order.status == 'processed' %}
|
||||||
|
{% if can_edit %}
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100 border-primary">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-pencil-square text-primary" style="font-size: 2rem;"></i>
|
||||||
|
<h6 class="mt-2">{% translate "Modifier le bon de préparation" %}</h6>
|
||||||
|
<p class="text-muted small">{% translate "Modifier les quantités ou les articles du bon sans changer son statut" %}</p>
|
||||||
|
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editPreparationOrderModal">
|
||||||
|
<i class="bi bi-pencil me-1"></i> {% translate "Modifier le bon" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if has_pending_stock_adjustments and can_manage %}
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card h-100 border-warning bg-light">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-arrow-repeat text-warning" style="font-size: 2rem;"></i>
|
||||||
|
<h6 class="mt-2">{% translate "Régulariser les stocks" %}</h6>
|
||||||
|
<p class="text-muted small">{% translate "Des ajustements de stock sont nécessaires suite aux modifications apportées" %}</p>
|
||||||
|
<a href="{% url 'stock:preparation_order_process' preparation_order.pk %}" class="btn btn-warning">
|
||||||
|
<i class="bi bi-check-circle me-1"></i> {% translate "Régulariser maintenant" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if can_manage_stock %}
|
{% if can_manage_stock %}
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<div class="card h-100 border-info">
|
<div class="card h-100 border-info">
|
||||||
|
|
@ -738,7 +858,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if can_cancel %}
|
{% if can_edit %}
|
||||||
<!-- Modale de modification du bon de préparation -->
|
<!-- Modale de modification du bon de préparation -->
|
||||||
<div class="modal fade" id="editPreparationOrderModal" tabindex="-1" aria-labelledby="editPreparationOrderModalLabel" aria-hidden="true">
|
<div class="modal fade" id="editPreparationOrderModal" tabindex="-1" aria-labelledby="editPreparationOrderModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-xl" style="height:90vh;max-height:90vh;">
|
<div class="modal-dialog modal-xl" style="height:90vh;max-height:90vh;">
|
||||||
|
|
|
||||||
|
|
@ -3519,6 +3519,13 @@ def new_preparation_order(request):
|
||||||
name=f.name,
|
name=f.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="created",
|
||||||
|
description=_("Création du bon de préparation"),
|
||||||
|
user=request.user,
|
||||||
|
details={'order_number': preparation_order.order_number},
|
||||||
|
)
|
||||||
|
|
||||||
messages.success(request, _("Commande de préparation enregistrée."))
|
messages.success(request, _("Commande de préparation enregistrée."))
|
||||||
return redirect("stock:stock_movements")
|
return redirect("stock:stock_movements")
|
||||||
else:
|
else:
|
||||||
|
|
@ -3662,6 +3669,15 @@ def new_preparation_order_from_intervention(request, intervention_id):
|
||||||
name=f.name,
|
name=f.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="created",
|
||||||
|
description=_("Création du bon de préparation depuis l'intervention #{intervention_id}").format(
|
||||||
|
intervention_id=intervention.id
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={'order_number': preparation_order.order_number, 'intervention_id': intervention.id},
|
||||||
|
)
|
||||||
|
|
||||||
messages.success(request, _("Bon de préparation créé avec succès."))
|
messages.success(request, _("Bon de préparation créé avec succès."))
|
||||||
return redirect(redirect_target, intervention_id=intervention_id)
|
return redirect(redirect_target, intervention_id=intervention_id)
|
||||||
else:
|
else:
|
||||||
|
|
@ -3869,11 +3885,17 @@ def preparation_order_detail(request, pk):
|
||||||
# Ajout de documents via POST
|
# Ajout de documents via POST
|
||||||
if request.method == 'POST' and request.FILES.getlist('documents'):
|
if request.method == 'POST' and request.FILES.getlist('documents'):
|
||||||
for f in request.FILES.getlist('documents'):
|
for f in request.FILES.getlist('documents'):
|
||||||
PreparationOrderDocument.objects.create(
|
doc = PreparationOrderDocument.objects.create(
|
||||||
preparation_order=preparation_order,
|
preparation_order=preparation_order,
|
||||||
file=f,
|
file=f,
|
||||||
name=f.name,
|
name=f.name,
|
||||||
)
|
)
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="document_added",
|
||||||
|
description=_("Ajout du document joint : {name}").format(name=doc.name or doc.filename()),
|
||||||
|
user=request.user,
|
||||||
|
details={'document_id': doc.pk, 'filename': doc.filename()},
|
||||||
|
)
|
||||||
messages.success(request, _("Document(s) ajouté(s) avec succès."))
|
messages.success(request, _("Document(s) ajouté(s) avec succès."))
|
||||||
return redirect('stock:preparation_order_detail', pk=pk)
|
return redirect('stock:preparation_order_detail', pk=pk)
|
||||||
|
|
||||||
|
|
@ -3942,11 +3964,13 @@ def preparation_order_detail(request, pk):
|
||||||
})
|
})
|
||||||
|
|
||||||
# Vérifier les permissions
|
# Vérifier les permissions
|
||||||
|
from stock.permissions import user_can_manage_preparation_orders, user_can_edit_preparation_order
|
||||||
from common.models import UserConfig
|
from common.models import UserConfig
|
||||||
try:
|
try:
|
||||||
user_config = request.user.config
|
user_config = request.user.config
|
||||||
can_manage = request.user.is_superuser or user_config.has_role('admin') or user_config.can_manage_preparation_orders_any()
|
can_manage = user_can_manage_preparation_orders(request.user)
|
||||||
can_cancel = user_config.can_manage_preparation_orders_any()
|
can_edit = user_can_edit_preparation_order(request.user, preparation_order)
|
||||||
|
can_cancel = can_manage and preparation_order.status != 'cancelled'
|
||||||
can_manage_purchase = user_config.can_manage_purchase_orders_any()
|
can_manage_purchase = user_config.can_manage_purchase_orders_any()
|
||||||
can_manage_stock = (
|
can_manage_stock = (
|
||||||
request.user.is_superuser
|
request.user.is_superuser
|
||||||
|
|
@ -3956,10 +3980,14 @@ def preparation_order_detail(request, pk):
|
||||||
)
|
)
|
||||||
except UserConfig.DoesNotExist:
|
except UserConfig.DoesNotExist:
|
||||||
can_manage = False
|
can_manage = False
|
||||||
|
can_edit = False
|
||||||
can_cancel = False
|
can_cancel = False
|
||||||
can_manage_purchase = False
|
can_manage_purchase = False
|
||||||
can_manage_stock = False
|
can_manage_stock = False
|
||||||
|
|
||||||
|
history_entries = preparation_order.history_entries.select_related('user').order_by('-created_at')
|
||||||
|
has_pending_stock_adjustments = any(it['delta'] != 0 for it in items_with_stock) or bool(deleted_items_to_return)
|
||||||
|
|
||||||
# Enrichir les autres produits avec leur bon de confection éventuel
|
# Enrichir les autres produits avec leur bon de confection éventuel
|
||||||
other_items_with_info = []
|
other_items_with_info = []
|
||||||
for oi in preparation_order.other_items.all():
|
for oi in preparation_order.other_items.all():
|
||||||
|
|
@ -4002,6 +4030,7 @@ def preparation_order_detail(request, pk):
|
||||||
'items_with_stock': items_with_stock,
|
'items_with_stock': items_with_stock,
|
||||||
'other_items': other_items_with_info,
|
'other_items': other_items_with_info,
|
||||||
'can_manage': can_manage,
|
'can_manage': can_manage,
|
||||||
|
'can_edit': can_edit,
|
||||||
'can_cancel': can_cancel,
|
'can_cancel': can_cancel,
|
||||||
'can_manage_purchase': can_manage_purchase,
|
'can_manage_purchase': can_manage_purchase,
|
||||||
'can_manage_stock': can_manage_stock,
|
'can_manage_stock': can_manage_stock,
|
||||||
|
|
@ -4011,6 +4040,8 @@ def preparation_order_detail(request, pk):
|
||||||
'has_executed_outputs': has_executed_outputs,
|
'has_executed_outputs': has_executed_outputs,
|
||||||
'is_reopened': is_reopened,
|
'is_reopened': is_reopened,
|
||||||
'deleted_items_to_return': deleted_items_to_return,
|
'deleted_items_to_return': deleted_items_to_return,
|
||||||
|
'history_entries': history_entries,
|
||||||
|
'has_pending_stock_adjustments': has_pending_stock_adjustments,
|
||||||
'discussion': discussion,
|
'discussion': discussion,
|
||||||
'discussion_messages': discussion_messages,
|
'discussion_messages': discussion_messages,
|
||||||
'can_post_discussion_message': can_post_discussion_message,
|
'can_post_discussion_message': can_post_discussion_message,
|
||||||
|
|
@ -4060,15 +4091,8 @@ def preparation_order_cancel(request, pk):
|
||||||
Annule un bon de préparation et libère les quantités réservées.
|
Annule un bon de préparation et libère les quantités réservées.
|
||||||
Si le bon avait déjà fait l'objet de sorties de stock, réintègre l'ensemble des produits sortis.
|
Si le bon avait déjà fait l'objet de sorties de stock, réintègre l'ensemble des produits sortis.
|
||||||
"""
|
"""
|
||||||
# Vérifier les permissions
|
from stock.permissions import user_can_manage_preparation_orders
|
||||||
from common.models import UserConfig
|
if not user_can_manage_preparation_orders(request.user):
|
||||||
try:
|
|
||||||
user_config = request.user.config
|
|
||||||
can_manage = user_config.can_manage_preparation_orders_any()
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
can_manage = False
|
|
||||||
|
|
||||||
if not can_manage:
|
|
||||||
messages.error(request, _("Vous n'avez pas la permission d'annuler des bons de préparation."))
|
messages.error(request, _("Vous n'avez pas la permission d'annuler des bons de préparation."))
|
||||||
return redirect('stock:preparation_order_detail', pk=pk)
|
return redirect('stock:preparation_order_detail', pk=pk)
|
||||||
|
|
||||||
|
|
@ -4108,6 +4132,12 @@ def preparation_order_cancel(request, pk):
|
||||||
stock.save()
|
stock.save()
|
||||||
|
|
||||||
preparation_order.cancel()
|
preparation_order.cancel()
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="cancelled",
|
||||||
|
description=_("Annulation du bon de préparation (quantités réservées libérées, stock restitué le cas échéant)"),
|
||||||
|
user=request.user,
|
||||||
|
details={'status': 'cancelled'},
|
||||||
|
)
|
||||||
messages.success(request, _("Le bon de préparation #{} a été annulé.").format(preparation_order.order_number))
|
messages.success(request, _("Le bon de préparation #{} a été annulé.").format(preparation_order.order_number))
|
||||||
else:
|
else:
|
||||||
messages.error(request, _("Ce bon ne peut pas être annulé."))
|
messages.error(request, _("Ce bon ne peut pas être annulé."))
|
||||||
|
|
@ -4120,14 +4150,8 @@ def preparation_order_reopen(request, pk):
|
||||||
"""
|
"""
|
||||||
Réouvre un bon de préparation traité pour permettre sa modification (statut : to_process).
|
Réouvre un bon de préparation traité pour permettre sa modification (statut : to_process).
|
||||||
"""
|
"""
|
||||||
from common.models import UserConfig
|
from stock.permissions import user_can_manage_preparation_orders
|
||||||
try:
|
if not user_can_manage_preparation_orders(request.user):
|
||||||
user_config = request.user.config
|
|
||||||
can_manage = user_config.can_manage_preparation_orders_any()
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
can_manage = False
|
|
||||||
|
|
||||||
if not can_manage:
|
|
||||||
messages.error(request, _("Vous n'avez pas la permission de réouvrir des bons de préparation."))
|
messages.error(request, _("Vous n'avez pas la permission de réouvrir des bons de préparation."))
|
||||||
return redirect('stock:preparation_order_detail', pk=pk)
|
return redirect('stock:preparation_order_detail', pk=pk)
|
||||||
|
|
||||||
|
|
@ -4135,6 +4159,12 @@ def preparation_order_reopen(request, pk):
|
||||||
|
|
||||||
if preparation_order.status == 'processed':
|
if preparation_order.status == 'processed':
|
||||||
preparation_order.reopen()
|
preparation_order.reopen()
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="reopened",
|
||||||
|
description=_("Réouverture du bon de préparation (statut : À traiter)"),
|
||||||
|
user=request.user,
|
||||||
|
details={'status': 'to_process'},
|
||||||
|
)
|
||||||
messages.success(request, _("Le bon de préparation #{} a été réouvert (statut : À traiter).").format(preparation_order.order_number))
|
messages.success(request, _("Le bon de préparation #{} a été réouvert (statut : À traiter).").format(preparation_order.order_number))
|
||||||
return redirect(f"{reverse('stock:preparation_order_detail', args=[pk])}?open_edit=1")
|
return redirect(f"{reverse('stock:preparation_order_detail', args=[pk])}?open_edit=1")
|
||||||
else:
|
else:
|
||||||
|
|
@ -4149,21 +4179,18 @@ def preparation_order_reopen(request, pk):
|
||||||
def preparation_order_edit(request, pk):
|
def preparation_order_edit(request, pk):
|
||||||
"""
|
"""
|
||||||
API JSON pour modifier les articles d'un bon de préparation (ajout, suppression, modification de quantité).
|
API JSON pour modifier les articles d'un bon de préparation (ajout, suppression, modification de quantité).
|
||||||
Réservé aux utilisateurs avec le droit de gérer les bons de préparation.
|
Réservé aux utilisateurs avec le droit de gérer les bons de préparation (y compris magasiniers).
|
||||||
|
Possible aux statuts 'to_process', 'pending' ou 'processed'.
|
||||||
"""
|
"""
|
||||||
from common.models import UserConfig
|
from stock.permissions import user_can_edit_preparation_order
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
try:
|
|
||||||
user_config = request.user.config
|
|
||||||
if not user_config.can_manage_preparation_orders_any():
|
|
||||||
return JsonResponse({'error': _("Droits insuffisants.")}, status=403)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
return JsonResponse({'error': _("Droits insuffisants.")}, status=403)
|
|
||||||
|
|
||||||
preparation_order = get_object_or_404(PreparationOrder, pk=pk)
|
preparation_order = get_object_or_404(PreparationOrder, pk=pk)
|
||||||
|
|
||||||
if preparation_order.status not in ['to_process', 'pending']:
|
if not user_can_edit_preparation_order(request.user, preparation_order):
|
||||||
|
return JsonResponse({'error': _("Droits insuffisants ou bon non modifiable.")}, status=403)
|
||||||
|
|
||||||
|
if preparation_order.status not in ['to_process', 'pending', 'processed']:
|
||||||
return JsonResponse({'error': _("Ce bon ne peut plus être modifié.")}, status=400)
|
return JsonResponse({'error': _("Ce bon ne peut plus être modifié.")}, status=400)
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
|
|
@ -4223,6 +4250,23 @@ def preparation_order_edit(request, pk):
|
||||||
if not created:
|
if not created:
|
||||||
item.quantity += quantity
|
item.quantity += quantity
|
||||||
item.save()
|
item.save()
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="item_added",
|
||||||
|
description=_("Ajout de {qty} × {product} (nouvelle quantité : {total})").format(
|
||||||
|
qty=quantity, product=product.name, total=item.quantity
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={'product_id': product.pk, 'product_name': product.name, 'quantity_added': quantity, 'new_quantity': item.quantity},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="item_added",
|
||||||
|
description=_("Ajout du produit {product} (quantité : {qty})").format(
|
||||||
|
product=product.name, qty=quantity
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={'product_id': product.pk, 'product_name': product.name, 'quantity': quantity},
|
||||||
|
)
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'id': item.pk,
|
'id': item.pk,
|
||||||
'product_id': product.pk,
|
'product_id': product.pk,
|
||||||
|
|
@ -4253,6 +4297,22 @@ def preparation_order_edit(request, pk):
|
||||||
if prod_order and prod_order.status == ProductionOrder.STATUS_TO_PROCESS:
|
if prod_order and prod_order.status == ProductionOrder.STATUS_TO_PROCESS:
|
||||||
prod_order.quantity = quantity
|
prod_order.quantity = quantity
|
||||||
prod_order.save(update_fields=['quantity', 'updated_at'])
|
prod_order.save(update_fields=['quantity', 'updated_at'])
|
||||||
|
|
||||||
|
if quantity != old_quantity:
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="quantity_changed",
|
||||||
|
description=_("Quantité de {product} modifiée de {old_qty} à {new_qty}").format(
|
||||||
|
product=item.product.name, old_qty=old_quantity, new_qty=quantity
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={
|
||||||
|
'item_id': item.pk,
|
||||||
|
'product_id': item.product.pk,
|
||||||
|
'product_name': item.product.name,
|
||||||
|
'old_quantity': old_quantity,
|
||||||
|
'new_quantity': quantity,
|
||||||
|
},
|
||||||
|
)
|
||||||
return JsonResponse({'id': item.pk, 'quantity': item.quantity})
|
return JsonResponse({'id': item.pk, 'quantity': item.quantity})
|
||||||
|
|
||||||
elif action == 'remove_item':
|
elif action == 'remove_item':
|
||||||
|
|
@ -4260,6 +4320,10 @@ def preparation_order_edit(request, pk):
|
||||||
if not item_id:
|
if not item_id:
|
||||||
return JsonResponse({'error': _("Données invalides.")}, status=400)
|
return JsonResponse({'error': _("Données invalides.")}, status=400)
|
||||||
item = get_object_or_404(PreparationOrderItem, pk=item_id, preparation_order=preparation_order)
|
item = get_object_or_404(PreparationOrderItem, pk=item_id, preparation_order=preparation_order)
|
||||||
|
prod_name = item.product.name
|
||||||
|
prod_id = item.product.pk
|
||||||
|
removed_qty = item.quantity
|
||||||
|
|
||||||
# Vérifier si un bon de confection est lié
|
# Vérifier si un bon de confection est lié
|
||||||
prod_order = item.production_orders.first()
|
prod_order = item.production_orders.first()
|
||||||
if prod_order and prod_order.status != ProductionOrder.STATUS_TO_PROCESS:
|
if prod_order and prod_order.status != ProductionOrder.STATUS_TO_PROCESS:
|
||||||
|
|
@ -4279,6 +4343,15 @@ def preparation_order_edit(request, pk):
|
||||||
else:
|
else:
|
||||||
item.delete()
|
item.delete()
|
||||||
|
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="item_removed",
|
||||||
|
description=_("Suppression du produit {product} (quantité initiale : {qty})").format(
|
||||||
|
product=prod_name, qty=removed_qty
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={'product_id': prod_id, 'product_name': prod_name, 'quantity': removed_qty},
|
||||||
|
)
|
||||||
|
|
||||||
return JsonResponse({'deleted': item_id})
|
return JsonResponse({'deleted': item_id})
|
||||||
|
|
||||||
elif action == 'add_other_item':
|
elif action == 'add_other_item':
|
||||||
|
|
@ -4311,6 +4384,14 @@ def preparation_order_edit(request, pk):
|
||||||
category=category,
|
category=category,
|
||||||
sign_panel_type=sign_panel_type,
|
sign_panel_type=sign_panel_type,
|
||||||
)
|
)
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="other_item_added",
|
||||||
|
description=_("Ajout d'article personnalisé : {desc} (quantité : {qty})").format(
|
||||||
|
desc=description, qty=quantity
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={'description': description, 'quantity': quantity},
|
||||||
|
)
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'id': oi.pk,
|
'id': oi.pk,
|
||||||
'description': oi.description,
|
'description': oi.description,
|
||||||
|
|
@ -4331,6 +4412,8 @@ def preparation_order_edit(request, pk):
|
||||||
if prod_order and prod_order.status != ProductionOrder.STATUS_TO_PROCESS:
|
if prod_order and prod_order.status != ProductionOrder.STATUS_TO_PROCESS:
|
||||||
return JsonResponse({'error': _("Ce produit a un bon de confection en cours ou terminé ({code}, statut : {status}). Modification impossible.").format(
|
return JsonResponse({'error': _("Ce produit a un bon de confection en cours ou terminé ({code}, statut : {status}). Modification impossible.").format(
|
||||||
code=prod_order.code, status=prod_order.get_status_display())}, status=400)
|
code=prod_order.code, status=prod_order.get_status_display())}, status=400)
|
||||||
|
old_description = oi.description
|
||||||
|
old_qty = oi.quantity
|
||||||
description = (data.get('description') or '').strip()
|
description = (data.get('description') or '').strip()
|
||||||
quantity = data.get('quantity')
|
quantity = data.get('quantity')
|
||||||
if description:
|
if description:
|
||||||
|
|
@ -4363,6 +4446,23 @@ def preparation_order_edit(request, pk):
|
||||||
if quantity is not None and prod_order and prod_order.status == ProductionOrder.STATUS_TO_PROCESS:
|
if quantity is not None and prod_order and prod_order.status == ProductionOrder.STATUS_TO_PROCESS:
|
||||||
prod_order.quantity = oi.quantity
|
prod_order.quantity = oi.quantity
|
||||||
prod_order.save(update_fields=['quantity', 'updated_at'])
|
prod_order.save(update_fields=['quantity', 'updated_at'])
|
||||||
|
|
||||||
|
if old_description != oi.description or old_qty != oi.quantity:
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="other_item_updated",
|
||||||
|
description=_("Modification d'article personnalisé : {desc} (quantité : {qty})").format(
|
||||||
|
desc=oi.description, qty=oi.quantity
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={
|
||||||
|
'item_id': oi.pk,
|
||||||
|
'old_description': old_description,
|
||||||
|
'new_description': oi.description,
|
||||||
|
'old_quantity': old_qty,
|
||||||
|
'new_quantity': oi.quantity,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'id': oi.pk,
|
'id': oi.pk,
|
||||||
'description': oi.description,
|
'description': oi.description,
|
||||||
|
|
@ -4378,6 +4478,8 @@ def preparation_order_edit(request, pk):
|
||||||
if not oi_id:
|
if not oi_id:
|
||||||
return JsonResponse({'error': _("Données invalides.")}, status=400)
|
return JsonResponse({'error': _("Données invalides.")}, status=400)
|
||||||
oi = get_object_or_404(PreparationOrderOtherItem, pk=oi_id, preparation_order=preparation_order)
|
oi = get_object_or_404(PreparationOrderOtherItem, pk=oi_id, preparation_order=preparation_order)
|
||||||
|
oi_desc = oi.description
|
||||||
|
oi_qty = oi.quantity
|
||||||
# Vérifier si un bon de confection est lié
|
# Vérifier si un bon de confection est lié
|
||||||
prod_order = oi.production_orders.first()
|
prod_order = oi.production_orders.first()
|
||||||
if prod_order and prod_order.status != ProductionOrder.STATUS_TO_PROCESS:
|
if prod_order and prod_order.status != ProductionOrder.STATUS_TO_PROCESS:
|
||||||
|
|
@ -4387,6 +4489,14 @@ def preparation_order_edit(request, pk):
|
||||||
if prod_order and prod_order.status == ProductionOrder.STATUS_TO_PROCESS:
|
if prod_order and prod_order.status == ProductionOrder.STATUS_TO_PROCESS:
|
||||||
prod_order.cancel()
|
prod_order.cancel()
|
||||||
oi.delete()
|
oi.delete()
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="other_item_removed",
|
||||||
|
description=_("Suppression de l'article personnalisé : {desc} (quantité : {qty})").format(
|
||||||
|
desc=oi_desc, qty=oi_qty
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={'description': oi_desc, 'quantity': oi_qty},
|
||||||
|
)
|
||||||
return JsonResponse({'deleted': oi_id})
|
return JsonResponse({'deleted': oi_id})
|
||||||
|
|
||||||
return JsonResponse({'error': _("Action inconnue.")}, status=400)
|
return JsonResponse({'error': _("Action inconnue.")}, status=400)
|
||||||
|
|
@ -4396,38 +4506,37 @@ def preparation_order_edit(request, pk):
|
||||||
|
|
||||||
def preparation_order_process(request, pk):
|
def preparation_order_process(request, pk):
|
||||||
"""
|
"""
|
||||||
Traite un bon de préparation - permet de sortir les produits du stock
|
Traite un bon de préparation - permet de sortir les produits du stock.
|
||||||
|
Accessible également au statut 'processed' pour régulariser les stocks après modification.
|
||||||
"""
|
"""
|
||||||
# Vérifier les permissions
|
# Vérifier les permissions
|
||||||
try:
|
from stock.permissions import user_can_manage_preparation_orders
|
||||||
user_config = request.user.config
|
if not user_can_manage_preparation_orders(request.user):
|
||||||
can_manage = (
|
|
||||||
request.user.is_superuser
|
|
||||||
or user_config.has_role('admin')
|
|
||||||
or user_config.can_manage_preparation_orders_any()
|
|
||||||
)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
can_manage = False
|
|
||||||
|
|
||||||
if not can_manage:
|
|
||||||
messages.error(request, _("Vous n'avez pas la permission de traiter des bons de préparation."))
|
messages.error(request, _("Vous n'avez pas la permission de traiter des bons de préparation."))
|
||||||
return redirect('stock:stock_movements')
|
return redirect('stock:stock_movements')
|
||||||
|
|
||||||
|
user_config = getattr(request.user, 'config', None)
|
||||||
|
|
||||||
# Récupérer les entrepôts autorisés pour l'utilisateur
|
# Récupérer les entrepôts autorisés pour l'utilisateur
|
||||||
if request.user.is_superuser or user_config.has_role('admin'):
|
if request.user.is_superuser or (user_config and user_config.has_role('admin')):
|
||||||
|
allowed_locations = WarehouseLocation.objects.select_related('warehouse').all()
|
||||||
|
elif user_config:
|
||||||
|
user_warehouses = user_config.userwarehouses.filter(can_manage_preparation_orders=True).values_list('warehouse_id', flat=True)
|
||||||
|
if user_config.has_role('stock_manager') and not user_warehouses.exists():
|
||||||
allowed_locations = WarehouseLocation.objects.select_related('warehouse').all()
|
allowed_locations = WarehouseLocation.objects.select_related('warehouse').all()
|
||||||
else:
|
else:
|
||||||
user_warehouses = user_config.userwarehouses.filter(can_manage_preparation_orders=True).values_list('warehouse_id', flat=True)
|
|
||||||
allowed_locations = WarehouseLocation.objects.filter(
|
allowed_locations = WarehouseLocation.objects.filter(
|
||||||
warehouse_id__in=user_warehouses
|
warehouse_id__in=user_warehouses
|
||||||
).select_related('warehouse')
|
).select_related('warehouse')
|
||||||
|
else:
|
||||||
|
allowed_locations = WarehouseLocation.objects.none()
|
||||||
|
|
||||||
preparation_order = get_object_or_404(
|
preparation_order = get_object_or_404(
|
||||||
PreparationOrder.objects.prefetch_related('preparationorderitem_set__product'),
|
PreparationOrder.objects.prefetch_related('preparationorderitem_set__product'),
|
||||||
pk=pk
|
pk=pk
|
||||||
)
|
)
|
||||||
|
|
||||||
if preparation_order.status not in ['to_process', 'pending']:
|
if preparation_order.status not in ['to_process', 'pending', 'processed']:
|
||||||
messages.error(request, _("Ce bon ne peut plus être traité."))
|
messages.error(request, _("Ce bon ne peut plus être traité."))
|
||||||
return redirect('stock:preparation_order_detail', pk=pk)
|
return redirect('stock:preparation_order_detail', pk=pk)
|
||||||
|
|
||||||
|
|
@ -4626,6 +4735,12 @@ def preparation_order_process(request, pk):
|
||||||
|
|
||||||
# Marquer comme traité
|
# Marquer comme traité
|
||||||
preparation_order.mark_as_processed()
|
preparation_order.mark_as_processed()
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="processed",
|
||||||
|
description=_("Traitement du bon de préparation (mouvements de stock appliqués)"),
|
||||||
|
user=request.user,
|
||||||
|
details={'status': 'processed'},
|
||||||
|
)
|
||||||
messages.success(request, _("Le bon de préparation #{} a été traité avec succès.").format(
|
messages.success(request, _("Le bon de préparation #{} a été traité avec succès.").format(
|
||||||
preparation_order.order_number
|
preparation_order.order_number
|
||||||
))
|
))
|
||||||
|
|
@ -4881,18 +4996,15 @@ def preparation_order_document_upload(request, pk):
|
||||||
"""
|
"""
|
||||||
Upload AJAX d'un ou plusieurs documents sur un bon de préparation.
|
Upload AJAX d'un ou plusieurs documents sur un bon de préparation.
|
||||||
"""
|
"""
|
||||||
from common.models import UserConfig
|
from stock.permissions import user_can_edit_preparation_order
|
||||||
from common.private_files.helpers import build_private_url
|
from common.private_files.helpers import build_private_url
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
try:
|
preparation_order = get_object_or_404(PreparationOrder, pk=pk)
|
||||||
user_config = request.user.config
|
|
||||||
if not user_config.can_manage_preparation_orders_any():
|
if not user_can_edit_preparation_order(request.user, preparation_order):
|
||||||
return JsonResponse({'error': _("Droits insuffisants.")}, status=403)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
return JsonResponse({'error': _("Droits insuffisants.")}, status=403)
|
return JsonResponse({'error': _("Droits insuffisants.")}, status=403)
|
||||||
|
|
||||||
preparation_order = get_object_or_404(PreparationOrder, pk=pk)
|
|
||||||
files = request.FILES.getlist('documents')
|
files = request.FILES.getlist('documents')
|
||||||
if not files:
|
if not files:
|
||||||
return JsonResponse({'error': _("Aucun fichier fourni.")}, status=400)
|
return JsonResponse({'error': _("Aucun fichier fourni.")}, status=400)
|
||||||
|
|
@ -4904,6 +5016,12 @@ def preparation_order_document_upload(request, pk):
|
||||||
file=f,
|
file=f,
|
||||||
name=f.name,
|
name=f.name,
|
||||||
)
|
)
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="document_added",
|
||||||
|
description=_("Ajout du document joint : {name}").format(name=doc.name or doc.filename()),
|
||||||
|
user=request.user,
|
||||||
|
details={'document_id': doc.pk, 'filename': doc.filename()},
|
||||||
|
)
|
||||||
created.append({
|
created.append({
|
||||||
'id': doc.pk,
|
'id': doc.pk,
|
||||||
'filename': doc.filename(),
|
'filename': doc.filename(),
|
||||||
|
|
@ -4921,17 +5039,22 @@ def preparation_order_document_delete(request, doc_id):
|
||||||
"""
|
"""
|
||||||
Suppression AJAX d'un document de bon de préparation.
|
Suppression AJAX d'un document de bon de préparation.
|
||||||
"""
|
"""
|
||||||
from common.models import UserConfig
|
from stock.permissions import user_can_edit_preparation_order
|
||||||
|
|
||||||
try:
|
|
||||||
user_config = request.user.config
|
|
||||||
if not user_config.can_manage_preparation_orders_any():
|
|
||||||
return JsonResponse({'error': _("Droits insuffisants.")}, status=403)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
return JsonResponse({'error': _("Droits insuffisants.")}, status=403)
|
|
||||||
|
|
||||||
doc = get_object_or_404(PreparationOrderDocument, pk=doc_id)
|
doc = get_object_or_404(PreparationOrderDocument, pk=doc_id)
|
||||||
|
po = doc.preparation_order
|
||||||
|
|
||||||
|
if not user_can_edit_preparation_order(request.user, po):
|
||||||
|
return JsonResponse({'error': _("Droits insuffisants.")}, status=403)
|
||||||
|
|
||||||
|
doc_name = doc.name or doc.filename()
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
po.log_history(
|
||||||
|
action="document_removed",
|
||||||
|
description=_("Suppression du document joint : {name}").format(name=doc_name),
|
||||||
|
user=request.user,
|
||||||
|
details={'document_id': doc_id, 'filename': doc_name},
|
||||||
|
)
|
||||||
return JsonResponse({'deleted': doc_id})
|
return JsonResponse({'deleted': doc_id})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -6143,18 +6266,9 @@ def serialized_item_delete(request, pk):
|
||||||
@require_POST
|
@require_POST
|
||||||
def preparation_order_toggle_loan_return(request, pk):
|
def preparation_order_toggle_loan_return(request, pk):
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from common.models import UserConfig
|
from stock.permissions import user_can_manage_preparation_orders
|
||||||
try:
|
|
||||||
user_config = request.user.config
|
|
||||||
can_manage = (
|
|
||||||
request.user.is_superuser
|
|
||||||
or user_config.has_role('admin')
|
|
||||||
or user_config.can_manage_preparation_orders_any()
|
|
||||||
)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
can_manage = False
|
|
||||||
|
|
||||||
if not can_manage:
|
if not user_can_manage_preparation_orders(request.user):
|
||||||
messages.error(request, _("Vous n'avez pas la permission de modifier ce prêt."))
|
messages.error(request, _("Vous n'avez pas la permission de modifier ce prêt."))
|
||||||
return redirect('stock:preparation_order_detail', pk=pk)
|
return redirect('stock:preparation_order_detail', pk=pk)
|
||||||
|
|
||||||
|
|
@ -6166,6 +6280,14 @@ def preparation_order_toggle_loan_return(request, pk):
|
||||||
# Synchroniser les lots sortants liés
|
# Synchroniser les lots sortants liés
|
||||||
preparation_order.stock_batches.filter(movement_type=StockMovementBatch.TYPE_OUT).update(loan_returned=returned)
|
preparation_order.stock_batches.filter(movement_type=StockMovementBatch.TYPE_OUT).update(loan_returned=returned)
|
||||||
|
|
||||||
|
status_label = _("Retourné") if returned else _("Non retourné")
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="loan_updated",
|
||||||
|
description=_("Statut de retour du prêt modifié : {status}").format(status=status_label),
|
||||||
|
user=request.user,
|
||||||
|
details={'loan_returned': returned},
|
||||||
|
)
|
||||||
|
|
||||||
messages.success(request, _("Statut de retour mis à jour."))
|
messages.success(request, _("Statut de retour mis à jour."))
|
||||||
referer = request.META.get('HTTP_REFERER')
|
referer = request.META.get('HTTP_REFERER')
|
||||||
if referer:
|
if referer:
|
||||||
|
|
@ -6203,6 +6325,14 @@ def stock_movement_batch_toggle_loan_return(request, pk):
|
||||||
if batch.preparation_order:
|
if batch.preparation_order:
|
||||||
batch.preparation_order.loan_returned = returned
|
batch.preparation_order.loan_returned = returned
|
||||||
batch.preparation_order.save(update_fields=['loan_returned'])
|
batch.preparation_order.save(update_fields=['loan_returned'])
|
||||||
|
batch.preparation_order.log_history(
|
||||||
|
action="loan_updated",
|
||||||
|
description=_("Statut de retour du prêt synchronisé : {status}").format(
|
||||||
|
status=_("Retourné") if returned else _("Non retourné")
|
||||||
|
),
|
||||||
|
user=request.user,
|
||||||
|
details={'loan_returned': returned, 'batch_id': batch.pk},
|
||||||
|
)
|
||||||
|
|
||||||
messages.success(request, _("Statut de retour mis à jour."))
|
messages.success(request, _("Statut de retour mis à jour."))
|
||||||
referer = request.META.get('HTTP_REFERER')
|
referer = request.META.get('HTTP_REFERER')
|
||||||
|
|
@ -6215,18 +6345,9 @@ def stock_movement_batch_toggle_loan_return(request, pk):
|
||||||
@require_POST
|
@require_POST
|
||||||
def preparation_order_update_loan_info(request, pk):
|
def preparation_order_update_loan_info(request, pk):
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from common.models import UserConfig
|
from stock.permissions import user_can_manage_preparation_orders
|
||||||
try:
|
|
||||||
user_config = request.user.config
|
|
||||||
can_manage = (
|
|
||||||
request.user.is_superuser
|
|
||||||
or user_config.has_role('admin')
|
|
||||||
or user_config.can_manage_preparation_orders_any()
|
|
||||||
)
|
|
||||||
except UserConfig.DoesNotExist:
|
|
||||||
can_manage = False
|
|
||||||
|
|
||||||
if not can_manage:
|
if not user_can_manage_preparation_orders(request.user):
|
||||||
messages.error(request, _("Vous n'avez pas la permission de modifier ce prêt."))
|
messages.error(request, _("Vous n'avez pas la permission de modifier ce prêt."))
|
||||||
return redirect('stock:preparation_order_detail', pk=pk)
|
return redirect('stock:preparation_order_detail', pk=pk)
|
||||||
|
|
||||||
|
|
@ -6251,6 +6372,14 @@ def preparation_order_update_loan_info(request, pk):
|
||||||
estimated_return_date=estimated_return_date
|
estimated_return_date=estimated_return_date
|
||||||
)
|
)
|
||||||
|
|
||||||
|
loan_desc = _("Prêt activé (retour prévu : {date})").format(date=estimated_return_date_str or _("non défini")) if is_loan else _("Prêt désactivé")
|
||||||
|
preparation_order.log_history(
|
||||||
|
action="loan_updated",
|
||||||
|
description=_("Informations de prêt modifiées : {desc}").format(desc=loan_desc),
|
||||||
|
user=request.user,
|
||||||
|
details={'is_loan': is_loan, 'estimated_return_date': estimated_return_date_str},
|
||||||
|
)
|
||||||
|
|
||||||
messages.success(request, _("Informations de prêt enregistrées."))
|
messages.success(request, _("Informations de prêt enregistrées."))
|
||||||
referer = request.META.get('HTTP_REFERER')
|
referer = request.META.get('HTTP_REFERER')
|
||||||
if referer:
|
if referer:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue