mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-10 23:14:13 +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:
@ -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)
|
||||
|
Reference in New Issue
Block a user