mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Scheduling information is now calculated on the server, and provided via a new API endpoint
- Much simpler than sequencing multiple API calls
This commit is contained in:
parent
dfd6684699
commit
776dffe779
@ -12,11 +12,14 @@ import common.models
|
||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 27
|
||||
INVENTREE_API_VERSION = 28
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v28 -> 2022-03-01
|
||||
- Adds "scheduling" endpoint for predicted stock scheduling information
|
||||
|
||||
v27 -> 2022-02-28
|
||||
- Adds target_date field to individual line items for purchase orders and sales orders
|
||||
|
||||
|
@ -5,6 +5,8 @@ Provides a JSON API for the Part app
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Q, F, Count, Min, Max, Avg
|
||||
@ -38,14 +40,15 @@ from company.models import Company, ManufacturerPart, SupplierPart
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from build.models import Build
|
||||
from build.models import Build, BuildItem
|
||||
import order.models
|
||||
|
||||
from . import serializers as part_serializers
|
||||
|
||||
from InvenTree.helpers import str2bool, isNull, increment
|
||||
from InvenTree.api import AttachmentMixin
|
||||
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
||||
|
||||
|
||||
class CategoryList(generics.ListCreateAPIView):
|
||||
@ -427,6 +430,137 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
|
||||
]
|
||||
|
||||
|
||||
class PartScheduling(generics.RetrieveAPIView):
|
||||
"""
|
||||
API endpoint for delivering "scheduling" information about a given part via the API.
|
||||
|
||||
Returns a chronologically ordered list about future "scheduled" events,
|
||||
concerning stock levels for the part:
|
||||
|
||||
- Purchase Orders (incoming stock)
|
||||
- Sales Orders (outgoing stock)
|
||||
- Build Orders (incoming completed stock)
|
||||
- Build Orders (outgoing allocated stock)
|
||||
"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
today = datetime.datetime.now().date()
|
||||
|
||||
part = self.get_object()
|
||||
|
||||
schedule = []
|
||||
|
||||
def add_schedule_entry(date, quantity, label, url):
|
||||
"""
|
||||
Check if a scheduled entry should be added:
|
||||
- date must be non-null
|
||||
- date cannot be in the "past"
|
||||
- quantity must not be zero
|
||||
"""
|
||||
|
||||
if date and date >= today and quantity != 0:
|
||||
schedule.append({
|
||||
'date': date,
|
||||
'quantity': quantity,
|
||||
'url': url,
|
||||
'label': label,
|
||||
})
|
||||
|
||||
# Add purchase order (incoming stock) information
|
||||
po_lines = order.models.PurchaseOrderLineItem.objects.filter(
|
||||
part__part=part,
|
||||
order__status__in=PurchaseOrderStatus.OPEN,
|
||||
)
|
||||
|
||||
for line in po_lines:
|
||||
|
||||
target_date = line.target_date or line.order.target_date
|
||||
|
||||
quantity = max(line.quantity - line.received, 0)
|
||||
|
||||
add_schedule_entry(
|
||||
target_date,
|
||||
quantity,
|
||||
str(line.order),
|
||||
line.order.get_absolute_url()
|
||||
)
|
||||
|
||||
# Add sales order (outgoing stock) information
|
||||
so_lines = order.models.SalesOrderLineItem.objects.filter(
|
||||
part=part,
|
||||
order__status__in=SalesOrderStatus.OPEN,
|
||||
)
|
||||
|
||||
for line in so_lines:
|
||||
|
||||
target_date = line.target_date or line.order.target_date
|
||||
|
||||
quantity = max(line.quantity - line.shipped, 0)
|
||||
|
||||
add_schedule_entry(
|
||||
target_date,
|
||||
-quantity,
|
||||
str(line.order),
|
||||
line.order.get_absolute_url(),
|
||||
)
|
||||
|
||||
# Add build orders (incoming stock) information
|
||||
build_orders = Build.objects.filter(
|
||||
part=part,
|
||||
status__in=BuildStatus.ACTIVE_CODES
|
||||
)
|
||||
|
||||
for build in build_orders:
|
||||
|
||||
quantity = max(build.quantity - build.completed, 0)
|
||||
|
||||
add_schedule_entry(
|
||||
build.target_date,
|
||||
quantity,
|
||||
str(build),
|
||||
build.get_absolute_url(),
|
||||
)
|
||||
|
||||
"""
|
||||
Add build order allocation (outgoing stock) information.
|
||||
|
||||
Here we need some careful consideration:
|
||||
|
||||
- 'Tracked' stock items are removed from stock when the individual Build Output is completed
|
||||
- 'Untracked' stock items are removed from stock when the Build Order is completed
|
||||
|
||||
The 'simplest' approach here is to look at existing BuildItem allocations which reference this part,
|
||||
and "schedule" them for removal at the time of build order completion.
|
||||
|
||||
This assumes that the user is responsible for correctly allocating parts.
|
||||
|
||||
However, it has the added benefit of side-stepping the various BOM substition options,
|
||||
and just looking at what stock items the user has actually allocated against the Build.
|
||||
"""
|
||||
|
||||
build_allocations = BuildItem.objects.filter(
|
||||
stock_item__part=part,
|
||||
build__status__in=BuildStatus.ACTIVE_CODES,
|
||||
)
|
||||
|
||||
for allocation in build_allocations:
|
||||
|
||||
add_schedule_entry(
|
||||
allocation.build.target_date,
|
||||
-allocation.quantity,
|
||||
str(allocation.build),
|
||||
allocation.build.get_absolute_url(),
|
||||
)
|
||||
|
||||
# Sort by incrementing date values
|
||||
schedule = sorted(schedule, key=lambda entry: entry['date'])
|
||||
|
||||
return Response(schedule)
|
||||
|
||||
|
||||
class PartSerialNumberDetail(generics.RetrieveAPIView):
|
||||
"""
|
||||
API endpoint for returning extra serial number information about a particular part
|
||||
@ -1715,6 +1849,9 @@ part_api_urls = [
|
||||
# Endpoint for extra serial number information
|
||||
url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'),
|
||||
|
||||
# Endpoint for future scheduling information
|
||||
url(r'^scheduling/', PartScheduling.as_view(), name='api-part-scheduling'),
|
||||
|
||||
# Endpoint for duplicating a BOM for the specific Part
|
||||
url(r'^bom-copy/', PartCopyBOM.as_view(), name='api-part-bom-copy'),
|
||||
|
||||
|
@ -2002,158 +2002,27 @@ function loadPartSchedulingChart(canvas_id, part_id) {
|
||||
}
|
||||
];
|
||||
|
||||
function addScheduleEntry(date, delta, label, url) {
|
||||
// Adds a new entry to the schedule
|
||||
|
||||
// First, iterate through to find an insertion index (based on date)
|
||||
var found = false;
|
||||
|
||||
for (var idx = 0; idx < stock_schedule.length; idx++) {
|
||||
var entry = stock_schedule[idx];
|
||||
|
||||
if (date < entry.date) {
|
||||
stock_schedule.splice(idx, 0, {
|
||||
date: date,
|
||||
delta: delta,
|
||||
label: label,
|
||||
url: url,
|
||||
/* Request scheduling information for the part.
|
||||
* Note that this information has already been 'curated' by the server,
|
||||
* and arranged in increasing chronological order
|
||||
*/
|
||||
inventreeGet(
|
||||
`/api/part/${part_id}/scheduling/`,
|
||||
{},
|
||||
{
|
||||
async: false,
|
||||
success: function(response) {
|
||||
response.forEach(function(entry) {
|
||||
stock_schedule.push({
|
||||
date: moment(entry.date),
|
||||
delta: entry.quantity,
|
||||
label: entry.label,
|
||||
url: entry.url,
|
||||
})
|
||||
});
|
||||
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
stock_schedule.push({
|
||||
date: date,
|
||||
delta: delta,
|
||||
label: label,
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Extract purchase order information from the server
|
||||
if (part_info.purchaseable) {
|
||||
inventreeGet(
|
||||
`/api/order/po-line/`,
|
||||
{
|
||||
pending: true,
|
||||
base_part: part_id,
|
||||
order_detail: true,
|
||||
},
|
||||
{
|
||||
async: false,
|
||||
success: function(line_items) {
|
||||
|
||||
line_items.forEach(function(line_item) {
|
||||
|
||||
// Extract target_date information from the response.
|
||||
// If the line_item does not have an individual target date, maybe the parent order does?
|
||||
var target_date = line_item.target_date || line_item.order_detail.target_date;
|
||||
|
||||
// How many to receive?
|
||||
var delta = Math.max(line_item.quantity - line_item.received, 0);
|
||||
|
||||
// TODO: What do we do if there is no target_date set for a PO line item?
|
||||
// TODO: Do we just ignore it??
|
||||
|
||||
if (target_date && delta > 0) {
|
||||
|
||||
var td = moment(target_date);
|
||||
|
||||
if (td >= today) {
|
||||
// TODO: Improve labels for purchase order lines
|
||||
addScheduleEntry(td, delta, "Purchase Order", '/index/');
|
||||
|
||||
} else {
|
||||
// Ignore any entries that are in the "past"
|
||||
// TODO: Can we better handle this case?
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Extract sales order information from the server
|
||||
if (part_info.salable) {
|
||||
inventreeGet(
|
||||
`/api/order/so-line/`,
|
||||
{
|
||||
part: part_id,
|
||||
pending: true,
|
||||
order_detail: true,
|
||||
},
|
||||
{
|
||||
async: false,
|
||||
success: function(line_items) {
|
||||
|
||||
line_items.forEach(function(line_item) {
|
||||
|
||||
// Extract target_date information from the response.
|
||||
// If the line_item does not have an individual target date, maybe the parent order does?
|
||||
var target_date = line_item.target_date || line_item.order_detail.target_date;
|
||||
|
||||
var delta = Math.max(line_item.quantity - line_item.shipped, 0);
|
||||
|
||||
// TODO: What do we do if there is no target_date set for a PO line item?
|
||||
// TODO: Do we just ignore it??
|
||||
|
||||
if (target_date && delta > 0) {
|
||||
var td = moment(target_date);
|
||||
|
||||
if (td >= today) {
|
||||
// TODO: Improve labels for sales order items
|
||||
addScheduleEntry(td, -delta, "Sales Order", '/index/');
|
||||
} else {
|
||||
// Ignore any entries that are in the "past"
|
||||
// TODO: Can we better handle this case?
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Request build orders for this part
|
||||
if (part_info.assembly) {
|
||||
inventreeGet(
|
||||
`/api/build/`,
|
||||
{
|
||||
part: part_id,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
async: false,
|
||||
success: function(build_orders) {
|
||||
|
||||
build_orders.forEach(function(build_order) {
|
||||
|
||||
var target_date = build_order.target_date;
|
||||
|
||||
var delta = Math.max(build_order.quantity - build_order.completed, 0);
|
||||
|
||||
// TODO: How do we handle the case where the build order does not have a target date??
|
||||
// TODO: Do we just ignore it?
|
||||
|
||||
if (target_date && delta > 0) {
|
||||
var td = moment(target_date);
|
||||
|
||||
if (td >= today) {
|
||||
addScheduleEntry(td, delta, "Build Order", "");
|
||||
} else {
|
||||
// TODO: Handle case where the build order is in the "past"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
// Iterate through future "events" to calculate expected quantity
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user