mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' into matmair/issue2694
This commit is contained in:
commit
cc50eff3a1
@ -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 ¶meters=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
|
||||
|
||||
|
@ -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
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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() {
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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" %}
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -341,6 +341,15 @@ function getAvailableTableFilters(tableKey) {
|
||||
};
|
||||
}
|
||||
|
||||
if (tableKey == 'salesorderallocation') {
|
||||
return {
|
||||
outstanding: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Outstanding" %}',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (tableKey == 'salesorder') {
|
||||
return {
|
||||
status: {
|
||||
|
Loading…
Reference in New Issue
Block a user