Merge branch 'master' into matmair/issue2694

This commit is contained in:
Matthias Mair 2022-03-20 02:29:20 +01:00 committed by GitHub
commit cc50eff3a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 9082 additions and 8844 deletions

View File

@ -12,11 +12,16 @@ import common.models
INVENTREE_SW_VERSION = "0.7.0 dev"
# InvenTree API version
INVENTREE_API_VERSION = 31
INVENTREE_API_VERSION = 32
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v32 -> 2022-03-19
- Adds "parameters" detail to Part API endpoint (use &parameters=true)
- Adds ability to filter PartParameterTemplate API by Part instance
- Adds ability to filter PartParameterTemplate API by PartCategory instance
v31 -> 2022-03-14
- Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints

View File

@ -17,7 +17,7 @@ def currency_code_default():
from common.models import InvenTreeSetting
try:
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', create=False)
except ProgrammingError: # pragma: no cover
# database is not initialized yet
code = ''

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -970,9 +970,17 @@ class SOAllocationList(generics.ListAPIView):
outstanding = str2bool(outstanding)
if outstanding:
queryset = queryset.filter(line__order__status__in=SalesOrderStatus.OPEN)
# Filter only "open" orders
# Filter only allocations which have *not* shipped
queryset = queryset.filter(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None,
)
else:
queryset = queryset.exclude(line__order__status__in=SalesOrderStatus.OPEN)
queryset = queryset.exclude(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None
)
return queryset

View File

@ -855,6 +855,14 @@ class PartList(generics.ListCreateAPIView):
kwargs['starred_parts'] = self.starred_parts
try:
params = self.request.query_params
kwargs['parameters'] = str2bool(params.get('parameters', None))
except AttributeError:
pass
return self.serializer_class(*args, **kwargs)
def list(self, request, *args, **kwargs):
@ -1405,6 +1413,44 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
'name',
]
def filter_queryset(self, queryset):
"""
Custom filtering for the PartParameterTemplate API
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filtering against a "Part" - return only parameter templates which are referenced by a part
part = params.get('part', None)
if part is not None:
try:
part = Part.objects.get(pk=part)
parameters = PartParameter.objects.filter(part=part)
template_ids = parameters.values_list('template').distinct()
queryset = queryset.filter(pk__in=[el[0] for el in template_ids])
except (ValueError, Part.DoesNotExist):
pass
# Filtering against a "PartCategory" - return only parameter templates which are referenced by parts in this category
category = params.get('category', None)
if category is not None:
try:
category = PartCategory.objects.get(pk=category)
cats = category.get_descendants(include_self=True)
parameters = PartParameter.objects.filter(part__category__in=cats)
template_ids = parameters.values_list('template').distinct()
queryset = queryset.filter(pk__in=[el[0] for el in template_ids])
except (ValueError, PartCategory.DoesNotExist):
pass
return queryset
class PartParameterList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameter objects

View File

@ -417,7 +417,7 @@ class Part(MPTTModel):
context['allocated_build_order_quantity'] = self.build_order_allocation_count()
context['required_sales_order_quantity'] = self.required_sales_order_quantity()
context['allocated_sales_order_quantity'] = self.sales_order_allocation_count()
context['allocated_sales_order_quantity'] = self.sales_order_allocation_count(pending=True)
context['available'] = self.available_stock
context['on_order'] = self.on_order
@ -1118,7 +1118,9 @@ class Part(MPTTModel):
quantity = 0
for line in open_lines:
quantity += line.quantity
# Determine the quantity "remaining" to be shipped out
remaining = max(line.quantity - line.shipped, 0)
quantity += remaining
return quantity
@ -1336,19 +1338,36 @@ class Part(MPTTModel):
return query['total']
def sales_order_allocations(self):
def sales_order_allocations(self, **kwargs):
"""
Return all sales-order-allocation objects which allocate this part to a SalesOrder
"""
return OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
queryset = OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
def sales_order_allocation_count(self):
pending = kwargs.get('pending', None)
if pending is True:
# Look only for 'open' orders which have not shipped
queryset = queryset.filter(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None,
)
elif pending is False:
# Look only for 'closed' orders or orders which have shipped
queryset = queryset.exclude(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None,
)
return queryset
def sales_order_allocation_count(self, **kwargs):
"""
Return the tutal quantity of this part allocated to sales orders
Return the total quantity of this part allocated to sales orders
"""
query = self.sales_order_allocations().aggregate(
query = self.sales_order_allocations(**kwargs).aggregate(
total=Coalesce(
Sum(
'quantity',

View File

@ -211,6 +211,34 @@ class PartThumbSerializerUpdate(InvenTreeModelSerializer):
]
class PartParameterTemplateSerializer(InvenTreeModelSerializer):
""" JSON serializer for the PartParameterTemplate model """
class Meta:
model = PartParameterTemplate
fields = [
'pk',
'name',
'units',
]
class PartParameterSerializer(InvenTreeModelSerializer):
""" JSON serializers for the PartParameter model """
template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True)
class Meta:
model = PartParameter
fields = [
'pk',
'part',
'template',
'template_detail',
'data'
]
class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """
@ -259,11 +287,16 @@ class PartSerializer(InvenTreeModelSerializer):
category_detail = kwargs.pop('category_detail', False)
parameters = kwargs.pop('parameters', False)
super().__init__(*args, **kwargs)
if category_detail is not True:
self.fields.pop('category_detail')
if parameters is not True:
self.fields.pop('parameters')
@staticmethod
def annotate_queryset(queryset):
"""
@ -356,19 +389,18 @@ class PartSerializer(InvenTreeModelSerializer):
# PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...)
category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all())
# TODO - Include annotation for the following fields:
# allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
# bom_items = serializers.IntegerField(source='bom_count', read_only=True)
# used_in = serializers.IntegerField(source='used_in_count', read_only=True)
parameters = PartParameterSerializer(
many=True,
read_only=True,
)
class Meta:
model = Part
partial = True
fields = [
'active',
# 'allocated_stock',
'assembly',
# 'bom_items',
'category',
'category_detail',
'component',
@ -388,6 +420,7 @@ class PartSerializer(InvenTreeModelSerializer):
'minimum_stock',
'name',
'notes',
'parameters',
'pk',
'purchaseable',
'revision',
@ -398,7 +431,6 @@ class PartSerializer(InvenTreeModelSerializer):
'thumbnail',
'trackable',
'units',
# 'used_in',
'variant_of',
'virtual',
]
@ -600,34 +632,6 @@ class BomItemSerializer(InvenTreeModelSerializer):
]
class PartParameterTemplateSerializer(InvenTreeModelSerializer):
""" JSON serializer for the PartParameterTemplate model """
class Meta:
model = PartParameterTemplate
fields = [
'pk',
'name',
'units',
]
class PartParameterSerializer(InvenTreeModelSerializer):
""" JSON serializers for the PartParameter model """
template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True)
class Meta:
model = PartParameter
fields = [
'pk',
'part',
'template',
'template_detail',
'data'
]
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
""" Serializer for PartCategoryParameterTemplate """

View File

@ -223,13 +223,14 @@
{{ block.super }}
{% if category %}
loadParametricPartTable(
"#parametric-part-table",
{
headers: {{ headers|safe }},
data: {{ parameters|safe }},
}
);
onPanelLoad('parameters', function() {
loadParametricPartTable(
"#parametric-part-table",
{
category: {{ category.pk }},
}
);
});
$("#toggle-starred").click(function() {
toggleStar({
@ -240,9 +241,6 @@
{% endif %}
// Enable left-hand navigation sidebar
enableSidebar('category');
// Enable breadcrumb tree view
enableBreadcrumbTree({
label: 'category',
@ -258,18 +256,20 @@
}
});
loadPartCategoryTable(
$('#subcategory-table'), {
params: {
{% if category %}
parent: {{ category.pk }},
{% else %}
parent: null,
{% endif %}
},
allowTreeView: true,
}
);
onPanelLoad('subcategories', function() {
loadPartCategoryTable(
$('#subcategory-table'), {
params: {
{% if category %}
parent: {{ category.pk }},
{% else %}
parent: null,
{% endif %}
},
allowTreeView: true,
}
);
});
$("#cat-create").click(function() {
@ -339,19 +339,24 @@
{% endif %}
loadPartTable(
"#part-table",
"{% url 'api-part-list' %}",
{
params: {
{% if category %}category: {{ category.id }},
{% else %}category: "null",
{% endif %}
onPanelLoad('parts', function() {
loadPartTable(
"#part-table",
"{% url 'api-part-list' %}",
{
params: {
{% if category %}category: {{ category.id }},
{% else %}category: "null",
{% endif %}
},
buttons: ['#part-options'],
checkbox: true,
gridView: true,
},
buttons: ['#part-options'],
checkbox: true,
gridView: true,
},
);
);
});
// Enable left-hand navigation sidebar
enableSidebar('category');
{% endblock %}

View File

@ -47,23 +47,6 @@
</div>
{% endif %}
<div class='panel panel-hidden' id='panel-allocations'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Part Stock Allocations" %}</h4>
{% include "spacer.html" %}
</div>
</div>
<div class='panel-content'>
<div id='allocations-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="allocations" %}
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#allocations-button-toolbar' id='part-allocation-table'></table>
</div>
</div>
<div class='panel panel-hidden' id='panel-test-templates'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
@ -326,6 +309,7 @@
</div>
<div class='panel panel-hidden' id='panel-build-orders'>
{% if part.assembly %}
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Part Builds" %}</h4>
@ -351,7 +335,9 @@
<table class='table table-striped table-condensed' data-toolbar='#build-button-toolbar' id='build-table'>
</table>
</div>
{% endif %}
{% if part.component %}
<div class='panel-heading'>
<h4>{% trans "Build Order Allocations" %}</h4>
</div>
@ -363,6 +349,7 @@
</div>
<table class='table table-striped table-condensed' id='build-order-allocation-table' data-toolbar='#build-allocation-button-toolbar'></table>
</div>
{% endif %}
</div>
<div class='panel panel-hidden' id='panel-suppliers'>
@ -531,6 +518,7 @@
// Load the "builds" tab
onPanelLoad("build-orders", function() {
{% if part.assembly %}
$("#start-build").click(function() {
newBuildOrder({
part: {{ part.pk }},
@ -543,12 +531,15 @@
part: {{ part.id }},
}
});
{% endif %}
{% if part.component %}
loadBuildOrderAllocationTable("#build-order-allocation-table", {
params: {
part: {{ part.id }},
}
});
{% endif %}
});
@ -675,19 +666,6 @@
{% endif %}
});
// Load the "allocations" tab
onPanelLoad('allocations', function() {
loadStockAllocationTable(
$("#part-allocation-table"),
{
params: {
part: {{ part.pk }},
},
}
);
});
// Load the "related parts" tab
onPanelLoad("related-parts", function() {

View File

@ -204,44 +204,60 @@
<td>{% decimal on_order %}</td>
</tr>
{% endif %}
{% if part.component %}
{% if required_build_order_quantity > 0 %}
<tr>
<td><span class='fas fa-clipboard-list'></span></td>
<td>{% trans "Required for Build Orders" %}</td>
<td>{% decimal required_build_order_quantity %}
<td>{% decimal required_build_order_quantity %}</td>
</tr>
<tr>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Allocated to Build Orders" %}</td>
<td>
{% decimal allocated_build_order_quantity %}
{% if allocated_build_order_quantity < required_build_order_quantity %}
<span class='fas fa-times-circle icon-red float-right' title='{% trans "Required quantity has not been allocated" %}'></span>
{% else %}
<span class='fas fa-check-circle icon-green float-right' title='{% trans "Required quantity has been allocated" %}'></span>
{% endif %}
</td>
</tr>
{% endif %}
{% endif %}
{% if part.salable %}
{% if required_sales_order_quantity > 0 %}
<tr>
<td><span class='fas fa-clipboard-list'></span></td>
<td>{% trans "Required for Sales Orders" %}</td>
<td>{% decimal required_sales_order_quantity %}
</tr>
{% endif %}
{% if allocated > 0 %}
<tr>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Allocated to Orders" %}</td>
<td>{% decimal allocated %}</td>
</tr>
{% endif %}
{% if not part.is_template %}
{% if part.assembly %}
<tr>
<td><h5><span class='fas fa-tools'></span></h5></td>
<td colspan='2'>
<h5>{% trans "Build Status" %}</h5>
<td>
{% decimal required_sales_order_quantity %}
</td>
</tr>
<tr>
<td></td>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Allocated to Sales Orders" %}</td>
<td>
{% decimal allocated_sales_order_quantity %}
{% if allocated_sales_order_quantity < required_sales_order_quantity %}
<span class='fas fa-times-circle icon-red float-right' title='{% trans "Required quantity has not been allocated" %}'></span>
{% else %}
<span class='fas fa-check-circle icon-green float-right' title='{% trans "Required quantity has been allocated" %}'></span>
{% endif %}
</td>
</tr>
{% endif %}
{% endif %}
{% if not part.is_template %}
{% if part.assembly %}
<tr>
<td><span class='fas fa-tools'></span></td>
<td>{% trans "Can Build" %}</td>
<td>{% decimal part.can_build %}</td>
</tr>
{% if quantity_being_built > 0 %}
<tr>
<td></td>
<td><span class='fas fa-tools'></span></td>
<td>{% trans "Building" %}</td>
<td>{% decimal quantity_being_built %}</td>
</tr>

View File

@ -17,7 +17,9 @@
{% if part.assembly %}
{% trans "Bill of Materials" as text %}
{% include "sidebar_item.html" with label="bom" text=text icon="fa-list" %}
{% endif %}
{% if roles.build.view %}
{% if part.assembly or part.component %}
{% trans "Build Orders" as text %}
{% include "sidebar_item.html" with label="build-orders" text=text icon="fa-tools" %}
{% endif %}
@ -30,10 +32,6 @@
{% trans "Pricing" as text %}
{% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %}
{% endif %}
{% if part.salable or part.component %}
{% trans "Allocations" as text %}
{% include "sidebar_item.html" with label="allocations" text=text icon="fa-bookmark" %}
{% endif %}
{% if part.purchaseable and roles.purchase_order.view %}
{% trans "Suppliers" as text %}
{% include "sidebar_item.html" with label="suppliers" text=text icon="fa-building" %}

View File

@ -441,9 +441,9 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
# set date for graph labels
if stock_item.purchase_order and stock_item.purchase_order.issue_date:
line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y')
line['date'] = stock_item.purchase_order.issue_date.isoformat()
elif stock_item.tracking_info.count() > 0:
line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y')
line['date'] = stock_item.tracking_info.first().date.date().isoformat()
else:
# Not enough information
continue
@ -500,9 +500,9 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
# set date for graph labels
if sale_item.order.issue_date:
line['date'] = sale_item.order.issue_date.strftime('%d.%m.%Y')
line['date'] = sale_item.order.issue_date.isoformat()
elif sale_item.order.creation_date:
line['date'] = sale_item.order.creation_date.strftime('%d.%m.%Y')
line['date'] = sale_item.order.creation_date.isoformat()
else:
line['date'] = _('None')
@ -988,22 +988,6 @@ class CategoryDetail(InvenTreeRoleMixin, DetailView):
category = kwargs.get('object', None)
if category:
cascade = kwargs.get('cascade', True)
# Prefetch parts parameters
parts_parameters = category.prefetch_parts_parameters(cascade=cascade)
# Get table headers (unique parameters names)
context['headers'] = category.get_unique_parameters(cascade=cascade,
prefetch=parts_parameters)
# Insert part information
context['headers'].insert(0, 'description')
context['headers'].insert(0, 'part')
# Get parameters data
context['parameters'] = category.get_parts_parameters(cascade=cascade,
prefetch=parts_parameters)
# Insert "starred" information
context['starred'] = category.is_starred_by(self.request.user)

View File

@ -37,19 +37,36 @@
</div>
<div class='panel panel-hidden' id='panel-allocations'>
{% if item.part.component %}
<div class='panel-heading'>
<h4>{% trans "Stock Item Allocations" %}</h4>
<h4>{% trans "Build Order Allocations" %}</h4>
{% include "spacer.html" %}
</div>
<div class='panel-content'>
<div id='allocations-button-toolbar'>
<div id='build-order-allocations-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="allocations" %}
{% include "filter_list.html" with id="buildorderallocation" %}
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#allocatoins-button-toolbar' id='stock-allocation-table'></table>
<table class='table table-striped table-condensed' data-toolbar='#build-order-allocation-toolbar' id='build-order-allocation-table'></table>
</div>
{% endif %}
{% if item.part.salable %}
<div class='panel-heading'>
<h4>{% trans "Sales Order Allocations" %}</h4>
{% include "spacer.html" %}
</div>
<div class='panel-content'>
<div id='sales-order-allocations-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="salesorderallocation" %}
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#sales-order-allocation-toolbar' id='sales-order-allocation-table'></table>
</div>
{% endif %}
</div>
<div class='panel panel-hidden' id='panel-children'>
@ -164,14 +181,21 @@
// Load the "allocations" tab
onPanelLoad('allocations', function() {
loadStockAllocationTable(
$("#stock-allocation-table"),
{
params: {
stock_item: {{ item.pk }},
},
{% if item.part.component %}
loadBuildOrderAllocationTable('#build-order-allocation-table', {
params: {
stock_item: {{ item.pk }},
}
);
});
{% endif %}
{% if item.part.salable %}
loadSalesOrderAllocationTable('#sales-order-allocation-table', {
params: {
stock_item: {{ item.pk }},
}
});
{% endif %}
});
$('#stock-item-install').click(function() {

View File

@ -202,15 +202,17 @@
{% block js_ready %}
{{ block.super }}
loadStockLocationTable($('#sublocation-table'), {
params: {
{% if location %}
parent: {{ location.pk }},
{% else %}
parent: 'null',
{% endif %}
},
allowTreeView: true,
onPanelLoad('sublocations', function() {
loadStockLocationTable($('#sublocation-table'), {
params: {
{% if location %}
parent: {{ location.pk }},
{% else %}
parent: 'null',
{% endif %}
},
allowTreeView: true,
});
});
linkButtonsToSelection(
@ -325,19 +327,21 @@
});
});
loadStockTable($("#stock-table"), {
buttons: [
'#stock-options',
],
params: {
{% if location %}
location: {{ location.pk }},
{% endif %}
part_detail: true,
location_detail: true,
supplier_part_detail: true,
},
url: "{% url 'api-stock-list' %}",
onPanelLoad('stock', function() {
loadStockTable($("#stock-table"), {
buttons: [
'#stock-options',
],
params: {
{% if location %}
location: {{ location.pk }},
{% endif %}
part_detail: true,
location_detail: true,
supplier_part_detail: true,
},
url: "{% url 'api-stock-list' %}",
});
});
enableSidebar('stocklocation');

View File

@ -312,7 +312,13 @@ function renderPartCategory(name, data, parameters, options) {
// eslint-disable-next-line no-unused-vars
function renderPartParameterTemplate(name, data, parameters, options) {
var html = `<span>${data.name} - [${data.units}]</span>`;
var units = '';
if (data.units) {
units = ` [${data.units}]`;
}
var html = `<span>${data.name}${units}</span>`;
return html;
}

View File

@ -2298,15 +2298,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
field: 'location',
title: '{% trans "Location" %}',
formatter: function(value, row) {
if (!value) {
return '{% trans "Location not specified" %}';
}
var link = `/stock/location/${value}`;
var text = row.location_detail.description;
return renderLink(text, link);
return locationDetail(row.item_detail, true);
}
},
{

View File

@ -1068,68 +1068,84 @@ function loadRelatedPartsTable(table, part_id, options={}) {
}
/* Load parametric table for part parameters
*/
function loadParametricPartTable(table, options={}) {
/* Load parametric table for part parameters
*
* Args:
* - table: HTML reference to the table
* - table_headers: Unique parameters found in category
* - table_data: Parameters data
*/
var table_headers = options.headers;
var table_data = options.data;
var columns = [
{
field: 'name',
title: '{% trans "Part" %}',
switchable: false,
sortable: true,
formatter: function(value, row) {
var name = row.full_name;
var columns = [];
var display = imageHoverIcon(row.thumbnail) + renderLink(name, `/part/${row.pk}/`);
for (var header of table_headers) {
if (header === 'part') {
columns.push({
field: header,
title: '{% trans "Part" %}',
sortable: true,
sortName: 'name',
formatter: function(value, row) {
var name = '';
if (row.IPN) {
name += row.IPN + ' | ' + row.name;
} else {
name += row.name;
}
return renderLink(name, '/part/' + row.pk + '/');
}
});
} else if (header === 'description') {
columns.push({
field: header,
title: '{% trans "Description" %}',
sortable: true,
});
} else {
columns.push({
field: header,
title: header,
sortable: true,
filterControl: 'input',
});
return display;
}
}
}
];
// Request a list of parameters we are interested in for this category
inventreeGet(
'{% url "api-part-parameter-template-list" %}',
{
category: options.category,
},
{
async: false,
success: function(response) {
for (var template of response) {
columns.push({
field: `parameter_${template.pk}`,
title: template.name,
switchable: true,
sortable: true,
filterControl: 'input',
});
}
}
}
);
// TODO: Re-enable filter control for parameter values
$(table).inventreeTable({
sortName: 'part',
queryParams: table_headers,
url: '{% url "api-part-list" %}',
queryParams: {
category: options.category,
cascade: true,
parameters: true,
},
groupBy: false,
name: options.name || 'parametric',
name: options.name || 'part-parameters',
formatNoMatches: function() {
return '{% trans "No parts found" %}';
},
columns: columns,
showColumns: true,
data: table_data,
filterControl: true,
// filterControl: true,
sidePagination: 'server',
idField: 'pk',
uniqueId: 'pk',
onLoadSuccess: function() {
var data = $(table).bootstrapTable('getData');
for (var idx = 0; idx < data.length; idx++) {
var row = data[idx];
var pk = row.pk;
// Make each parameter accessible, based on the "template" columns
row.parameters.forEach(function(parameter) {
row[`parameter_${parameter.template}`] = parameter.data;
});
$(table).bootstrapTable('updateRow', pk, row);
}
}
});
}
@ -2028,6 +2044,22 @@ function loadPartSchedulingChart(canvas_id, part_id) {
}
);
// If no scheduling information is available for the part,
// remove the chart and display a message instead
if (stock_schedule.length <= 1) {
var message = `
<div class='alert alert-block alert-info'>
{% trans "No scheduling information available for this part" %}.<br>
</div>`;
var canvas_element = $('#part-schedule-chart');
canvas_element.closest('div').html(message);
return;
}
// Iterate through future "events" to calculate expected quantity
var quantity = part_info.in_stock;

View File

@ -46,7 +46,6 @@
findStockItemBySerialNumber,
installStockItem,
loadInstalledInTable,
loadStockAllocationTable,
loadStockLocationTable,
loadStockTable,
loadStockTestResultsTable,
@ -2302,157 +2301,6 @@ function loadStockTable(table, options) {
}
/*
* Display a table of allocated stock, for either a part or stock item
* Allocations are displayed for:
*
* a) Sales Orders
* b) Build Orders
*/
function loadStockAllocationTable(table, options={}) {
var params = options.params || {};
params.build_detail = true;
var filterListElement = options.filterList || '#filter-list-allocations';
var filters = {};
var filterKey = options.filterKey || options.name || 'allocations';
var original = {};
for (var k in params) {
original[k] = params[k];
filters[k] = params[k];
}
setupFilterList(filterKey, table, filterListElement);
/*
* We have two separate API queries to make here:
* a) Build Order Allocations
* b) Sales Order Allocations
*
* We will let the call to inventreeTable take care of build orders,
* and then load sales orders after that.
*/
table.inventreeTable({
url: '{% url "api-build-item-list" %}',
name: 'allocations',
original: original,
method: 'get',
queryParams: filters,
sidePagination: 'client',
showColumns: false,
onLoadSuccess: function(tableData) {
var query_params = params;
query_params.customer_detail = true;
query_params.order_detail = true;
delete query_params.build_detail;
// Load sales order allocation data
inventreeGet('{% url "api-so-allocation-list" %}', query_params, {
success: function(data) {
// Update table to include sales order data
$(table).bootstrapTable('append', data);
}
});
},
columns: [
{
field: 'order',
title: '{% trans "Order" %}',
formatter: function(value, row) {
var html = '';
if (row.build) {
// Add an icon for the part being built
html += thumbnailImage(row.build_detail.part_detail.thumbnail, {
title: row.build_detail.part_detail.full_name
});
html += ' ';
html += renderLink(
global_settings.BUILDORDER_REFERENCE_PREFIX + row.build_detail.reference,
`/build/${row.build}/`
);
html += makeIconBadge('fa-tools', '{% trans "Build Order" %}');
} else if (row.order) {
// Add an icon for the customer
html += thumbnailImage(row.customer_detail.thumbnail || row.customer_detail.image, {
title: row.customer_detail.name,
});
html += ' ';
html += renderLink(
global_settings.SALESORDER_REFERENCE_PREFIX + row.order_detail.reference,
`/order/sales-order/${row.order}/`
);
html += makeIconBadge('fa-truck', '{% trans "Sales Order" %}');
} else {
return '-';
}
return html;
}
},
{
field: 'description',
title: '{% trans "Description" %}',
formatter: function(value, row) {
if (row.order_detail) {
return row.order_detail.description;
} else if (row.build_detail) {
return row.build_detail.title;
} else {
return '-';
}
}
},
{
field: 'status',
title: '{% trans "Order Status" %}',
formatter: function(value, row) {
if (row.build) {
return buildStatusDisplay(row.build_detail.status);
} else if (row.order) {
return salesOrderStatusDisplay(row.order_detail.status);
} else {
return '-';
}
}
},
{
field: 'quantity',
title: '{% trans "Allocated Quantity" %}',
formatter: function(value, row) {
var text = value;
var pk = row.stock_item || row.item;
if (pk) {
var url = `/stock/item/${pk}/`;
return renderLink(text, url);
} else {
return value;
}
}
},
]
});
}
/*
* Display a table of stock locations
*/

View File

@ -341,6 +341,15 @@ function getAvailableTableFilters(tableKey) {
};
}
if (tableKey == 'salesorderallocation') {
return {
outstanding: {
type: 'bool',
title: '{% trans "Outstanding" %}',
}
};
}
if (tableKey == 'salesorder') {
return {
status: {