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 @@