Compare commits

..

2 commits

29 changed files with 572 additions and 39 deletions

View file

@ -541,3 +541,15 @@ msgstr ""
#: common/views.py:644 #: common/views.py:644
msgid "Erreur lors de la lecture du document" msgid "Erreur lors de la lecture du document"
msgstr "Erreur lors de la lecture du document" msgstr "Erreur lors de la lecture du document"
#: common/templates/common/components/document_viewer.html
msgid "Dézoomer"
msgstr "Zoom out"
#: common/templates/common/components/document_viewer.html
msgid "Zoomer"
msgstr "Zoom in"
#: common/templates/common/components/document_viewer.html
msgid "Réinitialiser"
msgstr "Reset"

View file

@ -527,3 +527,15 @@ msgstr "Accès interdit"
#: common/views.py:644 #: common/views.py:644
msgid "Erreur lors de la lecture du document" msgid "Erreur lors de la lecture du document"
msgstr "Erreur lors de la lecture du document" msgstr "Erreur lors de la lecture du document"
#: common/templates/common/components/document_viewer.html
msgid "Dézoomer"
msgstr "Dézoomer"
#: common/templates/common/components/document_viewer.html
msgid "Zoomer"
msgstr "Zoomer"
#: common/templates/common/components/document_viewer.html
msgid "Réinitialiser"
msgstr "Réinitialiser"

View file

@ -527,3 +527,15 @@ msgstr ""
#: common/views.py:644 #: common/views.py:644
msgid "Erreur lors de la lecture du document" msgid "Erreur lors de la lecture du document"
msgstr "" msgstr ""
#: common/templates/common/components/document_viewer.html
msgid "Dézoomer"
msgstr "Uitzoomen"
#: common/templates/common/components/document_viewer.html
msgid "Zoomer"
msgstr "Inzoomen"
#: common/templates/common/components/document_viewer.html
msgid "Réinitialiser"
msgstr "Herstellen"

View file

@ -221,10 +221,29 @@
style="width: 100%; height: 70vh; border: none;" style="width: 100%; height: 70vh; border: none;"
title="{{ doc.title }}"></iframe> title="{{ doc.title }}"></iframe>
{% elif doc.is_image %} {% elif doc.is_image %}
<img src="{{ doc.file_url }}" <div class="doc-viewer-image-viewport position-relative w-100 d-flex justify-content-center align-items-center overflow-hidden"
class="d-block" style="height: 70vh; min-height: 60vh; user-select: none;">
alt="{{ doc.title }}" <img src="{{ doc.file_url }}"
style="max-height: 70vh; max-width: 100%; object-fit: contain;"> class="doc-viewer-zoom-img"
alt="{{ doc.title }}"
draggable="false"
style="max-height: 70vh; max-width: 100%; object-fit: contain; cursor: zoom-in; transition: transform 0.15s ease-out; transform-origin: center center;">
{# Floating zoom controls toolbar #}
<div class="doc-viewer-zoom-toolbar position-absolute bottom-0 start-50 translate-middle-x mb-2 d-flex align-items-center gap-1 px-2 py-1 rounded-pill shadow-sm"
style="background: rgba(33, 37, 41, 0.78); backdrop-filter: blur(4px); z-index: 5;">
<button type="button" class="btn btn-sm btn-link text-white p-1 text-decoration-none doc-zoom-out" title="{% translate 'Dézoomer' %}">
<i class="bi bi-dash-circle fs-6"></i>
</button>
<span class="text-white-50 px-1 doc-zoom-level fw-semibold" style="font-size: 0.75rem; min-width: 44px; text-align: center;">100%</span>
<button type="button" class="btn btn-sm btn-link text-white p-1 text-decoration-none doc-zoom-in" title="{% translate 'Zoomer' %}">
<i class="bi bi-plus-circle fs-6"></i>
</button>
<button type="button" class="btn btn-sm btn-link text-white p-1 text-decoration-none doc-zoom-reset" title="{% translate 'Réinitialiser' %}">
<i class="bi bi-arrow-counterclockwise fs-6"></i>
</button>
</div>
</div>
{% elif doc.text_preview %} {% elif doc.text_preview %}
<div class="w-100 p-3 overflow-auto" style="max-height: 70vh;"> <div class="w-100 p-3 overflow-auto" style="max-height: 70vh;">
<pre class="bg-light border rounded p-3 mb-0" style="font-size:.85rem;white-space:pre-wrap;word-break:break-all;">{{ doc.text_preview }}</pre> <pre class="bg-light border rounded p-3 mb-0" style="font-size:.85rem;white-space:pre-wrap;word-break:break-all;">{{ doc.text_preview }}</pre>
@ -366,6 +385,19 @@
</div>{# /modal #} </div>{# /modal #}
{% endif %} {% endif %}
<style>
.doc-viewer-zoom-toolbar button:hover {
color: #0dcaf0 !important;
transform: scale(1.15);
}
.doc-viewer-zoom-toolbar button {
transition: transform 0.1s ease, color 0.1s ease;
}
.doc-viewer-image-viewport {
background-color: #f8f9fa;
}
</style>
{# ═══════════════════════════ Inline JS ══════════════════════════════ #} {# ═══════════════════════════ Inline JS ══════════════════════════════ #}
<script> <script>
(function () { (function () {
@ -434,6 +466,223 @@
/* ── Initialise download button when modal becomes visible ── */ /* ── Initialise download button when modal becomes visible ── */
modal.addEventListener('shown.bs.modal', updateDownloadBtn); modal.addEventListener('shown.bs.modal', updateDownloadBtn);
/* ── Image Zoom & Pan functionality ── */
function initImageZoom() {
const viewports = carousel.querySelectorAll('.doc-viewer-image-viewport');
viewports.forEach(function (vp) {
const img = vp.querySelector('.doc-viewer-zoom-img');
const zoomInBtn = vp.querySelector('.doc-zoom-in');
const zoomOutBtn = vp.querySelector('.doc-zoom-out');
const zoomResetBtn = vp.querySelector('.doc-zoom-reset');
const zoomLevel = vp.querySelector('.doc-zoom-level');
if (!img) return;
let scale = 1;
let posX = 0;
let posY = 0;
let isDragging = false;
let startPointerX = 0;
let startPointerY = 0;
let initialPinchDist = 0;
let initialScale = 1;
function clampPan(targetX, targetY, curScale) {
if (curScale <= 1) return { x: 0, y: 0 };
const vpWidth = vp.clientWidth;
const vpHeight = vp.clientHeight;
const maxPanX = Math.max(0, (vpWidth * (curScale - 1)) / 2 + 50);
const maxPanY = Math.max(0, (vpHeight * (curScale - 1)) / 2 + 50);
return {
x: Math.min(maxPanX, Math.max(-maxPanX, targetX)),
y: Math.min(maxPanY, Math.max(-maxPanY, targetY))
};
}
function applyTransform(animate) {
img.style.transition = animate ? 'transform 0.15s ease-out' : 'none';
img.style.transform = 'translate(' + posX + 'px, ' + posY + 'px) scale(' + scale + ')';
if (zoomLevel) {
zoomLevel.textContent = Math.round(scale * 100) + '%';
if (scale > 1.05) {
zoomLevel.classList.remove('text-white-50');
zoomLevel.classList.add('text-white');
} else {
zoomLevel.classList.remove('text-white');
zoomLevel.classList.add('text-white-50');
}
}
if (scale > 1.05) {
img.style.cursor = isDragging ? 'grabbing' : 'grab';
} else {
img.style.cursor = 'zoom-in';
}
}
function setZoom(newScale, focalX, focalY, animate) {
const prevScale = scale;
scale = Math.min(5, Math.max(1, newScale));
if (scale <= 1) {
scale = 1;
posX = 0;
posY = 0;
} else if (focalX !== undefined && focalY !== undefined && prevScale !== scale) {
const vpRect = vp.getBoundingClientRect();
const cx = focalX - (vpRect.left + vpRect.width / 2);
const cy = focalY - (vpRect.top + vpRect.height / 2);
const ratio = scale / prevScale;
posX = posX - cx * (ratio - 1);
posY = posY - cy * (ratio - 1);
const clamped = clampPan(posX, posY, scale);
posX = clamped.x;
posY = clamped.y;
} else {
const clamped = clampPan(posX, posY, scale);
posX = clamped.x;
posY = clamped.y;
}
applyTransform(animate !== false);
}
function resetZoom() {
scale = 1;
posX = 0;
posY = 0;
applyTransform(true);
}
vp._resetZoom = resetZoom;
if (zoomInBtn) {
zoomInBtn.addEventListener('click', function (e) {
e.stopPropagation();
setZoom(scale * 1.35, undefined, undefined, true);
});
}
if (zoomOutBtn) {
zoomOutBtn.addEventListener('click', function (e) {
e.stopPropagation();
setZoom(scale / 1.35, undefined, undefined, true);
});
}
if (zoomResetBtn) {
zoomResetBtn.addEventListener('click', function (e) {
e.stopPropagation();
resetZoom();
});
}
vp.addEventListener('wheel', function (e) {
e.preventDefault();
const factor = e.deltaY < 0 ? 1.15 : (1 / 1.15);
setZoom(scale * factor, e.clientX, e.clientY, false);
}, { passive: false });
vp.addEventListener('dblclick', function (e) {
if (e.target.closest('.doc-viewer-zoom-toolbar')) return;
e.preventDefault();
if (scale > 1.2) {
resetZoom();
} else {
setZoom(2.5, e.clientX, e.clientY, true);
}
});
vp.addEventListener('mousedown', function (e) {
if (e.target.closest('.doc-viewer-zoom-toolbar')) return;
if (scale <= 1) return;
isDragging = true;
startPointerX = e.clientX - posX;
startPointerY = e.clientY - posY;
img.style.cursor = 'grabbing';
e.preventDefault();
e.stopPropagation();
});
window.addEventListener('mousemove', function (e) {
if (!isDragging) return;
const clamped = clampPan(e.clientX - startPointerX, e.clientY - startPointerY, scale);
posX = clamped.x;
posY = clamped.y;
applyTransform(false);
});
window.addEventListener('mouseup', function () {
if (!isDragging) return;
isDragging = false;
if (scale > 1) img.style.cursor = 'grab';
});
function getTouchDistance(t1, t2) {
const dx = t1.clientX - t2.clientX;
const dy = t1.clientY - t2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
vp.addEventListener('touchstart', function (e) {
if (e.target.closest('.doc-viewer-zoom-toolbar')) return;
if (e.touches.length === 2) {
initialPinchDist = getTouchDistance(e.touches[0], e.touches[1]);
initialScale = scale;
e.preventDefault();
e.stopPropagation();
} else if (e.touches.length === 1 && scale > 1) {
isDragging = true;
startPointerX = e.touches[0].clientX - posX;
startPointerY = e.touches[0].clientY - posY;
e.stopPropagation();
}
}, { passive: false });
vp.addEventListener('touchmove', function (e) {
if (e.target.closest('.doc-viewer-zoom-toolbar')) return;
if (e.touches.length === 2 && initialPinchDist > 0) {
const currentDist = getTouchDistance(e.touches[0], e.touches[1]);
const factor = currentDist / initialPinchDist;
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
setZoom(initialScale * factor, midX, midY, false);
e.preventDefault();
e.stopPropagation();
} else if (e.touches.length === 1 && isDragging && scale > 1) {
const clamped = clampPan(e.touches[0].clientX - startPointerX, e.touches[0].clientY - startPointerY, scale);
posX = clamped.x;
posY = clamped.y;
applyTransform(false);
e.preventDefault();
e.stopPropagation();
}
}, { passive: false });
vp.addEventListener('touchend', function (e) {
if (e.touches.length < 2) {
initialPinchDist = 0;
}
if (e.touches.length === 0) {
isDragging = false;
}
});
});
}
function resetAllZooms() {
carousel.querySelectorAll('.doc-viewer-image-viewport').forEach(function (vp) {
if (typeof vp._resetZoom === 'function') {
vp._resetZoom();
}
});
}
carousel.addEventListener('slide.bs.carousel', resetAllZooms);
modal.addEventListener('hidden.bs.modal', resetAllZooms);
initImageZoom();
})(); })();
</script> </script>

View file

@ -1137,3 +1137,12 @@ msgstr "No interventions to control"
#: home/templates/home/widgets/controller_shortcuts.html:72 #: home/templates/home/widgets/controller_shortcuts.html:72
msgid "Voir toutes les interventions à contrôler" msgid "Voir toutes les interventions à contrôler"
msgstr "View all interventions to control" msgstr "View all interventions to control"
#: home/templates/home/widgets/stock_summary.html
msgid "à traiter &middot; en cours"
msgstr "to process &middot; in progress"
#: home/templates/home/widgets/stock_summary.html
msgid "Aucun bon à traiter ou en cours"
msgstr "No orders to process or in progress"

View file

@ -1146,3 +1146,12 @@ msgstr "Aucune intervention à contrôler"
#: home/templates/home/widgets/controller_shortcuts.html:72 #: home/templates/home/widgets/controller_shortcuts.html:72
msgid "Voir toutes les interventions à contrôler" msgid "Voir toutes les interventions à contrôler"
msgstr "Voir toutes les interventions à contrôler" msgstr "Voir toutes les interventions à contrôler"
#: home/templates/home/widgets/stock_summary.html
msgid "à traiter &middot; en cours"
msgstr "à traiter &middot; en cours"
#: home/templates/home/widgets/stock_summary.html
msgid "Aucun bon à traiter ou en cours"
msgstr "Aucun bon à traiter ou en cours"

View file

@ -1143,3 +1143,12 @@ msgstr "Geen te controleren interventies"
#: home/templates/home/widgets/controller_shortcuts.html:72 #: home/templates/home/widgets/controller_shortcuts.html:72
msgid "Voir toutes les interventions à contrôler" msgid "Voir toutes les interventions à contrôler"
msgstr "Alle te controleren interventies bekijken" msgstr "Alle te controleren interventies bekijken"
#: home/templates/home/widgets/stock_summary.html
msgid "à traiter &middot; en cours"
msgstr "te verwerken &middot; in behandeling"
#: home/templates/home/widgets/stock_summary.html
msgid "Aucun bon à traiter ou en cours"
msgstr "Geen bonnen te verwerken of in behandeling"

View file

@ -16,15 +16,25 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="card h-100 border-primary shadow-sm"> <div class="card h-100 border-primary shadow-sm">
<div class="card-header bg-primary text-white d-flex align-items-center"> <div class="card-header bg-primary text-white d-flex align-items-center">
<a href="{% url 'stock:stock_movements' %}" class="text-white text-decoration-none fw-semibold small text-uppercase flex-grow-1" style="letter-spacing:.04em;"> <a href="{% url 'stock:stock_movements' %}?tab=preparation-orders&status=active" class="text-white text-decoration-none fw-semibold small text-uppercase flex-grow-1" style="letter-spacing:.04em;">
<i class="bi bi-clipboard-check me-2"></i>{% translate "Bons de préparation" %} <i class="bi bi-clipboard-check me-2"></i>{% translate "Bons de préparation" %}
</a> </a>
</div> </div>
<div class="text-center py-3 border-bottom"> <div class="text-center py-2 border-bottom">
<a href="{% url 'stock:stock_movements' %}" class="text-decoration-none"> <a href="{% url 'stock:stock_movements' %}?tab=preparation-orders&status=active" class="text-decoration-none">
<span class="display-5 fw-bold text-primary">{{ preparation_order_to_process_count }}</span> <span class="display-5 fw-bold text-primary">{{ preparation_order_to_process_count }}</span>
</a> </a>
<div class="text-muted small">{% translate "à traiter" %}</div> <div class="text-muted small">{% translate "à traiter &middot; en cours" %}</div>
<div class="d-flex justify-content-center gap-2 mt-2">
<a href="{% url 'stock:stock_movements' %}?tab=preparation-orders&status=to_process"
class="badge text-decoration-none bg-warning-subtle text-warning-emphasis border border-warning-subtle">
<i class="bi bi-hourglass-split me-1"></i>{% translate "À traiter" %} ({{ preparation_order_to_process_only_count|default:0 }})
</a>
<a href="{% url 'stock:stock_movements' %}?tab=preparation-orders&status=pending"
class="badge text-decoration-none bg-info-subtle text-info-emphasis border border-info-subtle">
<i class="bi bi-clock me-1"></i>{% translate "En attente" %} ({{ preparation_order_pending_count|default:0 }})
</a>
</div>
</div> </div>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% for order in preparation_order_to_process_list %} {% for order in preparation_order_to_process_list %}
@ -42,12 +52,12 @@
</li> </li>
{% empty %} {% empty %}
<li class="list-group-item text-center text-muted py-3 small"> <li class="list-group-item text-center text-muted py-3 small">
<i class="bi bi-check-circle text-success me-1"></i>{% translate "Aucun bon à traiter" %} <i class="bi bi-check-circle text-success me-1"></i>{% translate "Aucun bon à traiter ou en cours" %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<div class="card-footer text-center small bg-transparent"> <div class="card-footer text-center small bg-transparent">
<a href="{% url 'stock:stock_movements' %}" class="text-muted text-decoration-none"> <a href="{% url 'stock:stock_movements' %}?tab=preparation-orders&status=active" class="text-muted text-decoration-none">
<i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir tous" %} <i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir tous" %}
</a> </a>
</div> </div>
@ -58,15 +68,25 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="card h-100 border-info shadow-sm"> <div class="card h-100 border-info shadow-sm">
<div class="card-header bg-info text-white d-flex align-items-center"> <div class="card-header bg-info text-white d-flex align-items-center">
<a href="{% url 'stock:stock_movements' %}#production-orders-content" class="text-white text-decoration-none fw-semibold small text-uppercase flex-grow-1" style="letter-spacing:.04em;"> <a href="{% url 'stock:stock_movements' %}?tab=production-orders&prod_status=active" class="text-white text-decoration-none fw-semibold small text-uppercase flex-grow-1" style="letter-spacing:.04em;">
<i class="bi bi-tools me-2"></i>{% translate "Bons de confection" %} <i class="bi bi-tools me-2"></i>{% translate "Bons de confection" %}
</a> </a>
</div> </div>
<div class="text-center py-3 border-bottom"> <div class="text-center py-2 border-bottom">
<a href="{% url 'stock:stock_movements' %}#production-orders-content" class="text-decoration-none"> <a href="{% url 'stock:stock_movements' %}?tab=production-orders&prod_status=active" class="text-decoration-none">
<span class="display-5 fw-bold text-info">{{ production_order_to_process_count }}</span> <span class="display-5 fw-bold text-info">{{ production_order_to_process_count }}</span>
</a> </a>
<div class="text-muted small">{% translate "à traiter" %}</div> <div class="text-muted small">{% translate "à traiter &middot; en cours" %}</div>
<div class="d-flex justify-content-center gap-2 mt-2">
<a href="{% url 'stock:stock_movements' %}?tab=production-orders&prod_status=to_process"
class="badge text-decoration-none bg-danger-subtle text-danger-emphasis border border-danger-subtle">
<i class="bi bi-hourglass-split me-1"></i>{% translate "À traiter" %} ({{ production_order_to_process_only_count|default:0 }})
</a>
<a href="{% url 'stock:stock_movements' %}?tab=production-orders&prod_status=in_production"
class="badge text-decoration-none bg-primary-subtle text-primary-emphasis border border-primary-subtle">
<i class="bi bi-gear-wide-connected me-1"></i>{% translate "En confection" %} ({{ production_order_in_production_count|default:0 }})
</a>
</div>
</div> </div>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% for order in production_order_to_process_list %} {% for order in production_order_to_process_list %}
@ -83,12 +103,12 @@
</li> </li>
{% empty %} {% empty %}
<li class="list-group-item text-center text-muted py-3 small"> <li class="list-group-item text-center text-muted py-3 small">
<i class="bi bi-check-circle text-success me-1"></i>{% translate "Aucun bon à traiter" %} <i class="bi bi-check-circle text-success me-1"></i>{% translate "Aucun bon à traiter ou en cours" %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<div class="card-footer text-center small bg-transparent"> <div class="card-footer text-center small bg-transparent">
<a href="{% url 'stock:stock_movements' %}#production-orders-content" class="text-muted text-decoration-none"> <a href="{% url 'stock:stock_movements' %}?tab=production-orders&prod_status=active" class="text-muted text-decoration-none">
<i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir tous" %} <i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir tous" %}
</a> </a>
</div> </div>

View file

@ -439,6 +439,69 @@ class ControllerShortcutsWidgetTests(TestCase):
self.assertContains(response, 'ITV-CTRL-002') self.assertContains(response, 'ITV-CTRL-002')
class StockSummaryWidgetTests(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user('stockuser', password='pass')
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
self.role_admin, _ = Role.objects.get_or_create(name='admin')
self.user_config.roles.add(self.role_admin)
from stock.models import Warehouse, PreparationOrder, ProductionOrder
self.warehouse = Warehouse.objects.create(name='Entrepôt Principal')
from common.models import UserWarehouseAccess
UserWarehouseAccess.objects.create(user_config=self.user_config, warehouse=self.warehouse, can_manage=True)
# Bons de préparation
self.prep_to_process = PreparationOrder.objects.create(order_number='BP-TEST-001', status='to_process')
self.prep_pending = PreparationOrder.objects.create(order_number='BP-TEST-002', status='pending')
self.prep_processed = PreparationOrder.objects.create(order_number='BP-TEST-003', status='processed')
# Bons de confection
self.prod_to_process = ProductionOrder.objects.create(code='BC-TEST-001', warehouse=self.warehouse, status='to_process', quantity=1)
self.prod_in_production = ProductionOrder.objects.create(code='BC-TEST-002', warehouse=self.warehouse, status='in_production', quantity=1)
self.prod_completed = ProductionOrder.objects.create(code='BC-TEST-003', warehouse=self.warehouse, status='completed', quantity=1)
def test_stock_summary_widget_counts_and_context(self):
from django.urls import reverse
self.client.force_login(self.user)
response = self.client.get(reverse('home:index'), HTTP_ACCEPT_LANGUAGE='fr')
self.assertEqual(response.status_code, 200)
# Préparation: active = to_process (1) + pending (1) = 2
self.assertEqual(response.context['preparation_order_to_process_count'], 2)
self.assertEqual(response.context['preparation_order_to_process_only_count'], 1)
self.assertEqual(response.context['preparation_order_pending_count'], 1)
# Confection: active = to_process (1) + in_production (1) = 2
self.assertEqual(response.context['production_order_to_process_count'], 2)
self.assertEqual(response.context['production_order_to_process_only_count'], 1)
self.assertEqual(response.context['production_order_in_production_count'], 1)
def test_stock_movements_active_filter(self):
from django.urls import reverse
self.client.force_login(self.user)
# Préparation avec status=active
resp_prep = self.client.get(reverse('stock:stock_movements') + '?tab=preparation-orders&status=active')
self.assertEqual(resp_prep.status_code, 200)
prep_orders = list(resp_prep.context['preparation_orders'])
self.assertEqual(len(prep_orders), 2)
self.assertIn(self.prep_to_process, prep_orders)
self.assertIn(self.prep_pending, prep_orders)
self.assertNotIn(self.prep_processed, prep_orders)
# Confection avec prod_status=active
resp_prod = self.client.get(reverse('stock:stock_movements') + '?tab=production-orders&prod_status=active')
self.assertEqual(resp_prod.status_code, 200)
prod_orders = list(resp_prod.context['production_orders'])
self.assertEqual(len(prod_orders), 2)
self.assertIn(self.prod_to_process, prod_orders)
self.assertIn(self.prod_in_production, prod_orders)
self.assertNotIn(self.prod_completed, prod_orders)

View file

@ -479,12 +479,29 @@ def index(request):
t = _t('approval_counts', t, _perf_splits) t = _t('approval_counts', t, _perf_splits)
# ========== Données widget stock_summary ========== # ========== Données widget stock_summary ==========
_prep_counts = PreparationOrder.objects.aggregate(
active=Count('id', filter=Q(status__in=['to_process', 'pending'])),
to_process=Count('id', filter=Q(status='to_process')),
pending=Count('id', filter=Q(status='pending')),
)
preparation_order_to_process_count = _prep_counts['active']
preparation_order_to_process_only_count = _prep_counts['to_process']
preparation_order_pending_count = _prep_counts['pending']
_prep_qs = PreparationOrder.objects.filter(status__in=['to_process', 'pending']) _prep_qs = PreparationOrder.objects.filter(status__in=['to_process', 'pending'])
preparation_order_to_process_count = _prep_qs.count()
preparation_order_to_process_list = list(_prep_qs.order_by('-date')[:5]) preparation_order_to_process_list = list(_prep_qs.order_by('-date')[:5])
_prod_qs = ProductionOrder.objects.filter(status__in=['to_process', 'in_production']) from stock.permissions import get_user_accessible_warehouses
production_order_to_process_count = _prod_qs.count() accessible_warehouses = get_user_accessible_warehouses(request.user)
_prod_base_qs = ProductionOrder.objects.filter(warehouse__in=accessible_warehouses)
_prod_counts = _prod_base_qs.aggregate(
active=Count('id', filter=Q(status__in=['to_process', 'in_production'])),
to_process=Count('id', filter=Q(status=ProductionOrder.STATUS_TO_PROCESS)),
in_production=Count('id', filter=Q(status=ProductionOrder.STATUS_IN_PRODUCTION)),
)
production_order_to_process_count = _prod_counts['active']
production_order_to_process_only_count = _prod_counts['to_process']
production_order_in_production_count = _prod_counts['in_production']
_prod_qs = _prod_base_qs.filter(status__in=['to_process', 'in_production'])
production_order_to_process_list = list( production_order_to_process_list = list(
_prod_qs.select_related('warehouse').order_by('-created_at')[:5] _prod_qs.select_related('warehouse').order_by('-created_at')[:5]
) )
@ -827,8 +844,12 @@ def index(request):
'show_checked_split': show_checked_split, 'show_checked_split': show_checked_split,
**split_checked_counts, **split_checked_counts,
'preparation_order_to_process_count': preparation_order_to_process_count, 'preparation_order_to_process_count': preparation_order_to_process_count,
'preparation_order_to_process_only_count': preparation_order_to_process_only_count,
'preparation_order_pending_count': preparation_order_pending_count,
'preparation_order_to_process_list': preparation_order_to_process_list, 'preparation_order_to_process_list': preparation_order_to_process_list,
'production_order_to_process_count': production_order_to_process_count, 'production_order_to_process_count': production_order_to_process_count,
'production_order_to_process_only_count': production_order_to_process_only_count,
'production_order_in_production_count': production_order_in_production_count,
'production_order_to_process_list': production_order_to_process_list, 'production_order_to_process_list': production_order_to_process_list,
'stock_low_alerts_list': stock_low_alerts_list, 'stock_low_alerts_list': stock_low_alerts_list,
'loan_items_total': loan_items_total, 'loan_items_total': loan_items_total,

View file

@ -442,7 +442,10 @@ def stock_preparation_orders_mobile(request):
preparation_orders = PreparationOrder.objects.select_related('intervention').all() preparation_orders = PreparationOrder.objects.select_related('intervention').all()
if selected_status: if selected_status:
preparation_orders = preparation_orders.filter(status=selected_status) if selected_status == 'active':
preparation_orders = preparation_orders.filter(status__in=['to_process', 'pending'])
else:
preparation_orders = preparation_orders.filter(status=selected_status)
if selected_priority: if selected_priority:
preparation_orders = preparation_orders.filter(priority=selected_priority) preparation_orders = preparation_orders.filter(priority=selected_priority)
if search_q: if search_q:
@ -465,6 +468,7 @@ def stock_preparation_orders_mobile(request):
'selected_priority': selected_priority, 'selected_priority': selected_priority,
'search_q': search_q, 'search_q': search_q,
'preparation_order_status_choices': [ 'preparation_order_status_choices': [
('active', _("À traiter & En attente")),
('to_process', _("A traiter")), ('to_process', _("A traiter")),
('pending', _("En attente")), ('pending', _("En attente")),
('processed', _("Traité")), ('processed', _("Traité")),
@ -504,6 +508,7 @@ def stock_production_orders_mobile(request):
draft=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_DRAFT)), draft=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_DRAFT)),
cancelled=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_CANCELLED)), cancelled=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_CANCELLED)),
) )
prod_counts['active'] = prod_counts['to_process'] + prod_counts['in_production']
production_orders = base_qs.select_related( production_orders = base_qs.select_related(
'warehouse', 'preparation_order', 'output_product', 'output_location', 'warehouse', 'preparation_order', 'output_product', 'output_location',
@ -513,7 +518,12 @@ def stock_production_orders_mobile(request):
) )
if selected_status: if selected_status:
production_orders = production_orders.filter(status=selected_status) if selected_status == 'active':
production_orders = production_orders.filter(
status__in=[ProductionOrder.STATUS_TO_PROCESS, ProductionOrder.STATUS_IN_PRODUCTION]
)
else:
production_orders = production_orders.filter(status=selected_status)
if selected_warehouse: if selected_warehouse:
production_orders = production_orders.filter(warehouse_id=selected_warehouse) production_orders = production_orders.filter(warehouse_id=selected_warehouse)
if selected_destination: if selected_destination:
@ -542,7 +552,9 @@ def stock_production_orders_mobile(request):
'selected_status': selected_status, 'selected_status': selected_status,
'selected_warehouse': selected_warehouse, 'selected_warehouse': selected_warehouse,
'selected_destination': selected_destination, 'selected_destination': selected_destination,
'status_choices': ProductionOrder.STATUS_CHOICES, 'status_choices': [
('active', _("À traiter & En confection")),
] + list(ProductionOrder.STATUS_CHOICES),
'destination_choices': ProductionOrder.DESTINATION_CHOICES, 'destination_choices': ProductionOrder.DESTINATION_CHOICES,
'warehouses': accessible_warehouses, 'warehouses': accessible_warehouses,
'prod_counts': prod_counts, 'prod_counts': prod_counts,

View file

@ -5923,3 +5923,16 @@ msgstr "Intervention canceled"
#~ msgid "Rafraîchir la page" #~ msgid "Rafraîchir la page"
#~ msgstr "Refresh page" #~ msgstr "Refresh page"
msgid "À traiter & En attente"
msgstr "To process & Pending"
msgid "À traiter & En confection"
msgstr "To process & In production"
msgid "À traiter & en attente"
msgstr "To process & pending"
msgid "À traiter & en confection"
msgstr "To process & in production"

View file

@ -5730,3 +5730,16 @@ msgstr "Intervention annulée"
#~ msgid "Rafraîchir la page" #~ msgid "Rafraîchir la page"
#~ msgstr "Rafraîchir la page" #~ msgstr "Rafraîchir la page"
msgid "À traiter & En attente"
msgstr "À traiter & En attente"
msgid "À traiter & En confection"
msgstr "À traiter & En confection"
msgid "À traiter & en attente"
msgstr "À traiter & en attente"
msgid "À traiter & en confection"
msgstr "À traiter & en confection"

View file

@ -5730,3 +5730,16 @@ msgstr "Interventie geannuleerd"
#~ msgid "Rafraîchir la page" #~ msgid "Rafraîchir la page"
#~ msgstr "Pagina vernieuwen" #~ msgstr "Pagina vernieuwen"
msgid "À traiter & En attente"
msgstr "Te verwerken & In afwachting"
msgid "À traiter & En confection"
msgstr "Te verwerken & In productie"
msgid "À traiter & en attente"
msgstr "Te verwerken & in afwachting"
msgid "À traiter & en confection"
msgstr "Te verwerken & in productie"

View file

@ -2246,8 +2246,10 @@ class ProductionOrder(models.Model):
return self.output_product_name return self.output_product_name
if self.preparation_order_other_item and self.preparation_order_other_item.description: if self.preparation_order_other_item and self.preparation_order_other_item.description:
return 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: if self.notes and ': ' in self.notes:
return self.notes.split(': ', 1)[1].strip() for line in self.notes.splitlines():
if ': ' in line:
return line.split(': ', 1)[1].strip()
return "" return ""
def __str__(self): def __str__(self):

View file

@ -102,6 +102,10 @@
class="badge {% if not selected_status %}bg-primary text-white{% else %}bg-light text-dark border{% endif %} text-decoration-none py-1 px-2"> 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 }}) {% translate "Tous" %} ({{ prod_counts.total|default:0 }})
</a> </a>
<a href="?status=active{% 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 == 'active' %}bg-danger text-white{% else %}bg-light text-dark border{% endif %} text-decoration-none py-1 px-2">
<i class="bi bi-play-circle me-1"></i>{% translate "À traiter & En confection" %} ({{ prod_counts.active|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 %}" <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"> 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 }}) <i class="bi bi-hourglass-split me-1"></i>{% translate "À traiter" %} ({{ prod_counts.to_process|default:0 }})

View file

@ -347,7 +347,11 @@
{% 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.delta > 0 %} {% if 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' %}">
<i class="bi bi-arrow-return-left me-1"></i>{% translate "Restitué au stock" %} ({{ item_data.gross_in }})
</span>
{% elif item_data.delta > 0 %}
<span class="badge bg-warning text-dark fs-6" title="{% translate 'Complément à prélever' %}"> <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" %} <i class="bi bi-plus-circle me-1"></i>+{{ item_data.delta }} {% translate "à prélever" %}
</span> </span>
@ -357,10 +361,6 @@
<i class="bi bi-arrow-return-left me-1"></i>{{ item_data.delta }} {% translate "à restituer" %} <i class="bi bi-arrow-return-left me-1"></i>{{ item_data.delta }} {% translate "à restituer" %}
</span> </span>
<br><small class="text-muted">{% translate "Sorti net" %}: {{ item_data.executed_qty }}</small> <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' %}">
<i class="bi bi-arrow-return-left me-1"></i>{% translate "Restitué au stock" %} ({{ item_data.gross_in }})
</span>
{% elif item_data.gross_in > 0 %} {% elif item_data.gross_in > 0 %}
<span class="badge bg-success fs-6"> <span class="badge bg-success fs-6">
<i class="bi bi-check-circle me-1"></i>{% translate "Sorti net" %} ({{ item_data.executed_qty }}) <i class="bi bi-check-circle me-1"></i>{% translate "Sorti net" %} ({{ item_data.executed_qty }})

View file

@ -27,12 +27,19 @@
<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 {% 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> <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" %}
{% if prep_counts.active %}
<span class="badge bg-danger rounded-pill ms-1" title="{% translate 'À traiter & en attente' %}">{{ prep_counts.active }}</span>
{% endif %}
</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<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 %}"> <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" %} <i class="bi bi-tools"></i> {% translate "Bons de confection" %}
{% if prod_counts.to_process %} {% if prod_counts.active %}
<span class="badge bg-danger rounded-pill ms-1" title="{% translate 'À traiter & en confection' %}">{{ prod_counts.active }}</span>
{% elif prod_counts.to_process %}
<span class="badge bg-danger rounded-pill ms-1" title="{% translate 'À traiter' %}">{{ prod_counts.to_process }}</span> <span class="badge bg-danger rounded-pill ms-1" title="{% translate 'À traiter' %}">{{ prod_counts.to_process }}</span>
{% endif %} {% endif %}
</button> </button>
@ -104,6 +111,31 @@
</div> </div>
</div> </div>
<!-- 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=preparation-orders{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_priority %}&priority={{ selected_priority }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if not selected_status %}btn-primary{% else %}btn-outline-secondary{% endif %} rounded-pill">
{% translate "Tous" %} <span class="badge {% if not selected_status %}bg-white text-primary{% else %}bg-secondary{% endif %} ms-1">{{ prep_counts.total|default:0 }}</span>
</a>
<a href="?tab=preparation-orders&status=active{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_priority %}&priority={{ selected_priority }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if selected_status == 'active' %}btn-danger text-white{% else %}btn-outline-danger{% endif %} rounded-pill">
<i class="bi bi-play-circle me-1"></i>{% translate "À traiter & En attente" %} <span class="badge {% if selected_status == 'active' %}bg-white text-danger{% else %}bg-danger text-white{% endif %} ms-1">{{ prep_counts.active|default:0 }}</span>
</a>
<a href="?tab=preparation-orders&status=to_process{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_priority %}&priority={{ selected_priority }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if selected_status == 'to_process' %}btn-warning text-dark{% else %}btn-outline-warning text-dark{% endif %} rounded-pill">
<i class="bi bi-hourglass-split me-1"></i>{% translate "À traiter" %} <span class="badge {% if selected_status == 'to_process' %}bg-dark text-white{% else %}bg-warning text-dark{% endif %} ms-1">{{ prep_counts.to_process|default:0 }}</span>
</a>
<a href="?tab=preparation-orders&status=pending{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_priority %}&priority={{ selected_priority }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if selected_status == 'pending' %}btn-info text-white{% else %}btn-outline-info{% endif %} rounded-pill">
<i class="bi bi-clock me-1"></i>{% translate "En attente" %} <span class="badge {% if selected_status == 'pending' %}bg-white text-info{% else %}bg-info text-white{% endif %} ms-1">{{ prep_counts.pending|default:0 }}</span>
</a>
<a href="?tab=preparation-orders&status=processed{% if search_q %}&q={{ search_q }}{% endif %}{% if selected_priority %}&priority={{ selected_priority }}{% endif %}"
class="btn btn-sm py-1 px-2 {% if selected_status == 'processed' %}btn-success{% else %}btn-outline-success{% endif %} rounded-pill">
<i class="bi bi-check-circle me-1"></i>{% translate "Traité" %} <span class="badge {% if selected_status == 'processed' %}bg-white text-success{% else %}bg-success text-white{% endif %} ms-1">{{ prep_counts.processed|default:0 }}</span>
</a>
</div>
{% for order in preparation_orders %} {% for order in preparation_orders %}
<div class="card mb-3 shadow-sm <div class="card mb-3 shadow-sm
{% if order.status == 'to_process' %}border-warning {% if order.status == 'to_process' %}border-warning
@ -271,6 +303,10 @@
class="btn btn-sm py-1 px-2 {% if not prod_status %}btn-primary{% else %}btn-outline-secondary{% endif %} rounded-pill"> 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> {% 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>
<a href="?tab=production-orders&prod_status=active{% 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 == 'active' %}btn-danger text-white{% else %}btn-outline-danger{% endif %} rounded-pill">
<i class="bi bi-play-circle me-1"></i>{% translate "À traiter & En confection" %} <span class="badge {% if prod_status == 'active' %}bg-white text-danger{% else %}bg-danger text-white{% endif %} ms-1">{{ prod_counts.active|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 %}" <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"> 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> <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>

View file

@ -1814,11 +1814,13 @@ class PreparationOrderAutocompleteTests(TestCase):
b_in = StockMovementBatch.objects.create(movement_type="in", preparation_order=po) b_in = StockMovementBatch.objects.create(movement_type="in", preparation_order=po)
StockMovement.objects.create(batch=b_in, product=prod, quantity=2) StockMovement.objects.create(batch=b_in, product=prod, quantity=2)
from django.utils.translation import gettext as _ from django.utils import translation
response = self.client.get(reverse("stock:preparation_order_detail", args=[po.pk])) with translation.override('fr'):
self.assertEqual(response.status_code, 200) from django.utils.translation import gettext as _
self.assertContains(response, _("Restitué au stock")) response = self.client.get(reverse("stock:preparation_order_detail", args=[po.pk]), HTTP_ACCEPT_LANGUAGE='fr')
self.assertNotContains(response, "à prélever") self.assertEqual(response.status_code, 200)
self.assertContains(response, _("Restitué au stock"))
self.assertNotContains(response, "à prélever")
def test_create_return_button_and_prefilled_entry_form(self): def test_create_return_button_and_prefilled_entry_form(self):
from stock.models import Product, PreparationOrder, PreparationOrderItem from stock.models import Product, PreparationOrder, PreparationOrderItem

View file

@ -595,9 +595,21 @@ def stock_movements(request):
selected_priority = request.GET.get('priority', '').strip() selected_priority = request.GET.get('priority', '').strip()
search_q = request.GET.get('prep_q', '').strip() or (request.GET.get('q', '').strip() if tab != 'production-orders' else '') search_q = request.GET.get('prep_q', '').strip() or (request.GET.get('q', '').strip() if tab != 'production-orders' else '')
prep_counts = PreparationOrder.objects.aggregate(
total=models.Count('id'),
to_process=models.Count('id', filter=models.Q(status='to_process')),
pending=models.Count('id', filter=models.Q(status='pending')),
processed=models.Count('id', filter=models.Q(status='processed')),
cancelled=models.Count('id', filter=models.Q(status='cancelled')),
)
prep_counts['active'] = prep_counts['to_process'] + prep_counts['pending']
preparation_orders = PreparationOrder.objects.select_related('intervention').all() preparation_orders = PreparationOrder.objects.select_related('intervention').all()
if selected_status: if selected_status:
preparation_orders = preparation_orders.filter(status=selected_status) if selected_status == 'active':
preparation_orders = preparation_orders.filter(status__in=['to_process', 'pending'])
else:
preparation_orders = preparation_orders.filter(status=selected_status)
if selected_priority: if selected_priority:
preparation_orders = preparation_orders.filter(priority=selected_priority) preparation_orders = preparation_orders.filter(priority=selected_priority)
if search_q: if search_q:
@ -632,6 +644,7 @@ def stock_movements(request):
draft=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_DRAFT)), draft=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_DRAFT)),
cancelled=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_CANCELLED)), cancelled=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_CANCELLED)),
) )
prod_counts['active'] = prod_counts['to_process'] + prod_counts['in_production']
production_orders = prod_base_qs.select_related( production_orders = prod_base_qs.select_related(
'warehouse', 'preparation_order', 'output_product', 'output_location', 'warehouse', 'preparation_order', 'output_product', 'output_location',
@ -642,7 +655,12 @@ def stock_movements(request):
) )
if prod_status: if prod_status:
production_orders = production_orders.filter(status=prod_status) if prod_status == 'active':
production_orders = production_orders.filter(
status__in=[ProductionOrder.STATUS_TO_PROCESS, ProductionOrder.STATUS_IN_PRODUCTION]
)
else:
production_orders = production_orders.filter(status=prod_status)
if prod_warehouse: if prod_warehouse:
production_orders = production_orders.filter(warehouse_id=prod_warehouse) production_orders = production_orders.filter(warehouse_id=prod_warehouse)
if prod_destination: if prod_destination:
@ -757,11 +775,15 @@ def stock_movements(request):
"prod_search_q": prod_search_q, "prod_search_q": prod_search_q,
"prod_warehouse": prod_warehouse, "prod_warehouse": prod_warehouse,
"prod_destination": prod_destination, "prod_destination": prod_destination,
"prod_status_choices": ProductionOrder.STATUS_CHOICES, "prod_status_choices": [
('active', _("À traiter & En confection")),
] + list(ProductionOrder.STATUS_CHOICES),
"prod_destination_choices": ProductionOrder.DESTINATION_CHOICES, "prod_destination_choices": ProductionOrder.DESTINATION_CHOICES,
"prod_counts": prod_counts, "prod_counts": prod_counts,
"prep_counts": prep_counts,
"accessible_warehouses": accessible_warehouses, "accessible_warehouses": accessible_warehouses,
"preparation_order_status_choices": [ "preparation_order_status_choices": [
('active', _("À traiter & En attente")),
('to_process', _("A traiter")), ('to_process', _("A traiter")),
('pending', _("En attente")), ('pending', _("En attente")),
('processed', _("Traité")), ('processed', _("Traité")),