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:
Oliver Walters 2020-04-27 10:31:38 +10:00
parent 9b882f4d17
commit 5e309a62f7
6 changed files with 153 additions and 89 deletions

View File

@ -201,7 +201,8 @@ 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 }},
}, },
{ {

View File

@ -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")

View File

@ -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>

View File

@ -41,22 +41,9 @@ $("#new-so-line").click(function() {
}); });
}); });
$("#so-lines-table").inventreeTable({ {% if order.status == SalesOrderStatus.PENDING %}
formatNoMatches: function() { return "No matching line items"; }, function showAllocationSubTable(index, row, element) {
queryParams: { // Construct a table showing stock items which have been allocated against this line item
order: {{ order.id }},
part_detail: true,
allocations: true,
},
uniqueId: 'pk',
url: "{% url 'api-so-line-list' %}",
onPostBody: setupCallbacks,
detailViewByClick: true,
detailView: true,
detailFilter: function(index, row) {
return row.allocated > 0;
},
detailFormatter: function(index, row, element) {
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`; var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`;
@ -122,7 +109,74 @@ $("#so-lines-table").inventreeTable({
success: reloadTable, 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({
formatNoMatches: function() { return "No matching line items"; },
queryParams: {
order: {{ order.id }},
part_detail: true,
allocations: true,
},
uniqueId: 'pk',
url: "{% url 'api-so-line-list' %}",
onPostBody: setupCallbacks,
{% if order.status == SalesOrderStatus.PENDING or order.status == SalesOrderStatus.SHIPPED %}
detailViewByClick: true,
detailView: true,
detailFilter: function(index, row) {
{% if order.status == SalesOrderStatus.PENDING %}
return row.allocated > 0;
{% else %}
return row.fulfilled > 0;
{% endif %}
},
{% if order.status == SalesOrderStatus.PENDING %}
detailFormatter: showAllocationSubTable,
{% else %}
detailFormatter: showFulfilledSubTable,
{% endif %}
{% endif %}
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 %}
], ],
}); });

View File

@ -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

View File

@ -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>