2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-11 07:24:15 +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 2164 additions and 1874 deletions

View File

@ -13,7 +13,7 @@ from InvenTree import status_codes as status
import common.models
import build.tasks
from build.models import Build, BuildItem, generate_next_build_reference
from build.models import Build, BuildItem, BuildLine, generate_next_build_reference
from part.models import Part, BomItem, BomItemSubstitute
from stock.models import StockItem
from users.models import Owner
@ -107,6 +107,11 @@ class BuildTestBase(TestCase):
issued_by=get_user_model().objects.get(pk=1),
)
# Create some BuildLine items we can use later on
cls.line_1 = BuildLine.objects.get(build=cls.build, bom_item=cls.bom_item_1)
cls.line_2 = BuildLine.objects.get(build=cls.build, bom_item=cls.bom_item_2)
cls.line_3 = BuildLine.objects.get(build=cls.build, bom_item=cls.bom_item_3)
# Create some build output (StockItem) objects
cls.output_1 = StockItem.objects.create(
part=cls.assembly,
@ -248,13 +253,10 @@ class BuildTest(BuildTestBase):
for output in self.build.get_build_outputs().all():
self.assertFalse(self.build.is_fully_allocated(output))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_1, self.output_1))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2, self.output_2))
self.assertFalse(self.line_1.is_fully_allocated())
self.assertFalse(self.line_2.is_overallocated())
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1, self.output_1), 15)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1, self.output_2), 35)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2, self.output_1), 9)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2, self.output_2), 21)
self.assertEqual(self.line_1.allocated_quantity(), 0)
self.assertFalse(self.build.is_complete)
@ -264,25 +266,25 @@ class BuildTest(BuildTestBase):
stock = StockItem.objects.create(part=self.assembly, quantity=99)
# Create a BuiltItem which points to an invalid StockItem
b = BuildItem(stock_item=stock, build=self.build, quantity=10)
b = BuildItem(stock_item=stock, build_line=self.line_2, quantity=10)
with self.assertRaises(ValidationError):
b.save()
# Create a BuildItem which has too much stock assigned
b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=9999999)
b = BuildItem(stock_item=self.stock_1_1, build_line=self.line_1, quantity=9999999)
with self.assertRaises(ValidationError):
b.clean()
# Negative stock? Not on my watch!
b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=-99)
b = BuildItem(stock_item=self.stock_1_1, build_line=self.line_1, quantity=-99)
with self.assertRaises(ValidationError):
b.clean()
# Ok, what about we make one that does *not* fail?
b = BuildItem(stock_item=self.stock_1_2, build=self.build, install_into=self.output_1, quantity=10)
b = BuildItem(stock_item=self.stock_1_2, build_line=self.line_1, install_into=self.output_1, quantity=10)
b.save()
def test_duplicate_bom_line(self):
@ -302,13 +304,24 @@ class BuildTest(BuildTestBase):
allocations: Map of {StockItem: quantity}
"""
items_to_create = []
for item, quantity in allocations.items():
BuildItem.objects.create(
# Find an appropriate BuildLine to allocate against
line = BuildLine.objects.filter(
build=self.build,
bom_item__sub_part=item.part
).first()
items_to_create.append(BuildItem(
build_line=line,
stock_item=item,
quantity=quantity,
install_into=output
)
))
BuildItem.objects.bulk_create(items_to_create)
def test_partial_allocation(self):
"""Test partial allocation of stock"""
@ -321,7 +334,7 @@ class BuildTest(BuildTestBase):
}
)
self.assertTrue(self.build.is_fully_allocated(self.output_1))
self.assertTrue(self.build.is_output_fully_allocated(self.output_1))
# Partially allocate tracked stock against build output 2
self.allocate_stock(
@ -331,7 +344,7 @@ class BuildTest(BuildTestBase):
}
)
self.assertFalse(self.build.is_fully_allocated(self.output_2))
self.assertFalse(self.build.is_output_fully_allocated(self.output_2))
# Partially allocate untracked stock against build
self.allocate_stock(
@ -342,11 +355,12 @@ class BuildTest(BuildTestBase):
}
)
self.assertFalse(self.build.is_fully_allocated(None))
self.assertFalse(self.build.is_output_fully_allocated(None))
unallocated = self.build.unallocated_bom_items(None)
# Find lines which are *not* fully allocated
unallocated = self.build.unallocated_lines()
self.assertEqual(len(unallocated), 2)
self.assertEqual(len(unallocated), 3)
self.allocate_stock(
None,
@ -357,17 +371,17 @@ class BuildTest(BuildTestBase):
self.assertFalse(self.build.is_fully_allocated(None))
unallocated = self.build.unallocated_bom_items(None)
unallocated = self.build.unallocated_lines()
self.assertEqual(len(unallocated), 1)
self.assertEqual(len(unallocated), 2)
self.build.unallocateStock()
self.build.deallocate_stock()
unallocated = self.build.unallocated_bom_items(None)
unallocated = self.build.unallocated_lines(None)
self.assertEqual(len(unallocated), 2)
self.assertEqual(len(unallocated), 3)
self.assertFalse(self.build.are_untracked_parts_allocated())
self.assertFalse(self.build.is_fully_allocated(tracked=False))
self.stock_2_1.quantity = 500
self.stock_2_1.save()
@ -381,7 +395,7 @@ class BuildTest(BuildTestBase):
}
)
self.assertTrue(self.build.are_untracked_parts_allocated())
self.assertTrue(self.build.is_fully_allocated(tracked=False))
def test_overallocation_and_trim(self):
"""Test overallocation of stock and trim function"""
@ -425,10 +439,10 @@ class BuildTest(BuildTestBase):
}
)
self.assertTrue(self.build.has_overallocated_parts(None))
self.assertTrue(self.build.is_overallocated())
self.build.trim_allocated_stock()
self.assertFalse(self.build.has_overallocated_parts(None))
self.assertFalse(self.build.is_overallocated())
self.build.complete_build_output(self.output_1, None)
self.build.complete_build_output(self.output_2, None)
@ -587,7 +601,7 @@ class BuildTest(BuildTestBase):
"""Unit tests for the metadata field."""
# Make sure a BuildItem exists before trying to run this test
b = BuildItem(stock_item=self.stock_1_2, build=self.build, install_into=self.output_1, quantity=10)
b = BuildItem(stock_item=self.stock_1_2, build_line=self.line_1, install_into=self.output_1, quantity=10)
b.save()
for model in [Build, BuildItem]:
@ -644,7 +658,7 @@ class AutoAllocationTests(BuildTestBase):
# No build item allocations have been made against the build
self.assertEqual(self.build.allocated_stock.count(), 0)
self.assertFalse(self.build.are_untracked_parts_allocated())
self.assertFalse(self.build.is_fully_allocated(tracked=False))
# Stock is not interchangeable, nothing will happen
self.build.auto_allocate_stock(
@ -652,15 +666,15 @@ class AutoAllocationTests(BuildTestBase):
substitutes=False,
)
self.assertFalse(self.build.are_untracked_parts_allocated())
self.assertFalse(self.build.is_fully_allocated(tracked=False))
self.assertEqual(self.build.allocated_stock.count(), 0)
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_1))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2))
self.assertFalse(self.line_1.is_fully_allocated())
self.assertFalse(self.line_2.is_fully_allocated())
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 50)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 30)
self.assertEqual(self.line_1.unallocated_quantity(), 50)
self.assertEqual(self.line_2.unallocated_quantity(), 30)
# This time we expect stock to be allocated!
self.build.auto_allocate_stock(
@ -669,15 +683,15 @@ class AutoAllocationTests(BuildTestBase):
optional_items=True,
)
self.assertFalse(self.build.are_untracked_parts_allocated())
self.assertFalse(self.build.is_fully_allocated(tracked=False))
self.assertEqual(self.build.allocated_stock.count(), 7)
self.assertTrue(self.build.is_bom_item_allocated(self.bom_item_1))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2))
self.assertTrue(self.line_1.is_fully_allocated())
self.assertFalse(self.line_2.is_fully_allocated())
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 5)
self.assertEqual(self.line_1.unallocated_quantity(), 0)
self.assertEqual(self.line_2.unallocated_quantity(), 5)
# This time, allow substitute parts to be used!
self.build.auto_allocate_stock(
@ -685,12 +699,11 @@ class AutoAllocationTests(BuildTestBase):
substitutes=True,
)
# self.assertEqual(self.build.allocated_stock.count(), 8)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 5.0)
self.assertEqual(self.line_1.unallocated_quantity(), 0)
self.assertEqual(self.line_2.unallocated_quantity(), 5)
self.assertTrue(self.build.is_bom_item_allocated(self.bom_item_1))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2))
self.assertTrue(self.line_1.is_fully_allocated())
self.assertFalse(self.line_2.is_fully_allocated())
def test_fully_auto(self):
"""We should be able to auto-allocate against a build in a single go"""
@ -701,7 +714,7 @@ class AutoAllocationTests(BuildTestBase):
optional_items=True,
)
self.assertTrue(self.build.are_untracked_parts_allocated())
self.assertTrue(self.build.is_fully_allocated(tracked=False))
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 0)
self.assertEqual(self.line_1.unallocated_quantity(), 0)
self.assertEqual(self.line_2.unallocated_quantity(), 0)