diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index d76c0a4b51..cb6b3f6b2b 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -56,6 +56,28 @@ class BuildList(generics.ListCreateAPIView): params = self.request.query_params + # Filter by "parent" + parent = params.get('parent', None) + + if parent is not None: + queryset = queryset.filter(parent=parent) + + # Filter by "ancestor" builds + ancestor = params.get('ancestor', None) + + if ancestor is not None: + try: + ancestor = Build.objects.get(pk=ancestor) + + descendants = ancestor.get_descendants(include_self=True) + + queryset = queryset.filter( + parent__pk__in=[b.pk for b in descendants] + ) + + except (ValueError, Build.DoesNotExist): + pass + # Filter by build status? status = params.get('status', None) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 2872fecb55..ada9db1e08 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -259,6 +259,27 @@ class Build(MPTTModel): blank=True, help_text=_('Extra build notes') ) + def sub_builds(self, cascade=True): + """ + Return all Build Order objects under this one. + """ + + if cascade: + return Build.objects.filter(parent=self.pk) + else: + descendants = self.get_descendants(include_self=True) + Build.objects.filter(parent__pk__in=[d.pk for d in descendants]) + + def sub_build_count(self, cascade=True): + """ + Return the number of sub builds under this one. + + Args: + cascade: If True (defualt), include cascading builds under sub builds + """ + + return self.sub_builds(cascade=cascade).count() + @property def is_overdue(self): """ diff --git a/InvenTree/build/templates/build/build_children.html b/InvenTree/build/templates/build/build_children.html new file mode 100644 index 0000000000..c996aaa84f --- /dev/null +++ b/InvenTree/build/templates/build/build_children.html @@ -0,0 +1,39 @@ +{% extends "build/build_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include "build/tabs.html" with tab="children" %} + +

{% trans "Child Build Orders" %}

+ +
+ +
+
+
+ +
+
+ +
+ +
+ +{% endblock %} + +{% block js_ready %} + +{{ block.super }} + +loadBuildTable($('#sub-build-table'), { + url: '{% url "api-build-list" %}', + filterTarget: "#filter-list-sub-build", + params: { + part_detail: true, + ancestor: {{ build.pk }}, + } +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/tabs.html b/InvenTree/build/templates/build/tabs.html index c6d2893620..6b36c3d052 100644 --- a/InvenTree/build/templates/build/tabs.html +++ b/InvenTree/build/templates/build/tabs.html @@ -24,6 +24,12 @@ {{ build.output_count }} +
  • + + {% trans "Child Builds" %} + {{ build.sub_build_count }} + +
  • {% trans "Notes" %} diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py new file mode 100644 index 0000000000..bcfd600e9e --- /dev/null +++ b/InvenTree/build/test_api.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import datetime, timedelta + +from rest_framework.test import APITestCase + +from django.urls import reverse +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group + +from part.models import Part +from build.models import Build + +from InvenTree.status_codes import BuildStatus + + +class BuildAPITest(APITestCase): + """ + Series of tests for the Build DRF API + """ + + fixtures = [ + 'category', + 'part', + 'location', + 'bom', + 'build', + ] + + def setUp(self): + # Create a user for auth + user = get_user_model() + + self.user = user.objects.create_user( + username='testuser', + email='test@testing.com', + password='password' + ) + + # Put the user into a group with the correct permissions + group = Group.objects.create(name='mygroup') + self.user.groups.add(group) + + # Give the group *all* the permissions! + for rule in group.rule_sets.all(): + rule.can_view = True + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() + + group.save() + + self.client.login(username='testuser', password='password') + + +class BuildListTest(BuildAPITest): + """ + Tests for the BuildOrder LIST API + """ + + url = reverse('api-build-list') + + def get(self, status_code=200, data={}): + + response = self.client.get(self.url, data, format='json') + + self.assertEqual(response.status_code, status_code) + + return response.data + + def test_get_all_builds(self): + """ + Retrieve *all* builds via the API + """ + + builds = self.get() + + self.assertEqual(len(builds), 5) + + builds = self.get(data={'active': True}) + self.assertEqual(len(builds), 1) + + builds = self.get(data={'status': BuildStatus.COMPLETE}) + self.assertEqual(len(builds), 4) + + builds = self.get(data={'overdue': False}) + self.assertEqual(len(builds), 5) + + builds = self.get(data={'overdue': True}) + self.assertEqual(len(builds), 0) + + def test_overdue(self): + """ + Create a new build, in the past + """ + + in_the_past = datetime.now().date() - timedelta(days=50) + + part = Part.objects.get(pk=50) + + Build.objects.create( + part=part, + quantity=10, + title='Just some thing', + status=BuildStatus.PRODUCTION, + target_date=in_the_past + ) + + builds = self.get(data={'overdue': True}) + + self.assertEqual(len(builds), 1) + + def test_sub_builds(self): + """ + Test the build / sub-build relationship + """ + + parent = Build.objects.get(pk=5) + + part = Part.objects.get(pk=50) + + n = Build.objects.count() + + # Make some sub builds + for i in range(5): + Build.objects.create( + part=part, + quantity=10, + reference=f"build-000{i}", + title=f"Sub build {i}", + parent=parent + ) + + # And some sub-sub builds + for sub_build in Build.objects.filter(parent=parent): + + for i in range(3): + Build.objects.create( + part=part, + reference=f"{sub_build.reference}-00{i}-sub", + quantity=40, + title=f"sub sub build {i}", + parent=sub_build + ) + + # 20 new builds should have been created! + self.assertEqual(Build.objects.count(), (n + 20)) + + Build.objects.rebuild() + + # Search by parent + builds = self.get(data={'parent': parent.pk}) + + self.assertEqual(len(builds), 5) + + # Search by ancestor + builds = self.get(data={'ancestor': parent.pk}) + + self.assertEqual(len(builds), 20) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 6f681f5488..877b368817 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -20,6 +20,7 @@ build_detail_urls = [ url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'), + url(r'^children/', views.BuildDetail.as_view(template_name='build/build_children.html'), name='build-children'), url(r'^parts/', views.BuildDetail.as_view(template_name='build/parts.html'), name='build-parts'), url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'), url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 3c4b94c43d..272cd1e5d8 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -618,6 +618,7 @@ class BuildDetail(DetailView): ctx['bom_price'] = build.part.get_price_info(build.quantity, buy=False) ctx['BuildStatus'] = BuildStatus + ctx['sub_build_count'] = build.sub_build_count() return ctx diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index 9641181af8..46685ad064 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -9,7 +9,7 @@
    -
    +
    {% if part.active %} {% if roles.build.add %} diff --git a/InvenTree/report/templates/report/inventree_build_order_base.html b/InvenTree/report/templates/report/inventree_build_order_base.html index 0a4f8b3bb6..46e7544df5 100644 --- a/InvenTree/report/templates/report/inventree_build_order_base.html +++ b/InvenTree/report/templates/report/inventree_build_order_base.html @@ -2,9 +2,9 @@ {% load i18n %} {% load report %} +{% load barcode %} {% load inventree_extras %} {% load markdownify %} -{% load qr_code %} {% block page_margin %} margin: 2cm; diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index cb7e27486b..01d5fcdef0 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -618,7 +618,9 @@ function loadBuildTable(table, options) { filters[key] = params[key]; } - setupFilterList("build", table); + var filterTarget = options.filterTarget || null; + + setupFilterList("build", table, filterTarget); $(table).inventreeTable({ method: 'get',