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
|
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?
|
# Filter by build status?
|
||||||
status = params.get('status', None)
|
status = params.get('status', None)
|
||||||
|
|
||||||
|
@ -259,6 +259,27 @@ class Build(MPTTModel):
|
|||||||
blank=True, help_text=_('Extra build notes')
|
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
|
@property
|
||||||
def is_overdue(self):
|
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>
|
<span class='badge'>{{ build.output_count }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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 %}>
|
<li{% if tab == 'notes' %} class='active'{% endif %}>
|
||||||
<a href="{% url 'build-notes' build.id %}">
|
<a href="{% url 'build-notes' build.id %}">
|
||||||
{% trans "Notes" %}
|
{% 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'^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'^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'^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'),
|
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['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
||||||
ctx['BuildStatus'] = BuildStatus
|
ctx['BuildStatus'] = BuildStatus
|
||||||
|
ctx['sub_build_count'] = build.sub_build_count()
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
<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 part.active %}
|
||||||
{% if roles.build.add %}
|
{% if roles.build.add %}
|
||||||
<button class="btn btn-success" id='start-build'><span class='fas fa-tools'></span> {% trans "Start New Build" %}</button>
|
<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 i18n %}
|
||||||
{% load report %}
|
{% load report %}
|
||||||
|
{% load barcode %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load markdownify %}
|
{% load markdownify %}
|
||||||
{% load qr_code %}
|
|
||||||
|
|
||||||
{% block page_margin %}
|
{% block page_margin %}
|
||||||
margin: 2cm;
|
margin: 2cm;
|
||||||
|
@ -618,7 +618,9 @@ function loadBuildTable(table, options) {
|
|||||||
filters[key] = params[key];
|
filters[key] = params[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFilterList("build", table);
|
var filterTarget = options.filterTarget || null;
|
||||||
|
|
||||||
|
setupFilterList("build", table, filterTarget);
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
Loading…
Reference in New Issue
Block a user