Merge pull request #2751 from SchrodingersGat/sales-order-allocation-fixes

Sales order allocation fixes
This commit is contained in:
Oliver 2022-03-17 09:09:20 +11:00 committed by GitHub
commit 278887e11c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 128 additions and 236 deletions

View File

@ -886,9 +886,17 @@ class SOAllocationList(generics.ListAPIView):
outstanding = str2bool(outstanding)
if outstanding:
queryset = queryset.filter(line__order__status__in=SalesOrderStatus.OPEN)
# Filter only "open" orders
# Filter only allocations which have *not* shipped
queryset = queryset.filter(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None,
)
else:
queryset = queryset.exclude(line__order__status__in=SalesOrderStatus.OPEN)
queryset = queryset.exclude(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None
)
return queryset
@ -1028,7 +1036,7 @@ order_api_urls = [
url(r'^.*$', POLineItemList.as_view(), name='api-po-line-list'),
])),
# API endpoints for sales ordesr
# API endpoints for sales orders
url(r'^so/', include([
url(r'attachment/', include([
url(r'^(?P<pk>\d+)/$', SOAttachmentDetail.as_view(), name='api-so-attachment-detail'),

View File

@ -417,7 +417,7 @@ class Part(MPTTModel):
context['allocated_build_order_quantity'] = self.build_order_allocation_count()
context['required_sales_order_quantity'] = self.required_sales_order_quantity()
context['allocated_sales_order_quantity'] = self.sales_order_allocation_count()
context['allocated_sales_order_quantity'] = self.sales_order_allocation_count(pending=True)
context['available'] = self.available_stock
context['on_order'] = self.on_order
@ -1118,7 +1118,9 @@ class Part(MPTTModel):
quantity = 0
for line in open_lines:
quantity += line.quantity
# Determine the quantity "remaining" to be shipped out
remaining = max(line.quantity - line.shipped, 0)
quantity += remaining
return quantity
@ -1336,19 +1338,36 @@ class Part(MPTTModel):
return query['total']
def sales_order_allocations(self):
def sales_order_allocations(self, **kwargs):
"""
Return all sales-order-allocation objects which allocate this part to a SalesOrder
"""
return OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
queryset = OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
def sales_order_allocation_count(self):
pending = kwargs.get('pending', None)
if pending is True:
# Look only for 'open' orders which have not shipped
queryset = queryset.filter(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None,
)
elif pending is False:
# Look only for 'closed' orders or orders which have shipped
queryset = queryset.exclude(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None,
)
return queryset
def sales_order_allocation_count(self, **kwargs):
"""
Return the tutal quantity of this part allocated to sales orders
Return the total quantity of this part allocated to sales orders
"""
query = self.sales_order_allocations().aggregate(
query = self.sales_order_allocations(**kwargs).aggregate(
total=Coalesce(
Sum(
'quantity',

View File

@ -47,23 +47,6 @@
</div>
{% endif %}
<div class='panel panel-hidden' id='panel-allocations'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Part Stock Allocations" %}</h4>
{% include "spacer.html" %}
</div>
</div>
<div class='panel-content'>
<div id='allocations-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="allocations" %}
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#allocations-button-toolbar' id='part-allocation-table'></table>
</div>
</div>
<div class='panel panel-hidden' id='panel-test-templates'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
@ -326,6 +309,7 @@
</div>
<div class='panel panel-hidden' id='panel-build-orders'>
{% if part.assembly %}
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Part Builds" %}</h4>
@ -351,7 +335,9 @@
<table class='table table-striped table-condensed' data-toolbar='#build-button-toolbar' id='build-table'>
</table>
</div>
{% endif %}
{% if part.component %}
<div class='panel-heading'>
<h4>{% trans "Build Order Allocations" %}</h4>
</div>
@ -363,6 +349,7 @@
</div>
<table class='table table-striped table-condensed' id='build-order-allocation-table' data-toolbar='#build-allocation-button-toolbar'></table>
</div>
{% endif %}
</div>
<div class='panel panel-hidden' id='panel-suppliers'>
@ -531,6 +518,7 @@
// Load the "builds" tab
onPanelLoad("build-orders", function() {
{% if part.assembly %}
$("#start-build").click(function() {
newBuildOrder({
part: {{ part.pk }},
@ -543,12 +531,15 @@
part: {{ part.id }},
}
});
{% endif %}
{% if part.component %}
loadBuildOrderAllocationTable("#build-order-allocation-table", {
params: {
part: {{ part.id }},
}
});
{% endif %}
});
@ -675,19 +666,6 @@
{% endif %}
});
// Load the "allocations" tab
onPanelLoad('allocations', function() {
loadStockAllocationTable(
$("#part-allocation-table"),
{
params: {
part: {{ part.pk }},
},
}
);
});
// Load the "related parts" tab
onPanelLoad("related-parts", function() {

View File

@ -204,44 +204,60 @@
<td>{% decimal on_order %}</td>
</tr>
{% endif %}
{% if part.component %}
{% if required_build_order_quantity > 0 %}
<tr>
<td><span class='fas fa-clipboard-list'></span></td>
<td>{% trans "Required for Build Orders" %}</td>
<td>{% decimal required_build_order_quantity %}
<td>{% decimal required_build_order_quantity %}</td>
</tr>
<tr>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Allocated to Build Orders" %}</td>
<td>
{% decimal allocated_build_order_quantity %}
{% if allocated_build_order_quantity < required_build_order_quantity %}
<span class='fas fa-times-circle icon-red float-right' title='{% trans "Required quantity has not been allocated" %}'></span>
{% else %}
<span class='fas fa-check-circle icon-green float-right' title='{% trans "Required quantity has been allocated" %}'></span>
{% endif %}
</td>
</tr>
{% endif %}
{% endif %}
{% if part.salable %}
{% if required_sales_order_quantity > 0 %}
<tr>
<td><span class='fas fa-clipboard-list'></span></td>
<td>{% trans "Required for Sales Orders" %}</td>
<td>{% decimal required_sales_order_quantity %}
</tr>
{% endif %}
{% if allocated > 0 %}
<tr>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Allocated to Orders" %}</td>
<td>{% decimal allocated %}</td>
</tr>
{% endif %}
{% if not part.is_template %}
{% if part.assembly %}
<tr>
<td><h5><span class='fas fa-tools'></span></h5></td>
<td colspan='2'>
<h5>{% trans "Build Status" %}</h5>
<td>
{% decimal required_sales_order_quantity %}
</td>
</tr>
<tr>
<td></td>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Allocated to Sales Orders" %}</td>
<td>
{% decimal allocated_sales_order_quantity %}
{% if allocated_sales_order_quantity < required_sales_order_quantity %}
<span class='fas fa-times-circle icon-red float-right' title='{% trans "Required quantity has not been allocated" %}'></span>
{% else %}
<span class='fas fa-check-circle icon-green float-right' title='{% trans "Required quantity has been allocated" %}'></span>
{% endif %}
</td>
</tr>
{% endif %}
{% endif %}
{% if not part.is_template %}
{% if part.assembly %}
<tr>
<td><span class='fas fa-tools'></span></td>
<td>{% trans "Can Build" %}</td>
<td>{% decimal part.can_build %}</td>
</tr>
{% if quantity_being_built > 0 %}
<tr>
<td></td>
<td><span class='fas fa-tools'></span></td>
<td>{% trans "Building" %}</td>
<td>{% decimal quantity_being_built %}</td>
</tr>

View File

@ -17,7 +17,9 @@
{% if part.assembly %}
{% trans "Bill of Materials" as text %}
{% include "sidebar_item.html" with label="bom" text=text icon="fa-list" %}
{% endif %}
{% if roles.build.view %}
{% if part.assembly or part.component %}
{% trans "Build Orders" as text %}
{% include "sidebar_item.html" with label="build-orders" text=text icon="fa-tools" %}
{% endif %}
@ -30,10 +32,6 @@
{% trans "Pricing" as text %}
{% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %}
{% endif %}
{% if part.salable or part.component %}
{% trans "Allocations" as text %}
{% include "sidebar_item.html" with label="allocations" text=text icon="fa-bookmark" %}
{% endif %}
{% if part.purchaseable and roles.purchase_order.view %}
{% trans "Suppliers" as text %}
{% include "sidebar_item.html" with label="suppliers" text=text icon="fa-building" %}

View File

@ -37,19 +37,36 @@
</div>
<div class='panel panel-hidden' id='panel-allocations'>
{% if item.part.component %}
<div class='panel-heading'>
<h4>{% trans "Stock Item Allocations" %}</h4>
<h4>{% trans "Build Order Allocations" %}</h4>
{% include "spacer.html" %}
</div>
<div class='panel-content'>
<div id='allocations-button-toolbar'>
<div id='build-order-allocations-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="allocations" %}
{% include "filter_list.html" with id="buildorderallocation" %}
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#allocatoins-button-toolbar' id='stock-allocation-table'></table>
<table class='table table-striped table-condensed' data-toolbar='#build-order-allocation-toolbar' id='build-order-allocation-table'></table>
</div>
{% endif %}
{% if item.part.salable %}
<div class='panel-heading'>
<h4>{% trans "Sales Order Allocations" %}</h4>
{% include "spacer.html" %}
</div>
<div class='panel-content'>
<div id='sales-order-allocations-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="salesorderallocation" %}
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#sales-order-allocation-toolbar' id='sales-order-allocation-table'></table>
</div>
{% endif %}
</div>
<div class='panel panel-hidden' id='panel-children'>
@ -164,14 +181,21 @@
// Load the "allocations" tab
onPanelLoad('allocations', function() {
loadStockAllocationTable(
$("#stock-allocation-table"),
{
params: {
stock_item: {{ item.pk }},
},
{% if item.part.component %}
loadBuildOrderAllocationTable('#build-order-allocation-table', {
params: {
stock_item: {{ item.pk }},
}
);
});
{% endif %}
{% if item.part.salable %}
loadSalesOrderAllocationTable('#sales-order-allocation-table', {
params: {
stock_item: {{ item.pk }},
}
});
{% endif %}
});
$('#stock-item-install').click(function() {

View File

@ -2048,15 +2048,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
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);
return locationDetail(row.item_detail, true);
}
},
{

View File

@ -46,7 +46,6 @@
findStockItemBySerialNumber,
installStockItem,
loadInstalledInTable,
loadStockAllocationTable,
loadStockLocationTable,
loadStockTable,
loadStockTestResultsTable,
@ -2302,157 +2301,6 @@ function loadStockTable(table, options) {
}
/*
* Display a table of allocated stock, for either a part or stock item
* Allocations are displayed for:
*
* a) Sales Orders
* b) Build Orders
*/
function loadStockAllocationTable(table, options={}) {
var params = options.params || {};
params.build_detail = true;
var filterListElement = options.filterList || '#filter-list-allocations';
var filters = {};
var filterKey = options.filterKey || options.name || 'allocations';
var original = {};
for (var k in params) {
original[k] = params[k];
filters[k] = params[k];
}
setupFilterList(filterKey, table, filterListElement);
/*
* We have two separate API queries to make here:
* a) Build Order Allocations
* b) Sales Order Allocations
*
* We will let the call to inventreeTable take care of build orders,
* and then load sales orders after that.
*/
table.inventreeTable({
url: '{% url "api-build-item-list" %}',
name: 'allocations',
original: original,
method: 'get',
queryParams: filters,
sidePagination: 'client',
showColumns: false,
onLoadSuccess: function(tableData) {
var query_params = params;
query_params.customer_detail = true;
query_params.order_detail = true;
delete query_params.build_detail;
// Load sales order allocation data
inventreeGet('{% url "api-so-allocation-list" %}', query_params, {
success: function(data) {
// Update table to include sales order data
$(table).bootstrapTable('append', data);
}
});
},
columns: [
{
field: 'order',
title: '{% trans "Order" %}',
formatter: function(value, row) {
var html = '';
if (row.build) {
// Add an icon for the part being built
html += thumbnailImage(row.build_detail.part_detail.thumbnail, {
title: row.build_detail.part_detail.full_name
});
html += ' ';
html += renderLink(
global_settings.BUILDORDER_REFERENCE_PREFIX + row.build_detail.reference,
`/build/${row.build}/`
);
html += makeIconBadge('fa-tools', '{% trans "Build Order" %}');
} else if (row.order) {
// Add an icon for the customer
html += thumbnailImage(row.customer_detail.thumbnail || row.customer_detail.image, {
title: row.customer_detail.name,
});
html += ' ';
html += renderLink(
global_settings.SALESORDER_REFERENCE_PREFIX + row.order_detail.reference,
`/order/sales-order/${row.order}/`
);
html += makeIconBadge('fa-truck', '{% trans "Sales Order" %}');
} else {
return '-';
}
return html;
}
},
{
field: 'description',
title: '{% trans "Description" %}',
formatter: function(value, row) {
if (row.order_detail) {
return row.order_detail.description;
} else if (row.build_detail) {
return row.build_detail.title;
} else {
return '-';
}
}
},
{
field: 'status',
title: '{% trans "Order Status" %}',
formatter: function(value, row) {
if (row.build) {
return buildStatusDisplay(row.build_detail.status);
} else if (row.order) {
return salesOrderStatusDisplay(row.order_detail.status);
} else {
return '-';
}
}
},
{
field: 'quantity',
title: '{% trans "Allocated Quantity" %}',
formatter: function(value, row) {
var text = value;
var pk = row.stock_item || row.item;
if (pk) {
var url = `/stock/item/${pk}/`;
return renderLink(text, url);
} else {
return value;
}
}
},
]
});
}
/*
* Display a table of stock locations
*/

View File

@ -341,6 +341,15 @@ function getAvailableTableFilters(tableKey) {
};
}
if (tableKey == 'salesorderallocation') {
return {
outstanding: {
type: 'bool',
title: '{% trans "Outstanding" %}',
}
};
}
if (tableKey == 'salesorder') {
return {
status: {