mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-01 03:00:54 +00:00
Build Order Updates (#4855)
* Add new BuildLine model - Represents an instance of a BOM item against a BuildOrder * Create BuildLine instances automatically When a new Build is created, automatically generate new BuildLine items * Improve logic for handling exchange rate backends * logic fixes * Adds API endpoints Add list and detail API endpoints for new BuildLine model * update users/models.py - Add new model to roles definition * bulk-create on auto_allocate Save database hits by performing a bulk-create * Add skeleton data migration * Create BuildLines for existing orders * Working on building out BuildLine table * Adds link for "BuildLine" to "BuildItem" - A "BuildItem" will now be tracked against a BuildLine - Not tracked directly against a build - Not tracked directly against a BomItem - Add schema migration - Add data migration to update links * Adjust migration 0045 - bom_item and build fields are about to be removed - Set them to "nullable" so the data doesn't get removed * Remove old fields from BuildItem model - build fk - bom_item fk - A lot of other required changes too * Update BuildLine.bom_item field - Delete the BuildLine if the BomItem is removed - This is closer to current behaviour * Cleanup for Build model - tracked_bom_items -> tracked_line_items - untracked_bom_items -> tracked_bom_items - remove build.can_complete - move bom_item specific methods to the BuildLine model - Cleanup / consolidation * front-end work - Update javascript - Cleanup HTML templates * Add serializer annotation and filtering - Annotate 'allocated' quantity - Filter by allocated / trackable / optional / consumable * Make table sortable * Add buttons * Add callback for building new stock * Fix Part annotation * Adds callback to order parts * Allocation works again * template cleanup * Fix allocate / unallocate actions - Also turns out "unallocate" is not a word.. * auto-allocate works again * Fix call to build.is_over_allocated * Refactoring updates * Bump API version * Cleaner implementation of allocation sub-table * Fix rendering in build output table * Improvements to StockItem list API - Refactor very old code - Add option to include test results to queryset * Add TODO for later me * Fix for serializers.py * Working on cleaner implementation of build output table * Add function to determine if a single output is fully allocated * Updates to build.js - Button callbacks - Table rendering * Revert previous changes to build.serializers.py * Fix for forms.js * Rearrange code in build.js * Rebuild "allocated lines" for output table * Fix allocation calculation * Show or hide column for tracked parts * Improve debug messages * Refactor "loadBuildLineTable" - Allow it to also be used as output sub-table * Refactor "completed tests" column * Remove old javascript - Cleans up a *lot* of crusty old code * Annotate the available stock quantity to BuildLine serializer - Similar pattern to BomItem serializer - Needs refactoring in the future * Update available column * Fix build allocation table - Bug fix - Make pretty * linting fixes * Allow sorting by available stock * Tweak for "required tests" column * Bug fix for completing a build output * Fix for consumable stock * Fix for trim_allocated_stock * Fix for creating new build * Migration fix - Ensure initial django_q migrations are applied - Why on earth is this failing now? * Catch exception * Update for exception handling * Update migrations - Ensure inventreesetting is added * Catch all exceptions when getting default currency code * Bug fix for currency exchange rates update * Working on unit tests * Unit test fixes * More work on unit tests * Use bulk_create in unit test * Update required quantity when a BuildOrder is saved * Tweak overage display in BOM table * Fix icon in BOM table * Fix spelling error * More unit test fixes * Build reports - Add line_items - Update docs - Cleanup * Reimplement is_partially_allocated method * Update docs about overage * Unit testing for data migration * Add "required_for_build_orders" annotation - Makes API query *much* faster now - remove old "required_parts_to_complete_build" method - Cleanup part API filter code * Adjust order of fixture loading * Fix unit test * Prevent "schedule_pricing_update" in unit tests - Should cut down on DB hits significantly * Unit test updates * Improvements for unit test - Don't hard-code pk values - postgresql no likey * Better unit test
This commit is contained in:
@ -21,7 +21,7 @@ import stock.serializers as StockSerializers
|
||||
from build.models import Build
|
||||
from build.serializers import BuildSerializer
|
||||
from company.models import Company, SupplierPart
|
||||
from company.serializers import CompanySerializer, SupplierPartSerializer
|
||||
from company.serializers import CompanySerializer
|
||||
from generic.states import StatusView
|
||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||
ListCreateDestroyAPIView, MetadataView)
|
||||
@ -553,6 +553,28 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
queryset = StockItem.objects.all()
|
||||
filterset_class = StockFilter
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Set context before returning serializer.
|
||||
|
||||
Extra detail may be provided to the serializer via query parameters:
|
||||
|
||||
- part_detail: Include detail about the StockItem's part
|
||||
- location_detail: Include detail about the StockItem's location
|
||||
- supplier_part_detail: Include detail about the StockItem's supplier_part
|
||||
- tests: Include detail about the StockItem's test results
|
||||
"""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
for key in ['part_detail', 'location_detail', 'supplier_part_detail', 'tests']:
|
||||
kwargs[key] = str2bool(params.get(key, False))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extend serializer context."""
|
||||
ctx = super().get_serializer_context()
|
||||
@ -743,8 +765,6 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
"""
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
params = request.query_params
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
if page is not None:
|
||||
@ -754,78 +774,6 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
|
||||
data = serializer.data
|
||||
|
||||
# Keep track of which related models we need to query
|
||||
location_ids = set()
|
||||
part_ids = set()
|
||||
supplier_part_ids = set()
|
||||
|
||||
# Iterate through each StockItem and grab some data
|
||||
for item in data:
|
||||
loc = item['location']
|
||||
if loc:
|
||||
location_ids.add(loc)
|
||||
|
||||
part = item['part']
|
||||
if part:
|
||||
part_ids.add(part)
|
||||
|
||||
sp = item['supplier_part']
|
||||
|
||||
if sp:
|
||||
supplier_part_ids.add(sp)
|
||||
|
||||
# Do we wish to include Part detail?
|
||||
if str2bool(params.get('part_detail', False)):
|
||||
|
||||
# Fetch only the required Part objects from the database
|
||||
parts = Part.objects.filter(pk__in=part_ids).prefetch_related(
|
||||
'category',
|
||||
)
|
||||
|
||||
part_map = {}
|
||||
|
||||
for part in parts:
|
||||
part_map[part.pk] = PartBriefSerializer(part).data
|
||||
|
||||
# Now update each StockItem with the related Part data
|
||||
for stock_item in data:
|
||||
part_id = stock_item['part']
|
||||
stock_item['part_detail'] = part_map.get(part_id, None)
|
||||
|
||||
# Do we wish to include SupplierPart detail?
|
||||
if str2bool(params.get('supplier_part_detail', False)):
|
||||
|
||||
supplier_parts = SupplierPart.objects.filter(pk__in=supplier_part_ids)
|
||||
|
||||
supplier_part_map = {}
|
||||
|
||||
for part in supplier_parts:
|
||||
supplier_part_map[part.pk] = SupplierPartSerializer(part).data
|
||||
|
||||
for stock_item in data:
|
||||
part_id = stock_item['supplier_part']
|
||||
stock_item['supplier_part_detail'] = supplier_part_map.get(part_id, None)
|
||||
|
||||
# Do we wish to include StockLocation detail?
|
||||
if str2bool(params.get('location_detail', False)):
|
||||
|
||||
# Fetch only the required StockLocation objects from the database
|
||||
locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related(
|
||||
'parent',
|
||||
'children',
|
||||
)
|
||||
|
||||
location_map = {}
|
||||
|
||||
# Serialize each StockLocation object
|
||||
for location in locations:
|
||||
location_map[location.pk] = StockSerializers.LocationBriefSerializer(location).data
|
||||
|
||||
# Now update each StockItem with the related StockLocation data
|
||||
for stock_item in data:
|
||||
loc_id = stock_item['location']
|
||||
stock_item['location_detail'] = location_map.get(loc_id, None)
|
||||
|
||||
"""
|
||||
Determine the response type based on the request.
|
||||
a) For HTTP requests (e.g. via the browsable API) return a DRF response
|
||||
@ -852,6 +800,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
'part',
|
||||
'part__category',
|
||||
'location',
|
||||
'test_results',
|
||||
'tags',
|
||||
)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import common.settings
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0004_inventreesetting'),
|
||||
('stock', '0052_stockitem_is_building'),
|
||||
]
|
||||
|
||||
|
@ -4,6 +4,22 @@ import InvenTree.fields
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
|
||||
|
||||
def show_migrations(apps, schema_editor):
|
||||
"""Show the latest migrations from each app"""
|
||||
|
||||
for app in apps.get_app_configs():
|
||||
|
||||
label = app.label
|
||||
|
||||
migrations = MigrationRecorder.Migration.objects.filter(app=app).order_by('-applied')[:5]
|
||||
|
||||
print(f"{label} migrations:")
|
||||
for m in migrations:
|
||||
print(f" - {m.name}")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -11,7 +27,13 @@ class Migration(migrations.Migration):
|
||||
('stock', '0064_auto_20210621_1724'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
operations = []
|
||||
|
||||
xoperations = [
|
||||
migrations.RunPython(
|
||||
code=show_migrations,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='purchase_price',
|
||||
|
@ -44,6 +44,50 @@ class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for the StockItemTestResult model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = StockItemTestResult
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'stock_item',
|
||||
'key',
|
||||
'test',
|
||||
'result',
|
||||
'value',
|
||||
'attachment',
|
||||
'notes',
|
||||
'user',
|
||||
'user_detail',
|
||||
'date'
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'pk',
|
||||
'user',
|
||||
'date',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Add detail fields."""
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user_detail is not True:
|
||||
self.fields.pop('user_detail')
|
||||
|
||||
user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True)
|
||||
|
||||
key = serializers.CharField(read_only=True)
|
||||
|
||||
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=False)
|
||||
|
||||
|
||||
class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Brief serializers for a StockItem."""
|
||||
|
||||
@ -126,6 +170,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
'purchase_price',
|
||||
'purchase_price_currency',
|
||||
'use_pack_size',
|
||||
'tests',
|
||||
|
||||
'tags',
|
||||
]
|
||||
@ -234,11 +279,11 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
|
||||
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||
|
||||
# Optional detail fields, which can be appended via query parameters
|
||||
supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True)
|
||||
|
||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||
|
||||
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
|
||||
tests = StockItemTestResultSerializer(source='test_results', many=True, read_only=True)
|
||||
|
||||
quantity = InvenTreeDecimalField()
|
||||
|
||||
@ -266,18 +311,22 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
location_detail = kwargs.pop('location_detail', False)
|
||||
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
|
||||
tests = kwargs.pop('tests', False)
|
||||
|
||||
super(StockItemSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
if part_detail is not True:
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail')
|
||||
|
||||
if location_detail is not True:
|
||||
if not location_detail:
|
||||
self.fields.pop('location_detail')
|
||||
|
||||
if supplier_part_detail is not True:
|
||||
if not supplier_part_detail:
|
||||
self.fields.pop('supplier_part_detail')
|
||||
|
||||
if not tests:
|
||||
self.fields.pop('tests')
|
||||
|
||||
|
||||
class SerializeStockItemSerializer(serializers.Serializer):
|
||||
"""A DRF serializer for "serializing" a StockItem.
|
||||
@ -653,50 +702,6 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
|
||||
])
|
||||
|
||||
|
||||
class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for the StockItemTestResult model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = StockItemTestResult
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'stock_item',
|
||||
'key',
|
||||
'test',
|
||||
'result',
|
||||
'value',
|
||||
'attachment',
|
||||
'notes',
|
||||
'user',
|
||||
'user_detail',
|
||||
'date'
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'pk',
|
||||
'user',
|
||||
'date',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Add detail fields."""
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user_detail is not True:
|
||||
self.fields.pop('user_detail')
|
||||
|
||||
user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True)
|
||||
|
||||
key = serializers.CharField(read_only=True)
|
||||
|
||||
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=False)
|
||||
|
||||
|
||||
class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for StockItemTracking model."""
|
||||
|
||||
|
Reference in New Issue
Block a user