diff --git a/loko/home/templates/home/widgets/stock_summary.html b/loko/home/templates/home/widgets/stock_summary.html index 09c6964..49c0a96 100644 --- a/loko/home/templates/home/widgets/stock_summary.html +++ b/loko/home/templates/home/widgets/stock_summary.html @@ -16,15 +16,25 @@
-
- +
    {% for order in preparation_order_to_process_list %} @@ -42,12 +52,12 @@ {% empty %}
  • - {% translate "Aucun bon à traiter" %} + {% translate "Aucun bon à traiter ou en cours" %}
  • {% endfor %}
@@ -58,15 +68,25 @@
-
- +
    {% for order in production_order_to_process_list %} @@ -83,12 +103,12 @@ {% empty %}
  • - {% translate "Aucun bon à traiter" %} + {% translate "Aucun bon à traiter ou en cours" %}
  • {% endfor %}
diff --git a/loko/home/tests.py b/loko/home/tests.py index d859414..891a452 100644 --- a/loko/home/tests.py +++ b/loko/home/tests.py @@ -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) + + + diff --git a/loko/home/views.py b/loko/home/views.py index 491bdda..e5926f7 100644 --- a/loko/home/views.py +++ b/loko/home/views.py @@ -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, diff --git a/loko/mobile/views.py b/loko/mobile/views.py index 9fb4ae2..26d5d3a 100644 --- a/loko/mobile/views.py +++ b/loko/mobile/views.py @@ -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, diff --git a/loko/stock/locale/en/LC_MESSAGES/django.mo b/loko/stock/locale/en/LC_MESSAGES/django.mo index 6158c5a..41857b9 100644 Binary files a/loko/stock/locale/en/LC_MESSAGES/django.mo and b/loko/stock/locale/en/LC_MESSAGES/django.mo differ diff --git a/loko/stock/locale/en/LC_MESSAGES/django.po b/loko/stock/locale/en/LC_MESSAGES/django.po index a811dc9..ccee12c 100644 --- a/loko/stock/locale/en/LC_MESSAGES/django.po +++ b/loko/stock/locale/en/LC_MESSAGES/django.po @@ -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" + diff --git a/loko/stock/locale/fr/LC_MESSAGES/django.mo b/loko/stock/locale/fr/LC_MESSAGES/django.mo index 6d748df..bb6e51d 100644 Binary files a/loko/stock/locale/fr/LC_MESSAGES/django.mo and b/loko/stock/locale/fr/LC_MESSAGES/django.mo differ diff --git a/loko/stock/locale/fr/LC_MESSAGES/django.po b/loko/stock/locale/fr/LC_MESSAGES/django.po index 4560dfd..d226776 100644 --- a/loko/stock/locale/fr/LC_MESSAGES/django.po +++ b/loko/stock/locale/fr/LC_MESSAGES/django.po @@ -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" + diff --git a/loko/stock/locale/nl/LC_MESSAGES/django.mo b/loko/stock/locale/nl/LC_MESSAGES/django.mo index 4b56376..f186469 100644 Binary files a/loko/stock/locale/nl/LC_MESSAGES/django.mo and b/loko/stock/locale/nl/LC_MESSAGES/django.mo differ diff --git a/loko/stock/locale/nl/LC_MESSAGES/django.po b/loko/stock/locale/nl/LC_MESSAGES/django.po index 87c1a42..817115e 100644 --- a/loko/stock/locale/nl/LC_MESSAGES/django.po +++ b/loko/stock/locale/nl/LC_MESSAGES/django.po @@ -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" + diff --git a/loko/stock/models.py b/loko/stock/models.py index e9b2f66..6cf0e48 100644 --- a/loko/stock/models.py +++ b/loko/stock/models.py @@ -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): diff --git a/loko/stock/templates/stock/mobile/stock_production_orders_mobile.html b/loko/stock/templates/stock/mobile/stock_production_orders_mobile.html index 72d7e69..bb49708 100644 --- a/loko/stock/templates/stock/mobile/stock_production_orders_mobile.html +++ b/loko/stock/templates/stock/mobile/stock_production_orders_mobile.html @@ -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 }}) + + {% translate "À traiter & En confection" %} ({{ prod_counts.active|default:0 }}) + {% translate "À traiter" %} ({{ prod_counts.to_process|default:0 }}) diff --git a/loko/stock/templates/stock/preparation_order_detail.html b/loko/stock/templates/stock/preparation_order_detail.html index fed8e84..66575e4 100644 --- a/loko/stock/templates/stock/preparation_order_detail.html +++ b/loko/stock/templates/stock/preparation_order_detail.html @@ -347,7 +347,11 @@ {% if has_executed_outputs %} {% if preparation_order.status == 'processed' %} - {% if item_data.delta > 0 %} + {% if item_data.gross_in > 0 and item_data.executed_qty == 0 %} + + {% translate "Restitué au stock" %} ({{ item_data.gross_in }}) + + {% elif item_data.delta > 0 %} +{{ item_data.delta }} {% translate "à prélever" %} @@ -357,10 +361,6 @@ {{ item_data.delta }} {% translate "à restituer" %}
{% translate "Sorti net" %}: {{ item_data.executed_qty }} - {% elif item_data.gross_in > 0 and item_data.executed_qty == 0 %} - - {% translate "Restitué au stock" %} ({{ item_data.gross_in }}) - {% elif item_data.gross_in > 0 %} {% translate "Sorti net" %} ({{ item_data.executed_qty }}) diff --git a/loko/stock/templates/stock/stock_movements.html b/loko/stock/templates/stock/stock_movements.html index 7228f3f..eaf4e30 100644 --- a/loko/stock/templates/stock/stock_movements.html +++ b/loko/stock/templates/stock/stock_movements.html @@ -27,12 +27,19 @@
+ + + {% for order in preparation_orders %}
{% translate "Tous" %} {{ prod_counts.total|default:0 }} + + {% translate "À traiter & En confection" %} {{ prod_counts.active|default:0 }} + {% translate "À traiter" %} {{ prod_counts.to_process|default:0 }} diff --git a/loko/stock/tests.py b/loko/stock/tests.py index cf95b18..035640f 100644 --- a/loko/stock/tests.py +++ b/loko/stock/tests.py @@ -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 diff --git a/loko/stock/views.py b/loko/stock/views.py index f18a572..155c208 100644 --- a/loko/stock/views.py +++ b/loko/stock/views.py @@ -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é")),