Compare commits
2 commits
e00f9cf784
...
c25c4a4415
| Author | SHA1 | Date | |
|---|---|---|---|
| c25c4a4415 | |||
| b78c9da50e |
29 changed files with 572 additions and 39 deletions
Binary file not shown.
|
|
@ -541,3 +541,15 @@ msgstr ""
|
|||
#: common/views.py:644
|
||||
msgid "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"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -527,3 +527,15 @@ msgstr "Accès interdit"
|
|||
#: common/views.py:644
|
||||
msgid "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"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -527,3 +527,15 @@ msgstr ""
|
|||
#: common/views.py:644
|
||||
msgid "Erreur lors de la lecture du document"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -221,10 +221,29 @@
|
|||
style="width: 100%; height: 70vh; border: none;"
|
||||
title="{{ doc.title }}"></iframe>
|
||||
{% elif doc.is_image %}
|
||||
<img src="{{ doc.file_url }}"
|
||||
class="d-block"
|
||||
alt="{{ doc.title }}"
|
||||
style="max-height: 70vh; max-width: 100%; object-fit: contain;">
|
||||
<div class="doc-viewer-image-viewport position-relative w-100 d-flex justify-content-center align-items-center overflow-hidden"
|
||||
style="height: 70vh; min-height: 60vh; user-select: none;">
|
||||
<img src="{{ doc.file_url }}"
|
||||
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 %}
|
||||
<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>
|
||||
|
|
@ -366,6 +385,19 @@
|
|||
</div>{# /modal #}
|
||||
{% 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 ══════════════════════════════ #}
|
||||
<script>
|
||||
(function () {
|
||||
|
|
@ -434,6 +466,223 @@
|
|||
/* ── Initialise download button when modal becomes visible ── */
|
||||
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>
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1137,3 +1137,12 @@ msgstr "No interventions to control"
|
|||
#: home/templates/home/widgets/controller_shortcuts.html:72
|
||||
msgid "Voir toutes les interventions à contrôler"
|
||||
msgstr "View all interventions to control"
|
||||
|
||||
#: home/templates/home/widgets/stock_summary.html
|
||||
msgid "à traiter · en cours"
|
||||
msgstr "to process · in progress"
|
||||
|
||||
#: home/templates/home/widgets/stock_summary.html
|
||||
msgid "Aucun bon à traiter ou en cours"
|
||||
msgstr "No orders to process or in progress"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1146,3 +1146,12 @@ msgstr "Aucune intervention à contrôler"
|
|||
#: home/templates/home/widgets/controller_shortcuts.html:72
|
||||
msgid "Voir toutes les interventions à contrôler"
|
||||
msgstr "Voir toutes les interventions à contrôler"
|
||||
|
||||
#: home/templates/home/widgets/stock_summary.html
|
||||
msgid "à traiter · en cours"
|
||||
msgstr "à traiter · en cours"
|
||||
|
||||
#: home/templates/home/widgets/stock_summary.html
|
||||
msgid "Aucun bon à traiter ou en cours"
|
||||
msgstr "Aucun bon à traiter ou en cours"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1143,3 +1143,12 @@ msgstr "Geen te controleren interventies"
|
|||
#: home/templates/home/widgets/controller_shortcuts.html:72
|
||||
msgid "Voir toutes les interventions à contrôler"
|
||||
msgstr "Alle te controleren interventies bekijken"
|
||||
|
||||
#: home/templates/home/widgets/stock_summary.html
|
||||
msgid "à traiter · en cours"
|
||||
msgstr "te verwerken · in behandeling"
|
||||
|
||||
#: home/templates/home/widgets/stock_summary.html
|
||||
msgid "Aucun bon à traiter ou en cours"
|
||||
msgstr "Geen bonnen te verwerken of in behandeling"
|
||||
|
||||
|
|
|
|||
|
|
@ -16,15 +16,25 @@
|
|||
<div class="col-md-4">
|
||||
<div class="card h-100 border-primary shadow-sm">
|
||||
<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" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center py-3 border-bottom">
|
||||
<a href="{% url 'stock:stock_movements' %}" class="text-decoration-none">
|
||||
<div class="text-center py-2 border-bottom">
|
||||
<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>
|
||||
</a>
|
||||
<div class="text-muted small">{% translate "à traiter" %}</div>
|
||||
<div class="text-muted small">{% translate "à traiter · 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>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for order in preparation_order_to_process_list %}
|
||||
|
|
@ -42,12 +52,12 @@
|
|||
</li>
|
||||
{% empty %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<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" %}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -58,15 +68,25 @@
|
|||
<div class="col-md-4">
|
||||
<div class="card h-100 border-info shadow-sm">
|
||||
<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" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center py-3 border-bottom">
|
||||
<a href="{% url 'stock:stock_movements' %}#production-orders-content" class="text-decoration-none">
|
||||
<div class="text-center py-2 border-bottom">
|
||||
<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>
|
||||
</a>
|
||||
<div class="text-muted small">{% translate "à traiter" %}</div>
|
||||
<div class="text-muted small">{% translate "à traiter · 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>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for order in production_order_to_process_list %}
|
||||
|
|
@ -83,12 +103,12 @@
|
|||
</li>
|
||||
{% empty %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<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" %}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -439,6 +439,69 @@ class ControllerShortcutsWidgetTests(TestCase):
|
|||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -479,12 +479,29 @@ def index(request):
|
|||
t = _t('approval_counts', t, _perf_splits)
|
||||
|
||||
# ========== 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'])
|
||||
preparation_order_to_process_count = _prep_qs.count()
|
||||
preparation_order_to_process_list = list(_prep_qs.order_by('-date')[:5])
|
||||
|
||||
_prod_qs = ProductionOrder.objects.filter(status__in=['to_process', 'in_production'])
|
||||
production_order_to_process_count = _prod_qs.count()
|
||||
from stock.permissions import get_user_accessible_warehouses
|
||||
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(
|
||||
_prod_qs.select_related('warehouse').order_by('-created_at')[:5]
|
||||
)
|
||||
|
|
@ -827,8 +844,12 @@ def index(request):
|
|||
'show_checked_split': show_checked_split,
|
||||
**split_checked_counts,
|
||||
'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,
|
||||
'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,
|
||||
'stock_low_alerts_list': stock_low_alerts_list,
|
||||
'loan_items_total': loan_items_total,
|
||||
|
|
|
|||
|
|
@ -442,7 +442,10 @@ def stock_preparation_orders_mobile(request):
|
|||
|
||||
preparation_orders = PreparationOrder.objects.select_related('intervention').all()
|
||||
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:
|
||||
preparation_orders = preparation_orders.filter(priority=selected_priority)
|
||||
if search_q:
|
||||
|
|
@ -465,6 +468,7 @@ def stock_preparation_orders_mobile(request):
|
|||
'selected_priority': selected_priority,
|
||||
'search_q': search_q,
|
||||
'preparation_order_status_choices': [
|
||||
('active', _("À traiter & En attente")),
|
||||
('to_process', _("A traiter")),
|
||||
('pending', _("En attente")),
|
||||
('processed', _("Traité")),
|
||||
|
|
@ -504,6 +508,7 @@ def stock_production_orders_mobile(request):
|
|||
draft=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_DRAFT)),
|
||||
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(
|
||||
'warehouse', 'preparation_order', 'output_product', 'output_location',
|
||||
|
|
@ -513,7 +518,12 @@ def stock_production_orders_mobile(request):
|
|||
)
|
||||
|
||||
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:
|
||||
production_orders = production_orders.filter(warehouse_id=selected_warehouse)
|
||||
if selected_destination:
|
||||
|
|
@ -542,7 +552,9 @@ def stock_production_orders_mobile(request):
|
|||
'selected_status': selected_status,
|
||||
'selected_warehouse': selected_warehouse,
|
||||
'selected_destination': selected_destination,
|
||||
'status_choices': ProductionOrder.STATUS_CHOICES,
|
||||
'status_choices': [
|
||||
('active', _("À traiter & En confection")),
|
||||
] + list(ProductionOrder.STATUS_CHOICES),
|
||||
'destination_choices': ProductionOrder.DESTINATION_CHOICES,
|
||||
'warehouses': accessible_warehouses,
|
||||
'prod_counts': prod_counts,
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -5923,3 +5923,16 @@ msgstr "Intervention canceled"
|
|||
|
||||
#~ msgid "Rafraîchir la 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"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -5730,3 +5730,16 @@ msgstr "Intervention annulée"
|
|||
|
||||
#~ msgid "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"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -5730,3 +5730,16 @@ msgstr "Interventie geannuleerd"
|
|||
|
||||
#~ msgid "Rafraîchir la page"
|
||||
#~ 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"
|
||||
|
||||
|
|
|
|||
|
|
@ -2246,8 +2246,10 @@ class ProductionOrder(models.Model):
|
|||
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()
|
||||
if self.notes and ': ' in self.notes:
|
||||
for line in self.notes.splitlines():
|
||||
if ': ' in line:
|
||||
return line.split(': ', 1)[1].strip()
|
||||
return ""
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
{% translate "Tous" %} ({{ prod_counts.total|default:0 }})
|
||||
</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 %}"
|
||||
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 }})
|
||||
|
|
|
|||
|
|
@ -347,7 +347,11 @@
|
|||
{% if has_executed_outputs %}
|
||||
<td class="text-center">
|
||||
{% 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' %}">
|
||||
<i class="bi bi-plus-circle me-1"></i>+{{ item_data.delta }} {% translate "à prélever" %}
|
||||
</span>
|
||||
|
|
@ -357,10 +361,6 @@
|
|||
<i class="bi bi-arrow-return-left me-1"></i>{{ item_data.delta }} {% translate "à restituer" %}
|
||||
</span>
|
||||
<br><small class="text-muted">{% translate "Sorti net" %}: {{ item_data.executed_qty }}</small>
|
||||
{% elif item_data.gross_in > 0 and item_data.executed_qty == 0 %}
|
||||
<span class="badge bg-info text-dark fs-6" title="{% translate 'Tous les articles ont été restitués au stock' %}">
|
||||
<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 %}
|
||||
<span class="badge bg-success fs-6">
|
||||
<i class="bi bi-check-circle me-1"></i>{% translate "Sorti net" %} ({{ item_data.executed_qty }})
|
||||
|
|
|
|||
|
|
@ -27,12 +27,19 @@
|
|||
<div class="container mt-2">
|
||||
<ul class="nav nav-tabs" id="tablesTab" role="tablist">
|
||||
<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 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 %}">
|
||||
<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>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
|
@ -104,6 +111,31 @@
|
|||
</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 %}
|
||||
<div class="card mb-3 shadow-sm
|
||||
{% 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">
|
||||
{% 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=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 %}"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -1814,11 +1814,13 @@ class PreparationOrderAutocompleteTests(TestCase):
|
|||
b_in = StockMovementBatch.objects.create(movement_type="in", preparation_order=po)
|
||||
StockMovement.objects.create(batch=b_in, product=prod, quantity=2)
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
response = self.client.get(reverse("stock:preparation_order_detail", args=[po.pk]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, _("Restitué au stock"))
|
||||
self.assertNotContains(response, "à prélever")
|
||||
from django.utils import translation
|
||||
with translation.override('fr'):
|
||||
from django.utils.translation import gettext as _
|
||||
response = self.client.get(reverse("stock:preparation_order_detail", args=[po.pk]), HTTP_ACCEPT_LANGUAGE='fr')
|
||||
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):
|
||||
from stock.models import Product, PreparationOrder, PreparationOrderItem
|
||||
|
|
|
|||
|
|
@ -595,9 +595,21 @@ def stock_movements(request):
|
|||
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 '')
|
||||
|
||||
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()
|
||||
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:
|
||||
preparation_orders = preparation_orders.filter(priority=selected_priority)
|
||||
if search_q:
|
||||
|
|
@ -632,6 +644,7 @@ def stock_movements(request):
|
|||
draft=models.Count('id', filter=models.Q(status=ProductionOrder.STATUS_DRAFT)),
|
||||
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(
|
||||
'warehouse', 'preparation_order', 'output_product', 'output_location',
|
||||
|
|
@ -642,7 +655,12 @@ def stock_movements(request):
|
|||
)
|
||||
|
||||
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:
|
||||
production_orders = production_orders.filter(warehouse_id=prod_warehouse)
|
||||
if prod_destination:
|
||||
|
|
@ -757,11 +775,15 @@ def stock_movements(request):
|
|||
"prod_search_q": prod_search_q,
|
||||
"prod_warehouse": prod_warehouse,
|
||||
"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_counts": prod_counts,
|
||||
"prep_counts": prep_counts,
|
||||
"accessible_warehouses": accessible_warehouses,
|
||||
"preparation_order_status_choices": [
|
||||
('active', _("À traiter & En attente")),
|
||||
('to_process', _("A traiter")),
|
||||
('pending', _("En attente")),
|
||||
('processed', _("Traité")),
|
||||
|
|
|
|||
Loading…
Reference in a new issue