2
0
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:
Oliver
2023-06-13 20:18:32 +10:00
committed by GitHub
parent 98bddd32d0
commit 6ba777d363
54 changed files with 2193 additions and 1903 deletions

View File

@ -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',
)

View File

@ -8,6 +8,7 @@ import common.settings
class Migration(migrations.Migration):
dependencies = [
('common', '0004_inventreesetting'),
('stock', '0052_stockitem_is_building'),
]

View File

@ -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',

View File

@ -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."""