2
0
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:
Matthias
2022-03-20 00:23:29 +01:00
38 changed files with 9083 additions and 8845 deletions

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)