mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 04:56:45 +00:00
Sales order allocation fixes (#2751)
This commit is contained in:
parent
2c8dbb8308
commit
faa2044904
@ -860,9 +860,17 @@ class SOAllocationList(generics.ListAPIView):
|
|||||||
outstanding = str2bool(outstanding)
|
outstanding = str2bool(outstanding)
|
||||||
|
|
||||||
if 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:
|
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
|
return queryset
|
||||||
|
|
||||||
@ -1002,7 +1010,7 @@ order_api_urls = [
|
|||||||
url(r'^.*$', POLineItemList.as_view(), name='api-po-line-list'),
|
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'^so/', include([
|
||||||
url(r'attachment/', include([
|
url(r'attachment/', include([
|
||||||
url(r'^(?P<pk>\d+)/$', SOAttachmentDetail.as_view(), name='api-so-attachment-detail'),
|
url(r'^(?P<pk>\d+)/$', SOAttachmentDetail.as_view(), name='api-so-attachment-detail'),
|
||||||
|
@ -417,7 +417,7 @@ class Part(MPTTModel):
|
|||||||
context['allocated_build_order_quantity'] = self.build_order_allocation_count()
|
context['allocated_build_order_quantity'] = self.build_order_allocation_count()
|
||||||
|
|
||||||
context['required_sales_order_quantity'] = self.required_sales_order_quantity()
|
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['available'] = self.available_stock
|
||||||
context['on_order'] = self.on_order
|
context['on_order'] = self.on_order
|
||||||
@ -1118,7 +1118,9 @@ class Part(MPTTModel):
|
|||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
for line in open_lines:
|
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
|
return quantity
|
||||||
|
|
||||||
@ -1336,19 +1338,36 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
return query['total']
|
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 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(
|
total=Coalesce(
|
||||||
Sum(
|
Sum(
|
||||||
'quantity',
|
'quantity',
|
||||||
|
@ -204,44 +204,60 @@
|
|||||||
<td>{% decimal on_order %}</td>
|
<td>{% decimal on_order %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if part.component %}
|
||||||
{% if required_build_order_quantity > 0 %}
|
{% if required_build_order_quantity > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-clipboard-list'></span></td>
|
<td><span class='fas fa-clipboard-list'></span></td>
|
||||||
<td>{% trans "Required for Build Orders" %}</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>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if part.salable %}
|
||||||
{% if required_sales_order_quantity > 0 %}
|
{% if required_sales_order_quantity > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-clipboard-list'></span></td>
|
<td><span class='fas fa-clipboard-list'></span></td>
|
||||||
<td>{% trans "Required for Sales Orders" %}</td>
|
<td>{% trans "Required for Sales Orders" %}</td>
|
||||||
<td>{% decimal required_sales_order_quantity %}
|
<td>
|
||||||
</tr>
|
{% decimal required_sales_order_quantity %}
|
||||||
{% 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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>{% trans "Can Build" %}</td>
|
||||||
<td>{% decimal part.can_build %}</td>
|
<td>{% decimal part.can_build %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if quantity_being_built > 0 %}
|
{% if quantity_being_built > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td><span class='fas fa-tools'></span></td>
|
||||||
<td>{% trans "Building" %}</td>
|
<td>{% trans "Building" %}</td>
|
||||||
<td>{% decimal quantity_being_built %}</td>
|
<td>{% decimal quantity_being_built %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
{% if part.assembly %}
|
{% if part.assembly %}
|
||||||
{% trans "Bill of Materials" as text %}
|
{% trans "Bill of Materials" as text %}
|
||||||
{% include "sidebar_item.html" with label="bom" text=text icon="fa-list" %}
|
{% include "sidebar_item.html" with label="bom" text=text icon="fa-list" %}
|
||||||
|
{% endif %}
|
||||||
{% if roles.build.view %}
|
{% if roles.build.view %}
|
||||||
|
{% if part.assembly or part.component %}
|
||||||
{% trans "Build Orders" as text %}
|
{% trans "Build Orders" as text %}
|
||||||
{% include "sidebar_item.html" with label="build-orders" text=text icon="fa-tools" %}
|
{% include "sidebar_item.html" with label="build-orders" text=text icon="fa-tools" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -30,10 +32,6 @@
|
|||||||
{% trans "Pricing" as text %}
|
{% trans "Pricing" as text %}
|
||||||
{% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %}
|
{% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %}
|
||||||
{% endif %}
|
{% 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 %}
|
{% if part.purchaseable and roles.purchase_order.view %}
|
||||||
{% trans "Suppliers" as text %}
|
{% trans "Suppliers" as text %}
|
||||||
{% include "sidebar_item.html" with label="suppliers" text=text icon="fa-building" %}
|
{% include "sidebar_item.html" with label="suppliers" text=text icon="fa-building" %}
|
||||||
|
@ -37,19 +37,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='panel panel-hidden' id='panel-allocations'>
|
<div class='panel panel-hidden' id='panel-allocations'>
|
||||||
|
|
||||||
|
{% if item.part.component %}
|
||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
<h4>{% trans "Stock Item Allocations" %}</h4>
|
<h4>{% trans "Build Order Allocations" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<div id='allocations-button-toolbar'>
|
<div id='build-order-allocations-toolbar'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
{% include "filter_list.html" with id="allocations" %}
|
{% include "filter_list.html" with id="buildorderallocation" %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
||||||
|
|
||||||
<div class='panel panel-hidden' id='panel-children'>
|
<div class='panel panel-hidden' id='panel-children'>
|
||||||
@ -164,14 +181,21 @@
|
|||||||
// Load the "allocations" tab
|
// Load the "allocations" tab
|
||||||
onPanelLoad('allocations', function() {
|
onPanelLoad('allocations', function() {
|
||||||
|
|
||||||
loadStockAllocationTable(
|
{% if item.part.component %}
|
||||||
$("#stock-allocation-table"),
|
loadBuildOrderAllocationTable('#build-order-allocation-table', {
|
||||||
{
|
params: {
|
||||||
params: {
|
stock_item: {{ item.pk }},
|
||||||
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() {
|
$('#stock-item-install').click(function() {
|
||||||
|
@ -1843,15 +1843,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
|
|||||||
field: 'location',
|
field: 'location',
|
||||||
title: '{% trans "Location" %}',
|
title: '{% trans "Location" %}',
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
|
return locationDetail(row.item_detail, true);
|
||||||
if (!value) {
|
|
||||||
return '{% trans "Location not specified" %}';
|
|
||||||
}
|
|
||||||
|
|
||||||
var link = `/stock/location/${value}`;
|
|
||||||
var text = row.location_detail.description;
|
|
||||||
|
|
||||||
return renderLink(text, link);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -48,7 +48,6 @@
|
|||||||
findStockItemBySerialNumber,
|
findStockItemBySerialNumber,
|
||||||
installStockItem,
|
installStockItem,
|
||||||
loadInstalledInTable,
|
loadInstalledInTable,
|
||||||
loadStockAllocationTable,
|
|
||||||
loadStockLocationTable,
|
loadStockLocationTable,
|
||||||
loadStockTable,
|
loadStockTable,
|
||||||
loadStockTestResultsTable,
|
loadStockTestResultsTable,
|
||||||
@ -2328,157 +2327,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
|
* Display a table of stock locations
|
||||||
*/
|
*/
|
||||||
|
@ -341,6 +341,15 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tableKey == 'salesorderallocation') {
|
||||||
|
return {
|
||||||
|
outstanding: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Outstanding" %}',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (tableKey == 'salesorder') {
|
if (tableKey == 'salesorder') {
|
||||||
return {
|
return {
|
||||||
status: {
|
status: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user