Merge pull request #1344 from SchrodingersGat/sub-build-table

Build: Filter by parent or ancestor in API
This commit is contained in:
Oliver 2021-02-23 09:46:51 +11:00 committed by GitHub
commit 4dc093662d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 257 additions and 3 deletions

View File

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

View File

@ -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):
"""

View File

@ -0,0 +1,39 @@
{% extends "build/build_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include "build/tabs.html" with tab="children" %}
<h4>{% trans "Child Build Orders" %}</h4>
<hr>
<div id='button-toolbar'>
<div class='button-toolbar container-fluid float-right'>
<div class='filter-list' id='filter-list-sub-build'>
<!-- Empty div for filters -->
</div>
</div>
</div>
<table class='table table-striped table-condensed' id='sub-build-table' data-toolbar='#button-toolbar'></table>
{% 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 %}

View File

@ -24,6 +24,12 @@
<span class='badge'>{{ build.output_count }}</span>
</a>
</li>
<li {% if tab == 'children' %} class='active'{% endif %}>
<a href='{% url "build-children" build.id %}'>
{% trans "Child Builds" %}
<span class='badge'>{{ build.sub_build_count }}</span>
</a>
</li>
<li{% if tab == 'notes' %} class='active'{% endif %}>
<a href="{% url 'build-notes' build.id %}">
{% trans "Notes" %}

162
InvenTree/build/test_api.py Normal file
View File

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

View File

@ -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'),

View File

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

View File

@ -9,7 +9,7 @@
<hr>
<div id='button-toolbar'>
<div class='button-toolbar container-flui' style='float: right';>
<div class='button-toolbar container-fluid' style='float: right';>
{% if part.active %}
{% if roles.build.add %}
<button class="btn btn-success" id='start-build'><span class='fas fa-tools'></span> {% trans "Start New Build" %}</button>

View File

@ -2,9 +2,9 @@
{% load i18n %}
{% load report %}
{% load barcode %}
{% load inventree_extras %}
{% load markdownify %}
{% load qr_code %}
{% block page_margin %}
margin: 2cm;

View File

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