feat: add stock management module including views, models, templates, and tests

This commit is contained in:
Karl Determe 2026-09-29 17:07:29 +02:00
parent 001bee8ca9
commit e00f9cf784
6 changed files with 653 additions and 92 deletions

View file

@ -485,13 +485,67 @@ def stock_production_orders_mobile(request):
"""Mobile list of production orders (bons de confection).""" """Mobile list of production orders (bons de confection)."""
from stock.models import ProductionOrder from stock.models import ProductionOrder
from stock.permissions import get_user_accessible_warehouses from stock.permissions import get_user_accessible_warehouses
from django.db import models
search_q = request.GET.get('q', '').strip() or request.GET.get('prod_q', '').strip()
selected_status = request.GET.get('status', '').strip() or request.GET.get('prod_status', '').strip()
selected_warehouse = request.GET.get('warehouse', '').strip() or request.GET.get('prod_warehouse', '').strip()
selected_destination = request.GET.get('destination', '').strip() or request.GET.get('prod_destination', '').strip()
accessible_warehouses = get_user_accessible_warehouses(request.user) accessible_warehouses = get_user_accessible_warehouses(request.user)
production_orders = ProductionOrder.objects.filter( base_qs = ProductionOrder.objects.filter(warehouse__in=accessible_warehouses)
warehouse__id__in=accessible_warehouses
).select_related('warehouse').order_by('-created_at')[:200] # Compteurs par statut (pour les filtres rapides mobiles)
prod_counts = base_qs.aggregate(
total=models.Count('id'),
to_process=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_TO_PROCESS)),
in_production=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_IN_PRODUCTION)),
completed=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_COMPLETED)),
draft=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_DRAFT)),
cancelled=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_CANCELLED)),
)
production_orders = base_qs.select_related(
'warehouse', 'preparation_order', 'output_product', 'output_location',
'preparation_order__intervention', 'preparation_order_other_item',
).prefetch_related(
'order_components__product',
)
if selected_status:
production_orders = production_orders.filter(status=selected_status)
if selected_warehouse:
production_orders = production_orders.filter(warehouse_id=selected_warehouse)
if selected_destination:
production_orders = production_orders.filter(destination=selected_destination)
if search_q:
q_filter = (
models.Q(code__icontains=search_q) |
models.Q(output_product__name__icontains=search_q) |
models.Q(output_product_name__icontains=search_q) |
models.Q(preparation_order__order_number__icontains=search_q) |
models.Q(preparation_order__intervention__code__icontains=search_q) |
models.Q(preparation_order__intervention__title__icontains=search_q) |
models.Q(preparation_order_other_item__description__icontains=search_q) |
models.Q(notes__icontains=search_q) |
models.Q(order_components__product__name__icontains=search_q)
)
if search_q.isdigit():
q_filter |= models.Q(id=int(search_q)) | models.Q(preparation_order__intervention__id=int(search_q))
production_orders = production_orders.filter(q_filter).distinct()
production_orders = production_orders.order_by('-created_at')[:200]
return render(request, "stock/mobile/stock_production_orders_mobile.html", { return render(request, "stock/mobile/stock_production_orders_mobile.html", {
'production_orders': production_orders, 'production_orders': production_orders,
'search_q': search_q,
'selected_status': selected_status,
'selected_warehouse': selected_warehouse,
'selected_destination': selected_destination,
'status_choices': ProductionOrder.STATUS_CHOICES,
'destination_choices': ProductionOrder.DESTINATION_CHOICES,
'warehouses': accessible_warehouses,
'prod_counts': prod_counts,
}) })

View file

@ -2237,6 +2237,19 @@ class ProductionOrder(models.Model):
class Meta: class Meta:
ordering = ("-created_at",) ordering = ("-created_at",)
@property
def display_output_product_name(self):
"""Retourne le nom d'affichage du produit fini / confectionné."""
if self.output_product:
return self.output_product.name
if self.output_product_name:
return self.output_product_name
if self.preparation_order_other_item and self.preparation_order_other_item.description:
return self.preparation_order_other_item.description
if self.notes and 'Issu de' in self.notes and ': ' in self.notes:
return self.notes.split(': ', 1)[1].strip()
return ""
def __str__(self): def __str__(self):
return _("Confection {code} x{qty}").format( return _("Confection {code} x{qty}").format(
code=self.code or f"#{self.pk}", code=self.code or f"#{self.pk}",

View file

@ -1,4 +1,4 @@
{% extends 'mobile/mobile_base.html' %} {% extends 'mobile/mobile_base.html' %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
@ -31,44 +31,180 @@
} }
.list-card-title { font-weight: 600; font-size: 1rem; } .list-card-title { font-weight: 600; font-size: 1rem; }
.list-card-meta { font-size: 0.83rem; color: #6c757d; } .list-card-meta { font-size: 0.83rem; color: #6c757d; }
.status-pills-bar {
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
</style> </style>
{% endblock extra_head %} {% endblock extra_head %}
{% block content %} {% block content %}
<div class="d-flex justify-content-end mb-3"> <!-- En-tête avec bouton Nouveau bon -->
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-bold fs-6 text-dark">
<i class="bi bi-tools text-primary me-1"></i>{% translate "Bons de confection" %}
</span>
<a href="{% url 'mobile:production_order_create_mobile' %}" class="btn btn-primary btn-sm"> <a href="{% url 'mobile:production_order_create_mobile' %}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-1"></i>{% translate "Nouveau bon" %} <i class="bi bi-plus-lg me-1"></i>{% translate "Nouveau bon" %}
</a> </a>
</div> </div>
<!-- Filtres mobiles -->
<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, produit, bon de prép...' %}">
<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>
{% for val, label in status_choices %}
<option value="{{ val }}" {% if selected_status == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-6">
{% if warehouses|length > 1 %}
<select class="form-select form-select-sm" name="warehouse" onchange="this.form.submit()">
<option value="">-- {% translate "Tous entrepôts" %} --</option>
{% for wh in warehouses %}
<option value="{{ wh.pk }}" {% if selected_warehouse == wh.pk|stringformat:"s" %}selected{% endif %}>{{ wh.name }}</option>
{% endfor %}
</select>
{% else %}
<select class="form-select form-select-sm" name="destination" onchange="this.form.submit()">
<option value="">-- {% translate "Toutes destinations" %} --</option>
{% for val, label in destination_choices %}
<option value="{{ val }}" {% if selected_destination == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% endif %}
</div>
{% if selected_status or selected_warehouse or selected_destination or search_q %}
<div class="col-12 text-center mt-1">
<a href="{% url 'mobile:stock_production_orders_mobile' %}" 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" %}
</a>
</div>
{% endif %}
</form>
</div>
</div>
<!-- Filtres rapides par statut (Pills défilantes) -->
<div class="d-flex gap-1 overflow-auto pb-2 mb-2 status-pills-bar" style="white-space: nowrap;">
<a href="?{% if search_q %}q={{ search_q }}&{% endif %}{% if selected_warehouse %}warehouse={{ selected_warehouse }}&{% endif %}{% if selected_destination %}destination={{ selected_destination }}&{% endif %}"
class="badge {% if not selected_status %}bg-primary text-white{% else %}bg-light text-dark border{% endif %} text-decoration-none py-1 px-2">
{% translate "Tous" %} ({{ prod_counts.total|default:0 }})
</a>
<a href="?status=to_process{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_warehouse %}&warehouse={{ selected_warehouse }}{% endif %}{% if selected_destination %}&destination={{ selected_destination }}{% endif %}"
class="badge {% if selected_status == 'to_process' %}bg-danger text-white{% else %}bg-light text-dark border{% endif %} text-decoration-none py-1 px-2">
<i class="bi bi-hourglass-split me-1"></i>{% translate "À traiter" %} ({{ prod_counts.to_process|default:0 }})
</a>
<a href="?status=in_production{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_warehouse %}&warehouse={{ selected_warehouse }}{% endif %}{% if selected_destination %}&destination={{ selected_destination }}{% endif %}"
class="badge {% if selected_status == 'in_production' %}bg-primary text-white{% else %}bg-light text-dark border{% endif %} text-decoration-none py-1 px-2">
<i class="bi bi-gear-wide-connected me-1"></i>{% translate "En confection" %} ({{ prod_counts.in_production|default:0 }})
</a>
<a href="?status=completed{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_warehouse %}&warehouse={{ selected_warehouse }}{% endif %}{% if selected_destination %}&destination={{ selected_destination }}{% endif %}"
class="badge {% if selected_status == 'completed' %}bg-success text-white{% else %}bg-light text-dark border{% endif %} text-decoration-none py-1 px-2">
<i class="bi bi-check-circle me-1"></i>{% translate "Terminé" %} ({{ prod_counts.completed|default:0 }})
</a>
{% if prod_counts.draft %}
<a href="?status=draft{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_warehouse %}&warehouse={{ selected_warehouse }}{% endif %}{% if selected_destination %}&destination={{ selected_destination }}{% endif %}"
class="badge {% if selected_status == 'draft' %}bg-secondary text-white{% else %}bg-light text-dark border{% endif %} text-decoration-none py-1 px-2">
<i class="bi bi-pencil me-1"></i>{% translate "Brouillon" %} ({{ prod_counts.draft|default:0 }})
</a>
{% endif %}
{% if prod_counts.cancelled %}
<a href="?status=cancelled{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_warehouse %}&warehouse={{ selected_warehouse }}{% endif %}{% if selected_destination %}&destination={{ selected_destination }}{% endif %}"
class="badge {% if selected_status == 'cancelled' %}bg-secondary text-white{% else %}bg-light text-dark border{% endif %} text-decoration-none py-1 px-2">
<i class="bi bi-x-circle me-1"></i>{% translate "Annulé" %} ({{ prod_counts.cancelled|default:0 }})
</a>
{% endif %}
</div>
<!-- Liste des bons -->
{% if production_orders %} {% if production_orders %}
{% for order in production_orders %} {% for order in production_orders %}
<a href="{% url 'mobile:stock_production_order_detail_mobile' order.pk %}" class="list-card"> <a href="{% url 'mobile:stock_production_order_detail_mobile' order.pk %}" class="list-card"
style="border-left: 4px solid {% if order.status == 'to_process' %}#dc3545{% elif order.status == 'in_production' %}#0d6efd{% elif order.status == 'completed' %}#198754{% elif order.status == 'cancelled' %}#6c757d{% else %}#6c757d{% endif %} !important;">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div class="list-card-title"> <div class="list-card-title text-primary">
{% if order.code %}{{ order.code }}{% else %}#{{ order.id }}{% endif %} {{ order.code|default:order.id }}
</div>
<div class="d-flex flex-column align-items-end gap-1">
<span class="badge {{ order.status_badge_class }}">
<i class="bi {{ order.status_icon }} me-1"></i>{{ order.get_status_display }}
</span>
{% if order.destination == 'internal' %}
<span class="badge bg-light text-secondary border" style="font-size: 0.7rem;">
<i class="bi bi-house me-1"></i>{% translate "Interne" %}
</span>
{% else %}
<span class="badge bg-light text-info border" style="font-size: 0.7rem;">
<i class="bi bi-box-arrow-up-right me-1"></i>{% translate "Externe" %}
</span>
{% endif %}
</div> </div>
<span class="badge {{ order.status_badge_class }}">
<i class="bi {{ order.status_icon }} me-1"></i>{{ order.get_status_display }}
</span>
</div> </div>
{% if order.product_design %}
<div class="list-card-meta mt-1"> <!-- Produit confectionné -->
<i class="bi bi-diagram-3 me-1"></i>{{ order.product_design.name }} <div class="mt-2 fw-semibold text-dark" style="font-size: 0.95rem;">
<i class="bi bi-box-seam me-1 text-primary"></i>
{% if order.output_product %}
{{ order.output_product.name }}
{% elif order.output_product_name %}
{{ order.output_product_name }}
<span class="badge bg-warning bg-opacity-25 text-dark border border-warning ms-1" style="font-size:0.7rem;">{% translate "Nouveau" %}</span>
{% elif order.preparation_order_other_item %}
{{ order.preparation_order_other_item.description }}
{% else %}
<span class="text-muted fst-italic">{% translate "Produit non spécifié" %}</span>
{% endif %}
</div>
<!-- Bon de préparation lié si existant -->
{% if order.preparation_order %}
<div class="list-card-meta mt-1 text-primary">
<i class="bi bi-clipboard-check me-1"></i>{% translate "Bon de prép." %} : <strong>#{{ order.preparation_order.order_number }}</strong>
{% if order.preparation_order.intervention %}
<span class="text-muted">({{ order.preparation_order.intervention.code|default:order.preparation_order.intervention.id }})</span>
{% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="list-card-meta mt-1">
<span class="me-3"><i class="bi bi-calendar3 me-1"></i>{{ order.created_at|date:"d/m/Y" }}</span> <!-- Métadonnées : Entrepôt, Quantité, Composants, Date -->
<span><i class="bi bi-building me-1"></i>{{ order.warehouse.name }}</span> <div class="list-card-meta mt-2 d-flex justify-content-between align-items-center flex-wrap gap-1">
</div> <div>
<div class="list-card-meta mt-1"> <span class="me-2"><i class="bi bi-building me-1"></i>{{ order.warehouse.name }}</span>
<i class="bi bi-layers me-1"></i>{% translate "Qté" %} : {{ order.quantity }} <span class="badge bg-light text-dark border"><i class="bi bi-layers me-1"></i>{% translate "Qté" %} : <strong>{{ order.quantity }}</strong></span>
</div>
<div>
{% if order.order_components.count %}
<span class="badge bg-light text-muted border me-1">
<i class="bi bi-boxes me-1"></i>{{ order.order_components.count }}
</span>
{% endif %}
<span class="small text-muted"><i class="bi bi-calendar3 me-1"></i>{{ order.created_at|date:"d/m/Y" }}</span>
</div>
</div> </div>
</a> </a>
{% endfor %} {% endfor %}
{% else %} {% else %}
<div class="text-center text-muted py-5"> <div class="text-center text-muted py-5 card shadow-sm border-0">
<i class="bi bi-tools fs-1 d-block mb-2"></i> <i class="bi bi-tools fs-1 d-block mb-2 opacity-50"></i>
{% translate "Aucun bon de confection." %} <h6>{% translate "Aucun bon de confection." %}</h6>
{% if selected_status or selected_warehouse or selected_destination or search_q %}
<p class="small text-muted mb-2">{% translate "Aucun résultat pour cette recherche." %}</p>
<a href="{% url 'mobile:stock_production_orders_mobile' %}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer les filtres" %}
</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View file

@ -27,20 +27,25 @@
<div class="container mt-2"> <div class="container mt-2">
<ul class="nav nav-tabs" id="tablesTab" role="tablist"> <ul class="nav nav-tabs" id="tablesTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="preparation-orders-tab" data-bs-toggle="tab" data-bs-target="#preparation-orders-content" type="button" role="tab" aria-controls="preparation-orders" aria-selected="true"><i class="bi bi-clipboard-check"></i> {% translate "Bons de préparation" %}</button> <button class="nav-link {% if active_tab == 'preparation-orders' or not active_tab %}active{% endif %}" id="preparation-orders-tab" data-bs-toggle="tab" data-bs-target="#preparation-orders-content" type="button" role="tab" aria-controls="preparation-orders" aria-selected="{% if active_tab == 'preparation-orders' or not active_tab %}true{% else %}false{% endif %}"><i class="bi bi-clipboard-check"></i> {% translate "Bons de préparation" %}</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="production-orders-tab" data-bs-toggle="tab" data-bs-target="#production-orders-content" type="button" role="tab" aria-controls="production-orders" aria-selected="false"><i class="bi bi-pencil-square"></i> {% translate "Bons de confection" %}</button> <button class="nav-link {% if active_tab == 'production-orders' %}active{% endif %}" id="production-orders-tab" data-bs-toggle="tab" data-bs-target="#production-orders-content" type="button" role="tab" aria-controls="production-orders" aria-selected="{% if active_tab == 'production-orders' %}true{% else %}false{% endif %}">
<i class="bi bi-tools"></i> {% translate "Bons de confection" %}
{% if prod_counts.to_process %}
<span class="badge bg-danger rounded-pill ms-1" title="{% translate 'À traiter' %}">{{ prod_counts.to_process }}</span>
{% endif %}
</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="movements-tab" data-bs-toggle="tab" data-bs-target="#movements-content" type="button" role="tab" aria-controls="movements-table" aria-selected="false"><i class="bi bi-arrow-left-right"></i> {% translate "Mouvements de Stock" %}</button> <button class="nav-link {% if active_tab == 'movements' %}active{% endif %}" id="movements-tab" data-bs-toggle="tab" data-bs-target="#movements-content" type="button" role="tab" aria-controls="movements-table" aria-selected="{% if active_tab == 'movements' %}true{% else %}false{% endif %}"><i class="bi bi-arrow-left-right"></i> {% translate "Mouvements de Stock" %}</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="inventories-tab" data-bs-toggle="tab" data-bs-target="#inventories-content" type="button" role="tab" aria-controls="inventories-content" aria-selected="false"><i class="bi bi-journal-check"></i> {% translate "Inventaires" %}</button> <button class="nav-link {% if active_tab == 'inventories' %}active{% endif %}" id="inventories-tab" data-bs-toggle="tab" data-bs-target="#inventories-content" type="button" role="tab" aria-controls="inventories-content" aria-selected="{% if active_tab == 'inventories' %}true{% else %}false{% endif %}"><i class="bi bi-journal-check"></i> {% translate "Inventaires" %}</button>
</li> </li>
</ul> </ul>
<div class="tab-content my-2" id="tablesTabContent"> <div class="tab-content my-2" id="tablesTabContent">
<div class="tab-pane fade show active" id="preparation-orders-content" role="tabpanel" aria-labelledby="preparation-orders-tab"> <div class="tab-pane fade {% if active_tab == 'preparation-orders' or not active_tab %}show active{% endif %}" id="preparation-orders-content" role="tabpanel" aria-labelledby="preparation-orders-tab">
<div id="preparation-orders-container"> <div id="preparation-orders-container">
<!-- Filtres --> <!-- Filtres -->
<div class="card mb-3 shadow-sm bg-light"> <div class="card mb-3 shadow-sm bg-light">
@ -190,94 +195,283 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="tab-pane fade" id="production-orders-content" role="tabpanel" aria-labelledby="production-orders-tab"> <div class="tab-pane fade {% if active_tab == 'production-orders' %}show active{% endif %}" id="production-orders-content" role="tabpanel" aria-labelledby="production-orders-tab">
<div id="production-orders-container"> <div id="production-orders-container">
{% if can_create_production_order %} <!-- Filtres Bons de confection -->
<div class="d-flex justify-content-end align-items-center m-2"> <div class="card mb-3 shadow-sm bg-light">
<a href="{% url 'stock:production_order_create' %}" class="btn btn-light"> <div class="card-body py-2">
<i class="bi bi-plus-circle"></i> {% translate "Nouveau bon de confection" %} <form method="get" class="row g-2 align-items-center">
</a> <!-- Préserver l'onglet actif -->
<input type="hidden" name="tab" value="production-orders">
<div class="col-md-3">
<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="prod_q" value="{{ prod_search_q }}" placeholder="{% translate 'Code, produit, bon prép, composant...' %}">
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
</div>
</div>
<div class="col-md-2">
<div class="input-group input-group-sm">
<label class="input-group-text" for="filter-prod-status"><i class="bi bi-funnel me-1"></i>{% translate "Statut" %}</label>
<select class="form-select" id="filter-prod-status" name="prod_status" onchange="this.form.submit()">
<option value="">-- {% translate "Tous les statuts" %} --</option>
{% for val, label in prod_status_choices %}
<option value="{{ val }}" {% if prod_status == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-2">
<div class="input-group input-group-sm">
<label class="input-group-text" for="filter-prod-warehouse"><i class="bi bi-building me-1"></i>{% translate "Entrepôt" %}</label>
<select class="form-select" id="filter-prod-warehouse" name="prod_warehouse" onchange="this.form.submit()">
<option value="">-- {% translate "Tous entrepôts" %} --</option>
{% for wh in accessible_warehouses %}
<option value="{{ wh.pk }}" {% if prod_warehouse == wh.pk|stringformat:"s" %}selected{% endif %}>{{ wh.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-2">
<div class="input-group input-group-sm">
<label class="input-group-text" for="filter-prod-destination"><i class="bi bi-geo-alt me-1"></i>{% translate "Destination" %}</label>
<select class="form-select" id="filter-prod-destination" name="prod_destination" onchange="this.form.submit()">
<option value="">-- {% translate "Toutes" %} --</option>
{% for val, label in prod_destination_choices %}
<option value="{{ val }}" {% if prod_destination == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-3 d-flex justify-content-end align-items-center gap-1">
{% if prod_status or prod_warehouse or prod_destination or prod_search_q %}
<a href="?tab=production-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 %}
{% if can_create_production_order %}
<a href="{% url 'stock:production_order_create' %}" class="btn btn-sm btn-primary text-nowrap">
<i class="bi bi-plus-circle me-1"></i>{% translate "Nouveau bon" %}
</a>
{% endif %}
</div>
</form>
</div>
</div> </div>
{% endif %}
<!-- Filtres rapides par statut (Pills) -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
<span class="small text-muted fw-semibold me-1"><i class="bi bi-funnel me-1"></i>{% translate "Vue rapide :" %}</span>
<a href="?tab=production-orders{% if prod_search_q %}&prod_q={{ prod_search_q }}{% endif %}{% if prod_warehouse %}&prod_warehouse={{ prod_warehouse }}{% endif %}{% if prod_destination %}&prod_destination={{ prod_destination }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if not prod_status %}btn-primary{% else %}btn-outline-secondary{% endif %} rounded-pill">
{% translate "Tous" %} <span class="badge {% if not prod_status %}bg-white text-primary{% else %}bg-secondary{% endif %} ms-1">{{ prod_counts.total|default:0 }}</span>
</a>
<a href="?tab=production-orders&prod_status=to_process{% if prod_search_q %}&prod_q={{ prod_search_q }}{% endif %}{% if prod_warehouse %}&prod_warehouse={{ prod_warehouse }}{% endif %}{% if prod_destination %}&prod_destination={{ prod_destination }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if prod_status == 'to_process' %}btn-danger text-white{% else %}btn-outline-danger{% endif %} rounded-pill">
<i class="bi bi-hourglass-split me-1"></i>{% translate "À traiter" %} <span class="badge {% if prod_status == 'to_process' %}bg-white text-danger{% else %}bg-danger text-white{% endif %} ms-1">{{ prod_counts.to_process|default:0 }}</span>
</a>
<a href="?tab=production-orders&prod_status=in_production{% if prod_search_q %}&prod_q={{ prod_search_q }}{% endif %}{% if prod_warehouse %}&prod_warehouse={{ prod_warehouse }}{% endif %}{% if prod_destination %}&prod_destination={{ prod_destination }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if prod_status == 'in_production' %}btn-primary{% else %}btn-outline-primary{% endif %} rounded-pill">
<i class="bi bi-gear-wide-connected me-1"></i>{% translate "En confection" %} <span class="badge {% if prod_status == 'in_production' %}bg-white text-primary{% else %}bg-primary text-white{% endif %} ms-1">{{ prod_counts.in_production|default:0 }}</span>
</a>
<a href="?tab=production-orders&prod_status=completed{% if prod_search_q %}&prod_q={{ prod_search_q }}{% endif %}{% if prod_warehouse %}&prod_warehouse={{ prod_warehouse }}{% endif %}{% if prod_destination %}&prod_destination={{ prod_destination }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if prod_status == 'completed' %}btn-success{% else %}btn-outline-success{% endif %} rounded-pill">
<i class="bi bi-check-circle me-1"></i>{% translate "Terminé" %} <span class="badge {% if prod_status == 'completed' %}bg-white text-success{% else %}bg-success text-white{% endif %} ms-1">{{ prod_counts.completed|default:0 }}</span>
</a>
{% if prod_counts.draft %}
<a href="?tab=production-orders&prod_status=draft{% if prod_search_q %}&prod_q={{ prod_search_q }}{% endif %}{% if prod_warehouse %}&prod_warehouse={{ prod_warehouse }}{% endif %}{% if prod_destination %}&prod_destination={{ prod_destination }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if prod_status == 'draft' %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill">
<i class="bi bi-pencil me-1"></i>{% translate "Brouillon" %} <span class="badge {% if prod_status == 'draft' %}bg-white text-secondary{% else %}bg-secondary text-white{% endif %} ms-1">{{ prod_counts.draft|default:0 }}</span>
</a>
{% endif %}
{% if prod_counts.cancelled %}
<a href="?tab=production-orders&prod_status=cancelled{% if prod_search_q %}&prod_q={{ prod_search_q }}{% endif %}{% if prod_warehouse %}&prod_warehouse={{ prod_warehouse }}{% endif %}{% if prod_destination %}&prod_destination={{ prod_destination }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if prod_status == 'cancelled' %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill">
<i class="bi bi-x-circle me-1"></i>{% translate "Annulé" %} <span class="badge {% if prod_status == 'cancelled' %}bg-white text-secondary{% else %}bg-secondary text-white{% endif %} ms-1">{{ prod_counts.cancelled|default:0 }}</span>
</a>
{% endif %}
</div>
{% for order in production_orders %} {% for order in production_orders %}
<div class="card mb-3 shadow-sm <div class="card mb-3 shadow-sm
{% if order.status == 'draft' %}border-secondary {% if order.status == 'to_process' %}border-danger
{% elif order.status == 'in_production' %}border-primary {% elif order.status == 'in_production' %}border-primary
{% elif order.status == 'completed' %}border-success {% elif order.status == 'completed' %}border-success
{% elif order.status == 'cancelled' %}border-danger {% elif order.status == 'cancelled' %}border-secondary
{% else %}border-secondary{% endif %}" {% else %}border-secondary{% endif %}"
style="border-left-width: 4px !important;"> style="border-left-width: 4px !important;">
<!-- En-tête --> <!-- En-tête -->
<div class="card-header d-flex align-items-center gap-2 flex-wrap py-2 <div class="card-header d-flex align-items-center gap-2 flex-wrap py-2
{% if order.status == 'draft' %}bg-secondary bg-opacity-10 {% if order.status == 'to_process' %}bg-danger bg-opacity-10
{% elif order.status == 'in_production' %}bg-primary bg-opacity-10 {% elif order.status == 'in_production' %}bg-primary bg-opacity-10
{% elif order.status == 'completed' %}bg-success bg-opacity-10 {% elif order.status == 'completed' %}bg-success bg-opacity-10
{% elif order.status == 'cancelled' %}bg-danger bg-opacity-10 {% elif order.status == 'cancelled' %}bg-secondary bg-opacity-10
{% else %}bg-secondary bg-opacity-10{% endif %}"> {% else %}bg-secondary bg-opacity-10{% endif %}">
<a href="{% url 'stock:production_order_detail' order.pk %}" class="fw-bold text-decoration-none me-1"> <a href="{% url 'stock:production_order_detail' order.pk %}" class="fw-bold text-decoration-none me-1 fs-6">
{{ order.code }} {{ order.code|default:order.pk }}
</a> </a>
{% if order.status == 'draft' %} <span class="badge {{ order.status_badge_class }}">
<span class="badge bg-secondary">{{ order.get_status_display }}</span> <i class="bi {{ order.status_icon }} me-1"></i>{{ order.get_status_display }}
{% elif order.status == 'in_production' %} </span>
<span class="badge bg-primary"><i class="bi bi-gear me-1"></i>{{ order.get_status_display }}</span>
{% elif order.status == 'completed' %} {% if order.destination == 'internal' %}
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>{{ order.get_status_display }}</span> <span class="badge bg-light text-secondary border"><i class="bi bi-house me-1"></i>{% translate "Interne" %}</span>
{% elif order.status == 'cancelled' %} {% else %}
<span class="badge bg-danger"><i class="bi bi-x-circle me-1"></i>{{ order.get_status_display }}</span> <span class="badge bg-light text-info border"><i class="bi bi-box-arrow-up-right me-1"></i>{% translate "Externe" %}</span>
{% endif %} {% endif %}
<span class="text-muted small ms-auto"><i class="bi bi-calendar3 me-1"></i>{{ order.created_at|date:"d/m/Y" }}</span> {% if order.preparation_order %}
<a href="{% url 'stock:preparation_order_detail' order.preparation_order.pk %}" class="badge bg-light text-dark border text-decoration-none" title="{% translate 'Bon de préparation lié' %}">
<i class="bi bi-clipboard-check me-1 text-primary"></i>#{{ order.preparation_order.order_number }}
</a>
{% if order.preparation_order.intervention %}
<a href="{% url 'interventions:intervention_detail' order.preparation_order.intervention.id %}" class="badge bg-light text-dark border text-decoration-none" title="{% translate 'Intervention liée' %}">
<i class="bi bi-wrench me-1 text-primary"></i>{{ order.preparation_order.intervention.code|default:order.preparation_order.intervention.id }}{% if order.preparation_order.intervention.title %} — {{ order.preparation_order.intervention.title|truncatechars:30 }}{% endif %}
</a>
{% endif %}
{% endif %}
<span class="badge bg-light text-dark border">
<i class="bi bi-building me-1 text-muted"></i>{{ order.warehouse.name }}
</span>
{% if order.documents.count %}
<span class="badge bg-light text-muted border" title="{% translate 'Documents attachés' %}">
<i class="bi bi-paperclip me-1"></i>{{ order.documents.count }}
</span>
{% endif %}
<span class="text-muted small ms-auto"><i class="bi bi-calendar3 me-1"></i>{{ order.created_at|date:"d/m/Y H:i" }}</span>
<a href="{% url 'stock:production_order_detail' order.pk %}" class="btn btn-sm btn-outline-primary ms-1" title="{% translate 'Voir le détail' %}"> <a href="{% url 'stock:production_order_detail' order.pk %}" class="btn btn-sm btn-outline-primary ms-1" title="{% translate 'Voir le détail' %}">
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</a> </a>
</div> </div>
<!-- Corps --> <!-- Corps du bon de confection -->
<div class="card-body p-0"> <div class="card-body p-3">
<table class="table table-sm mb-0" style="table-layout:fixed;width:100%;"> <div class="row align-items-center g-3">
<colgroup> <!-- Produit fini -->
<col style="width:40%"> <div class="col-md-5">
<col style="width:20%"> <div class="small text-muted text-uppercase fw-semibold mb-1">
<col style="width:40%"> <i class="bi bi-box-seam me-1"></i>{% translate "Produit confectionné" %}
</colgroup> </div>
<thead> <div class="fs-6 fw-bold text-dark">
<tr style="border-bottom:1px solid rgba(0,0,0,0.1);"> {% if order.output_product %}
<th class="ps-3 text-muted small fw-semibold text-uppercase">{% translate "Entrepôt" %}</th> <a href="{% url 'stock:product_detail' order.output_product.pk %}" class="text-decoration-none text-dark">
<th class="text-muted small fw-semibold text-uppercase">{% translate "Quantité" %}</th> {{ order.output_product.name }}
<th class="text-muted small fw-semibold text-uppercase">{% translate "Bon de prép." %}</th> </a>
</tr> {% if order.output_product.code %}
</thead> <span class="badge bg-light text-muted border fw-normal ms-1">{{ order.output_product.code }}</span>
<tbody>
<tr>
<td class="ps-3 py-2">{{ order.warehouse.name }}</td>
<td class="py-2 fw-semibold">{{ order.quantity }}</td>
<td class="py-2">
{% if order.preparation_order %}
<a href="{% url 'stock:preparation_order_detail' order.preparation_order.pk %}">
#{{ order.preparation_order.order_number }}
</a>
{% else %}
<span class="text-muted">-</span>
{% endif %} {% endif %}
</td> {% elif order.output_product_name %}
</tr> <span>{{ order.output_product_name }}</span>
</tbody> <span class="badge bg-warning bg-opacity-25 text-dark border border-warning fw-normal ms-1">{% translate "Nouveau produit" %}</span>
</table> {% elif order.preparation_order_other_item %}
<span>{{ order.preparation_order_other_item.description }}</span>
<span class="badge bg-light text-muted border fw-normal ms-1">{% translate "Article sur mesure" %}</span>
{% else %}
<span class="text-muted fst-italic">{% translate "Non spécifié" %}</span>
{% endif %}
</div>
{% if order.output_location %}
<div class="small text-muted mt-1">
<i class="bi bi-geo-alt me-1"></i>{% translate "Emplacement cible :" %} {{ order.output_location.name }}
</div>
{% endif %}
</div>
<!-- Quantité & Destination -->
<div class="col-md-3">
<div class="d-flex flex-column gap-1">
<div>
<span class="text-muted small">{% translate "Quantité à concevoir :" %}</span>
<span class="badge bg-primary fs-6 ms-1">{{ order.quantity }}</span>
</div>
<div>
<span class="text-muted small">{% translate "Entrepôt :" %}</span>
<strong>{{ order.warehouse.name }}</strong>
</div>
</div>
</div>
<!-- Statut & Dates -->
<div class="col-md-4">
<div class="d-flex flex-column gap-1 small">
{% if order.completed_at %}
<div class="text-success fw-semibold">
<i class="bi bi-check-circle me-1"></i>{% translate "Terminé le" %} {{ order.completed_at|date:"d/m/Y H:i" }}
</div>
{% elif order.status == 'in_production' %}
<div class="text-primary fw-semibold">
<i class="bi bi-gear-wide-connected me-1"></i>{% translate "En cours de confection" %}
</div>
{% elif order.status == 'to_process' %}
<div class="text-danger fw-semibold">
<i class="bi bi-hourglass-split me-1"></i>{% translate "En attente de confection" %}
</div>
{% endif %}
{% if order.notes %}
<div class="text-muted fst-italic">
<i class="bi bi-chat-left-text me-1"></i>{{ order.notes|truncatechars:90 }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Composants associés -->
{% if order.order_components.all %}
<div class="mt-3 pt-2 border-top">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small text-muted fw-semibold text-uppercase">
<i class="bi bi-boxes me-1"></i>{% translate "Composants requis" %} ({{ order.order_components.count }}) :
</span>
</div>
<div class="d-flex flex-wrap gap-2">
{% for comp in order.order_components.all %}
<span class="badge bg-light text-dark border p-2 d-inline-flex align-items-center">
<i class="bi bi-box me-1 text-muted"></i>
{{ comp.product.name }}
<span class="badge bg-secondary ms-2" title="{% translate 'Quantité requise' %}">
x{{ comp.quantity }}
</span>
</span>
{% endfor %}
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div class="text-center text-muted py-5"> <div class="text-center text-muted py-5 card shadow-sm">
<i class="bi bi-pencil-square fs-2 d-block mb-2 opacity-50"></i> <i class="bi bi-tools fs-1 d-block mb-2 text-secondary opacity-50"></i>
{% translate "Aucun bon de confection." %} <h5>{% translate "Aucun bon de confection trouvé." %}</h5>
{% if prod_status or prod_warehouse or prod_destination or prod_search_q %}
<p class="small text-muted">{% translate "Aucun résultat ne correspond aux filtres appliqués." %}</p>
<a href="?tab=production-orders" class="btn btn-sm btn-outline-secondary mt-1">
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer les filtres" %}
</a>
{% else %}
<p class="small text-muted">{% translate "Aucun bon de confection enregistré." %}</p>
{% if can_create_production_order %}
<a href="{% url 'stock:production_order_create' %}" class="btn btn-sm btn-primary mt-1">
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer un bon de confection" %}
</a>
{% endif %}
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="tab-pane fade" id="movements-content" role="tabpanel" aria-labelledby="movements-tab"> <div class="tab-pane fade {% if active_tab == 'movements' %}show active{% endif %}" id="movements-content" role="tabpanel" aria-labelledby="movements-tab">
<div id="movements-container"> <div id="movements-container">
{% if can_create_movement %} {% if can_create_movement %}
<div class="d-flex justify-content-end align-items-center gap-2 m-2 flex-wrap"> <div class="d-flex justify-content-end align-items-center gap-2 m-2 flex-wrap">
@ -398,7 +592,7 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="tab-pane fade" id="inventories-content" role="tabpanel" aria-labelledby="inventories-tab"> <div class="tab-pane fade {% if active_tab == 'inventories' %}show active{% endif %}" id="inventories-content" role="tabpanel" aria-labelledby="inventories-tab">
<div id="inventories-container"> <div id="inventories-container">
{% if can_create_inventory %} {% if can_create_inventory %}
<div class="d-flex justify-content-end align-items-center m-2"> <div class="d-flex justify-content-end align-items-center m-2">

View file

@ -452,6 +452,104 @@ class PreparationOrderPriorityTests(TestCase):
self.assertIn(po2, list(response.context["preparation_orders"])) self.assertIn(po2, list(response.context["preparation_orders"]))
self.assertNotIn(po1, list(response.context["preparation_orders"])) self.assertNotIn(po1, list(response.context["preparation_orders"]))
def test_stock_movements_production_order_filters(self):
from .models import ProductionOrder, Warehouse, Product
wh1 = Warehouse.objects.create(name="Entrepôt Nord", code="WH-N-TEST")
wh2 = Warehouse.objects.create(name="Entrepôt Sud", code="WH-S-TEST")
prod = Product.objects.create(name="Armoire sur mesure", code="CONF-01-TEST", sku="SKU-CONF-01-TEST")
order1 = ProductionOrder.objects.create(
code="CONF-2026-0001",
warehouse=wh1,
status="to_process",
destination="internal",
output_product=prod,
quantity=2,
notes="Confection spéciale pour bureau",
)
order2 = ProductionOrder.objects.create(
code="CONF-2026-0002",
warehouse=wh2,
status="completed",
destination="external",
output_product_name="Table extérieure",
quantity=1,
notes="Chantier voirie",
)
# 1. Test status filter
response = self.client.get(reverse("stock:stock_movements") + "?tab=production-orders&prod_status=to_process")
self.assertEqual(response.status_code, 200)
self.assertIn(order1, list(response.context["production_orders"]))
self.assertNotIn(order2, list(response.context["production_orders"]))
# 2. Test search filter
response = self.client.get(reverse("stock:stock_movements") + "?tab=production-orders&prod_q=extérieure")
self.assertEqual(response.status_code, 200)
self.assertIn(order2, list(response.context["production_orders"]))
self.assertNotIn(order1, list(response.context["production_orders"]))
# 3. Test destination filter
response = self.client.get(reverse("stock:stock_movements") + "?tab=production-orders&prod_destination=internal")
self.assertEqual(response.status_code, 200)
self.assertIn(order1, list(response.context["production_orders"]))
self.assertNotIn(order2, list(response.context["production_orders"]))
# 4. Test warehouse filter
response = self.client.get(reverse("stock:stock_movements") + f"?tab=production-orders&prod_warehouse={wh2.id}")
self.assertEqual(response.status_code, 200)
self.assertIn(order2, list(response.context["production_orders"]))
self.assertNotIn(order1, list(response.context["production_orders"]))
# 5. Check prod_counts in context
self.assertIn("prod_counts", response.context)
self.assertGreaterEqual(response.context["prod_counts"]["to_process"], 1)
self.assertGreaterEqual(response.context["prod_counts"]["completed"], 1)
def test_mobile_production_orders_filters(self):
from .models import ProductionOrder, Warehouse
wh = Warehouse.objects.create(name="Atelier Mobile", code="WH-MOB-TEST")
order1 = ProductionOrder.objects.create(
code="CONF-MOB-01",
warehouse=wh,
status="to_process",
destination="internal",
output_product_name="Banc public",
)
order2 = ProductionOrder.objects.create(
code="CONF-MOB-02",
warehouse=wh,
status="in_production",
destination="external",
output_product_name="Potelet spécial",
)
# Status filter
response = self.client.get(reverse("mobile:stock_production_orders_mobile") + "?status=to_process")
self.assertEqual(response.status_code, 200)
self.assertIn(order1, list(response.context["production_orders"]))
self.assertNotIn(order2, list(response.context["production_orders"]))
# Search filter
response = self.client.get(reverse("mobile:stock_production_orders_mobile") + "?q=Potelet")
self.assertEqual(response.status_code, 200)
self.assertIn(order2, list(response.context["production_orders"]))
self.assertNotIn(order1, list(response.context["production_orders"]))
def test_production_order_display_output_product_name(self):
from .models import ProductionOrder, Warehouse, Product
wh = Warehouse.objects.create(name="WH-Disp", code="WH-D-TEST")
p = Product.objects.create(name="Produit Existant", code="PR-D-TEST", sku="SKU-D-TEST")
o1 = ProductionOrder.objects.create(code="CONF-D1", warehouse=wh, output_product=p)
self.assertEqual(o1.display_output_product_name, "Produit Existant")
o2 = ProductionOrder.objects.create(code="CONF-D2", warehouse=wh, output_product_name="Nouveau Produit")
self.assertEqual(o2.display_output_product_name, "Nouveau Produit")
o3 = ProductionOrder.objects.create(code="CONF-D3", warehouse=wh, notes="Confection de : Panneau bois\nAutre note")
self.assertEqual(o3.display_output_product_name, "Panneau bois")
def test_preparation_order_detail_shows_intervention_manager(self): def test_preparation_order_detail_shows_intervention_manager(self):
from django.contrib.auth.models import User from django.contrib.auth.models import User
from interventions.models import Intervention from interventions.models import Intervention

View file

@ -588,9 +588,12 @@ def sign_panel_types_api(request):
def stock_movements(request): def stock_movements(request):
selected_status = request.GET.get('status', '').strip() tab = request.GET.get('tab', 'preparation-orders').strip()
# Filtres Bons de préparation
selected_status = request.GET.get('prep_status', '').strip() or (request.GET.get('status', '').strip() if tab != 'production-orders' else '')
selected_priority = request.GET.get('priority', '').strip() selected_priority = request.GET.get('priority', '').strip()
search_q = request.GET.get('q', '').strip() search_q = request.GET.get('prep_q', '').strip() or (request.GET.get('q', '').strip() if tab != 'production-orders' else '')
preparation_orders = PreparationOrder.objects.select_related('intervention').all() preparation_orders = PreparationOrder.objects.select_related('intervention').all()
if selected_status: if selected_status:
@ -609,11 +612,59 @@ def stock_movements(request):
preparation_orders = preparation_orders.filter(q_filter) preparation_orders = preparation_orders.filter(q_filter)
preparation_orders = preparation_orders.order_by("-date")[:1000] preparation_orders = preparation_orders.order_by("-date")[:1000]
production_orders = ProductionOrder.objects.all().select_related( # Filtrer selon les entrepôts accessibles
'warehouse', 'preparation_order'
).order_by("-created_at")[:1000]
# Filtrer les mouvements de stock et les inventaires selon les entrepôts accessibles
accessible_warehouses = get_user_accessible_warehouses(request.user) accessible_warehouses = get_user_accessible_warehouses(request.user)
# Filtres Bons de confection
prod_status = request.GET.get('prod_status', '').strip() or (request.GET.get('status', '').strip() if tab == 'production-orders' else '')
prod_search_q = request.GET.get('prod_q', '').strip() or (request.GET.get('q', '').strip() if tab == 'production-orders' else '')
prod_warehouse = request.GET.get('prod_warehouse', '').strip() or (request.GET.get('warehouse', '').strip() if tab == 'production-orders' else '')
prod_destination = request.GET.get('prod_destination', '').strip() or (request.GET.get('destination', '').strip() if tab == 'production-orders' else '')
prod_base_qs = ProductionOrder.objects.filter(warehouse__in=accessible_warehouses)
# Compteurs par statut (pour les badges / filtres rapides)
prod_counts = prod_base_qs.aggregate(
total=models.Count('id'),
to_process=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_TO_PROCESS)),
in_production=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_IN_PRODUCTION)),
completed=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_COMPLETED)),
draft=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_DRAFT)),
cancelled=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_CANCELLED)),
)
production_orders = prod_base_qs.select_related(
'warehouse', 'preparation_order', 'output_product', 'output_location',
'preparation_order__intervention', 'preparation_order_other_item',
).prefetch_related(
'order_components__product',
'documents',
)
if prod_status:
production_orders = production_orders.filter(status=prod_status)
if prod_warehouse:
production_orders = production_orders.filter(warehouse_id=prod_warehouse)
if prod_destination:
production_orders = production_orders.filter(destination=prod_destination)
if prod_search_q:
prod_q_filter = (
models.Q(code__icontains=prod_search_q) |
models.Q(output_product__name__icontains=prod_search_q) |
models.Q(output_product_name__icontains=prod_search_q) |
models.Q(preparation_order__order_number__icontains=prod_search_q) |
models.Q(preparation_order__intervention__code__icontains=prod_search_q) |
models.Q(preparation_order__intervention__title__icontains=prod_search_q) |
models.Q(preparation_order_other_item__description__icontains=prod_search_q) |
models.Q(notes__icontains=prod_search_q) |
models.Q(order_components__product__name__icontains=prod_search_q)
)
if prod_search_q.isdigit():
prod_q_filter |= models.Q(id=int(prod_search_q)) | models.Q(preparation_order__intervention__id=int(prod_search_q))
production_orders = production_orders.filter(prod_q_filter).distinct()
production_orders = production_orders.order_by("-created_at")[:1000]
movements = StockMovement.objects.filter( movements = StockMovement.objects.filter(
models.Q(source_location__warehouse__id__in=accessible_warehouses) | models.Q(source_location__warehouse__id__in=accessible_warehouses) |
models.Q(destination_location__warehouse__id__in=accessible_warehouses) models.Q(destination_location__warehouse__id__in=accessible_warehouses)
@ -698,9 +749,18 @@ def stock_movements(request):
"company_members": company_members, "company_members": company_members,
"companies": companies, "companies": companies,
"contracts": contracts, "contracts": contracts,
"active_tab": tab,
"selected_status": selected_status, "selected_status": selected_status,
"selected_priority": selected_priority, "selected_priority": selected_priority,
"search_q": search_q, "search_q": search_q,
"prod_status": prod_status,
"prod_search_q": prod_search_q,
"prod_warehouse": prod_warehouse,
"prod_destination": prod_destination,
"prod_status_choices": ProductionOrder.STATUS_CHOICES,
"prod_destination_choices": ProductionOrder.DESTINATION_CHOICES,
"prod_counts": prod_counts,
"accessible_warehouses": accessible_warehouses,
"preparation_order_status_choices": [ "preparation_order_status_choices": [
('to_process', _("A traiter")), ('to_process', _("A traiter")),
('pending', _("En attente")), ('pending', _("En attente")),
@ -5650,7 +5710,13 @@ def product_delete(request, product_id):
def production_center(request): def production_center(request):
"""Redirige vers l'onglet Bons de confection de la page mouvements de stock.""" """Redirige vers l'onglet Bons de confection de la page mouvements de stock."""
from django.urls import reverse from django.urls import reverse
return redirect(reverse('stock:stock_movements') + '?tab=production-orders') url = reverse('stock:stock_movements') + '?tab=production-orders'
params = request.GET.copy()
if 'tab' in params:
del params['tab']
if params:
url += '&' + params.urlencode()
return redirect(url)
def product_design_list(request): def product_design_list(request):