Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-08 22:09:40 +10:00
commit 0deb6e8bea
32 changed files with 535 additions and 253 deletions

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-05-07 21:48
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0010_auto_20190505_2233'),
]
operations = [
migrations.AlterField(
model_name='build',
name='status',
field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@ -13,9 +13,11 @@ from django.core.exceptions import ValidationError
from django.urls import reverse
from django.db import models, transaction
from django.db.models import Sum
from django.core.validators import MinValueValidator
from stock.models import StockItem
from part.models import BomItem
class Build(models.Model):
@ -33,49 +35,6 @@ class Build(models.Model):
notes: Text notes
"""
def save(self, *args, **kwargs):
""" Called when the Build model is saved to the database.
If this is a new Build, try to allocate StockItem objects automatically.
- If there is only one StockItem for a Part, use that one.
- If there are multiple StockItem objects, leave blank and let the user decide
"""
allocate_parts = False
# If there is no PK yet, then this is the first time the Build has been saved
if not self.pk:
allocate_parts = True
# Save this Build first
super(Build, self).save(*args, **kwargs)
if allocate_parts:
for item in self.part.bom_items.all():
part = item.sub_part
# Number of parts required for this build
q_req = item.quantity * self.quantity
stock = StockItem.objects.filter(part=part)
if len(stock) == 1:
stock_item = stock[0]
# Are there any parts available?
if stock_item.quantity > 0:
# If there are not enough parts, reduce the amount we will take
if stock_item.quantity < q_req:
q_req = stock_item.quantity
# Allocate parts to this build
build_item = BuildItem(
build=self,
stock_item=stock_item,
quantity=q_req)
build_item.save()
def __str__(self):
return "Build {q} x {part}".format(q=self.quantity, part=str(self.part))
@ -103,11 +62,13 @@ class Build(models.Model):
# Build status codes
PENDING = 10 # Build is pending / active
ALLOCATED = 20 # Parts have been removed from stock
CANCELLED = 30 # Build was cancelled
COMPLETE = 40 # Build is complete
#: Build status codes
BUILD_STATUS_CODES = {PENDING: _("Pending"),
ALLOCATED: _("Allocated"),
CANCELLED: _("Cancelled"),
COMPLETE: _("Complete"),
}
@ -153,6 +114,68 @@ class Build(models.Model):
self.status = self.CANCELLED
self.save()
def getAutoAllocations(self):
""" Return a list of parts which will be allocated
using the 'AutoAllocate' function.
For each item in the BOM for the attached Part:
- If there is a single StockItem, use that StockItem
- Take as many parts as available (up to the quantity required for the BOM)
- If there are multiple StockItems available, ignore (leave up to the user)
Returns:
A dict object containing the StockItem objects to be allocated (and the quantities)
"""
allocations = {}
for item in self.part.bom_items.all():
# How many parts required for this build?
q_required = item.quantity * self.quantity
stock = StockItem.objects.filter(part=item.sub_part)
# Only one StockItem to choose from? Default to that one!
if len(stock) == 1:
stock_item = stock[0]
# Check that we have not already allocated this stock-item against this build
build_items = BuildItem.objects.filter(build=self, stock_item=stock_item)
if len(build_items) > 0:
continue
# Are there any parts available?
if stock_item.quantity > 0:
# Only take as many as are available
if stock_item.quantity < q_required:
q_required = stock_item.quantity
# Add the item to the allocations list
allocations[stock_item] = q_required
return allocations
@transaction.atomic
def autoAllocate(self):
""" Run auto-allocation routine to allocate StockItems to this Build.
See: getAutoAllocations()
"""
allocations = self.getAutoAllocations()
for item in allocations:
# Create a new allocation
build_item = BuildItem(
build=self,
stock_item=item,
quantity=allocations[item])
build_item.save()
@transaction.atomic
def completeBuild(self, location, user):
""" Mark the Build as COMPLETE
@ -200,6 +223,41 @@ class Build(models.Model):
self.status = self.COMPLETE
self.save()
def getRequiredQuantity(self, part):
""" Calculate the quantity of <part> required to make this build.
"""
try:
item = BomItem.objects.get(part=self.part.id, sub_part=part.id)
return item.quantity * self.quantity
except BomItem.DoesNotExist:
return 0
def getAllocatedQuantity(self, part):
""" Calculate the total number of <part> currently allocated to this build
"""
allocated = BuildItem.objects.filter(build=self.id, stock_item__part=part.id).aggregate(Sum('quantity'))
q = allocated['quantity__sum']
if q:
return int(q)
else:
return 0
def getUnallocatedQuantity(self, part):
""" Calculate the quantity of <part> which still needs to be allocated to this build.
Args:
Part - the part to be tested
Returns:
The remaining allocated quantity
"""
return max(self.getRequiredQuantity(part) - self.getAllocatedQuantity(part), 0)
@property
def required_parts(self):
""" Returns a dict of parts required to build this part (BOM) """
@ -208,7 +266,8 @@ class Build(models.Model):
for item in self.part.bom_items.all():
part = {'part': item.sub_part,
'per_build': item.quantity,
'quantity': item.quantity * self.quantity
'quantity': item.quantity * self.quantity,
'allocated': self.getAllocatedQuantity(item.sub_part)
}
parts.append(part)

View File

@ -6,17 +6,24 @@
<h3>Allocate Parts for Build</h3>
<h4><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></h4>
{{ build.quantity }} x {{ build.part.name }}
<div class='row'>
<div class='col-sm-6'>
<h4><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></h4>
{{ build.quantity }} x {{ build.part.name }}
</div>
<div class='col-sm-6'>
<div class='btn-group' style='float: right;'>
<button class='btn btn-warning' type='button' id='complete-build'>Complete Build</button>
</div>
</div>
</div>
<hr>
{% for bom_item in bom_items.all %}
{% include "build/allocation_item.html" with item=bom_item build=build collapse_id=bom_item.id %}
{% endfor %}
<div>
<button class='btn btn-warning' type='button' id='complete-build'>Complete Build</button>
</div>
{% endblock %}

View File

@ -1,9 +1,23 @@
{% extends "collapse.html" %}
{% load static %}
{% load inventree_extras %}
{% block collapse_title %}
{{ item.sub_part.name }}
<div>
<div class='media-left'>
<img class='part-thumb' style='height: auto; width: 48px;'
{% if item.sub_part.image %}
src="{{ item.sub_part.image.url }}" alt='{{ item.sub_part.name }}'>
{% else %}
src="{% static 'img/blank_image.png' %}" alt='No image'>
{% endif %}
</div>
<div class='media-body'>
{{ item.sub_part.name }}<br>
<small><i>{{ item.sub_part.description }}</i></small>
</div>
</div>
{% endblock %}
{% block collapse_heading %}

View File

@ -12,6 +12,11 @@
<th>Part</th>
<th>Quantity</th>
<th>Status</th>
{% if completed %}
<th>Completed</th>
{% else %}
<th>Created</th>
{% endif %}
</tr>
</thead>
<tbody>
@ -21,6 +26,11 @@
<td><a href="{% url 'part-build' build.part.id %}">{{ build.part.name }}</a></td>
<td>{{ build.quantity }}</td>
<td>{% include "build_status.html" with build=build %}
{% if completed %}
<td>{{ build.completion_date }}<span class='badge'>{{ build.completed_by.username }}</span></td>
{% else %}
<td>{{ build.creation_date }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>

View File

@ -85,7 +85,8 @@
<tr>
<th>Part</th>
<th>Required</th>
<th>Stock</th>
<th>Available</th>
<th>Allocated</th>
</tr>
</thead>
<tbody>
@ -94,6 +95,7 @@
<td><a href="{% url 'part-detail' item.part.id %}">{{ item.part.name }}</a></td>
<td>{{ item.quantity }}</td>
<td>{{ item.part.total_stock }}</td>
<td>{{ item.allocated }}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -3,39 +3,26 @@
{% block content %}
<h4>Active Builds</h4>
<div id='active-build-toolbar'>
<div class='btn-group'>
<button class="btn btn-success" id='new-build'>Start New Build</button>
<div class='row'>
<div class='col-sm-6'>
<h3>Part Builds</h3>
</div>
<div class='col-sm-6'>
<div class='container' id='active-build-toolbar' style='float: right;'>
<div class='btn-group' style='float: right;'>
<button type='button' class="btn btn-success" id='new-build'>Start New Build</button>
</div>
</div>
</div>
</div>
<table class='table table-striped table-condensed build-table' id='build-table-{{collapse_id}}' data-toolbar='#active-build-toolbar'>
<thead>
<tr>
<th>Build</th>
<th>Part</th>
<th>Quantity</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for build in active %}
<tr>
<td><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></td>
<td><a href="{% url 'part-build' build.part.id %}">{{ build.part.name }}</a></td>
<td>{{ build.quantity }}</td>
<td>{% include "build_status.html" with build=build %}
</tr>
{% endfor %}
</tbody>
</table>
<hr>
<p></p>
{% include "build/build_list.html" with builds=completed title="Completed Builds" collapse_id="complete" %}
<p></p>
{% include "build/build_list.html" with builds=cancelled title="Cancelled Builds" collapse_id="cancelled" %}
{% include "build/build_list.html" with builds=active title="Active Builds" completed=False collapse_id='active' %}
{% include "build/build_list.html" with builds=completed completed=True title="Completed Builds" collapse_id="complete" %}
{% include "build/build_list.html" with builds=cancelled title="Cancelled Builds" completed=False collapse_id="cancelled" %}
{% include 'modals.html' %}
@ -43,6 +30,9 @@
{% block js_ready %}
{{ block.super }}
$("#collapse-item-active").collapse().show();
$("#new-build").click(function() {
launchModalForm(
"{% url 'build-create' %}",
@ -74,7 +64,10 @@
{
title: 'Status',
sortable: true,
}
},
{
sortable: true,
},
]
});

View File

@ -289,6 +289,16 @@ class BuildItemCreate(AjaxCreateView):
query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)])
form.fields['stock_item'].queryset = query
stocks = query.all()
# If there is only one item selected, select it
if len(stocks) == 1:
form.fields['stock_item'].initial = stocks[0].id
# There is no stock available
elif len(stocks) == 0:
# TODO - Add a message to the form describing the problem
pass
except Part.DoesNotExist:
pass
@ -303,10 +313,24 @@ class BuildItemCreate(AjaxCreateView):
initials = super(AjaxCreateView, self).get_initial().copy()
build_id = self.get_param('build')
part_id = self.get_param('part')
if part_id:
try:
part = Part.objects.get(pk=part_id)
except Part.DoesNotExist:
part = None
if build_id:
try:
initials['build'] = Build.objects.get(pk=build_id)
build = Build.objects.get(pk=build_id)
initials['build'] = build
# Try to work out how many parts to allocate
if part:
unallocated = build.getUnallocatedQuantity(part)
initials['quantity'] = unallocated
except Build.DoesNotExist:
pass

View File

@ -9,6 +9,7 @@ import os
from django.db import models
from django.urls import reverse
from django.conf import settings
def rename_company_image(instance, filename):
@ -78,6 +79,14 @@ class Company(models.Model):
""" Get the web URL for the detail view for this Company """
return reverse('company-detail', kwargs={'pk': self.id})
def get_image_url(self):
""" Return the URL of the image for this company """
if self.image:
return os.path.join(settings.MEDIA_URL, str(self.image.url))
else:
return ''
@property
def part_count(self):
""" The number of parts supplied by this company """

View File

@ -51,10 +51,10 @@
},
{
sortable: true,
field: 'part_name',
field: 'part_detail.name',
title: 'Part',
formatter: function(value, row, index, field) {
return renderLink(value, '/part/' + row.part + '/suppliers/');
return imageHoverIcon(row.part_detail.image_url) + renderLink(value, '/part/' + row.part + '/suppliers/');
}
},
{

View File

@ -4,11 +4,21 @@
{% block content %}
<h3>Companies</h3>
<div id='button-toolbar'>
<button style='float: right;' class="btn btn-success" id='new-company'>New Company</button>
<div class='row'>
<div class='col-sm-6'>
<h3>Company List</h3>
</div>
<div class='col-sm-6'>
<div class='container' id='active-build-toolbar' style='float: right;'>
<div class='btn-group' style='float: right;'>
<button type='button' class="btn btn-success" id='new-company'>New Company</button>
</div>
</div>
</div>
</div>
<hr>
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
</table>

View File

@ -14,6 +14,7 @@ import tablib
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.conf import settings
from django.db import models
from django.core.validators import MinValueValidator
@ -120,8 +121,17 @@ class Part(models.Model):
"""
def get_absolute_url(self):
""" Return the web URL for viewing this part """
return reverse('part-detail', kwargs={'pk': self.id})
def get_image_url(self):
""" Return the URL of the image for this part """
if self.image:
return os.path.join(settings.MEDIA_URL, str(self.image.url))
else:
return ''
# Short name of the part
name = models.CharField(max_length=100, unique=True, blank=False, help_text='Part name (must be unique)')

View File

@ -33,6 +33,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
""" Serializer for Part (brief detail) """
url = serializers.CharField(source='get_absolute_url', read_only=True)
image_url = serializers.CharField(source='get_image_url', read_only=True)
class Meta:
model = Part
@ -40,6 +41,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
'pk',
'url',
'name',
'image_url',
'description',
'available_stock',
]
@ -51,6 +53,7 @@ class PartSerializer(serializers.ModelSerializer):
"""
url = serializers.CharField(source='get_absolute_url', read_only=True)
image_url = serializers.CharField(source='get_image_url', read_only=True)
category_name = serializers.CharField(source='category_path', read_only=True)
class Meta:
@ -60,6 +63,7 @@ class PartSerializer(serializers.ModelSerializer):
'pk',
'url', # Link to the part detail page
'name',
'image_url',
'IPN',
'URL', # Link to an external URL (optional)
'description',
@ -121,9 +125,10 @@ class SupplierPartSerializer(serializers.ModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True)
part_name = serializers.CharField(source='part.name', read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
supplier_logo = serializers.CharField(source='supplier.get_image_url', read_only=True)
class Meta:
model = SupplierPart
@ -131,9 +136,10 @@ class SupplierPartSerializer(serializers.ModelSerializer):
'pk',
'url',
'part',
'part_name',
'part_detail',
'supplier',
'supplier_name',
'supplier_logo',
'SKU',
'manufacturer',
'MPN',

View File

@ -5,12 +5,19 @@
{% include 'part/tabs.html' with tab='attachments' %}
<h4>Attachments</h4>
<div id='toolbar' class='btn-group'>
<button type='button' class='btn btn-success' id='new-attachment'>Add Attachment</button>
<div class='row'>
<div class='col-sm-6'>
<h4>Part Attachments</h4>
</div>
<div class='col-sm-6'>
<div class="btn-group" style="float: right;">
<button type='button' class='btn btn-success' id='new-attachment'>Add Attachment</button>
</div>
</div>
</div>
<hr>
<table class='table table-striped table-condensed' data-toolbar='#toolbar' id='attachment-table'>
<tr>
<th>File</th>

View File

@ -119,81 +119,18 @@
{% endif %}
$("#part-table").bootstrapTable({
sortable: true,
search: true,
sortName: 'description',
idField: 'pk',
method: 'get',
pagination: true,
rememberOrder: true,
queryParams: function(p) {
return {
active: true,
loadPartTable(
"#part-table",
"{% url 'api-part-list' %}",
{
query: {
{% if category %}
category: {{ category.id }},
include_child_categories: true,
{% endif %}
}
},
buttons: ['#part-options'],
},
columns: [
{
checkbox: true,
title: 'Select',
searchable: false,
},
{
field: 'pk',
title: 'ID',
visible: false,
},
{
field: 'name',
title: 'Part',
sortable: true,
formatter: function(value, row, index, field) {
return renderLink(value, row.url);
}
},
{
sortable: true,
field: 'description',
title: 'Description',
},
{
sortable: true,
field: 'category_name',
title: 'Category',
formatter: function(value, row, index, field) {
if (row.category) {
return renderLink(row.category_name, "/part/category/" + row.category + "/");
}
else {
return '';
}
}
},
{
field: 'total_stock',
title: 'Stock',
searchable: false,
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, row.url + 'stock/');
}
else {
return "<span class='label label-warning'>No stock</span>";
}
}
}
],
url: "{% url 'api-part-list' %}",
});
linkButtonsToSelection(
$("#part-table"),
['#part-options']
);
{% endblock %}

View File

@ -6,26 +6,24 @@
<div class='row'>
<div class='col-sm-6'>
<h3>Part Details</h3>
<h4>Part Details</h4>
</div>
<div class='col-sm-6'>
<h3>
<div class="dropdown" style="float: right;">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
<span class="caret"></span></button>
<ul class="dropdown-menu">
{% if part.active %}
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
<li><a href='#' id='stocktake-part' title='Stocktake'>Stocktake</a></li>
<hr>
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
{% else %}
<li><a href="#" id='activate-part' title='Activate part'>Activate</a></li>
{% endif %}
</ul>
</div>
</h3>
<div class="btn-group" style="float: right;">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
<span class="caret"></span></button>
<ul class="dropdown-menu">
{% if part.active %}
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
<li><a href='#' id='stocktake-part' title='Stocktake'>Stocktake</a></li>
<hr>
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
{% else %}
<li><a href="#" id='activate-part' title='Activate part'>Activate</a></li>
{% endif %}
</ul>
</div>
</div>
</div>

View File

@ -4,7 +4,14 @@
{% include 'part/tabs.html' with tab='stock' %}
<h3>Part Stock</h3>
<div class='row'>
<div class='col-sm-6'>
<h4>Part Stock</h4>
</div>
<div class='col-sm-6'>
</div>
</div>
<hr>
<div id='button-toolbar'>
{% if part.active %}

View File

@ -4,7 +4,15 @@
{% include 'part/tabs.html' with tab='suppliers' %}
<h3>Part Suppliers</h3>
<div class='row'>
<div class='col-sm-6'>
<h4>Part Suppliers</h4>
</div>
<div class='col-sm-6'>
</div>
</div>
<hr>
<div id='button-toolbar'>
<button class="btn btn-success" id='supplier-create'>New Supplier Part</button>
@ -55,7 +63,7 @@
field: 'supplier_name',
title: 'Supplier',
formatter: function(value, row, index, field) {
return renderLink(value, '/company/' + row.supplier + '/');
return imageHoverIcon(row.supplier_logo) + renderLink(value, '/company/' + row.supplier + '/');
}
},
{

View File

@ -38,3 +38,5 @@
<a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachments.all|length > 0 %}<span class="badge">{{ part.attachments.all|length }}</span>{% endif %}</a>
</li>
</ul>
<br>

View File

@ -4,7 +4,17 @@
{% include 'part/tabs.html' with tab='used' %}
<h3>Used In</h3>
<div class='row'>
<div class='col-sm-6'>
<h4>Used to Build</h4>
</div>
<div class='col-sm-6'>
<div class="btn-group" style="float: right;">
</div>
</div>
</div>
<hr>
<table class="table table-striped table-condensed" id='used-table'>
</table>
@ -33,7 +43,7 @@
field: 'part_detail',
title: 'Part',
formatter: function(value, row, index, field) {
return renderLink(value.name, value.url + 'bom/');
return imageHoverIcon(row.part_detail.image_url) + renderLink(value.name, value.url + 'bom/');
}
},
{

View File

@ -404,12 +404,30 @@ class CategoryEdit(AjaxUpdateView):
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
try:
context['category'] = PartCategory.objects.get(pk=self.kwargs['pk'])
context['category'] = self.get_object()
except:
pass
return context
def get_form(self):
""" Customize form data for PartCategory editing.
Limit the choices for 'parent' field to those which make sense
"""
form = super(AjaxUpdateView, self).get_form()
category = self.get_object()
# Remove any invalid choices for the parent category part
parent_choices = PartCategory.objects.all()
parent_choices = parent_choices.exclude(id__in=category.getUniqueChildren())
form.fields['parent'].queryset = parent_choices
return form
class CategoryDelete(AjaxDeleteView):
""" Delete view to delete a PartCategory """

View File

@ -10,6 +10,32 @@
color: #ffcc00;
}
/* Part image icons with full-display on mouse hover */
.hover-img-thumb {
background: #eee;
width: 28px;
height: 28px;
border: 1px solid #cce;
}
.hover-img-large {
background: #eee;
display: none;
position: absolute;
z-index: 999;
border: 1px solid #555;
max-width: 250px;
}
.hover-icon {
margin-right: 10px;
}
.hover-icon:hover > .hover-img-large {
display: block;
}
/* dropzone class - for Drag-n-Drop file uploads */
.dropzone {
border: 1px solid #555;
@ -159,6 +185,14 @@
margin-bottom: 5px;
}
.panel-group .panel {
border-radius: 8px;
}
.panel-heading {
padding: 5px 10px;
}
.float-right {
float: right;
}

View File

@ -114,6 +114,16 @@ function loadBomTable(table, options) {
}
);
// Part notes
cols.push(
{
field: 'note',
title: 'Notes',
searchable: true,
sortable: true,
}
);
if (options.editable) {
cols.push({
formatter: function(value, row, index, field) {
@ -124,6 +134,7 @@ function loadBomTable(table, options) {
}
});
}
else {
cols.push(
{
@ -149,16 +160,6 @@ function loadBomTable(table, options) {
);
}
// Part notes
cols.push(
{
field: 'note',
title: 'Notes',
searchable: true,
sortable: false,
}
);
// Configure the table (bootstrap-table)
table.bootstrapTable({

View File

@ -118,3 +118,18 @@ function enableDragAndDrop(element, url, options) {
}
});
}
function imageHoverIcon(url) {
/* Render a small thumbnail icon for an image.
* On mouseover, display a full-size version of the image
*/
var html = `
<a class='hover-icon'>
<img class='hover-img-thumb' src='` + url + `'>
<img class='hover-img-large' src='` + url + `'>
</a>
`;
return html;
}

View File

@ -73,3 +73,99 @@ function toggleStar(options) {
}
);
}
function loadPartTable(table, url, options={}) {
/* Load part listing data into specified table.
*
* Args:
* - table: HTML reference to the table
* - url: Base URL for API query
* - options: object containing following (optional) fields
* allowInactive: If true, allow display of inactive parts
* query: extra query params for API request
* buttons: If provided, link buttons to selection status of this table
*/
// Default query params
query = options.query;
if (!options.allowInactive) {
// Only display active parts
query.active = true;
}
$(table).bootstrapTable({
url: url,
sortable: true,
search: true,
sortName: 'name',
method: 'get',
pagination: true,
pageSize: 25,
rememberOrder: true,
formatNoMatches: function() { return "No parts found"; },
queryParams: function(p) {
return query;
},
columns: [
{
checkbox: true,
title: 'Select',
searchable: false,
},
{
field: 'pk',
title: 'ID',
visible: false,
},
{
field: 'name',
title: 'Part',
sortable: true,
formatter: function(value, row, index, field) {
var display = imageHoverIcon(row.image_url) + renderLink(value, row.url);
if (!row.active) {
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>";
}
return display;
}
},
{
sortable: true,
field: 'description',
title: 'Description',
},
{
sortable: true,
field: 'category_name',
title: 'Category',
formatter: function(value, row, index, field) {
if (row.category) {
return renderLink(row.category_name, "/part/category/" + row.category + "/");
}
else {
return '';
}
}
},
{
field: 'total_stock',
title: 'Stock',
searchable: false,
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, row.url + 'stock/');
}
else {
return "<span class='label label-warning'>No stock</span>";
}
}
}
],
});
if (options.buttons) {
linkButtonsToSelection($(table), options.buttons);
}
}

View File

@ -347,6 +347,7 @@ function loadStockTable(table, options) {
search: true,
method: 'get',
pagination: true,
pageSize: 25,
rememberOrder: true,
queryParams: options.params,
columns: [
@ -365,9 +366,14 @@ function loadStockTable(table, options) {
title: 'Part',
sortable: true,
formatter: function(value, row, index, field) {
return renderLink(value, row.part.url);
return imageHoverIcon(row.part.image_url) + renderLink(value, row.part.url);
}
},
{
field: 'part.description',
title: 'Description',
sortable: true,
},
{
field: 'location',
title: 'Location',

View File

@ -75,6 +75,24 @@ class StockLocationEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Stock Location'
def get_form(self):
""" Customize form data for StockLocation editing.
Limit the choices for 'parent' field to those which make sense.
"""
form = super(AjaxUpdateView, self).get_form()
location = self.get_object()
# Remove any invalid choices for the 'parent' field
parent_choices = StockLocation.objects.all()
parent_choices = parent_choices.exclude(id__in=location.getUniqueChildren())
form.fields['parent'].queryset = parent_choices
return form
class StockLocationQRCode(QRCodeView):
""" View for displaying a QR code for a StockLocation object """

View File

@ -2,7 +2,7 @@
{% block content %}
<h3>InvenTree</h3>
<hr>
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %}
{% if to_order %}

View File

@ -23,56 +23,14 @@
$("#part-result-count").html("(found " + n + " results)");
});
$("#part-results-table").bootstrapTable({
sortable: true,
search: true,
pagination: true,
formatNoMatches: function() { return "No parts found matching search query"; },
queryParams: function(p) {
return {
loadPartTable("#part-results-table",
"{% url 'api-part-list' %}",
{
query: {
search: "{{ query }}",
}
},
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
},
{
field: 'name',
title: 'Name',
sortable: true,
searchable: true,
formatter: function(value, row, index, field) {
return renderLink(value, row.url);
}
},
{
field: 'IPN',
title: 'Internal Part Number',
searchable: true,
},
{
field: 'description',
title: 'Description',
searchable: true,
},
{
field: 'available_stock',
title: 'Stock',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, row.url + 'stock/');
} else {
return renderLink('No stock', row.url + 'stock/');
}
}
},
],
url: "{% url 'api-part-list' %}"
});
allowInactive: true,
}
);
{% endblock %}

View File

@ -86,6 +86,7 @@ InvenTree
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/part.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>

View File

@ -1,7 +1,7 @@
{% if build.status == build.PENDING %}
<span class='label label-info'>
{% elif build.status == build.HOLDING %}
<span class='label label-warning'>
{% elif build.status == build.ALLOCATED %}
<span class='label label-primary'>
{% elif build.status == build.CANCELLED %}
<span class='label label-danger'>
{% elif build.status == build.COMPLETE %}

View File

@ -14,6 +14,9 @@
{% crispy form %}
{% block form_data %}
{% endblock %}
</form>
{% block post_form_content %}