mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
Merge pull request #2413 from SchrodingersGat/part-po-refactor
Part PO table refactor
This commit is contained in:
commit
825b1473b2
@ -12,11 +12,15 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 19
|
INVENTREE_API_VERSION = 20
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v20 -> 2021-12-03
|
||||||
|
- Adds ability to filter POLineItem endpoint by "base_part"
|
||||||
|
- Adds optional "order_detail" to POLineItem list endpoint
|
||||||
|
|
||||||
v19 -> 2021-12-02
|
v19 -> 2021-12-02
|
||||||
- Adds the ability to filter the StockItem API by "part_tree"
|
- Adds the ability to filter the StockItem API by "part_tree"
|
||||||
- Returns only stock items which match a particular part.tree_id field
|
- Returns only stock items which match a particular part.tree_id field
|
||||||
|
@ -13,7 +13,6 @@ from rest_framework import generics
|
|||||||
from rest_framework import filters, status
|
from rest_framework import filters, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
@ -300,6 +299,7 @@ class POLineItemList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False))
|
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False))
|
||||||
|
kwargs['order_detail'] = str2bool(self.request.query_params.get('order_detail', False))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -307,6 +307,28 @@ class POLineItemList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
"""
|
||||||
|
Additional filtering options
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
base_part = params.get('base_part', None)
|
||||||
|
|
||||||
|
if base_part:
|
||||||
|
try:
|
||||||
|
base_part = Part.objects.get(pk=base_part)
|
||||||
|
|
||||||
|
queryset = queryset.filter(part__part=base_part)
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
rest_filters.DjangoFilterBackend,
|
rest_filters.DjangoFilterBackend,
|
||||||
filters.SearchFilter,
|
filters.SearchFilter,
|
||||||
|
@ -143,12 +143,17 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
|
|
||||||
|
order_detail = kwargs.pop('order_detail', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if part_detail is not True:
|
if part_detail is not True:
|
||||||
self.fields.pop('part_detail')
|
self.fields.pop('part_detail')
|
||||||
self.fields.pop('supplier_part_detail')
|
self.fields.pop('supplier_part_detail')
|
||||||
|
|
||||||
|
if order_detail is not True:
|
||||||
|
self.fields.pop('order_detail')
|
||||||
|
|
||||||
quantity = serializers.FloatField(default=1)
|
quantity = serializers.FloatField(default=1)
|
||||||
received = serializers.FloatField(default=0)
|
received = serializers.FloatField(default=0)
|
||||||
|
|
||||||
@ -170,6 +175,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
help_text=_('Purchase price currency'),
|
help_text=_('Purchase price currency'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
order_detail = POSerializer(source='order', read_only=True, many=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrderLineItem
|
model = PurchaseOrderLineItem
|
||||||
|
|
||||||
@ -179,6 +186,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
'reference',
|
'reference',
|
||||||
'notes',
|
'notes',
|
||||||
'order',
|
'order',
|
||||||
|
'order_detail',
|
||||||
'part',
|
'part',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
'supplier_part_detail',
|
'supplier_part_detail',
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<div id='po-button-bar'>
|
<div id='po-button-bar'>
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
{% include "filter_list.html" with id="purchaseorder" %}
|
{% include "filter_list.html" with id="partpurchaseorders" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -703,12 +703,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onPanelLoad("purchase-orders", function() {
|
onPanelLoad("purchase-orders", function() {
|
||||||
loadPurchaseOrderTable($("#purchase-order-table"), {
|
loadPartPurchaseOrderTable(
|
||||||
url: "{% url 'api-po-list' %}",
|
"#purchase-order-table",
|
||||||
params: {
|
{{ part.pk }},
|
||||||
part: {{ part.id }},
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onPanelLoad("sales-orders", function() {
|
onPanelLoad("sales-orders", function() {
|
||||||
|
@ -648,6 +648,13 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
|
|
||||||
var html = renderLink(value, `/order/purchase-order/${row.pk}/`);
|
var html = renderLink(value, `/order/purchase-order/${row.pk}/`);
|
||||||
|
|
||||||
|
html += purchaseOrderStatusDisplay(
|
||||||
|
row.status,
|
||||||
|
{
|
||||||
|
classes: 'float-right',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (row.overdue) {
|
if (row.overdue) {
|
||||||
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}');
|
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}');
|
||||||
}
|
}
|
||||||
@ -672,14 +679,6 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
field: 'description',
|
field: 'description',
|
||||||
title: '{% trans "Description" %}',
|
title: '{% trans "Description" %}',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
field: 'status',
|
|
||||||
title: '{% trans "Status" %}',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row) {
|
|
||||||
return purchaseOrderStatusDisplay(row.status, row.status_text);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'creation_date',
|
field: 'creation_date',
|
||||||
title: '{% trans "Date" %}',
|
title: '{% trans "Date" %}',
|
||||||
@ -989,7 +988,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
field: 'buttons',
|
field: 'buttons',
|
||||||
title: '',
|
title: '',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
var html = `<div class='btn-group'>`;
|
var html = `<div class='btn-group' role='group'>`;
|
||||||
|
|
||||||
var pk = row.pk;
|
var pk = row.pk;
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
loadParametricPartTable,
|
loadParametricPartTable,
|
||||||
loadPartCategoryTable,
|
loadPartCategoryTable,
|
||||||
loadPartParameterTable,
|
loadPartParameterTable,
|
||||||
|
loadPartPurchaseOrderTable,
|
||||||
loadPartTable,
|
loadPartTable,
|
||||||
loadPartTestTemplateTable,
|
loadPartTestTemplateTable,
|
||||||
loadPartVariantTable,
|
loadPartVariantTable,
|
||||||
@ -712,6 +713,180 @@ function loadPartParameterTable(table, url, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a table showing a list of purchase orders for a given part.
|
||||||
|
*
|
||||||
|
* This requests API data from the PurchaseOrderLineItem endpoint
|
||||||
|
*/
|
||||||
|
function loadPartPurchaseOrderTable(table, part_id, options={}) {
|
||||||
|
|
||||||
|
options.params = options.params || {};
|
||||||
|
|
||||||
|
// Construct API filterset
|
||||||
|
options.params.base_part = part_id;
|
||||||
|
options.params.part_detail = true;
|
||||||
|
options.params.order_detail = true;
|
||||||
|
|
||||||
|
var filters = loadTableFilters('partpurchaseorders');
|
||||||
|
|
||||||
|
for (var key in options.params) {
|
||||||
|
filters[key] = options.params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFilterList('purchaseorderlineitem', $(table), '#filter-list-partpurchaseorders');
|
||||||
|
|
||||||
|
$(table).inventreeTable({
|
||||||
|
url: '{% url "api-po-line-list" %}',
|
||||||
|
queryParams: filters,
|
||||||
|
name: 'partpurchaseorders',
|
||||||
|
original: options.params,
|
||||||
|
showColumns: true,
|
||||||
|
uniqueId: 'pk',
|
||||||
|
formatNoMatches: function() {
|
||||||
|
return '{% trans "No purchase orders found" %}';
|
||||||
|
},
|
||||||
|
onPostBody: function() {
|
||||||
|
$(table).find('.button-line-receive').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
|
if (!line_item) {
|
||||||
|
console.log('WARNING: getRowByUniqueId returned null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
receivePurchaseOrderItems(
|
||||||
|
line_item.order,
|
||||||
|
[
|
||||||
|
line_item,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
success: function() {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'order',
|
||||||
|
title: '{% trans "Purchase Order" %}',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var order = row.order_detail;
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
var ref = global_settings.PURCHASEORDER_REFERENCE_PREFIX + order.reference;
|
||||||
|
|
||||||
|
var html = renderLink(ref, `/order/purchase-order/${order.pk}/`);
|
||||||
|
|
||||||
|
html += purchaseOrderStatusDisplay(
|
||||||
|
order.status,
|
||||||
|
{
|
||||||
|
classes: 'float-right',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'supplier',
|
||||||
|
title: '{% trans "Supplier" %}',
|
||||||
|
switchable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
|
||||||
|
if (row.supplier_part_detail && row.supplier_part_detail.supplier_detail) {
|
||||||
|
var supp = row.supplier_part_detail.supplier_detail;
|
||||||
|
var html = imageHoverIcon(supp.thumbnail || supp.image);
|
||||||
|
|
||||||
|
html += ' ' + renderLink(supp.name, `/company/${supp.pk}/`);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sku',
|
||||||
|
title: '{% trans "SKU" %}',
|
||||||
|
switchable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
if (row.supplier_part_detail) {
|
||||||
|
var supp = row.supplier_part_detail;
|
||||||
|
|
||||||
|
return renderLink(supp.SKU, `/supplier-part/${supp.pk}/`);
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'mpn',
|
||||||
|
title: '{% trans "MPN" %}',
|
||||||
|
switchable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
if (row.supplier_part_detail && row.supplier_part_detail.manufacturer_part_detail) {
|
||||||
|
var manu = row.supplier_part_detail.manufacturer_part_detail;
|
||||||
|
return renderLink(manu.MPN, `/manufacturer-part/${manu.pk}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'quantity',
|
||||||
|
title: '{% trans "Quantity" %}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'received',
|
||||||
|
title: '{% trans "Received" %}',
|
||||||
|
switchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'purchase_price',
|
||||||
|
title: '{% trans "Price" %}',
|
||||||
|
switchable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var formatter = new Intl.NumberFormat(
|
||||||
|
'en-US',
|
||||||
|
{
|
||||||
|
style: 'currency',
|
||||||
|
currency: row.purchase_price_currency,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return formatter.format(row.purchase_price);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
title: '',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
|
||||||
|
if (row.received >= row.quantity) {
|
||||||
|
// Already recevied
|
||||||
|
return `<span class='badge bg-success rounded-pill'>{% trans "Received" %}</span>`;
|
||||||
|
} else {
|
||||||
|
var html = `<div class='btn-group' role='group'>`;
|
||||||
|
var pk = row.pk;
|
||||||
|
|
||||||
|
html += makeIconButton('fa-sign-in-alt', 'button-line-receive', pk, '{% trans "Receive line item" %}');
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadRelatedPartsTable(table, part_id, options={}) {
|
function loadRelatedPartsTable(table, part_id, options={}) {
|
||||||
/*
|
/*
|
||||||
* Load table of "related" parts
|
* Load table of "related" parts
|
||||||
|
Loading…
x
Reference in New Issue
Block a user