mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Display "Fulfilled" items
- Once a salesorder has been marked as "shipped" then the table is displayed differently - The sub rows show stock items which have been fulfilled against the sales order
This commit is contained in:
parent
9b882f4d17
commit
5e309a62f7
@ -201,8 +201,9 @@ InvenTree | Allocate Parts
|
|||||||
formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; },
|
formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; },
|
||||||
onLoadSuccess: function(tableData) {
|
onLoadSuccess: function(tableData) {
|
||||||
// Once the BOM data are loaded, request allocation data for the build
|
// Once the BOM data are loaded, request allocation data for the build
|
||||||
inventreeGet('/api/build/item/', {
|
inventreeGet('/api/build/item/',
|
||||||
build: {{ build.id }},
|
{
|
||||||
|
build: {{ build.id }},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
|
@ -325,7 +325,7 @@ class SalesOrder(Order):
|
|||||||
allocation.complete_allocation(user)
|
allocation.complete_allocation(user)
|
||||||
|
|
||||||
# Remove the allocation from the database once it has been 'fulfilled'
|
# Remove the allocation from the database once it has been 'fulfilled'
|
||||||
if allocation.item.sales_order == self.order:
|
if allocation.item.sales_order == self:
|
||||||
allocation.delete()
|
allocation.delete()
|
||||||
else:
|
else:
|
||||||
raise ValidationError("Could not complete order - allocation item not fulfilled")
|
raise ValidationError("Could not complete order - allocation item not fulfilled")
|
||||||
|
@ -10,7 +10,7 @@ InvenTree | {% trans "Sales Order" %}
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pre_content %}
|
{% block pre_content %}
|
||||||
{% if not order.is_fully_allocated %}
|
{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
|
||||||
<div class='alert alert-block alert-danger'>
|
<div class='alert alert-block alert-danger'>
|
||||||
{% trans "This SalesOrder has not been fully allocated" %}
|
{% trans "This SalesOrder has not been fully allocated" %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,6 +41,116 @@ $("#new-so-line").click(function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
|
function showAllocationSubTable(index, row, element) {
|
||||||
|
// Construct a table showing stock items which have been allocated against this line item
|
||||||
|
|
||||||
|
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`;
|
||||||
|
|
||||||
|
element.html(html);
|
||||||
|
|
||||||
|
var lineItem = row;
|
||||||
|
|
||||||
|
var table = $(`#allocation-table-${row.pk}`);
|
||||||
|
|
||||||
|
table.bootstrapTable({
|
||||||
|
data: row.allocations,
|
||||||
|
showHeader: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
width: '50%',
|
||||||
|
field: 'allocated',
|
||||||
|
title: 'Quantity',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
return renderLink(value, `/stock/item/${row.item}/`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'location_id',
|
||||||
|
title: 'Location',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
return renderLink(row.location_path, `/stock/location/${row.location_id}/`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'buttons',
|
||||||
|
title: 'Actions',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var html = "<div class='btn-group float-right' role='group'>";
|
||||||
|
var pk = row.pk;
|
||||||
|
|
||||||
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
|
html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
||||||
|
html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
table.find(".button-allocation-edit").click(function() {
|
||||||
|
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, {
|
||||||
|
success: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
table.find(".button-allocation-delete").click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, {
|
||||||
|
success: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
function showFulfilledSubTable(index, row, element) {
|
||||||
|
// Construct a table showing stock items which have been fulfilled against this line item
|
||||||
|
|
||||||
|
var id = `fulfilled-table-${row.pk}`;
|
||||||
|
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='${id}'></table></div>`;
|
||||||
|
|
||||||
|
element.html(html);
|
||||||
|
|
||||||
|
var lineItem = row;
|
||||||
|
|
||||||
|
$(`#${id}`).bootstrapTable({
|
||||||
|
url: "{% url 'api-stock-list' %}",
|
||||||
|
queryParams: {
|
||||||
|
part: row.part,
|
||||||
|
sales_order: {{ order.id }},
|
||||||
|
},
|
||||||
|
showHeader: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stock',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var text = '';
|
||||||
|
if (row.serial) {
|
||||||
|
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||||
|
} else {
|
||||||
|
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLink(text, `/stock/item/${row.pk}/`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$("#so-lines-table").inventreeTable({
|
$("#so-lines-table").inventreeTable({
|
||||||
formatNoMatches: function() { return "No matching line items"; },
|
formatNoMatches: function() { return "No matching line items"; },
|
||||||
queryParams: {
|
queryParams: {
|
||||||
@ -51,78 +161,22 @@ $("#so-lines-table").inventreeTable({
|
|||||||
uniqueId: 'pk',
|
uniqueId: 'pk',
|
||||||
url: "{% url 'api-so-line-list' %}",
|
url: "{% url 'api-so-line-list' %}",
|
||||||
onPostBody: setupCallbacks,
|
onPostBody: setupCallbacks,
|
||||||
|
{% if order.status == SalesOrderStatus.PENDING or order.status == SalesOrderStatus.SHIPPED %}
|
||||||
detailViewByClick: true,
|
detailViewByClick: true,
|
||||||
detailView: true,
|
detailView: true,
|
||||||
detailFilter: function(index, row) {
|
detailFilter: function(index, row) {
|
||||||
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
return row.allocated > 0;
|
return row.allocated > 0;
|
||||||
|
{% else %}
|
||||||
|
return row.fulfilled > 0;
|
||||||
|
{% endif %}
|
||||||
},
|
},
|
||||||
detailFormatter: function(index, row, element) {
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
|
detailFormatter: showAllocationSubTable,
|
||||||
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`;
|
{% else %}
|
||||||
|
detailFormatter: showFulfilledSubTable,
|
||||||
element.html(html);
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
var lineItem = row;
|
|
||||||
|
|
||||||
var table = $(`#allocation-table-${row.pk}`);
|
|
||||||
|
|
||||||
table.bootstrapTable({
|
|
||||||
data: row.allocations,
|
|
||||||
showHeader: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
width: '50%',
|
|
||||||
field: 'allocated',
|
|
||||||
title: 'Quantity',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return renderLink(value, `/stock/item/${row.item}/`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'location_id',
|
|
||||||
title: 'Location',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return renderLink(row.location_path, `/stock/location/${row.location_id}/`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'buttons',
|
|
||||||
title: 'Actions',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
|
|
||||||
var html = "<div class='btn-group float-right' role='group'>";
|
|
||||||
var pk = row.pk;
|
|
||||||
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
|
||||||
html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
html += "</div>";
|
|
||||||
|
|
||||||
return html;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-allocation-edit").click(function() {
|
|
||||||
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, {
|
|
||||||
success: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-allocation-delete").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, {
|
|
||||||
success: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
field: 'pk',
|
||||||
@ -154,20 +208,36 @@ $("#so-lines-table").inventreeTable({
|
|||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'allocated',
|
field: 'allocated',
|
||||||
title: 'Allocated',
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
|
title: '{% trans "Allocated" %}',
|
||||||
|
{% else %}
|
||||||
|
title: '{% trans "Fulfilled" %}',
|
||||||
|
{% endif %}
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return makeProgressBar(row.allocated, row.quantity, {
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
|
var quantity = row.allocated;
|
||||||
|
{% else %}
|
||||||
|
var quantity = row.fulfilled;
|
||||||
|
{% endif %}
|
||||||
|
return makeProgressBar(quantity, row.quantity, {
|
||||||
id: `order-line-progress-${row.pk}`,
|
id: `order-line-progress-${row.pk}`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sorter: function(valA, valB, rowA, rowB) {
|
sorter: function(valA, valB, rowA, rowB) {
|
||||||
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
|
var A = rowA.allocated;
|
||||||
|
var B = rowB.allocated;
|
||||||
|
{% else %}
|
||||||
|
var A = rowA.fulfilled;
|
||||||
|
var B = rowB.fulfilled;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
if (rowA.allocated == 0 && rowB.allocated == 0) {
|
if (A == 0 && B == 0) {
|
||||||
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressA = parseFloat(rowA.allocated) / rowA.quantity;
|
var progressA = parseFloat(A) / rowA.quantity;
|
||||||
var progressB = parseFloat(rowB.allocated) / rowB.quantity;
|
var progressB = parseFloat(B) / rowB.quantity;
|
||||||
|
|
||||||
return (progressA < progressB) ? 1 : -1;
|
return (progressA < progressB) ? 1 : -1;
|
||||||
}
|
}
|
||||||
@ -176,6 +246,7 @@ $("#so-lines-table").inventreeTable({
|
|||||||
field: 'notes',
|
field: 'notes',
|
||||||
title: 'Notes',
|
title: 'Notes',
|
||||||
},
|
},
|
||||||
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
{
|
{
|
||||||
field: 'buttons',
|
field: 'buttons',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
@ -184,7 +255,6 @@ $("#so-lines-table").inventreeTable({
|
|||||||
|
|
||||||
var pk = row.pk;
|
var pk = row.pk;
|
||||||
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
if (row.part) {
|
if (row.part) {
|
||||||
var part = row.part_detail;
|
var part = row.part_detail;
|
||||||
|
|
||||||
@ -202,13 +272,12 @@ $("#so-lines-table").inventreeTable({
|
|||||||
html += makeIconButton('fa-edit', 'button-edit', pk, '{% trans "Edit line item" %}');
|
html += makeIconButton('fa-edit', 'button-edit', pk, '{% trans "Edit line item" %}');
|
||||||
html += makeIconButton('fa-trash-alt', 'button-delete', pk, '{% trans "Delete line item " %}');
|
html += makeIconButton('fa-trash-alt', 'button-delete', pk, '{% trans "Delete line item " %}');
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{% endif %}
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -541,6 +541,10 @@ class StockItem(MPTTModel):
|
|||||||
if self.build_order is not None:
|
if self.build_order is not None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Not 'in stock' if the status code makes it unavailable
|
||||||
|
if self.status in StockStatus.UNAVAILABLE_CODES:
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -50,17 +50,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
{% block page_data %}
|
{% block page_data %}
|
||||||
<h3>
|
<h3>
|
||||||
{% trans "Stock Item" %}
|
{% trans "Stock Item" %}
|
||||||
{% if item.sales_order %}
|
|
||||||
<a href="{% url 'so-detail' item.sales_order.id %}">
|
|
||||||
<div class='label label-large label-large-blue'>{% trans "Sold" $}</div>
|
|
||||||
</a>
|
|
||||||
{% elif item.build_order %}
|
|
||||||
<a href="{% url 'build-detail' item.build_order.id %}">
|
|
||||||
<div class='label label-large label-large-blue'>{% trans "Used in Build" %}</div>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
{% stock_status_label item.status large=True %}
|
{% stock_status_label item.status large=True %}
|
||||||
{% endif %}
|
|
||||||
</h3>
|
</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<h4>
|
<h4>
|
||||||
|
Loading…
Reference in New Issue
Block a user