InvenTree/InvenTree/build/models.py

403 lines
13 KiB
Python
Raw Normal View History

2019-04-27 10:35:14 +00:00
"""
2019-04-27 10:43:27 +00:00
Build database model definitions
2019-04-27 10:35:14 +00:00
"""
2018-04-16 14:32:02 +00:00
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from django.contrib.auth.models import User
2018-04-17 06:58:37 +00:00
from django.utils.translation import ugettext as _
from django.core.exceptions import ValidationError
2018-04-17 06:58:37 +00:00
from django.urls import reverse
2019-05-01 12:21:13 +00:00
from django.db import models, transaction
from django.db.models import Sum
2018-04-16 14:32:02 +00:00
from django.core.validators import MinValueValidator
from stock.models import StockItem
from part.models import BomItem
2018-04-17 14:03:42 +00:00
2018-04-16 14:32:02 +00:00
class Build(models.Model):
2019-04-27 10:35:14 +00:00
""" A Build object organises the creation of new parts from the component parts.
Attributes:
part: The part to be built (from component BOM items)
title: Brief title describing the build (required)
quantity: Number of units to be built
2019-05-10 09:03:10 +00:00
take_from: Location to take stock from to make this build (if blank, can take from anywhere)
status: Build status code
batch: Batch code transferred to build parts (optional)
creation_date: Date the build was created (auto)
completion_date: Date the build was completed
URL: External URL for extra information
notes: Text notes
2018-04-16 14:32:02 +00:00
"""
def __str__(self):
return "Build {q} x {part}".format(q=self.quantity, part=str(self.part))
def get_absolute_url(self):
return reverse('build-detail', kwargs={'pk': self.id})
2019-05-10 09:03:10 +00:00
title = models.CharField(
blank=False,
max_length=100,
help_text='Brief description of the build')
2019-04-27 10:35:14 +00:00
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='builds',
limit_choices_to={
'buildable': True,
'active': True
2019-05-05 12:35:39 +00:00
},
2019-05-08 13:32:57 +00:00
help_text='Select part to build',
2019-04-27 10:35:14 +00:00
)
2019-05-10 09:03:10 +00:00
take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
related_name='sourcing_builds',
null=True, blank=True,
help_text='Select location to take stock from for this build (leave blank to take from any stock location'
)
2019-04-27 10:35:14 +00:00
quantity = models.PositiveIntegerField(
default=1,
validators=[MinValueValidator(1)],
help_text='Number of parts to build'
)
2018-04-17 06:58:37 +00:00
# Build status codes
PENDING = 10 # Build is pending / active
ALLOCATED = 20 # Parts have been removed from stock
2018-04-17 14:03:42 +00:00
CANCELLED = 30 # Build was cancelled
COMPLETE = 40 # Build is complete
2018-04-17 06:58:37 +00:00
2019-04-27 10:35:14 +00:00
#: Build status codes
2018-04-17 14:03:42 +00:00
BUILD_STATUS_CODES = {PENDING: _("Pending"),
ALLOCATED: _("Allocated"),
2018-04-17 14:03:42 +00:00
CANCELLED: _("Cancelled"),
COMPLETE: _("Complete"),
}
2018-04-16 14:32:02 +00:00
2018-04-17 06:58:37 +00:00
status = models.PositiveIntegerField(default=PENDING,
choices=BUILD_STATUS_CODES.items(),
2019-05-08 13:32:57 +00:00
validators=[MinValueValidator(0)],
help_text='Build status')
2019-04-27 10:35:14 +00:00
batch = models.CharField(max_length=100, blank=True, null=True,
help_text='Batch code for this build output')
2019-04-27 10:35:14 +00:00
creation_date = models.DateField(auto_now=True, editable=False)
2019-04-27 10:35:14 +00:00
completion_date = models.DateField(null=True, blank=True)
completed_by = models.ForeignKey(User,
2019-05-02 22:04:30 +00:00
on_delete=models.SET_NULL,
blank=True, null=True,
related_name='builds_completed'
)
URL = models.URLField(blank=True, help_text='Link to external URL')
2019-05-08 13:32:57 +00:00
notes = models.TextField(blank=True, help_text='Extra build notes')
""" Notes attached to each build output """
2019-05-01 12:21:13 +00:00
@transaction.atomic
def cancelBuild(self, user):
""" Mark the Build as CANCELLED
- Delete any pending BuildItem objects (but do not remove items from stock)
2019-05-01 12:21:13 +00:00
- Set build status to CANCELLED
- Save the Build object
"""
2019-05-01 12:21:13 +00:00
for item in self.allocated_stock.all():
2019-05-01 12:21:13 +00:00
item.delete()
2019-05-02 11:23:53 +00:00
# Date of 'completion' is the date the build was cancelled
self.completion_date = datetime.now().date()
self.completed_by = user
2019-05-02 11:23:53 +00:00
2019-05-01 12:21:13 +00:00
self.status = self.CANCELLED
self.save()
def getAutoAllocations(self):
""" Return a list of parts which will be allocated
using the 'AutoAllocate' function.
For each item in the BOM for the attached Part:
- If there is a single StockItem, use that StockItem
- Take as many parts as available (up to the quantity required for the BOM)
- If there are multiple StockItems available, ignore (leave up to the user)
Returns:
A list object containing the StockItem objects to be allocated (and the quantities)
"""
allocations = []
for item in self.part.bom_items.all():
# How many parts required for this build?
q_required = item.quantity * self.quantity
stock = StockItem.objects.filter(part=item.sub_part)
# Ensure that the available stock items are in the correct location
if self.take_from is not None:
# Filter for stock that is located downstream of the designated location
stock = stock.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()])
# Only one StockItem to choose from? Default to that one!
if len(stock) == 1:
stock_item = stock[0]
# Check that we have not already allocated this stock-item against this build
build_items = BuildItem.objects.filter(build=self, stock_item=stock_item)
if len(build_items) > 0:
continue
# Are there any parts available?
if stock_item.quantity > 0:
# Only take as many as are available
if stock_item.quantity < q_required:
q_required = stock_item.quantity
allocation = {
'stock_item': stock_item,
'quantity': q_required,
}
allocations.append(allocation)
return allocations
@transaction.atomic
def unallocateStock(self):
""" Deletes all stock allocations for this build. """
BuildItem.objects.filter(build=self.id).delete()
@transaction.atomic
def autoAllocate(self):
""" Run auto-allocation routine to allocate StockItems to this Build.
Returns a list of dict objects with keys like:
{
'stock_item': item,
'quantity': quantity,
}
See: getAutoAllocations()
"""
allocations = self.getAutoAllocations()
for item in allocations:
# Create a new allocation
build_item = BuildItem(
build=self,
stock_item=item['stock_item'],
quantity=item['quantity'])
build_item.save()
2019-05-01 12:21:13 +00:00
@transaction.atomic
def completeBuild(self, location, user):
""" Mark the Build as COMPLETE
- Takes allocated items from stock
- Delete pending BuildItem objects
"""
for item in self.allocated_stock.all():
# 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
)
)
# Delete the item
item.delete()
# Mark the date of completion
self.completion_date = datetime.now().date()
self.completed_by = user
# Add stock of the newly created item
item = StockItem.objects.create(
part=self.part,
location=location,
quantity=self.quantity,
batch=str(self.batch) if self.batch else '',
notes='Built {q} on {now}'.format(
q=self.quantity,
now=str(datetime.now().date())
)
)
item.save()
# Finally, mark the build as complete
self.status = self.COMPLETE
self.save()
2019-05-07 13:03:05 +00:00
def getRequiredQuantity(self, part):
""" Calculate the quantity of <part> required to make this build.
"""
try:
item = BomItem.objects.get(part=self.part.id, sub_part=part.id)
2019-05-14 21:23:02 +00:00
return item.get_required_quantity(self.quantity)
2019-05-07 13:03:05 +00:00
except BomItem.DoesNotExist:
return 0
def getAllocatedQuantity(self, part):
""" Calculate the total number of <part> currently allocated to this build
"""
allocated = BuildItem.objects.filter(build=self.id, stock_item__part=part.id).aggregate(Sum('quantity'))
q = allocated['quantity__sum']
if q:
return int(q)
else:
return 0
def getUnallocatedQuantity(self, part):
""" Calculate the quantity of <part> which still needs to be allocated to this build.
Args:
Part - the part to be tested
Returns:
The remaining allocated quantity
"""
2019-05-07 13:03:05 +00:00
return max(self.getRequiredQuantity(part) - self.getAllocatedQuantity(part), 0)
2018-04-30 22:55:51 +00:00
@property
def required_parts(self):
2019-04-27 10:35:14 +00:00
""" Returns a dict of parts required to build this part (BOM) """
2018-04-30 22:55:51 +00:00
parts = []
for item in self.part.bom_items.all():
part = {'part': item.sub_part,
'per_build': item.quantity,
'quantity': item.quantity * self.quantity,
'allocated': self.getAllocatedQuantity(item.sub_part)
2018-04-30 22:55:51 +00:00
}
parts.append(part)
return parts
@property
def can_build(self):
2019-04-27 10:35:14 +00:00
""" Return true if there are enough parts to supply build """
for item in self.required_parts:
if item['part'].total_stock < item['quantity']:
return False
return True
@property
def is_active(self):
2019-04-27 10:35:14 +00:00
""" Is this build active? An active build is either:
- PENDING
- HOLDING
"""
return self.status in [
self.PENDING,
]
@property
def is_complete(self):
2019-04-27 10:35:14 +00:00
""" Returns True if the build status is COMPLETE """
return self.status == self.COMPLETE
class BuildItem(models.Model):
""" A BuildItem links multiple StockItem objects to a Build.
These are used to allocate part stock to a build.
Once the Build is completed, the parts are removed from stock and the
BuildItemAllocation objects are removed.
Attributes:
build: Link to a Build object
stock_item: Link to a StockItem object
quantity: Number of units allocated
"""
def get_absolute_url(self):
# TODO - Fix!
return '/build/item/{pk}/'.format(pk=self.id)
2019-04-30 09:17:54 +00:00
# return reverse('build-detail', kwargs={'pk': self.id})
class Meta:
unique_together = [
('build', 'stock_item'),
]
def clean(self):
""" Check validity of the BuildItem model.
The following checks are performed:
- StockItem.part must be in the BOM of the Part object referenced by Build
2019-04-30 06:35:55 +00:00
- Allocation quantity cannot exceed available quantity
"""
2019-04-30 06:35:55 +00:00
super(BuildItem, self).clean()
errors = {}
if self.stock_item is not None and self.stock_item.part is not None:
if self.stock_item.part not in self.build.part.required_parts():
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'".format(p=self.build.part.full_name))]
2019-04-30 06:35:55 +00:00
if self.stock_item is not None and self.quantity > self.stock_item.quantity:
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})".format(
2019-04-30 06:35:55 +00:00
n=self.quantity,
q=self.stock_item.quantity
))]
2019-04-30 06:35:55 +00:00
if len(errors) > 0:
raise ValidationError(errors)
build = models.ForeignKey(
Build,
on_delete=models.CASCADE,
related_name='allocated_stock',
2019-05-01 12:21:13 +00:00
help_text='Build to allocate parts'
)
2019-04-29 12:33:39 +00:00
stock_item = models.ForeignKey(
'stock.StockItem',
on_delete=models.CASCADE,
related_name='allocations',
help_text='Stock Item to allocate to build',
)
quantity = models.PositiveIntegerField(
default=1,
validators=[MinValueValidator(1)],
help_text='Stock quantity to allocate to build'
)