Merge pull request #2043 from SchrodingersGat/received-stock-items

Fixes for purchase order stock table
This commit is contained in:
Oliver 2021-09-08 13:29:05 +10:00 committed by GitHub
commit 829dd0d637
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 284 additions and 246 deletions

View File

@ -38,7 +38,7 @@
<h4>{% trans "Received Items" %}</h4>
</div>
<div class='panel-content'>
{% include "stock_table.html" with read_only=True %}
{% include "stock_table.html" with prevent_new_stock=True %}
</div>
</div>
@ -207,251 +207,24 @@ $('#new-po-line').click(function() {
{% endif %}
function reloadTable() {
$("#po-line-table").bootstrapTable("refresh");
}
function setupCallbacks() {
// Setup callbacks for the line buttons
var table = $("#po-line-table");
{% if order.status == PurchaseOrderStatus.PENDING %}
table.find(".button-line-edit").click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/po-line/${pk}/`, {
fields: {
part: {
filters: {
part_detail: true,
supplier_detail: true,
loadPurchaseOrderLineItemTable('#po-line-table', {
order: {{ order.pk }},
supplier: {{ order.supplier.pk }},
}
},
quantity: {},
reference: {},
purchase_price: {},
purchase_price_currency: {},
destination: {},
notes: {},
},
title: '{% trans "Edit Line Item" %}',
onSuccess: reloadTable,
});
});
table.find(".button-line-delete").click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/po-line/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Line Item" %}',
onSuccess: reloadTable,
});
});
{% if order.status == PurchaseOrderStatus.PENDING %}
allow_edit: true,
{% else %}
allow_edit: false,
{% endif %}
table.find(".button-line-receive").click(function() {
var pk = $(this).attr('pk');
launchModalForm("{% url 'po-receive' order.id %}", {
success: reloadTable,
data: {
line: pk,
},
secondary: [
{
field: 'location',
label: '{% trans "New Location" %}',
title: '{% trans "Create new stock location" %}',
url: "{% url 'stock-location-create' %}",
},
]
});
});
}
$("#po-line-table").inventreeTable({
onPostBody: setupCallbacks,
name: 'purchaseorderlines',
sidePagination: 'server',
formatNoMatches: function() { return "{% trans 'No line items found' %}"; },
queryParams: {
order: {{ order.id }},
part_detail: true,
},
url: "{% url 'api-po-line-list' %}",
showFooter: true,
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'part',
sortable: true,
sortName: 'part_name',
title: '{% trans "Part" %}',
switchable: false,
formatter: function(value, row, index, field) {
if (row.part) {
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
} else {
return '-';
}
},
footerFormatter: function() {
return '{% trans "Total" %}'
}
},
{
field: 'part_detail.description',
title: '{% trans "Description" %}',
},
{
sortable: true,
sortName: 'SKU',
field: 'supplier_part_detail.SKU',
title: '{% trans "SKU" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, `/supplier-part/${row.part}/`);
} else {
return '-';
}
},
},
{
sortable: true,
sortName: 'MPN',
field: 'supplier_part_detail.manufacturer_part_detail.MPN',
title: '{% trans "MPN" %}',
formatter: function(value, row, index, field) {
if (row.supplier_part_detail && row.supplier_part_detail.manufacturer_part) {
return renderLink(value, `/manufacturer-part/${row.supplier_part_detail.manufacturer_part}/`);
} else {
return "-";
}
},
},
{
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
},
{
sortable: true,
field: 'quantity',
title: '{% trans "Quantity" %}',
footerFormatter: function(data) {
return data.map(function (row) {
return +row['quantity']
}).reduce(function (sum, i) {
return sum + i
}, 0)
}
},
{
sortable: true,
field: 'purchase_price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
return row.purchase_price_string || row.purchase_price;
}
},
{
field: 'total_price',
sortable: true,
field: 'total_price',
title: '{% trans "Total price" %}',
formatter: function(value, row) {
var total = row.purchase_price * row.quantity;
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency});
return formatter.format(total)
},
footerFormatter: function(data) {
var total = data.map(function (row) {
return +row['purchase_price']*row['quantity']
}).reduce(function (sum, i) {
return sum + i
}, 0)
var currency = (data.slice(-1)[0] && data.slice(-1)[0].purchase_price_currency) || 'USD';
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: currency});
return formatter.format(total)
}
},
{
sortable: false,
field: 'received',
switchable: false,
title: '{% trans "Received" %}',
formatter: function(value, row, index, field) {
return makeProgressBar(row.received, row.quantity, {
id: `order-line-progress-${row.pk}`,
});
},
sorter: function(valA, valB, rowA, rowB) {
if (rowA.received == 0 && rowB.received == 0) {
return (rowA.quantity > rowB.quantity) ? 1 : -1;
}
var progressA = parseFloat(rowA.received) / rowA.quantity;
var progressB = parseFloat(rowB.received) / rowB.quantity;
return (progressA < progressB) ? 1 : -1;
}
},
{
field: 'destination',
title: '{% trans "Destination" %}',
formatter: function(value, row) {
if (value) {
return renderLink(row.destination_detail.pathstring, `/stock/location/${value}/`);
} else {
return '-';
}
}
},
{
field: 'notes',
title: '{% trans "Notes" %}',
},
{
switchable: false,
field: 'buttons',
title: '',
formatter: function(value, row, index, field) {
var html = `<div class='btn-group'>`;
var pk = row.pk;
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.delete %}
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
{% endif %}
{% if order.status == PurchaseOrderStatus.PLACED and roles.purchase_order.change %}
if (row.received < row.quantity) {
html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}');
}
allow_receive: true,
{% else %}
allow_receive: false,
{% endif %}
html += `</div>`;
return html;
},
}
]
});
attachNavCallbacks({
attachNavCallbacks({
name: 'purchase-order',
default: 'order-items'
});
});
{% endblock %}

View File

@ -20,6 +20,7 @@
/* exported
createSalesOrder,
editPurchaseOrderLineItem,
loadPurchaseOrderLineItemTable,
loadPurchaseOrderTable,
loadSalesOrderAllocationTable,
loadSalesOrderTable,
@ -144,7 +145,6 @@ function newSupplierPartFromOrderWizard(e) {
if (!part) {
part = $(src).closest('button').attr('part');
console.log('parent: ' + part);
}
createSupplierPart({
@ -367,6 +367,271 @@ function loadPurchaseOrderTable(table, options) {
});
}
/**
* Load a table displaying line items for a particular PurchasesOrder
* @param {String} table - HTML ID tag e.g. '#table'
* @param {Object} options - options which must provide:
* - order (integer PK)
* - supplier (integer PK)
* - allow_edit (boolean)
* - allow_receive (boolean)
*/
function loadPurchaseOrderLineItemTable(table, options={}) {
function setupCallbacks() {
if (options.allow_edit) {
$(table).find('.button-line-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/po-line/${pk}/`, {
fields: {
part: {
filters: {
part_detail: true,
supplier_detail: true,
supplier: options.supplier,
}
},
quantity: {},
reference: {},
purchase_price: {},
purchase_price_currency: {},
destination: {},
notes: {},
},
title: '{% trans "Edit Line Item" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
$(table).find('.button-line-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/po-line/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Line Item" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
}
if (options.allow_receive) {
$(table).find('.button-line-receive').click(function() {
var pk = $(this).attr('pk');
launchModalForm(`/order/purchase-order/${options.order}/receive/`, {
success: function() {
$(table).bootstrapTable('refresh');
},
data: {
line: pk,
},
secondary: [
{
field: 'location',
label: '{% trans "New Location" %}',
title: '{% trans "Create new stock location" %}',
url: '{% url "stock-location-create" %}',
},
]
});
});
}
}
$(table).inventreeTable({
onPostBody: setupCallbacks,
name: 'purchaseorderlines',
sidePagination: 'server',
formatNoMatches: function() {
return '{% trans "No line items found" %}';
},
queryParams: {
order: options.order,
part_detail: true
},
url: '{% url "api-po-line-list" %}',
showFooter: true,
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'part',
sortable: true,
sortName: 'part_name',
title: '{% trans "Part" %}',
switchable: false,
formatter: function(value, row, index, field) {
if (row.part) {
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
} else {
return '-';
}
},
footerFormatter: function() {
return '{% trans "Total" %}';
}
},
{
field: 'part_detail.description',
title: '{% trans "Description" %}',
},
{
sortable: true,
sortName: 'SKU',
field: 'supplier_part_detail.SKU',
title: '{% trans "SKU" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, `/supplier-part/${row.part}/`);
} else {
return '-';
}
},
},
{
sortable: true,
sortName: 'MPN',
field: 'supplier_part_detail.manufacturer_part_detail.MPN',
title: '{% trans "MPN" %}',
formatter: function(value, row, index, field) {
if (row.supplier_part_detail && row.supplier_part_detail.manufacturer_part) {
return renderLink(value, `/manufacturer-part/${row.supplier_part_detail.manufacturer_part}/`);
} else {
return '-';
}
},
},
{
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
},
{
sortable: true,
field: 'quantity',
title: '{% trans "Quantity" %}',
footerFormatter: function(data) {
return data.map(function(row) {
return +row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
}
},
{
sortable: true,
field: 'purchase_price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
return row.purchase_price_string || row.purchase_price;
}
},
{
field: 'total_price',
sortable: true,
field: 'total_price',
title: '{% trans "Total price" %}',
formatter: function(value, row) {
var total = row.purchase_price * row.quantity;
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency});
return formatter.format(total);
},
footerFormatter: function(data) {
var total = data.map(function(row) {
return +row['purchase_price']*row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
var currency = (data.slice(-1)[0] && data.slice(-1)[0].purchase_price_currency) || 'USD';
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: currency
}
);
return formatter.format(total);
}
},
{
sortable: false,
field: 'received',
switchable: false,
title: '{% trans "Received" %}',
formatter: function(value, row, index, field) {
return makeProgressBar(row.received, row.quantity, {
id: `order-line-progress-${row.pk}`,
});
},
sorter: function(valA, valB, rowA, rowB) {
if (rowA.received == 0 && rowB.received == 0) {
return (rowA.quantity > rowB.quantity) ? 1 : -1;
}
var progressA = parseFloat(rowA.received) / rowA.quantity;
var progressB = parseFloat(rowB.received) / rowB.quantity;
return (progressA < progressB) ? 1 : -1;
}
},
{
field: 'destination',
title: '{% trans "Destination" %}',
formatter: function(value, row) {
if (value) {
return renderLink(row.destination_detail.pathstring, `/stock/location/${value}/`);
} else {
return '-';
}
}
},
{
field: 'notes',
title: '{% trans "Notes" %}',
},
{
switchable: false,
field: 'buttons',
title: '',
formatter: function(value, row, index, field) {
var html = `<div class='btn-group'>`;
var pk = row.pk;
if (options.allow_edit) {
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
}
if (options.allow_receive && row.received < row.quantity) {
html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}');
}
html += `</div>`;
return html;
},
}
]
});
}
function loadSalesOrderTable(table, options) {
options.params = options.params || {};

View File

@ -16,7 +16,7 @@
</button>
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
{% if not read_only and roles.stock.add %}
{% if not read_only and not prevent_new_stock and roles.stock.add %}
<button class="btn btn-success" id='item-create' title='{% trans "New Stock Item" %}'>
<span class='fas fa-plus-circle'></span>
</button>

View File

@ -2,7 +2,7 @@
Django==3.2.4 # Django package
gunicorn>=20.1.0 # Gunicorn web server
pillow==8.2.0 # Image manipulation
pillow==8.3.2 # Image manipulation
djangorestframework==3.12.4 # DRF framework
django-cors-headers==3.2.0 # CORS headers extension for DRF
django-filter==2.4.0 # Extended filtering options