mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-19 05:25:42 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue2279
This commit is contained in:
@ -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)
|
||||
|
Reference in New Issue
Block a user