feat: add stock return workflow from preparation orders with automated field prefilling and enhanced search capabilities
This commit is contained in:
parent
91149d2597
commit
37500e1fbc
10 changed files with 217 additions and 13 deletions
|
|
@ -414,15 +414,27 @@ def stock_serialized_item_detail_mobile(request, pk):
|
|||
@stock_access_required
|
||||
def stock_preparation_orders_mobile(request):
|
||||
from stock.models import PreparationOrder
|
||||
from django.db import models
|
||||
|
||||
selected_status = request.GET.get('status', '').strip()
|
||||
selected_priority = request.GET.get('priority', '').strip()
|
||||
search_q = request.GET.get('q', '').strip()
|
||||
|
||||
preparation_orders = PreparationOrder.objects.all()
|
||||
preparation_orders = PreparationOrder.objects.select_related('intervention').all()
|
||||
if selected_status:
|
||||
preparation_orders = preparation_orders.filter(status=selected_status)
|
||||
if selected_priority:
|
||||
preparation_orders = preparation_orders.filter(priority=selected_priority)
|
||||
if search_q:
|
||||
q_filter = (
|
||||
models.Q(order_number__icontains=search_q) |
|
||||
models.Q(intervention__code__icontains=search_q) |
|
||||
models.Q(intervention__title__icontains=search_q) |
|
||||
models.Q(requester__icontains=search_q)
|
||||
)
|
||||
if search_q.isdigit():
|
||||
q_filter |= models.Q(intervention__id=int(search_q))
|
||||
preparation_orders = preparation_orders.filter(q_filter)
|
||||
|
||||
preparation_orders = preparation_orders.prefetch_related(
|
||||
'preparationorderitem_set__product'
|
||||
|
|
@ -431,6 +443,7 @@ def stock_preparation_orders_mobile(request):
|
|||
'preparation_orders': preparation_orders,
|
||||
'selected_status': selected_status,
|
||||
'selected_priority': selected_priority,
|
||||
'search_q': search_q,
|
||||
'preparation_order_status_choices': [
|
||||
('to_process', _("A traiter")),
|
||||
('pending', _("En attente")),
|
||||
|
|
@ -1028,10 +1041,17 @@ def stock_preparation_order_detail_mobile(request, pk):
|
|||
can_manage = user_config.has_role('stock_manager') or user_config.has_role('admin') or user_config.can_manage_preparation_orders_any()
|
||||
can_cancel = user_config.can_manage_preparation_orders_any()
|
||||
can_manage_purchase = user_config.can_manage_purchase_orders_any()
|
||||
can_manage_stock = (
|
||||
request.user.is_superuser
|
||||
or user_config.has_role('admin')
|
||||
or user_config.has_role('stock_manager')
|
||||
or user_config.get_accessible_warehouses(can_manage=True).exists()
|
||||
)
|
||||
except UserConfig.DoesNotExist:
|
||||
can_manage = False
|
||||
can_cancel = False
|
||||
can_manage_purchase = False
|
||||
can_manage_stock = False
|
||||
|
||||
other_items_with_info = []
|
||||
for oi in preparation_order.other_items.all():
|
||||
|
|
@ -1050,6 +1070,7 @@ def stock_preparation_order_detail_mobile(request, pk):
|
|||
'can_manage': can_manage,
|
||||
'can_cancel': can_cancel,
|
||||
'can_manage_purchase': can_manage_purchase,
|
||||
'can_manage_stock': can_manage_stock,
|
||||
'exit_batches': exit_batches,
|
||||
'return_batches': return_batches,
|
||||
'has_executed_outputs': has_executed_outputs,
|
||||
|
|
|
|||
|
|
@ -1454,6 +1454,29 @@ class PreparationOrder(models.Model):
|
|||
summary[p_id]['net'] -= move.quantity
|
||||
return summary
|
||||
|
||||
def get_default_return_location_id(self):
|
||||
"""
|
||||
Si tous les produits sortis pour ce bon proviennent du même emplacement unique,
|
||||
retourne l'ID de cet emplacement (pour préremplir l'emplacement de destination lors d'un retour).
|
||||
"""
|
||||
executed_by_loc = self.get_executed_outputs_by_product_and_location()
|
||||
loc_set = set()
|
||||
for p_id, loc_dict in executed_by_loc.items():
|
||||
for loc_id, qty in loc_dict.items():
|
||||
if qty > 0:
|
||||
loc_set.add(loc_id)
|
||||
if len(loc_set) == 1:
|
||||
return list(loc_set)[0]
|
||||
|
||||
gross_locs = set()
|
||||
for batch in self.stock_batches.filter(movement_type='out'):
|
||||
for move in batch.movements.all():
|
||||
if move.source_location_id:
|
||||
gross_locs.add(move.source_location_id)
|
||||
if len(gross_locs) == 1:
|
||||
return list(gross_locs)[0]
|
||||
return None
|
||||
|
||||
|
||||
|
||||
class PreparationOrderItem(models.Model):
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@
|
|||
<option value="">-- {% translate "Choisir un sous-type" %} --</option>
|
||||
{% if movement_type == 'in' %}
|
||||
<option value="purchase">{% translate "Achat" %}</option>
|
||||
<option value="return">{% translate "Retour" %}</option>
|
||||
<option value="return" {% if preselected_po %}selected{% endif %}>{% translate "Retour" %}</option>
|
||||
<option value="confection">{% translate "Confection" %}</option>
|
||||
<option value="correction" style="display: none;">{% translate "Correction (Administrateur uniquement)" %}</option>
|
||||
{% elif movement_type == 'out' %}
|
||||
|
|
@ -721,6 +721,15 @@ const bpResults = document.getElementById('bp-results');
|
|||
const bpLoadBtn = document.getElementById('bp-load-btn');
|
||||
const bpInfo = document.getElementById('bp-info');
|
||||
|
||||
{% if preselected_po %}
|
||||
if (bpSearchInput && bpHiddenId && bpLoadBtn) {
|
||||
bpHiddenId.value = '{{ preselected_po.pk }}';
|
||||
bpSearchInput.value = '{{ preselected_po.order_number }}{% if preselected_po.intervention %} (Int. {{ preselected_po.intervention.code|default:preselected_po.intervention.id }}){% endif %}';
|
||||
bpLoadBtn.disabled = false;
|
||||
setTimeout(() => { bpLoadBtn.click(); }, 150);
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
let bpTimeout = null;
|
||||
|
||||
if (bpSearchInput) {
|
||||
|
|
@ -779,6 +788,14 @@ if (bpLoadBtn) {
|
|||
bpInfo.innerHTML = `{% translate "Chargement de" %} <strong>${data.items.length}</strong> {% translate "produit(s) depuis le bon" %} <strong>${escHtml(data.order_number)}</strong>`;
|
||||
bpInfo.className = 'alert alert-info mt-2 mb-0 py-2 small';
|
||||
bpInfo.style.display = '';
|
||||
// Pré-remplir l'emplacement de destination si unique
|
||||
if (data.default_location_id) {
|
||||
const globalDestSelect = document.getElementById('global_dest_location_id');
|
||||
if (globalDestSelect) {
|
||||
globalDestSelect.value = data.default_location_id;
|
||||
globalDestSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
// Effacer les lignes existantes
|
||||
document.getElementById('lines-container').innerHTML = '';
|
||||
updateNoLinesPlaceholder();
|
||||
|
|
|
|||
|
|
@ -456,6 +456,19 @@
|
|||
<i class="bi bi-x-circle me-1"></i>{% translate "Annuler le bon" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif preparation_order.status == 'processed' %}
|
||||
{% if can_manage_stock %}
|
||||
<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" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'stock:preparation_order_reopen' preparation_order.pk %}"
|
||||
onsubmit="return confirm('{% translate "Êtes-vous sûr de vouloir réouvrir ce bon de préparation ?" %}');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-warning w-100">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Réouvrir le bon" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@
|
|||
<div class="card mb-3 shadow-sm bg-light border-0">
|
||||
<div class="card-body p-2">
|
||||
<form method="get" class="row g-2">
|
||||
<div class="col-12">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control" name="q" value="{{ search_q }}" placeholder="{% translate 'N° bon, code/titre d\'intervention...' %}">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<select class="form-select form-select-sm" name="status" onchange="this.form.submit()">
|
||||
<option value="">-- {% translate "Tous statuts" %} --</option>
|
||||
|
|
@ -56,7 +63,7 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if selected_status or selected_priority %}
|
||||
{% if selected_status or selected_priority or search_q %}
|
||||
<div class="col-12 text-center mt-1">
|
||||
<a href="?" class="btn btn-sm btn-outline-secondary py-0" style="font-size:0.75rem;">
|
||||
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer les filtres" %}
|
||||
|
|
@ -87,7 +94,7 @@
|
|||
</div>
|
||||
{% if order.intervention %}
|
||||
<div class="list-card-meta mt-1">
|
||||
<i class="bi bi-wrench me-1"></i>{% translate "Intervention" %} #{{ order.intervention.id }}
|
||||
<i class="bi bi-wrench me-1 text-primary"></i><strong>{% if order.intervention.code %}{{ order.intervention.code }}{% else %}#{{ order.intervention.id }}{% endif %}</strong>{% if order.intervention.title %} — {{ order.intervention.title }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@
|
|||
<option value="">-- {% translate "Choisir un sous-type" %} --</option>
|
||||
{% if movement_type == 'in' %}
|
||||
<option value="purchase">{% translate "Achat" %}</option>
|
||||
<option value="return">{% translate "Retour" %}</option>
|
||||
<option value="return" {% if preselected_po %}selected{% endif %}>{% translate "Retour" %}</option>
|
||||
<option value="confection">{% translate "Confection" %}</option>
|
||||
<option value="correction" style="display: none;">{% translate "Correction (Administrateur uniquement)" %}</option>
|
||||
{% elif movement_type == 'out' %}
|
||||
|
|
@ -915,6 +915,15 @@ const bpResults = document.getElementById('bp-results');
|
|||
const bpLoadBtn = document.getElementById('bp-load-btn');
|
||||
const bpInfo = document.getElementById('bp-info');
|
||||
|
||||
{% if preselected_po %}
|
||||
if (bpSearchInput && bpHiddenId && bpLoadBtn) {
|
||||
bpHiddenId.value = '{{ preselected_po.pk }}';
|
||||
bpSearchInput.value = '{{ preselected_po.order_number }}{% if preselected_po.intervention %} (Int. {{ preselected_po.intervention.code|default:preselected_po.intervention.id }}){% endif %}';
|
||||
bpLoadBtn.disabled = false;
|
||||
setTimeout(() => { bpLoadBtn.click(); }, 150);
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
let bpTimeout = null;
|
||||
|
||||
if (bpSearchInput) {
|
||||
|
|
@ -973,6 +982,14 @@ if (bpLoadBtn) {
|
|||
bpInfo.innerHTML = `{% translate "Chargement de" %} <strong>${data.items.length}</strong> {% translate "produit(s) depuis le bon" %} <strong>${escHtml(data.order_number)}</strong>`;
|
||||
bpInfo.className = 'alert alert-info mt-2 mb-0 py-2 small';
|
||||
bpInfo.style.display = '';
|
||||
// Pré-remplir l'emplacement de destination si unique
|
||||
if (data.default_location_id) {
|
||||
const globalDestSelect = document.getElementById('global_dest_location_id');
|
||||
if (globalDestSelect) {
|
||||
globalDestSelect.value = data.default_location_id;
|
||||
globalDestSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
// Effacer les lignes existantes
|
||||
document.getElementById('lines-container').innerHTML = '';
|
||||
updateNoLinesPlaceholder();
|
||||
|
|
|
|||
|
|
@ -647,6 +647,21 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% elif preparation_order.status == 'processed' %}
|
||||
{% if can_manage_stock %}
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100 border-info">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-box-arrow-in-down text-info" style="font-size: 2rem;"></i>
|
||||
<h6 class="mt-2">{% translate "Créer un retour de stock" %}</h6>
|
||||
<p class="text-muted small">{% translate "Enregistrer la réintégration de matériel en stock préremplie depuis ce bon" %}</p>
|
||||
<a href="{% url 'stock:new_stock_entry' %}?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" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100 border-warning">
|
||||
<div class="card-body text-center">
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@
|
|||
<input type="hidden" name="tab" value="preparation-orders">
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control" name="q" value="{{ search_q }}" placeholder="{% translate 'N° bon, code ou titre d\'intervention...' %}">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="input-group input-group-sm">
|
||||
<label class="input-group-text" for="filter-status"><i class="bi bi-funnel me-1"></i>{% translate "Statut" %}</label>
|
||||
<select class="form-select" id="filter-status" name="status" onchange="this.form.submit()">
|
||||
|
|
@ -61,7 +69,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<div class="input-group input-group-sm">
|
||||
<label class="input-group-text" for="filter-priority"><i class="bi bi-exclamation-triangle me-1"></i>{% translate "Priorité" %}</label>
|
||||
<select class="form-select" id="filter-priority" name="priority" onchange="this.form.submit()">
|
||||
|
|
@ -73,16 +81,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 d-flex justify-content-between align-items-center gap-2">
|
||||
<div class="col-md-2 d-flex justify-content-between align-items-center gap-1">
|
||||
<div>
|
||||
{% if selected_status or selected_priority %}
|
||||
<a href="?tab=preparation-orders" class="btn btn-sm btn-outline-secondary">
|
||||
{% if selected_status or selected_priority or search_q %}
|
||||
<a href="?tab=preparation-orders" class="btn btn-sm btn-outline-secondary" title="{% translate 'Effacer les filtres' %}">
|
||||
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if can_create_preparation_order %}
|
||||
<a href="{% url 'stock:new_preparation_order' %}" class="btn btn-sm btn-primary">
|
||||
<a href="{% url 'stock:new_preparation_order' %}" class="btn btn-sm btn-primary text-nowrap">
|
||||
<i class="bi bi-plus-circle me-1"></i>{% translate "Nouveau bon" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
@ -110,6 +118,12 @@
|
|||
{{ order.order_number }}
|
||||
</a>
|
||||
|
||||
{% if order.intervention %}
|
||||
<a href="{% url 'interventions:intervention_detail' order.intervention.id %}" class="badge bg-light text-dark border text-decoration-none me-1" title="{% translate 'Intervention liée' %}">
|
||||
<i class="bi bi-wrench me-1 text-primary"></i>{% if order.intervention.code %}{{ order.intervention.code }}{% else %}#{{ order.intervention.id }}{% endif %}{% if order.intervention.title %} — {{ order.intervention.title }}{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if order.status == 'to_process' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-hourglass-split me-1"></i>{{ order.get_status_display }}</span>
|
||||
{% elif order.status == 'pending' %}
|
||||
|
|
|
|||
|
|
@ -1719,6 +1719,52 @@ class PreparationOrderAutocompleteTests(TestCase):
|
|||
self.assertContains(response, "Restitué au stock")
|
||||
self.assertNotContains(response, "à prélever")
|
||||
|
||||
def test_create_return_button_and_prefilled_entry_form(self):
|
||||
from stock.models import Product, PreparationOrder, PreparationOrderItem
|
||||
from common.models import UserConfig, Role
|
||||
self.user.is_superuser = True
|
||||
self.user.save()
|
||||
user_cfg, _ = UserConfig.objects.get_or_create(user=self.user)
|
||||
admin_role, _ = Role.objects.get_or_create(name='admin')
|
||||
user_cfg.roles.add(admin_role)
|
||||
prod = Product.objects.create(name="Produit Retour Test", code="PROD-RET-2", total_stock=5)
|
||||
po = PreparationOrder.objects.create(order_number="BP-RET-200", status="processed")
|
||||
PreparationOrderItem.objects.create(preparation_order=po, product=prod, quantity=3)
|
||||
|
||||
# 1. Vérifier la présence du bouton 'Créer un retour' sur la page détail du BP
|
||||
response_detail = self.client.get(reverse("stock:preparation_order_detail", args=[po.pk]))
|
||||
self.assertEqual(response_detail.status_code, 200)
|
||||
self.assertContains(response_detail, f"preparation_order={po.pk}")
|
||||
self.assertContains(response_detail, "Créer un retour")
|
||||
|
||||
# 2. Accéder à la page de création d'entrée de stock avec la QueryParam preparation_order
|
||||
url_entry = reverse("stock:new_stock_entry") + f"?preparation_order={po.pk}"
|
||||
response_entry = self.client.get(url_entry)
|
||||
self.assertEqual(response_entry.status_code, 200)
|
||||
self.assertContains(response_entry, "BP-RET-200")
|
||||
|
||||
def test_unique_location_return_prefilled_in_api(self):
|
||||
from stock.models import Product, PreparationOrder, PreparationOrderItem, StockMovementBatch, StockMovement, Warehouse, WarehouseLocation
|
||||
wh = Warehouse.objects.create(name="Entrepôt Unique", code="WH-U")
|
||||
loc = WarehouseLocation.objects.create(warehouse=wh, name="Rayon A", code="LOC-RA")
|
||||
prod = Product.objects.create(name="Produit Emplacement Unique", code="PROD-LOC-U", total_stock=10)
|
||||
po = PreparationOrder.objects.create(order_number="BP-LOC-100", status="processed")
|
||||
PreparationOrderItem.objects.create(preparation_order=po, product=prod, quantity=5)
|
||||
|
||||
# Mouvement de sortie depuis loc
|
||||
b_out = StockMovementBatch.objects.create(movement_type="out", preparation_order=po)
|
||||
StockMovement.objects.create(batch=b_out, product=prod, quantity=5, source_location=loc)
|
||||
|
||||
# Vérifier que get_default_return_location_id renvoie loc.id
|
||||
self.assertEqual(po.get_default_return_location_id(), loc.id)
|
||||
|
||||
# Vérifier que l'API preparation_order_items_api renvoie default_location_id
|
||||
url = reverse("stock:preparation_order_items_api", args=[po.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(data["default_location_id"], loc.id)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -429,7 +429,12 @@ def preparation_order_items_api(request, pk):
|
|||
'quantity': item.quantity,
|
||||
'unit': item.product.unit or '',
|
||||
})
|
||||
return JsonResponse({'items': items, 'order_number': po.order_number})
|
||||
default_loc_id = po.get_default_return_location_id()
|
||||
return JsonResponse({
|
||||
'items': items,
|
||||
'order_number': po.order_number,
|
||||
'default_location_id': default_loc_id,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
|
|
@ -555,12 +560,23 @@ def sign_panel_types_api(request):
|
|||
def stock_movements(request):
|
||||
selected_status = request.GET.get('status', '').strip()
|
||||
selected_priority = request.GET.get('priority', '').strip()
|
||||
search_q = request.GET.get('q', '').strip()
|
||||
|
||||
preparation_orders = PreparationOrder.objects.all()
|
||||
preparation_orders = PreparationOrder.objects.select_related('intervention').all()
|
||||
if selected_status:
|
||||
preparation_orders = preparation_orders.filter(status=selected_status)
|
||||
if selected_priority:
|
||||
preparation_orders = preparation_orders.filter(priority=selected_priority)
|
||||
if search_q:
|
||||
q_filter = (
|
||||
models.Q(order_number__icontains=search_q) |
|
||||
models.Q(intervention__code__icontains=search_q) |
|
||||
models.Q(intervention__title__icontains=search_q) |
|
||||
models.Q(requester__icontains=search_q)
|
||||
)
|
||||
if search_q.isdigit():
|
||||
q_filter |= models.Q(intervention__id=int(search_q))
|
||||
preparation_orders = preparation_orders.filter(q_filter)
|
||||
preparation_orders = preparation_orders.order_by("-date")[:1000]
|
||||
|
||||
production_orders = ProductionOrder.objects.all().select_related(
|
||||
|
|
@ -654,6 +670,7 @@ def stock_movements(request):
|
|||
"contracts": contracts,
|
||||
"selected_status": selected_status,
|
||||
"selected_priority": selected_priority,
|
||||
"search_q": search_q,
|
||||
"preparation_order_status_choices": [
|
||||
('to_process', _("A traiter")),
|
||||
('pending', _("En attente")),
|
||||
|
|
@ -1166,6 +1183,11 @@ def _new_stock_batch_view(request, movement_type,
|
|||
return success_redirect_fn(request, batch)
|
||||
return redirect('stock:stock_movement_batch_detail', pk=batch.pk)
|
||||
|
||||
preselected_po_id = request.GET.get('preparation_order')
|
||||
preselected_po = None
|
||||
if preselected_po_id and str(preselected_po_id).isdigit():
|
||||
preselected_po = PreparationOrder.objects.filter(pk=int(preselected_po_id)).select_related('intervention').first()
|
||||
|
||||
type_labels = {
|
||||
'in': _("Nouvelle entrée de stock"),
|
||||
'out': _("Nouvelle sortie de stock"),
|
||||
|
|
@ -1181,6 +1203,7 @@ def _new_stock_batch_view(request, movement_type,
|
|||
"companies": companies,
|
||||
"contracts": contracts,
|
||||
"cancel_url_name": cancel_url_name,
|
||||
"preselected_po": preselected_po,
|
||||
})
|
||||
|
||||
|
||||
|
|
@ -3870,10 +3893,17 @@ def preparation_order_detail(request, pk):
|
|||
can_manage = request.user.is_superuser or user_config.has_role('admin') or user_config.can_manage_preparation_orders_any()
|
||||
can_cancel = user_config.can_manage_preparation_orders_any()
|
||||
can_manage_purchase = user_config.can_manage_purchase_orders_any()
|
||||
can_manage_stock = (
|
||||
request.user.is_superuser
|
||||
or user_config.has_role('admin')
|
||||
or user_config.has_role('stock_manager')
|
||||
or user_config.get_accessible_warehouses(can_manage=True).exists()
|
||||
)
|
||||
except UserConfig.DoesNotExist:
|
||||
can_manage = False
|
||||
can_cancel = False
|
||||
can_manage_purchase = False
|
||||
can_manage_stock = False
|
||||
|
||||
# Enrichir les autres produits avec leur bon de confection éventuel
|
||||
other_items_with_info = []
|
||||
|
|
@ -3895,6 +3925,7 @@ def preparation_order_detail(request, pk):
|
|||
'can_manage': can_manage,
|
||||
'can_cancel': can_cancel,
|
||||
'can_manage_purchase': can_manage_purchase,
|
||||
'can_manage_stock': can_manage_stock,
|
||||
'categories': list(ProductCategory.objects.order_by('name_fr').values('id', 'code', 'name_fr')),
|
||||
'exit_batches': exit_batches,
|
||||
'return_batches': return_batches,
|
||||
|
|
|
|||
Loading…
Reference in a new issue