2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

[Breaking] Remove part scheduling feature (#9811)

* Remove frontend code

* Remove references to setting

* Remove API endpoint

* Docs updates

* Bump API version

* Remove check for old tab
This commit is contained in:
Oliver
2025-06-20 17:17:44 +10:00
committed by GitHub
parent c90fc2feda
commit b4f3fd46f9
13 changed files with 4 additions and 627 deletions

View File

@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 354
INVENTREE_API_VERSION = 355
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v355 -> 2025-06-20 : https://github.com/inventree/InvenTree/pull/9811
- Removes legacy "PartScheduling" API endpoints
v354 -> 2025-06-09 : https://github.com/inventree/InvenTree/pull/9532
- Adds "merge" field to the ReportTemplate model

View File

@ -211,12 +211,6 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
('MMM DD YYYY', 'Feb 22 2022'),
],
},
'DISPLAY_SCHEDULE_TAB': {
'name': _('Part Scheduling'),
'description': _('Display part scheduling information'),
'default': True,
'validator': bool,
},
'DISPLAY_STOCKTAKE_TAB': {
'name': _('Part Stocktake'),
'description': _(

View File

@ -1,8 +1,6 @@
"""Provides a JSON API for the Part app."""
import functools
import re
from datetime import datetime
from django.db.models import Count, F, Q
from django.urls import include, path
@ -17,10 +15,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
import InvenTree.permissions
import order.models
import part.filters
from build.models import Build, BuildItem
from build.status_codes import BuildStatusGroups
from data_exporter.mixins import DataExportViewMixin
from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView
from InvenTree.filters import (
@ -43,7 +38,6 @@ from InvenTree.mixins import (
UpdateAPI,
)
from InvenTree.serializers import EmptySerializer
from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
from stock.models import StockLocation
from . import serializers as part_serializers
@ -547,205 +541,6 @@ class PartThumbsUpdate(RetrieveUpdateAPI):
filter_backends = [DjangoFilterBackend]
class PartScheduling(RetrieveAPI):
"""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()
serializer_class = part_serializers.PartSchedulingSerializer
def retrieve(self, request, *args, **kwargs):
"""Return scheduling information for the referenced Part instance."""
part = self.get_object()
schedule = []
def add_schedule_entry(
date: datetime,
quantity: float,
title: str,
instance,
speculative_quantity: float = 0,
):
"""Add a new entry to the schedule list.
Args:
date (datetime): The date of the scheduled event.
quantity (float): The quantity of stock to be added or removed.
title (str): The title of the scheduled event.
instance (Model): The associated model instance (e.g., SalesOrder object).
speculative_quantity (float, optional): A speculative quantity to be added or removed. Defaults to 0.
"""
schedule.append({
'date': date,
'quantity': quantity,
'speculative_quantity': speculative_quantity,
'title': title,
'label': str(instance.reference),
'model': instance.__class__.__name__.lower(),
'model_id': instance.pk,
})
# Add purchase order (incoming stock) information
po_lines = order.models.PurchaseOrderLineItem.objects.filter(
part__part=part, order__status__in=PurchaseOrderStatusGroups.OPEN
)
for line in po_lines:
target_date = line.target_date or line.order.target_date
line_quantity = max(line.quantity - line.received, 0)
# Multiply by the pack quantity of the SupplierPart
quantity = line.part.base_quantity(line_quantity)
add_schedule_entry(
target_date, quantity, _('Incoming Purchase Order'), line.order
)
# Add sales order (outgoing stock) information
so_lines = order.models.SalesOrderLineItem.objects.filter(
part=part, order__status__in=SalesOrderStatusGroups.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, _('Outgoing Sales Order'), line.order
)
# Add build orders (incoming stock) information
build_orders = Build.objects.filter(
part=part, status__in=BuildStatusGroups.ACTIVE_CODES
)
for build in build_orders:
quantity = max(build.quantity - build.completed, 0)
add_schedule_entry(
build.target_date, quantity, _('Stock produced by Build Order'), build
)
"""
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 substitution options,
and just looking at what stock items the user has actually allocated against the Build.
"""
# Grab a list of BomItem objects that this part might be used in
bom_items = BomItem.objects.filter(part.get_used_in_bom_item_filter())
# Track all outstanding build orders
seen_builds = set()
for bom_item in bom_items:
# Find a list of active builds for this BomItem
if bom_item.inherited:
# An "inherited" BOM item filters down to variant parts also
children = bom_item.part.get_descendants(include_self=True)
builds = Build.objects.filter(
status__in=BuildStatusGroups.ACTIVE_CODES, part__in=children
)
else:
builds = Build.objects.filter(
status__in=BuildStatusGroups.ACTIVE_CODES, part=bom_item.part
)
for build in builds:
# Ensure we don't double-count any builds
if build in seen_builds:
continue
seen_builds.add(build)
if bom_item.sub_part.trackable:
# Trackable parts are allocated against the outputs
required_quantity = build.remaining * bom_item.quantity
else:
# Non-trackable parts are allocated against the build itself
required_quantity = build.quantity * bom_item.quantity
# Grab all allocations against the specified BomItem
allocations = BuildItem.objects.filter(
build_line__bom_item=bom_item, build_line__build=build
)
# Total allocated for *this* part
part_allocated_quantity = 0
# Total allocated for *any* part
total_allocated_quantity = 0
for allocation in allocations:
total_allocated_quantity += allocation.quantity
if allocation.stock_item.part == part:
part_allocated_quantity += allocation.quantity
speculative_quantity = 0
# Consider the case where the build order is *not* fully allocated
if required_quantity > total_allocated_quantity:
speculative_quantity = -1 * (
required_quantity - total_allocated_quantity
)
add_schedule_entry(
build.target_date,
-part_allocated_quantity,
_('Stock required for Build Order'),
build,
speculative_quantity=speculative_quantity,
)
def compare(entry_1, entry_2):
"""Comparison function for sorting entries by date.
Account for the fact that either date might be None
"""
date_1 = entry_1['date']
date_2 = entry_2['date']
if date_1 is None:
return -1
elif date_2 is None:
return 1
return -1 if date_1 < date_2 else 1
# Sort by incrementing date values
schedules = sorted(schedule, key=functools.cmp_to_key(compare))
serializers = part_serializers.PartSchedulingSerializer(
schedules, many=True, context={'request': request}
)
return Response(serializers.data)
class PartRequirements(RetrieveAPI):
"""API endpoint detailing 'requirements' information for a particular part.
@ -2210,8 +2005,6 @@ part_api_urls = [
PartSerialNumberDetail.as_view(),
name='api-part-serial-number-detail',
),
# Endpoint for future scheduling information
path('scheduling/', PartScheduling.as_view(), name='api-part-scheduling'),
path(
'requirements/',
PartRequirements.as_view(),

View File

@ -253,39 +253,6 @@ class PartInternalPriceSerializer(InvenTree.serializers.InvenTreeModelSerializer
)
class PartSchedulingSerializer(serializers.Serializer):
"""Serializer class for a PartScheduling entry."""
class Meta:
"""Metaclass options for this serializer."""
fields = [
'date',
'quantity',
'speculative_quantity',
'title',
'label',
'model',
'model_id',
]
date = serializers.DateField(label=_('Date'), required=True, allow_null=True)
quantity = serializers.FloatField(label=_('Quantity'), required=True)
speculative_quantity = serializers.FloatField(
label=_('Speculative Quantity'), required=False
)
title = serializers.CharField(label=_('Title'), required=True)
label = serializers.CharField(label=_('Label'), required=True)
model = serializers.CharField(label=_('Model'), required=True)
model_id = serializers.IntegerField(label=_('Model ID'), required=True)
class PartThumbSerializer(serializers.Serializer):
"""Serializer for the 'image' field of the Part model.

View File

@ -3063,22 +3063,6 @@ class PartMetadataAPITest(InvenTreeAPITestCase):
self.metatester(apikey, model)
class PartSchedulingTest(PartAPITestBase):
"""Unit tests for the 'part scheduling' API endpoint."""
def test_get_schedule(self):
"""Test that the scheduling endpoint returns OK."""
part_ids = [1, 3, 100, 101]
for pk in part_ids:
url = reverse('api-part-scheduling', kwargs={'pk': pk})
data = self.get(url, expected_code=200).data
for entry in data:
for k in ['date', 'quantity', 'label']:
self.assertIn(k, entry)
class PartTestTemplateTest(PartAPITestBase):
"""API unit tests for the PartTestTemplate model."""