From 0892b160c6efe02da4eb50d8ea6e7d6be8a24b4f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 Apr 2020 00:32:09 +1000 Subject: [PATCH] "Fixes" for completing a build - This will require a lot of unit testing to get right --- InvenTree/InvenTree/status_codes.py | 5 +- InvenTree/build/models.py | 46 +++++++++++-------- InvenTree/order/models.py | 9 ++-- InvenTree/stock/api.py | 6 +-- .../migrations/0032_stockitem_build_order.py | 20 ++++++++ InvenTree/stock/models.py | 28 ++++++++++- 6 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 InvenTree/stock/migrations/0032_stockitem_build_order.py diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index bc57f5d3f1..a9f0048867 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -168,7 +168,9 @@ class StockStatus(StatusCode): # This can be used as a quick check for filtering NOT_IN_STOCK = 100 - SHIPPED = 110 # Item has been shipped to a customer + SENT_TO_CUSTOMER = 110 # Item has been shipped to a customer + ASSIGNED_TO_BUILD = 120 + ASSIGNED_TO_OTHER_ITEM = 130 options = { OK: _("OK"), @@ -176,7 +178,6 @@ class StockStatus(StatusCode): DAMAGED: _("Damaged"), DESTROYED: _("Destroyed"), LOST: _("Lost"), - SHIPPED: _("Shipped"), RETURNED: _("Returned"), } diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 0f2084f019..28bcc6e62e 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -21,7 +21,7 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey -from InvenTree.status_codes import BuildStatus +from InvenTree.status_codes import BuildStatus, StockStatus from InvenTree.fields import InvenTreeURLField from InvenTree.helpers import decimal2string @@ -261,32 +261,18 @@ class Build(MPTTModel): - Delete pending BuildItem objects """ - for item in self.allocated_stock.all().prefetch_related('stock_item'): - - # Subtract stock from the item - item.stock_item.take_stock( - item.quantity, - user, - 'Removed {n} items to build {m} x {part}'.format( - n=item.quantity, - m=self.quantity, - part=self.part.full_name - ) - ) + print("Complete build...") - # Delete the item - item.delete() - - # Mark the date of completion - self.completion_date = datetime.now().date() - - self.completed_by = user + # Complete the build allocation for each BuildItem + for build_item in self.allocated_stock.all().prefetch_related('stock_item'): + build_item.complete_allocation(user) notes = 'Built {q} on {now}'.format( q=self.quantity, now=str(datetime.now().date()) ) + # Generate the build outputs if self.part.trackable and serial_numbers: # Add new serial numbers for serial in serial_numbers: @@ -316,9 +302,13 @@ class Build(MPTTModel): item.save() # Finally, mark the build as complete + self.completion_date = datetime.now().date() + self.completed_by = user self.status = BuildStatus.COMPLETE self.save() + return True + def isFullyAllocated(self): """ Return True if this build has been fully allocated. @@ -477,6 +467,22 @@ class BuildItem(models.Model): if len(errors) > 0: raise ValidationError(errors) + def complete_allocation(self, user): + + item = self.stock_item + + # Split the allocated stock if there are more available than allocated + if item.quantity > self.quantity: + item = item.splitStock(self.quantity, None, user) + + # Update our own reference to the new item + self.stock_item = item + self.save() + + item.status = StockStatus.ASSIGNED_TO_BUILD + item.build_order = self.build + item.save() + build = models.ForeignKey( Build, on_delete=models.CASCADE, diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7e56858126..beed4457e6 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -25,7 +25,7 @@ from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField from InvenTree.helpers import decimal2string, normalize -from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus +from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus from InvenTree.models import InvenTreeAttachment @@ -574,12 +574,15 @@ class SalesOrderAllocation(models.Model): # Grab a copy of the new stock item (which will keep track of its "parent") item = item.splitStock(self.quantity, None, user) + # Update our own reference to the new item + self.item = item + self.save() + # Assign the StockItem to the SalesOrder customer item.customer = self.line.order.customer # Clear the location item.location = None + item.status = StockStatus.SENT_TO_CUSTOMER item.save() - - print("Finalizing allocation for: " + str(self.item)) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 9b7518580e..75deff6bd3 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -314,7 +314,6 @@ class StockList(generics.ListCreateAPIView): - POST: Create a new StockItem Additional query parameters are available: - - aggregate: If 'true' then stock items are aggregated by Part and Location - location: Filter stock by location - category: Filter by parts belonging to a certain category - supplier: Filter by supplier @@ -370,10 +369,10 @@ class StockList(generics.ListCreateAPIView): if in_stock: # Filter out parts which are not actually "in stock" - stock_list = stock_list.filter(customer=None, belongs_to=None) + stock_list = stock_list.filter(customer=None, belongs_to=None, build_order=None) else: # Only show parts which are not in stock - stock_list = stock_list.exclude(customer=None, belongs_to=None) + stock_list = stock_list.exclude(customer=None, belongs_to=None, build_order=None) # Filter by 'allocated' patrs? allocated = self.request.query_params.get('allocated', None) @@ -516,6 +515,7 @@ class StockList(generics.ListCreateAPIView): 'belongs_to', 'build', 'sales_order', + 'build_order', ] diff --git a/InvenTree/stock/migrations/0032_stockitem_build_order.py b/InvenTree/stock/migrations/0032_stockitem_build_order.py new file mode 100644 index 0000000000..849178c39c --- /dev/null +++ b/InvenTree/stock/migrations/0032_stockitem_build_order.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-25 14:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0015_auto_20200425_1350'), + ('stock', '0031_auto_20200422_0209'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='build_order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='build.Build'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index cc2c08b2d0..89103703c6 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -130,6 +130,7 @@ class StockItem(MPTTModel): purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder) + build_order: Link to a BuildOrder object (if the StockItem has been assigned to a BuildOrder) """ def save(self, *args, **kwargs): @@ -363,6 +364,13 @@ class StockItem(MPTTModel): related_name='stock_items', null=True, blank=True) + build_order = models.ForeignKey( + 'build.Build', + on_delete=models.SET_NULL, + related_name='stock_items', + null=True, blank=True + ) + # last time the stock was checked / counted stocktake_date = models.DateField(blank=True, null=True) @@ -439,6 +447,8 @@ class StockItem(MPTTModel): - Has child StockItems - Has a serial number and is tracked - Is installed inside another StockItem + - It has been delivered to a customer + - It has been assigned to a BuildOrder """ if self.child_count > 0: @@ -447,6 +457,12 @@ class StockItem(MPTTModel): if self.part.trackable and self.serial is not None: return False + if self.customer is not None: + return False + + if self.build_order is not None: + return False + return True @property @@ -464,7 +480,16 @@ class StockItem(MPTTModel): @property def in_stock(self): - if self.belongs_to or self.customer: + # Not 'in stock' if it has been installed inside another StockItem + if self.belongs_to is not None: + return False + + # Not 'in stock' if it has been sent to a customer + if self.customer is not None: + return False + + # Not 'in stock' if it has been allocated to a BuildOrder + if self.build_order is not None: return False return True @@ -642,6 +667,7 @@ class StockItem(MPTTModel): self.take_stock(quantity, user, 'Split {n} items into new stock item'.format(n=quantity)) # Return a copy of the "new" stock item + return new_stock @transaction.atomic def move(self, location, notes, user, **kwargs):