Merge branch 'master' of https://github.com/inventree/InvenTree into order-modal-show-price

This commit is contained in:
Matthias 2021-06-18 16:46:11 +02:00
commit 15804f873b
13 changed files with 462 additions and 106 deletions

View File

@ -0,0 +1,60 @@
"""
Custom management command to rebuild all MPTT models
- This is crucial after importing any fixtures, etc
"""
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""
Rebuild all database models which leverage the MPTT structure.
"""
def handle(self, *args, **kwargs):
# Part model
try:
print("Rebuilding Part objects")
from part.models import Part
Part.objects.rebuild()
except:
print("Error rebuilding Part objects")
# Part category
try:
print("Rebuilding PartCategory objects")
from part.models import PartCategory
PartCategory.objects.rebuild()
except:
print("Error rebuilding PartCategory objects")
# StockItem model
try:
print("Rebuilding StockItem objects")
from stock.models import StockItem
StockItem.objects.rebuild()
except:
print("Error rebuilding StockItem objects")
# StockLocation model
try:
print("Rebuilding StockLocation objects")
from stock.models import StockLocation
StockLocation.objects.rebuild()
except:
print("Error rebuilding StockLocation objects")
# Build model
try:
print("Rebuilding Build objects")
from build.models import Build
Build.objects.rebuild()
except:
print("Error rebuilding Build objects")

View File

@ -165,6 +165,19 @@ class BuildItemList(generics.ListCreateAPIView):
serializer_class = BuildItemSerializer
def get_serializer(self, *args, **kwargs):
try:
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
kwargs['build_detail'] = str2bool(params.get('build_detail', False))
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
except AttributeError:
pass
return self.serializer_class(*args, **kwargs)
def get_queryset(self):
""" Override the queryset method,
to allow filtering by stock_item.part

View File

@ -13,7 +13,8 @@ from rest_framework import serializers
from InvenTree.serializers import InvenTreeModelSerializer
from stock.serializers import StockItemSerializerBrief
from part.serializers import PartBriefSerializer
from stock.serializers import LocationSerializer
from part.serializers import PartSerializer, PartBriefSerializer
from .models import Build, BuildItem
@ -99,22 +100,45 @@ class BuildItemSerializer(InvenTreeModelSerializer):
bom_part = serializers.IntegerField(source='bom_item.sub_part.pk', read_only=True)
part = serializers.IntegerField(source='stock_item.part.pk', read_only=True)
part_name = serializers.CharField(source='stock_item.part.full_name', read_only=True)
part_thumb = serializers.CharField(source='getStockItemThumbnail', read_only=True)
location = serializers.IntegerField(source='stock_item.location.pk', read_only=True)
# Extra (optional) detail fields
part_detail = PartSerializer(source='stock_item.part', many=False, read_only=True)
build_detail = BuildSerializer(source='build', many=False, read_only=True)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
location_detail = LocationSerializer(source='stock_item.location', read_only=True)
quantity = serializers.FloatField()
def __init__(self, *args, **kwargs):
build_detail = kwargs.pop('build_detail', False)
part_detail = kwargs.pop('part_detail', False)
location_detail = kwargs.pop('location_detail', False)
super().__init__(*args, **kwargs)
if not build_detail:
self.fields.pop('build_detail')
if not part_detail:
self.fields.pop('part_detail')
if not location_detail:
self.fields.pop('location_detail')
class Meta:
model = BuildItem
fields = [
'pk',
'bom_part',
'build',
'build_detail',
'install_into',
'location',
'location_detail',
'part',
'part_name',
'part_thumb',
'part_detail',
'stock_item',
'stock_item_detail',
'quantity'

View File

@ -103,17 +103,11 @@ class ManufacturerPartList(generics.ListCreateAPIView):
# Do we wish to include extra detail?
try:
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
except AttributeError:
pass
params = self.request.query_params
try:
kwargs['manufacturer_detail'] = str2bool(self.request.query_params.get('manufacturer_detail', None))
except AttributeError:
pass
try:
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
kwargs['part_detail'] = str2bool(params.get('part_detail', None))
kwargs['manufacturer_detail'] = str2bool(params.get('manufacturer_detail', None))
kwargs['pretty'] = str2bool(params.get('pretty', None))
except AttributeError:
pass
@ -252,22 +246,11 @@ class SupplierPartList(generics.ListCreateAPIView):
# Do we wish to include extra detail?
try:
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
except AttributeError:
pass
try:
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', None))
except AttributeError:
pass
try:
kwargs['manufacturer_detail'] = str2bool(self.request.query_params.get('manufacturer_detail', None))
except AttributeError:
pass
try:
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', None))
kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', None))
kwargs['manufacturer_detail'] = str2bool(self.params.get('manufacturer_detail', None))
kwargs['pretty'] = str2bool(params.get('pretty', None))
except AttributeError:
pass

View File

@ -22,9 +22,10 @@ from .models import PurchaseOrder, PurchaseOrderLineItem
from .models import PurchaseOrderAttachment
from .serializers import POSerializer, POLineItemSerializer, POAttachmentSerializer
from .models import SalesOrder, SalesOrderLineItem
from .models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation
from .models import SalesOrderAttachment
from .serializers import SalesOrderSerializer, SOLineItemSerializer, SOAttachmentSerializer
from .serializers import SalesOrderAllocationSerializer
class POList(generics.ListCreateAPIView):
@ -422,17 +423,11 @@ class SOLineItemList(generics.ListCreateAPIView):
def get_serializer(self, *args, **kwargs):
try:
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False))
except AttributeError:
pass
params = self.request.query_params
try:
kwargs['order_detail'] = str2bool(self.request.query_params.get('order_detail', False))
except AttributeError:
pass
try:
kwargs['allocations'] = str2bool(self.request.query_params.get('allocations', False))
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['allocations'] = str2bool(params.get('allocations', False))
except AttributeError:
pass
@ -486,6 +481,70 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView):
serializer_class = SOLineItemSerializer
class SOAllocationList(generics.ListCreateAPIView):
"""
API endpoint for listing SalesOrderAllocation objects
"""
queryset = SalesOrderAllocation.objects.all()
serializer_class = SalesOrderAllocationSerializer
def get_serializer(self, *args, **kwargs):
try:
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
kwargs['item_detail'] = str2bool(params.get('item_detail', False))
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
except AttributeError:
pass
return self.serializer_class(*args, **kwargs)
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# Filter by order
params = self.request.query_params
# Filter by "part" reference
part = params.get('part', None)
if part is not None:
queryset = queryset.filter(item__part=part)
# Filter by "order" reference
order = params.get('order', None)
if order is not None:
queryset = queryset.filter(line__order=order)
# Filter by "outstanding" order status
outstanding = params.get('outstanding', None)
if outstanding is not None:
outstanding = str2bool(outstanding)
if outstanding:
queryset = queryset.filter(line__order__status__in=SalesOrderStatus.OPEN)
else:
queryset = queryset.exclude(line__order__status__in=SalesOrderStatus.OPEN)
return queryset
filter_backends = [
DjangoFilterBackend,
]
# Default filterable fields
filter_fields = [
'item',
]
class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)
@ -494,10 +553,6 @@ class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
queryset = PurchaseOrderAttachment.objects.all()
serializer_class = POAttachmentSerializer
filter_fields = [
'order',
]
order_api_urls = [
# API endpoints for purchase orders
@ -512,14 +567,26 @@ order_api_urls = [
url(r'^po-line/$', POLineItemList.as_view(), name='api-po-line-list'),
# API endpoints for sales ordesr
url(r'^so/(?P<pk>\d+)/$', SODetail.as_view(), name='api-so-detail'),
url(r'so/attachment/', include([
url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'),
url(r'^so/', include([
url(r'^(?P<pk>\d+)/$', SODetail.as_view(), name='api-so-detail'),
url(r'attachment/', include([
url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'),
])),
# List all sales orders
url(r'^.*$', SOList.as_view(), name='api-so-list'),
])),
url(r'^so/.*$', SOList.as_view(), name='api-so-list'),
# API endpoints for sales order line items
url(r'^so-line/(?P<pk>\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'),
url(r'^so-line/$', SOLineItemList.as_view(), name='api-so-line-list'),
url(r'^so-line/', include([
url(r'^(?P<pk>\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'),
url(r'^$', SOLineItemList.as_view(), name='api-so-line-list'),
])),
# API endpoints for sales order allocations
url(r'^so-allocation', include([
# List all sales order allocations
url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'),
])),
]

View File

@ -18,6 +18,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from part.serializers import PartBriefSerializer
from stock.serializers import LocationBriefSerializer
from stock.serializers import StockItemSerializer, LocationSerializer
from .models import PurchaseOrder, PurchaseOrderLineItem
from .models import PurchaseOrderAttachment, SalesOrderAttachment
@ -42,7 +43,7 @@ class POSerializer(InvenTreeModelSerializer):
"""
Add extra information to the queryset
- Number of liens in the PurchaseOrder
- Number of lines in the PurchaseOrder
- Overdue status of the PurchaseOrder
"""
@ -236,11 +237,38 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
This includes some fields from the related model objects.
"""
location_path = serializers.CharField(source='get_location_path')
location_id = serializers.IntegerField(source='get_location')
serial = serializers.CharField(source='get_serial')
po = serializers.CharField(source='get_po')
quantity = serializers.FloatField()
part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True)
order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True)
serial = serializers.CharField(source='get_serial', read_only=True)
quantity = serializers.FloatField(read_only=True)
location = serializers.PrimaryKeyRelatedField(source='item.location', many=False, read_only=True)
# Extra detail fields
order_detail = SalesOrderSerializer(source='line.order', many=False, read_only=True)
part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)
item_detail = StockItemSerializer(source='item', many=False, read_only=True)
location_detail = LocationSerializer(source='item.location', many=False, read_only=True)
def __init__(self, *args, **kwargs):
order_detail = kwargs.pop('order_detail', False)
part_detail = kwargs.pop('part_detail', False)
item_detail = kwargs.pop('item_detail', False)
location_detail = kwargs.pop('location_detail', False)
super().__init__(*args, **kwargs)
if not order_detail:
self.fields.pop('order_detail')
if not part_detail:
self.fields.pop('part_detail')
if not item_detail:
self.fields.pop('item_detail')
if not location_detail:
self.fields.pop('location_detail')
class Meta:
model = SalesOrderAllocation
@ -250,10 +278,14 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
'line',
'serial',
'quantity',
'location_id',
'location_path',
'po',
'location',
'location_detail',
'item',
'item_detail',
'order',
'order_detail',
'part',
'part_detail',
]

View File

@ -81,10 +81,10 @@ function showAllocationSubTable(index, row, element) {
},
},
{
field: 'location_id',
field: 'location',
title: 'Location',
formatter: function(value, row, index, field) {
return renderLink(row.location_path, `/stock/location/${row.location_id}/`);
return renderLink(row.location_path, `/stock/location/${row.location}/`);
},
},
{

View File

@ -8,52 +8,43 @@
{% endblock %}
{% block heading %}
{% trans "Part Stock Allocations" %}
{% trans "Build Order Allocations" %}
{% endblock %}
{% block details %}
<table class='table table-striped table-condensed' id='build-table'>
<tr>
<th>{% trans "Order" %}</th>
<th>{% trans "Stock Item" %}</th>
<th>{% trans "Quantity" %}</th>
</tr>
{% for allocation in part.build_order_allocations %}
<tr>
<td><a href="{% url 'build-detail' allocation.build.id %}">{% trans "Build Order" %}: {{ allocation.build }}</a></td>
<td><a href="{% url 'stock-item-detail' allocation.stock_item.id %}">{% trans "Stock Item" %}: {{ allocation.stock_item }}</a></td>
<td>{% decimal allocation.quantity %}</td>
</tr>
{% endfor %}
{% for allocation in part.sales_order_allocations %}
<tr>
<td><a href="{% url 'so-detail' allocation.line.order.id %}">{% trans "Sales Order" %}: {{ allocation.line.order }}</a></td>
<td><a href="{% url 'stock-item-detail' allocation.item.id %}">{% trans "Stock Item" %}: {{ allocation.item }}</a></td>
<td>{% decimal allocation.quantity %}</td>
</tr>
{% endfor %}
</table>
<table class='table table-striped table-condensed' id='build-order-table'></table>
{% endblock %}
{% block pre_content_panel %}
<div class='panel panel-default panel-inventree'>
<div class='panel-heading'>
<h4>{% trans "Sales Order Allocations" %}</h4>
</div>
<div class='panel-content'>
<table class='table table-striped table-condensed' id='sales-order-table'></table>
</div>
</div>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$("#build-table").inventreeTable({
columns: [
{
title: '{% trans "Order" %}',
sortable: true,
},
{
title: '{% trans "Stock Item" %}',
sortable: true,
},
{
title: '{% trans "Quantity" %}',
sortable: true,
}
]
loadSalesOrderAllocationTable("#sales-order-table", {
params: {
part: {{ part.id }},
}
});
loadBuildOrderAllocationTable("#build-order-table", {
params: {
part: {{ part.id }},
}
});
{% endblock %}

View File

@ -195,8 +195,13 @@
</div>
{% block pre_content_panel %}
{% endblock %}
<div class='panel panel-default panel-inventree'>
<div class='panel-heading'>
<h4>
{% block heading %}
@ -210,7 +215,11 @@
<!-- Specific part details go here... -->
{% endblock %}
</div>
</div>
{% block post_content_panel %}
{% endblock %}
{% endblock %}

View File

@ -155,6 +155,88 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
}
function loadBuildOrderAllocationTable(table, options={}) {
/**
* Load a table showing all the BuildOrder allocations for a given part
*/
options.params['part_detail'] = true;
options.params['build_detail'] = true;
options.params['location_detail'] = true;
var filters = loadTableFilters("buildorderallocation");
for (var key in options.params) {
filters[key] = options.params[key];
}
setupFilterList("buildorderallocation", $(table));
$(table).inventreeTable({
url: '{% url "api-build-item-list" %}',
queryParams: filters,
name: 'buildorderallocation',
groupBy: false,
search: false,
paginationVAlign: 'bottom',
original: options.params,
formatNoMatches: function() {
return '{% trans "No build order allocations found" %}'
},
columns: [
{
field: 'pk',
visible: false,
switchable: false,
},
{
field: 'build',
switchable: false,
title: '{% trans "Build Order" %}',
formatter: function(value, row) {
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
var ref = `${prefix}${row.build_detail.reference}`;
return renderLink(ref, `/build/${row.build}/`);
}
},
{
field: 'item',
title: '{% trans "Stock Item" %}',
formatter: function(value, row) {
// Render a link to the particular stock item
var link = `/stock/item/${row.stock_item}/`;
var text = `{% trans "Stock Item" %} ${row.stock_item}`;
return renderLink(text, link);
}
},
{
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);
}
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
}
]
});
}
function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
/*
* Load the "allocation table" for a particular build output.
@ -347,6 +429,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var params = {
build: buildId,
part_detail: true,
location_detail: true,
}
if (output) {
@ -466,8 +550,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
title: '{% trans "Part" %}',
formatter: function(value, row) {
var html = imageHoverIcon(row.part_thumb);
html += renderLink(row.part_name, `/part/${value}/`);
var html = imageHoverIcon(row.part_detail.thumbnail);
html += renderLink(row.part_detail.full_name, `/part/${value}/`);
return html;
}
},

View File

@ -310,3 +310,88 @@ function loadSalesOrderTable(table, options) {
],
});
}
function loadSalesOrderAllocationTable(table, options={}) {
/**
* Load a table with SalesOrderAllocation items
*/
options.params = options.params || {};
options.params['location_detail'] = true;
options.params['part_detail'] = true;
options.params['item_detail'] = true;
options.params['order_detail'] = true;
var filters = loadTableFilters("salesorderallocation");
for (var key in options.params) {
filters[key] = options.params[key];
}
setupFilterList("salesorderallocation", $(table));
$(table).inventreeTable({
url: '{% url "api-so-allocation-list" %}',
queryParams: filters,
name: 'salesorderallocation',
groupBy: false,
search: false,
paginationVAlign: 'bottom',
original: options.params,
formatNoMatches: function() { return '{% trans "No sales order allocations found" %}'; },
columns: [
{
field: 'pk',
visible: false,
switchable: false,
},
{
field: 'order',
switchable: false,
title: '{% trans "Order" %}',
switchable: false,
formatter: function(value, row) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
var ref = `${prefix}${row.order_detail.reference}`;
return renderLink(ref, `/order/sales-order/${row.order}/`);
}
},
{
field: 'item',
title: '{% trans "Stock Item" %}',
formatter: function(value, row) {
// Render a link to the particular stock item
var link = `/stock/item/${row.item}/`;
var text = `{% trans "Stock Item" %} ${row.item}`;
return renderLink(text, link);
}
},
{
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);
}
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
}
]
});
}

View File

@ -135,7 +135,7 @@ $.fn.inventreeTable = function(options) {
// Pagingation options (can be server-side or client-side as specified by the caller)
options.pagination = true;
options.paginationVAlign = 'both';
options.paginationVAlign = options.paginationVAlign || 'both';
options.pageSize = inventreeLoad(varName, 25);
options.pageList = [25, 50, 100, 250, 'all'];
options.totalField = 'count';

View File

@ -129,6 +129,14 @@ def wait(c):
manage(c, "wait_for_db")
@task
def rebuild(c):
"""
Rebuild database models with MPTT structures
"""
manage(c, "rebuild_models")
@task
def migrate(c):
"""
@ -311,7 +319,7 @@ def export_records(c, filename='data.json'):
print("Data export completed")
@task(help={'filename': 'Input filename'})
@task(help={'filename': 'Input filename'}, post=[rebuild])
def import_records(c, filename='data.json'):
"""
Import database records from a file
@ -354,7 +362,7 @@ def import_records(c, filename='data.json'):
print("Data import completed")
@task
@task(post=[rebuild])
def import_fixtures(c):
"""
Import fixture data into the database.