mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1344 from SchrodingersGat/sub-build-table
Build: Filter by parent or ancestor in API
This commit is contained in:
commit
4dc093662d
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
39
InvenTree/build/templates/build/build_children.html
Normal file
39
InvenTree/build/templates/build/build_children.html
Normal 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 %}
|
@ -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
162
InvenTree/build/test_api.py
Normal 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)
|
@ -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'),
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
{% load i18n %}
|
||||
{% load report %}
|
||||
{% load barcode %}
|
||||
{% load inventree_extras %}
|
||||
{% load markdownify %}
|
||||
{% load qr_code %}
|
||||
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user