mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Lots of work towards multiple build output
This commit is contained in:
parent
f1b83f1c17
commit
b02c87ea50
@ -6,13 +6,14 @@ Django Forms for interacting with Build objects
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django import forms
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from InvenTree.fields import RoundingDecimalFormField
|
from InvenTree.fields import RoundingDecimalFormField
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from .models import Build, BuildItem, BuildOrderAttachment
|
from .models import Build, BuildItem, BuildOrderAttachment
|
||||||
from stock.models import StockLocation
|
|
||||||
|
from stock.models import StockLocation, StockItem
|
||||||
|
|
||||||
|
|
||||||
class EditBuildForm(HelperForm):
|
class EditBuildForm(HelperForm):
|
||||||
@ -106,18 +107,18 @@ class UnallocateBuildForm(HelperForm):
|
|||||||
class AutoAllocateForm(HelperForm):
|
class AutoAllocateForm(HelperForm):
|
||||||
""" Form for auto-allocation of stock to a build """
|
""" Form for auto-allocation of stock to a build """
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=False, help_text=_('Confirm'))
|
confirm = forms.BooleanField(required=True, help_text=_('Confirm stock allocation'))
|
||||||
|
|
||||||
output_id = forms.IntegerField(
|
# Keep track of which build output we are interested in
|
||||||
required=False,
|
output = forms.ModelChoiceField(
|
||||||
widget=forms.HiddenInput()
|
queryset=StockItem.objects.all(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Build
|
model = Build
|
||||||
fields = [
|
fields = [
|
||||||
'confirm',
|
'confirm',
|
||||||
'output_id',
|
'output',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -136,20 +137,25 @@ class CompleteBuildForm(HelperForm):
|
|||||||
help_text=_('Location of completed parts'),
|
help_text=_('Location of completed parts'),
|
||||||
)
|
)
|
||||||
|
|
||||||
serial_numbers = forms.CharField(
|
confirm_incomplete = forms.BooleanField(
|
||||||
label=_('Serial numbers'),
|
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('Enter unique serial numbers (or leave blank)')
|
help_text=_("Confirm completion with incomplete stock allocation")
|
||||||
)
|
)
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=False, help_text=_('Confirm build completion'))
|
confirm = forms.BooleanField(required=True, help_text=_('Confirm build completion'))
|
||||||
|
|
||||||
|
output = forms.ModelChoiceField(
|
||||||
|
queryset=StockItem.objects.all(), # Queryset is narrowed in the view
|
||||||
|
widget=forms.HiddenInput(),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Build
|
model = Build
|
||||||
fields = [
|
fields = [
|
||||||
'serial_numbers',
|
|
||||||
'location',
|
'location',
|
||||||
'confirm'
|
'output',
|
||||||
|
'confirm',
|
||||||
|
'confirm_incomplete',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
from InvenTree.helpers import increment, getSetting, normalize
|
from InvenTree.helpers import increment, getSetting, normalize
|
||||||
from InvenTree.helpers import ExtractSerialNumbers
|
|
||||||
from InvenTree.validators import validate_build_order_reference
|
from InvenTree.validators import validate_build_order_reference
|
||||||
from InvenTree.models import InvenTreeAttachment
|
from InvenTree.models import InvenTreeAttachment
|
||||||
|
|
||||||
@ -183,6 +182,16 @@ class Build(MPTTModel):
|
|||||||
blank=True, help_text=_('Extra build notes')
|
blank=True, help_text=_('Extra build notes')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bom_items(self):
|
||||||
|
"""
|
||||||
|
Returns the BOM items for the part referenced by this BuildOrder
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.part.bom_items.all().prefetch_related(
|
||||||
|
'sub_part'
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remaining(self):
|
def remaining(self):
|
||||||
"""
|
"""
|
||||||
@ -195,9 +204,13 @@ class Build(MPTTModel):
|
|||||||
def output_count(self):
|
def output_count(self):
|
||||||
return self.build_outputs.count()
|
return self.build_outputs.count()
|
||||||
|
|
||||||
def getBuildOutputs(self, **kwargs):
|
def get_build_outputs(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return a list of build outputs
|
Return a list of build outputs.
|
||||||
|
|
||||||
|
kwargs:
|
||||||
|
complete = (True / False) - If supplied, filter by completed status
|
||||||
|
in_stock = (True / False) - If supplied, filter by 'in-stock' status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
outputs = self.build_outputs
|
outputs = self.build_outputs
|
||||||
@ -228,7 +241,7 @@ class Build(MPTTModel):
|
|||||||
Return all the "completed" build outputs
|
Return all the "completed" build outputs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
outputs = self.getBuildOutputs(complete=True)
|
outputs = self.get_build_outputs(complete=True)
|
||||||
|
|
||||||
# TODO - Ordering?
|
# TODO - Ordering?
|
||||||
|
|
||||||
@ -240,7 +253,7 @@ class Build(MPTTModel):
|
|||||||
Return all the "incomplete" build outputs"
|
Return all the "incomplete" build outputs"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
outputs = self.getBuildOutputs(complete=False)
|
outputs = self.get_build_outputs(complete=False)
|
||||||
|
|
||||||
# TODO - Order by how "complete" they are?
|
# TODO - Order by how "complete" they are?
|
||||||
|
|
||||||
@ -278,49 +291,6 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
return new_ref
|
return new_ref
|
||||||
|
|
||||||
def createInitialStockItem(self, serial_numbers, user):
|
|
||||||
"""
|
|
||||||
Create an initial output StockItem to be completed by this build.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.part.trackable:
|
|
||||||
# Trackable part? Create individual build outputs?
|
|
||||||
|
|
||||||
# Serial numbers specified for the build?
|
|
||||||
if serial_numbers:
|
|
||||||
serials = ExtractSerialNumbers(serial_numbers, self.quantity)
|
|
||||||
else:
|
|
||||||
serials = [None] * self.quantity
|
|
||||||
|
|
||||||
for serial in serials:
|
|
||||||
output = StockModels.StockItem.objects.create(
|
|
||||||
part=self.part,
|
|
||||||
location=self.destination,
|
|
||||||
quantity=1,
|
|
||||||
batch=self.batch,
|
|
||||||
serial=serial,
|
|
||||||
build=self,
|
|
||||||
is_building=True
|
|
||||||
)
|
|
||||||
|
|
||||||
output.save()
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Create a single build output
|
|
||||||
|
|
||||||
output = StockModels.StockItem.objects.create(
|
|
||||||
part=self.part, # Link to the parent part
|
|
||||||
location=None, # No location (yet) until it is completed
|
|
||||||
quantity=self.quantity,
|
|
||||||
batch='', # The 'batch' code is not set until the item is completed
|
|
||||||
build=self, # Point back to this build
|
|
||||||
is_building=True, # Mark this StockItem as building
|
|
||||||
)
|
|
||||||
|
|
||||||
output.save()
|
|
||||||
|
|
||||||
# TODO - Add a transaction note to the new StockItem
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def cancelBuild(self, user):
|
def cancelBuild(self, user):
|
||||||
""" Mark the Build as CANCELLED
|
""" Mark the Build as CANCELLED
|
||||||
@ -368,59 +338,49 @@ class Build(MPTTModel):
|
|||||||
Iterate through each item in the BOM
|
Iterate through each item in the BOM
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Only look at the "untracked" BOM items
|
for bom_item in self.bom_items:
|
||||||
# Tracked BOM items must be handled separately
|
|
||||||
untracked_bom_items = self.part.bom_items.filter(sub_part__trackable=False)
|
|
||||||
|
|
||||||
for item in untracked_bom_items.prefetch_related('sub_part'):
|
part = bom_item.sub_part
|
||||||
|
|
||||||
# How many parts are still required for this build?
|
# Skip any parts which are already fully allocated
|
||||||
#q_required = item.quantity * self.remaining
|
if self.isPartFullyAllocated(part, output):
|
||||||
q_required = self.getUnallocatedQuantity(item.sub_part)
|
continue
|
||||||
|
|
||||||
# Grab a list of StockItem objects which are "in stock"
|
# How many parts are required to complete the output?
|
||||||
stock = StockModels.StockItem.objects.filter(
|
required = self.unallocatedQuantity(part, output)
|
||||||
StockModels.StockItem.IN_STOCK_FILTER
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter by part reference
|
# Grab a list of stock items which are available
|
||||||
stock = stock.filter(part=item.sub_part)
|
stock_items = self.availableStockItems(part, output)
|
||||||
|
|
||||||
# Exclude any stock items which have already been allocated to this build
|
|
||||||
allocated = BuildItem.objects.filter(
|
|
||||||
build=self,
|
|
||||||
stock_item__part=item.sub_part
|
|
||||||
)
|
|
||||||
|
|
||||||
stock = stock.exclude(
|
|
||||||
pk__in=[build_item.stock_item.pk for build_item in allocated]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure that the available stock items are in the correct location
|
# Ensure that the available stock items are in the correct location
|
||||||
if self.take_from is not None:
|
if self.take_from is not None:
|
||||||
# Filter for stock that is located downstream of the designated location
|
# Filter for stock that is located downstream of the designated location
|
||||||
stock = stock.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()])
|
stock_items = stock_items.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()])
|
||||||
|
|
||||||
# Only one StockItem to choose from? Default to that one!
|
# Only one StockItem to choose from? Default to that one!
|
||||||
if stock.count() == 1:
|
if stock_items.count() == 1:
|
||||||
stock_item = stock[0]
|
stock_item = stock_items[0]
|
||||||
|
|
||||||
# Check that we have not already allocated this stock-item against this build
|
# Double check that we have not already allocated this stock-item against this build
|
||||||
build_items = BuildItem.objects.filter(build=self, stock_item=stock_item)
|
build_items = BuildItem.objects.filter(
|
||||||
|
build=self,
|
||||||
|
stock_item=stock_item,
|
||||||
|
install_into=output
|
||||||
|
)
|
||||||
|
|
||||||
if len(build_items) > 0:
|
if len(build_items) > 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Are there any parts available?
|
# How many items are actually available?
|
||||||
if stock_item.quantity > 0:
|
if stock_item.quantity > 0:
|
||||||
|
|
||||||
# Only take as many as are available
|
# Only take as many as are available
|
||||||
if stock_item.quantity < q_required:
|
if stock_item.quantity < required:
|
||||||
q_required = stock_item.quantity
|
required = stock_item.quantity
|
||||||
|
|
||||||
allocation = {
|
allocation = {
|
||||||
'stock_item': stock_item,
|
'stock_item': stock_item,
|
||||||
'quantity': q_required,
|
'quantity': required,
|
||||||
}
|
}
|
||||||
|
|
||||||
allocations.append(allocation)
|
allocations.append(allocation)
|
||||||
@ -472,7 +432,7 @@ class Build(MPTTModel):
|
|||||||
output.delete()
|
output.delete()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def auto_allocate(self, output=None):
|
def autoAllocate(self, output):
|
||||||
"""
|
"""
|
||||||
Run auto-allocation routine to allocate StockItems to this Build.
|
Run auto-allocation routine to allocate StockItems to this Build.
|
||||||
|
|
||||||
@ -496,151 +456,160 @@ class Build(MPTTModel):
|
|||||||
build_item = BuildItem(
|
build_item = BuildItem(
|
||||||
build=self,
|
build=self,
|
||||||
stock_item=item['stock_item'],
|
stock_item=item['stock_item'],
|
||||||
quantity=item['quantity'])
|
quantity=item['quantity'],
|
||||||
|
install_into=output,
|
||||||
|
)
|
||||||
|
|
||||||
build_item.save()
|
build_item.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def completeBuild(self, location, serial_numbers, user):
|
def completeBuildOutput(self, output, user, **kwargs):
|
||||||
""" Mark the Build as COMPLETE
|
"""
|
||||||
|
Complete a particular build output
|
||||||
|
|
||||||
- Takes allocated items from stock
|
- Remove allocated StockItems
|
||||||
- Delete pending BuildItem objects
|
- Mark the output as complete
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Complete the build allocation for each BuildItem
|
# List the allocated BuildItem objects for the given output
|
||||||
for build_item in self.allocated_stock.all().prefetch_related('stock_item'):
|
allocated_items = output.items_to_install.all()
|
||||||
build_item.complete_allocation(user)
|
|
||||||
|
|
||||||
# Check that the stock-item has been assigned to this build, and remove the builditem from the database
|
for build_item in allocated_items:
|
||||||
if build_item.stock_item.build_order == self:
|
|
||||||
|
# Complete the allocation of stock for that item
|
||||||
|
build_item.completeAllocation(user)
|
||||||
|
|
||||||
|
# Remove the build item from the database
|
||||||
build_item.delete()
|
build_item.delete()
|
||||||
|
|
||||||
notes = 'Built {q} on {now}'.format(
|
# Ensure that the output is updated correctly
|
||||||
q=self.quantity,
|
output.build = self
|
||||||
now=str(datetime.now().date())
|
output.is_building = False
|
||||||
|
|
||||||
|
output.save()
|
||||||
|
|
||||||
|
output.addTransactionNote(
|
||||||
|
_('Completed build output'),
|
||||||
|
user,
|
||||||
|
system=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate the build outputs
|
# Increase the completed quantity for this build
|
||||||
if self.part.trackable and serial_numbers:
|
self.completed += output.quantity
|
||||||
# Add new serial numbers
|
|
||||||
for serial in serial_numbers:
|
|
||||||
item = StockModels.StockItem.objects.create(
|
|
||||||
part=self.part,
|
|
||||||
build=self,
|
|
||||||
location=location,
|
|
||||||
quantity=1,
|
|
||||||
serial=serial,
|
|
||||||
batch=str(self.batch) if self.batch else '',
|
|
||||||
notes=notes
|
|
||||||
)
|
|
||||||
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Add stock of the newly created item
|
|
||||||
item = StockModels.StockItem.objects.create(
|
|
||||||
part=self.part,
|
|
||||||
build=self,
|
|
||||||
location=location,
|
|
||||||
quantity=self.quantity,
|
|
||||||
batch=str(self.batch) if self.batch else '',
|
|
||||||
notes=notes
|
|
||||||
)
|
|
||||||
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
# Finally, mark the build as complete
|
|
||||||
self.completion_date = datetime.now().date()
|
|
||||||
self.completed_by = user
|
|
||||||
self.status = BuildStatus.COMPLETE
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
return True
|
def requiredQuantity(self, part, output):
|
||||||
|
|
||||||
def isFullyAllocated(self):
|
|
||||||
"""
|
"""
|
||||||
Return True if this build has been fully allocated.
|
Get the quantity of a part required to complete the particular build output.
|
||||||
"""
|
|
||||||
|
|
||||||
bom_items = self.part.bom_items.all()
|
|
||||||
|
|
||||||
for item in bom_items:
|
|
||||||
part = item.sub_part
|
|
||||||
|
|
||||||
if not self.isPartFullyAllocated(part):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def isPartFullyAllocated(self, part):
|
|
||||||
"""
|
|
||||||
Check if a given Part is fully allocated for this Build
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.getAllocatedQuantity(part) >= self.getRequiredQuantity(part)
|
|
||||||
|
|
||||||
def getRequiredQuantity(self, part, output=None):
|
|
||||||
"""
|
|
||||||
Calculate the quantity of <part> required to make this build.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
part: The 'Part' archetype reference
|
part: The Part object
|
||||||
output: A particular build output (StockItem) (or None to specify the entire build)
|
output - The particular build output (StockItem)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Extract the BOM line item from the database
|
||||||
try:
|
try:
|
||||||
item = PartModels.BomItem.objects.get(part=self.part.id, sub_part=part.id)
|
bom_item = PartModels.BomItem.objects.get(part=self.part.pk, sub_part=part.pk)
|
||||||
q = item.quantity
|
quantity = bom_item.quantity
|
||||||
except PartModels.BomItem.DoesNotExist:
|
except (PartModels.BomItem.DoesNotExist):
|
||||||
q = 0
|
quantity = 0
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
return q * output.quantity
|
quantity *= output.quantity
|
||||||
else:
|
else:
|
||||||
return q * self.remaining
|
quantity *= self.remaining
|
||||||
|
|
||||||
def getAllocatedQuantity(self, part, output=None):
|
return quantity
|
||||||
|
|
||||||
|
def allocatedItems(self, part, output):
|
||||||
"""
|
"""
|
||||||
Calculate the total number of <part> currently allocated to this build.
|
Return all BuildItem objects which allocate stock of <part> to <output>
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
part: The 'Part' archetype reference
|
part - The part object
|
||||||
output: A particular build output (StockItem) (or None to specify the entire build)
|
output - Build output (StockItem).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allocations = BuildItem.objects.filter(
|
allocations = BuildItem.objects.filter(
|
||||||
build=self.id,
|
build=self,
|
||||||
stock_item__part=part.id
|
stock_item__part=part,
|
||||||
|
install_into=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optionally, filter by the specified build output StockItem
|
return allocations
|
||||||
if output is not None:
|
|
||||||
allocations = allocations.filter(
|
def allocatedQuantity(self, part, output):
|
||||||
install_into=output
|
"""
|
||||||
)
|
Return the total quantity of given part allocated to a given build output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
allocations = self.allocatedItems(part, output)
|
||||||
|
|
||||||
allocated = allocations.aggregate(q=Coalesce(Sum('quantity'), 0))
|
allocated = allocations.aggregate(q=Coalesce(Sum('quantity'), 0))
|
||||||
|
|
||||||
return allocated['q']
|
return allocated['q']
|
||||||
|
|
||||||
def getUnallocatedQuantity(self, part, output=None):
|
def unallocatedQuantity(self, part, output):
|
||||||
"""
|
"""
|
||||||
Calculate the quantity of <part> which still needs to be allocated to this build.
|
Return the total unallocated (remaining) quantity of a part against a particular output.
|
||||||
|
|
||||||
Args:
|
|
||||||
part - the part to be tested
|
|
||||||
output - A particular build output (StockItem) (or None to specify the entire build)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The remaining allocated quantity
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
required = self.getRequiredQuantity(part, output=output)
|
required = self.requiredQuantity(part, output)
|
||||||
allocated = self.getAllocatedQuantity(part, output=output)
|
allocated = self.allocatedQuantity(part, output)
|
||||||
|
|
||||||
return max(required - allocated, 0)
|
return max(required - allocated, 0)
|
||||||
|
|
||||||
|
def isPartFullyAllocated(self, part, output):
|
||||||
|
"""
|
||||||
|
Returns True if the part has been fully allocated to the particular build output
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.unallocatedQuantity(part, output) == 0
|
||||||
|
|
||||||
|
def isFullyAllocated(self, output):
|
||||||
|
"""
|
||||||
|
Returns True if the particular build output is fully allocated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for bom_item in self.bom_items:
|
||||||
|
part = bom_item.sub_part
|
||||||
|
|
||||||
|
if not self.isPartFullyAllocated(part, output):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# All parts must be fully allocated!
|
||||||
|
return True
|
||||||
|
|
||||||
|
def allocatedParts(self, output):
|
||||||
|
"""
|
||||||
|
Return a list of parts which have been fully allocated against a particular output
|
||||||
|
"""
|
||||||
|
|
||||||
|
allocated = []
|
||||||
|
|
||||||
|
for bom_item in self.bom_items:
|
||||||
|
part = bom_item.sub_part
|
||||||
|
|
||||||
|
if self.isPartFullyAllocated(part, output):
|
||||||
|
allocated.append(part)
|
||||||
|
|
||||||
|
return allocated
|
||||||
|
|
||||||
|
def unallocatedParts(self, output):
|
||||||
|
"""
|
||||||
|
Return a list of parts which have *not* been fully allocated against a particular output
|
||||||
|
"""
|
||||||
|
|
||||||
|
unallocated = []
|
||||||
|
|
||||||
|
for bom_item in self.bom_items:
|
||||||
|
part = bom_item.sub_part
|
||||||
|
|
||||||
|
if not self.isPartFullyAllocated(part, output):
|
||||||
|
unallocated.append(part)
|
||||||
|
|
||||||
|
return unallocated
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def required_parts(self):
|
def required_parts(self):
|
||||||
""" Returns a dict of parts required to build this part (BOM) """
|
""" Returns a dict of parts required to build this part (BOM) """
|
||||||
@ -658,28 +627,31 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
def getAvailableStockItems(self, part=None, output=None):
|
def availableStockItems(self, part, output):
|
||||||
"""
|
"""
|
||||||
Return available stock items for the build.
|
Returns stock items which are available for allocation to this build.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part - Part object
|
||||||
|
output - The particular build output
|
||||||
"""
|
"""
|
||||||
|
|
||||||
items = StockModels.StockItem.objects.filter(StockModels.StockItem.IN_STOCK_FILTER)
|
# Grab initial query for items which are "in stock" and match the part
|
||||||
|
items = StockModels.StockItem.objects.filter(
|
||||||
|
StockModels.StockItem.IN_STOCK_FILTER
|
||||||
|
)
|
||||||
|
|
||||||
if part:
|
|
||||||
# Filter items which match the given Part
|
|
||||||
items = items.filter(part=part)
|
items = items.filter(part=part)
|
||||||
|
|
||||||
if output:
|
# Exclude any items which have already been allocated
|
||||||
# Exclude items which are already allocated to the particular build output
|
allocated = BuildItem.objects.filter(
|
||||||
|
|
||||||
to_exclude = BuildItem.objects.filter(
|
|
||||||
build=self,
|
build=self,
|
||||||
stock_item__part=part,
|
stock_item__part=part,
|
||||||
install_into=output
|
install_into=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
items = items.exclude(
|
items = items.exclude(
|
||||||
id__in=[item.stock_item.id for item in to_exclude.all()]
|
id__in=[item.stock_item.id for item in allocated.all()]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Limit query to stock items which are "downstream" of the source location
|
# Limit query to stock items which are "downstream" of the source location
|
||||||
@ -690,16 +662,6 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@property
|
|
||||||
def can_build(self):
|
|
||||||
""" 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
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
""" Is this build active? An active build is either:
|
""" Is this build active? An active build is either:
|
||||||
@ -807,31 +769,23 @@ class BuildItem(models.Model):
|
|||||||
errors['quantity'] = _('Allocation quantity must be greater than zero')
|
errors['quantity'] = _('Allocation quantity must be greater than zero')
|
||||||
|
|
||||||
# Quantity must be 1 for serialized stock
|
# Quantity must be 1 for serialized stock
|
||||||
if self.stock_item.serial and not self.quantity == 1:
|
if self.stock_item.serialized and not self.quantity == 1:
|
||||||
errors['quantity'] = _('Quantity must be 1 for serialized stock')
|
errors['quantity'] = _('Quantity must be 1 for serialized stock')
|
||||||
|
|
||||||
# Part reference must match between output stock item and built part
|
|
||||||
if self.install_into is not None:
|
|
||||||
if not self.install_into.part == self.build.part:
|
|
||||||
errors['install_into'] = _('Part reference differs between build and build output')
|
|
||||||
|
|
||||||
# A trackable StockItem *must* point to a build output
|
|
||||||
if self.stock_item.part.trackable and self.install_into is None:
|
|
||||||
errors['install_into'] = _('Trackable BuildItem must reference a build output')
|
|
||||||
|
|
||||||
# A non-trackable StockItem *must not* point to a build output
|
|
||||||
if not self.stock_item.part.trackable and self.install_into is not None:
|
|
||||||
errors['install_into'] = _('Non-trackable BuildItem must not reference a build output')
|
|
||||||
|
|
||||||
except (StockModels.StockItem.DoesNotExist, PartModels.Part.DoesNotExist):
|
except (StockModels.StockItem.DoesNotExist, PartModels.Part.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
def complete_allocation(self, user):
|
@transaction.atomic
|
||||||
|
def completeAllocation(self, user):
|
||||||
|
"""
|
||||||
|
Complete the allocation of this BuildItem into the output stock item.
|
||||||
|
|
||||||
# TODO : This required much reworking!!
|
- If the referenced part is trackable, the stock item will be *installed* into the build output
|
||||||
|
- If the referenced part is *not* trackable, the stock item will be removed from stock
|
||||||
|
"""
|
||||||
|
|
||||||
item = self.stock_item
|
item = self.stock_item
|
||||||
|
|
||||||
@ -843,10 +797,13 @@ class BuildItem(models.Model):
|
|||||||
self.stock_item = item
|
self.stock_item = item
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
# TODO - If the item__part object is not trackable, delete the stock item here
|
if item.part.trackable:
|
||||||
|
# If the part is trackable, install into the build output
|
||||||
item.build_order = self.build
|
item.belongs_to = self.install_into
|
||||||
item.save()
|
item.save()
|
||||||
|
else:
|
||||||
|
# Part is *not* trackable, so just delete it
|
||||||
|
item.delete()
|
||||||
|
|
||||||
build = models.ForeignKey(
|
build = models.ForeignKey(
|
||||||
Build,
|
Build,
|
||||||
|
@ -11,6 +11,9 @@ InvenTree | Allocate Parts
|
|||||||
|
|
||||||
{% include "build/tabs.html" with tab='allocate' %}
|
{% include "build/tabs.html" with tab='allocate' %}
|
||||||
|
|
||||||
|
<div class='btn-group' role='group'>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4>{% trans "Incomplete Build Ouputs" %}</h4>
|
<h4>{% trans "Incomplete Build Ouputs" %}</h4>
|
||||||
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
||||||
@ -19,9 +22,7 @@ InvenTree | Allocate Parts
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<!---
|
||||||
<h4>{% trans "Assigned Stock" %}</h4>
|
|
||||||
|
|
||||||
<div id='build-item-toolbar'>
|
<div id='build-item-toolbar'>
|
||||||
{% if build.status == BuildStatus.PENDING %}
|
{% if build.status == BuildStatus.PENDING %}
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
@ -31,8 +32,7 @@ InvenTree | Allocate Parts
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
<table class='table table-striped table-condensed' id='build-item-list' data-toolbar='#build-item-toolbar'></table>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -46,14 +46,6 @@ InvenTree | Allocate Parts
|
|||||||
part: {{ build.part.pk }},
|
part: {{ build.part.pk }},
|
||||||
};
|
};
|
||||||
|
|
||||||
loadBuildOutputAllocationTable(
|
|
||||||
buildInfo,
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
table: '#build-item-list',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
{% for item in build.incomplete_outputs %}
|
{% for item in build.incomplete_outputs %}
|
||||||
// Get the build output as a javascript object
|
// Get the build output as a javascript object
|
||||||
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
||||||
|
@ -7,18 +7,9 @@
|
|||||||
|
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
<b>{% trans "Automatically Allocate Stock" %}</b><br>
|
<b>{% trans "Automatically Allocate Stock" %}</b><br>
|
||||||
{% trans "Where the following conditions are met, stock will be automatically allocated to this build" %}:<br>
|
{% trans "The following stock items will be allocated to the specified build output" %}
|
||||||
<hr>
|
|
||||||
{% trans "For each part in the BOM, the following tests are performed" %}:<br>
|
|
||||||
<ul>
|
|
||||||
<li>{% trans "The part is not marked as trackable" %}</li>
|
|
||||||
<li>{% trans "Only single stock items exists" %}</li>
|
|
||||||
<li>{% trans "The stock item is not already allocated to this build" %}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if allocations %}
|
{% if allocations %}
|
||||||
|
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
@ -1,36 +1,47 @@
|
|||||||
{% extends "modal_form.html" %}
|
{% extends "modal_form.html" %}
|
||||||
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
<h4>{% trans "Build" %} - {{ build }}</h4>
|
{% if fully_allocated %}
|
||||||
|
|
||||||
{% if build.isFullyAllocated %}
|
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
<h4>{% trans "Build order allocation is complete" %}</h4>
|
<h4>{% trans "Stock allocation is complete" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class='alert alert-block alert-danger'>
|
<div class='alert alert-block alert-danger'>
|
||||||
<h4>{% trans "Warning: Build order allocation is not complete" %}</h4>
|
<h4>{% trans "Stock allocation is incomplete" %}</h4>
|
||||||
{% trans "Build Order has not been fully allocated. Ensure that all Stock Items have been allocated to the Build" %}
|
|
||||||
|
<div class='panel-group'>
|
||||||
|
<div class='panel panel-default'>
|
||||||
|
<div class='panel panel-heading'>
|
||||||
|
<a data-toggle='collapse' href='#collapse-unallocated'>
|
||||||
|
{{ unallocated_parts|length }} {% trans "parts have not been fully allocated" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class='panel-collapse collapse' id='collapse-unallocated'>
|
||||||
|
<div class='panel-body'>
|
||||||
|
<ul class='list-group'>
|
||||||
|
{% for part in unallocated_parts %}
|
||||||
|
<li class='list-group-item'>
|
||||||
|
{% include "hover_image.html" with image=part.image %} {{ part }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class='alert alert-block alert-success'>
|
<div class='panel panel-info'>
|
||||||
<h4>{% trans "The following actions will be performed:" %}</h4>
|
|
||||||
<ul>
|
|
||||||
<li>{% trans "Remove allocated items from stock" %}</li>
|
|
||||||
<li>{% trans "Add completed items to stock" %}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='panel panel-default'>
|
|
||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
{% trans "The following items will be created" %}
|
{% trans "The following items will be created" %}
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% include "hover_image.html" with image=build.part.image hover=True %}
|
{% include "hover_image.html" with image=build.part.image hover=True %}
|
||||||
{{ build.quantity }} x {{ build.part.full_name }}
|
{% decimal output.quantity %} x {{ output.part.full_name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -114,19 +114,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if build.is_active %}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>{% trans "Enough Parts?" %}</td>
|
|
||||||
<td>
|
|
||||||
{% if build.can_build %}
|
|
||||||
{% trans "Yes" %}
|
|
||||||
{% else %}
|
|
||||||
{% trans "No" %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if build.completion_date %}
|
{% if build.completion_date %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
|
@ -82,12 +82,10 @@ class BuildTest(TestCase):
|
|||||||
self.assertEqual(self.build.getRequiredQuantity(self.sub_part_1), 100)
|
self.assertEqual(self.build.getRequiredQuantity(self.sub_part_1), 100)
|
||||||
self.assertEqual(self.build.getRequiredQuantity(self.sub_part_2), 250)
|
self.assertEqual(self.build.getRequiredQuantity(self.sub_part_2), 250)
|
||||||
|
|
||||||
self.assertTrue(self.build.can_build)
|
|
||||||
self.assertFalse(self.build.is_complete)
|
self.assertFalse(self.build.is_complete)
|
||||||
|
|
||||||
# Delete some stock and see if the build can still be completed
|
# Delete some stock and see if the build can still be completed
|
||||||
self.stock_2_1.delete()
|
self.stock_2_1.delete()
|
||||||
self.assertFalse(self.build.can_build)
|
|
||||||
|
|
||||||
def test_build_item_clean(self):
|
def test_build_item_clean(self):
|
||||||
# Ensure that dodgy BuildItem objects cannot be created
|
# Ensure that dodgy BuildItem objects cannot be created
|
||||||
|
@ -95,6 +95,9 @@ class BuildAutoAllocate(AjaxUpdateView):
|
|||||||
role_required = 'build.change'
|
role_required = 'build.change'
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
|
"""
|
||||||
|
Initial values for the form.
|
||||||
|
"""
|
||||||
|
|
||||||
initials = super().get_initial()
|
initials = super().get_initial()
|
||||||
|
|
||||||
@ -102,62 +105,73 @@ class BuildAutoAllocate(AjaxUpdateView):
|
|||||||
output = self.get_param('output')
|
output = self.get_param('output')
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
initials['output_id'] = output
|
try:
|
||||||
|
output = StockItem.objects.get(pk=output)
|
||||||
|
initials['output'] = output
|
||||||
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
""" Get the context data for form rendering. """
|
"""
|
||||||
|
Get the context data for form rendering.
|
||||||
|
"""
|
||||||
|
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
output = self.get_form()['output_id'].value()
|
|
||||||
|
|
||||||
try:
|
|
||||||
build = Build.objects.get(id=self.kwargs['pk'])
|
|
||||||
context['build'] = build
|
|
||||||
context['allocations'] = build.getAutoAllocations(output)
|
|
||||||
except Build.DoesNotExist:
|
|
||||||
context['error'] = _('No matching build found')
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
""" Handle POST request. Perform auto allocations.
|
|
||||||
|
|
||||||
- If the form validation passes, perform allocations
|
|
||||||
- Otherwise, the form is passed back to the client
|
|
||||||
"""
|
|
||||||
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
|
|
||||||
confirm = request.POST.get('confirm', False)
|
output_id = form['output'].value()
|
||||||
|
|
||||||
output = None
|
|
||||||
output_id = request.POST.get('output_id', None)
|
|
||||||
|
|
||||||
if output_id:
|
|
||||||
try:
|
try:
|
||||||
output = StockItem.objects.get(pk=output_id)
|
output = StockItem.objects.get(pk=output_id)
|
||||||
except (ValueError, StockItem.DoesNotExist):
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
pass
|
output = None
|
||||||
|
|
||||||
valid = False
|
if output:
|
||||||
|
context['output'] = output
|
||||||
|
context['allocations'] = build.getAutoAllocations(output)
|
||||||
|
|
||||||
if confirm:
|
context['build'] = build
|
||||||
build.auto_allocate(output)
|
|
||||||
valid = True
|
|
||||||
else:
|
|
||||||
form.add_error('confirm', _('Confirm stock allocation'))
|
|
||||||
form.add_error(None, _('Check the confirmation box at the bottom of the list'))
|
|
||||||
|
|
||||||
data = {
|
return context
|
||||||
'form_valid': valid,
|
|
||||||
|
def get_form(self):
|
||||||
|
|
||||||
|
form = super().get_form()
|
||||||
|
|
||||||
|
if form['output'].value():
|
||||||
|
# Hide the 'output' field
|
||||||
|
form.fields['output'].widget = HiddenInput()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
def validate(self, build, form, **kwargs):
|
||||||
|
|
||||||
|
output = form.cleaned_data.get('output', None)
|
||||||
|
|
||||||
|
if not output:
|
||||||
|
form.add_error(None, _('Build output must be specified'))
|
||||||
|
|
||||||
|
def post_save(self, build, form, **kwargs):
|
||||||
|
"""
|
||||||
|
Once the form has been validated,
|
||||||
|
perform auto-allocations
|
||||||
|
"""
|
||||||
|
|
||||||
|
build = self.get_object()
|
||||||
|
output = form.cleaned_data.get('output', None)
|
||||||
|
|
||||||
|
build.autoAllocate(output)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return {
|
||||||
|
'success': _('Allocated stock to build output'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, data, context=self.get_context_data())
|
|
||||||
|
|
||||||
|
|
||||||
class BuildOutputDelete(AjaxUpdateView):
|
class BuildOutputDelete(AjaxUpdateView):
|
||||||
"""
|
"""
|
||||||
@ -292,37 +306,60 @@ class BuildComplete(AjaxUpdateView):
|
|||||||
model = Build
|
model = Build
|
||||||
form_class = forms.CompleteBuildForm
|
form_class = forms.CompleteBuildForm
|
||||||
context_object_name = "build"
|
context_object_name = "build"
|
||||||
ajax_form_title = _("Complete Build")
|
ajax_form_title = _("Complete Build Output")
|
||||||
ajax_template_name = "build/complete.html"
|
ajax_template_name = "build/complete.html"
|
||||||
role_required = 'build.change'
|
role_required = 'build.change'
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
""" Get the form object.
|
|
||||||
|
|
||||||
If the part is trackable, include a field for serial numbers.
|
|
||||||
"""
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
form = super().get_form()
|
form = super().get_form()
|
||||||
|
|
||||||
if not build.part.trackable:
|
# Extract the build output object
|
||||||
form.fields.pop('serial_numbers')
|
output = None
|
||||||
else:
|
output_id = form['output'].value()
|
||||||
|
|
||||||
form.field_placeholder['serial_numbers'] = build.part.getSerialNumberString(build.quantity)
|
try:
|
||||||
|
output = StockItem.objects.get(pk=output_id)
|
||||||
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
form.rebuild_layout()
|
if output:
|
||||||
|
if build.isFullyAllocated(output):
|
||||||
|
form.fields['confirm_incomplete'].widget = HiddenInput()
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
def validate(self, build, form, **kwargs):
|
||||||
|
|
||||||
|
data = form.cleaned_data
|
||||||
|
|
||||||
|
output = data.get('output', None)
|
||||||
|
|
||||||
|
if output:
|
||||||
|
|
||||||
|
quantity = data.get('quantity', None)
|
||||||
|
|
||||||
|
if quantity and quantity > output.quantity:
|
||||||
|
form.add_error('quantity', _('Quantity to complete cannot exceed build output quantity'))
|
||||||
|
|
||||||
|
if not build.isFullyAllocated(output):
|
||||||
|
confirm = str2bool(data.get('confirm_incomplete', False))
|
||||||
|
|
||||||
|
if not confirm:
|
||||||
|
form.add_error('confirm_incomplete', _('Confirm completion of incomplete build'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
form.add_error(None, _('Build output must be specified'))
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
""" Get initial form data for the CompleteBuild form
|
""" Get initial form data for the CompleteBuild form
|
||||||
|
|
||||||
- If the part being built has a default location, pre-select that location
|
- If the part being built has a default location, pre-select that location
|
||||||
"""
|
"""
|
||||||
|
|
||||||
initials = super(BuildComplete, self).get_initial().copy()
|
initials = super().get_initial()
|
||||||
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
if build.part.default_location is not None:
|
if build.part.default_location is not None:
|
||||||
@ -332,94 +369,77 @@ class BuildComplete(AjaxUpdateView):
|
|||||||
except StockLocation.DoesNotExist:
|
except StockLocation.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
output = self.get_param('output', None)
|
||||||
|
|
||||||
|
if output:
|
||||||
|
try:
|
||||||
|
output = StockItem.objects.get(pk=output)
|
||||||
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
|
output = None
|
||||||
|
|
||||||
|
# Output has not been supplied? Try to "guess"
|
||||||
|
if not output:
|
||||||
|
|
||||||
|
incomplete = build.get_build_outputs(complete=False)
|
||||||
|
|
||||||
|
if incomplete.count() == 1:
|
||||||
|
output = incomplete[0]
|
||||||
|
|
||||||
|
if output is not None:
|
||||||
|
initials['output'] = output
|
||||||
|
|
||||||
|
initials['location'] = build.destination
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Get context data for passing to the rendered form
|
"""
|
||||||
|
Get context data for passing to the rendered form
|
||||||
|
|
||||||
- Build information is required
|
- Build information is required
|
||||||
"""
|
"""
|
||||||
|
|
||||||
build = Build.objects.get(id=self.kwargs['pk'])
|
build = self.get_object()
|
||||||
|
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
# Build object
|
# Build object
|
||||||
context['build'] = build
|
context['build'] = build
|
||||||
|
|
||||||
# Items to be removed from stock
|
form = self.get_form()
|
||||||
taking = BuildItem.objects.filter(build=build.id)
|
|
||||||
context['taking'] = taking
|
output = form['output'].value()
|
||||||
|
|
||||||
|
if output:
|
||||||
|
try:
|
||||||
|
output = StockItem.objects.get(pk=output)
|
||||||
|
context['output'] = output
|
||||||
|
context['fully_allocated'] = build.isFullyAllocated(output)
|
||||||
|
context['allocated_parts'] = build.allocatedParts(output)
|
||||||
|
context['unallocated_parts'] = build.unallocatedParts(output)
|
||||||
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post_save(self, build, form, **kwargs):
|
||||||
""" Handle POST request. Mark the build as COMPLETE
|
|
||||||
|
|
||||||
- If the form validation passes, the Build objects completeBuild() method is called
|
data = form.cleaned_data
|
||||||
- Otherwise, the form is passed back to the client
|
|
||||||
"""
|
|
||||||
|
|
||||||
build = self.get_object()
|
location = data.get('location', None)
|
||||||
|
output = data.get('output', None)
|
||||||
|
|
||||||
form = self.get_form()
|
# Complete the build output
|
||||||
|
build.completeBuildOutput(
|
||||||
confirm = str2bool(request.POST.get('confirm', False))
|
output,
|
||||||
|
self.request.user,
|
||||||
loc_id = request.POST.get('location', None)
|
location=location,
|
||||||
|
)
|
||||||
valid = False
|
|
||||||
|
|
||||||
if confirm is False:
|
|
||||||
form.add_error('confirm', _('Confirm completion of build'))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
location = StockLocation.objects.get(id=loc_id)
|
|
||||||
valid = True
|
|
||||||
except (ValueError, StockLocation.DoesNotExist):
|
|
||||||
form.add_error('location', _('Invalid location selected'))
|
|
||||||
|
|
||||||
serials = []
|
|
||||||
|
|
||||||
if build.part.trackable:
|
|
||||||
# A build for a trackable part may optionally specify serial numbers.
|
|
||||||
|
|
||||||
sn = request.POST.get('serial_numbers', '')
|
|
||||||
|
|
||||||
sn = str(sn).strip()
|
|
||||||
|
|
||||||
# If the user has specified serial numbers, check they are valid
|
|
||||||
if len(sn) > 0:
|
|
||||||
try:
|
|
||||||
# Exctract a list of provided serial numbers
|
|
||||||
serials = ExtractSerialNumbers(sn, build.quantity)
|
|
||||||
|
|
||||||
existing = build.part.find_conflicting_serial_numbers(serials)
|
|
||||||
|
|
||||||
if len(existing) > 0:
|
|
||||||
exists = ",".join([str(x) for x in existing])
|
|
||||||
form.add_error('serial_numbers', _('The following serial numbers already exist: ({sn})'.format(sn=exists)))
|
|
||||||
valid = False
|
|
||||||
|
|
||||||
except ValidationError as e:
|
|
||||||
form.add_error('serial_numbers', e.messages)
|
|
||||||
valid = False
|
|
||||||
|
|
||||||
if valid:
|
|
||||||
if not build.completeBuild(location, serials, request.user):
|
|
||||||
form.add_error(None, _('Build could not be completed'))
|
|
||||||
valid = False
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'form_valid': valid,
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, data, context=self.get_context_data())
|
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
""" Provide feedback data back to the form """
|
""" Provide feedback data back to the form """
|
||||||
return {
|
return {
|
||||||
'info': _('Build marked as COMPLETE')
|
'success': _('Build output completed')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -589,19 +609,6 @@ class BuildCreate(AjaxCreateView):
|
|||||||
_('Serial numbers already exist') + ': ' + msg
|
_('Serial numbers already exist') + ': ' + msg
|
||||||
)
|
)
|
||||||
|
|
||||||
def post_save(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Called immediately after a new Build object is created.
|
|
||||||
"""
|
|
||||||
|
|
||||||
build = kwargs['new_object']
|
|
||||||
request = kwargs['request']
|
|
||||||
data = kwargs['data']
|
|
||||||
|
|
||||||
serials = data['serial_numbers']
|
|
||||||
|
|
||||||
build.createInitialStockItem(serials, request.user)
|
|
||||||
|
|
||||||
|
|
||||||
class BuildUpdate(AjaxUpdateView):
|
class BuildUpdate(AjaxUpdateView):
|
||||||
""" View for editing a Build object """
|
""" View for editing a Build object """
|
||||||
@ -684,11 +691,13 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def validate(self, request, form, data):
|
def validate(self, build_item, form, **kwargs):
|
||||||
"""
|
"""
|
||||||
Extra validation steps as required
|
Extra validation steps as required
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
data = form.cleaned_data
|
||||||
|
|
||||||
stock_item = data.get('stock_item', None)
|
stock_item = data.get('stock_item', None)
|
||||||
quantity = data.get('quantity', None)
|
quantity = data.get('quantity', None)
|
||||||
|
|
||||||
@ -702,7 +711,9 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
available = stock_item.unallocated_quantity()
|
available = stock_item.unallocated_quantity()
|
||||||
if quantity > available:
|
if quantity > available:
|
||||||
form.add_error('stock_item', _('Stock item is over-allocated'))
|
form.add_error('stock_item', _('Stock item is over-allocated'))
|
||||||
form.add_error('quantity', _('Avaialabe') + ': ' + str(normalize(available)))
|
form.add_error('quantity', _('Available') + ': ' + str(normalize(available)))
|
||||||
|
else:
|
||||||
|
form.add_error('stock_item', _('Stock item must be selected'))
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
""" Create Form for making / editing new Part object """
|
""" Create Form for making / editing new Part object """
|
||||||
@ -758,7 +769,7 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
form.fields['install_into'].widget = HiddenInput()
|
form.fields['install_into'].widget = HiddenInput()
|
||||||
|
|
||||||
if self.build and self.part:
|
if self.build and self.part:
|
||||||
available_items = self.build.getAvailableStockItems(part=self.part, output=self.output)
|
available_items = self.build.availableStockItems(self.part, self.output)
|
||||||
form.fields['stock_item'].queryset = available_items
|
form.fields['stock_item'].queryset = available_items
|
||||||
|
|
||||||
self.available_stock = form.fields['stock_item'].queryset.all()
|
self.available_stock = form.fields['stock_item'].queryset.all()
|
||||||
@ -819,7 +830,7 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
# Work out how much stock is required
|
# Work out how much stock is required
|
||||||
if build and part:
|
if build and part:
|
||||||
required_quantity = build.getUnallocatedQuantity(part, output=output)
|
required_quantity = build.unallocatedQuantity(part, output)
|
||||||
else:
|
else:
|
||||||
required_quantity = None
|
required_quantity = None
|
||||||
|
|
||||||
|
@ -905,6 +905,18 @@ class Part(MPTTModel):
|
|||||||
def has_bom(self):
|
def has_bom(self):
|
||||||
return self.bom_count > 0
|
return self.bom_count > 0
|
||||||
|
|
||||||
|
def has_trackable_parts(self):
|
||||||
|
"""
|
||||||
|
Return True if any parts linked in the Bill of Materials are trackable.
|
||||||
|
This is important when building the part.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for bom_item in self.bom_items.all():
|
||||||
|
if bom_item.sub_part.trackable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bom_count(self):
|
def bom_count(self):
|
||||||
""" Return the number of items contained in the BOM for this part """
|
""" Return the number of items contained in the BOM for this part """
|
||||||
@ -1188,7 +1200,7 @@ class Part(MPTTModel):
|
|||||||
parameter.save()
|
parameter.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def deepCopy(self, other, **kwargs):
|
def deep_copy(self, other, **kwargs):
|
||||||
""" Duplicates non-field data from another part.
|
""" Duplicates non-field data from another part.
|
||||||
Does not alter the normal fields of this part,
|
Does not alter the normal fields of this part,
|
||||||
but can be used to copy other data linked by ForeignKey refernce.
|
but can be used to copy other data linked by ForeignKey refernce.
|
||||||
|
@ -99,7 +99,7 @@ class PartTest(TestCase):
|
|||||||
self.assertIn(self.R1.name, barcode)
|
self.assertIn(self.R1.name, barcode)
|
||||||
|
|
||||||
def test_copy(self):
|
def test_copy(self):
|
||||||
self.R2.deepCopy(self.R1, image=True, bom=True)
|
self.R2.deep_copy(self.R1, image=True, bom=True)
|
||||||
|
|
||||||
def test_match_names(self):
|
def test_match_names(self):
|
||||||
|
|
||||||
|
@ -360,7 +360,7 @@ class MakePartVariant(AjaxCreateView):
|
|||||||
parameters_copy = str2bool(request.POST.get('parameters_copy', False))
|
parameters_copy = str2bool(request.POST.get('parameters_copy', False))
|
||||||
|
|
||||||
# Copy relevent information from the template part
|
# Copy relevent information from the template part
|
||||||
part.deepCopy(part_template, bom=bom_copy, parameters=parameters_copy)
|
part.deep_copy(part_template, bom=bom_copy, parameters=parameters_copy)
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, data, context=context)
|
return self.renderJsonResponse(request, form, data, context=context)
|
||||||
|
|
||||||
@ -473,7 +473,7 @@ class PartDuplicate(AjaxCreateView):
|
|||||||
original = self.get_part_to_copy()
|
original = self.get_part_to_copy()
|
||||||
|
|
||||||
if original:
|
if original:
|
||||||
part.deepCopy(original, bom=bom_copy, parameters=parameters_copy)
|
part.deep_copy(original, bom=bom_copy, parameters=parameters_copy)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data['url'] = part.get_absolute_url()
|
data['url'] = part.get_absolute_url()
|
||||||
|
@ -175,7 +175,7 @@ class StockItem(MPTTModel):
|
|||||||
if add_note:
|
if add_note:
|
||||||
# This StockItem is being saved for the first time
|
# This StockItem is being saved for the first time
|
||||||
self.addTransactionNote(
|
self.addTransactionNote(
|
||||||
'Created stock item',
|
_('Created stock item'),
|
||||||
user,
|
user,
|
||||||
notes="Created new stock item for part '{p}'".format(p=str(self.part)),
|
notes="Created new stock item for part '{p}'".format(p=str(self.part)),
|
||||||
system=True
|
system=True
|
||||||
@ -200,12 +200,6 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
super(StockItem, self).validate_unique(exclude)
|
super(StockItem, self).validate_unique(exclude)
|
||||||
|
|
||||||
# If the part is trackable, either serial number or batch number must be set
|
|
||||||
if self.part.trackable:
|
|
||||||
if not self.serial and not self.batch:
|
|
||||||
msg = _('Serial or batch number must be specified for trackable stock')
|
|
||||||
raise ValidationError(msg)
|
|
||||||
|
|
||||||
# If the serial number is set, make sure it is not a duplicate
|
# If the serial number is set, make sure it is not a duplicate
|
||||||
if self.serial is not None:
|
if self.serial is not None:
|
||||||
# Query to look for duplicate serial numbers
|
# Query to look for duplicate serial numbers
|
||||||
|
@ -152,17 +152,7 @@ function loadBomTable(table, options) {
|
|||||||
|
|
||||||
var sub_part = row.sub_part_detail;
|
var sub_part = row.sub_part_detail;
|
||||||
|
|
||||||
if (sub_part.trackable) {
|
html += makePartIcons(row.sub_part_detail);
|
||||||
html += makeIconBadge('fa-directions', '{% trans "Trackable part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub_part.virtual) {
|
|
||||||
html += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub_part.is_template) {
|
|
||||||
html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display an extra icon if this part is an assembly
|
// Display an extra icon if this part is an assembly
|
||||||
if (sub_part.assembly) {
|
if (sub_part.assembly) {
|
||||||
@ -171,10 +161,6 @@ function loadBomTable(table, options) {
|
|||||||
html += renderLink(text, `/part/${row.sub_part}/bom/`);
|
html += renderLink(text, `/part/${row.sub_part}/bom/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sub_part.active) {
|
|
||||||
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,11 +36,8 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
|||||||
/* Generate action buttons for a build output.
|
/* Generate action buttons for a build output.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var outputId = 'untracked';
|
var buildId = buildInfo.pk;
|
||||||
|
var outputId = output.pk;
|
||||||
if (output) {
|
|
||||||
outputId = output.pk;
|
|
||||||
}
|
|
||||||
|
|
||||||
var panel = `#allocation-panel-${outputId}`;
|
var panel = `#allocation-panel-${outputId}`;
|
||||||
|
|
||||||
@ -54,23 +51,19 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
|||||||
var html = `<div class='btn-group float-right' role='group'>`;
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
// Add a button to "auto allocate" against the build
|
// Add a button to "auto allocate" against the build
|
||||||
if (!output) {
|
|
||||||
html += makeIconButton(
|
html += makeIconButton(
|
||||||
'fa-magic icon-blue', 'button-output-auto', outputId,
|
'fa-magic icon-blue', 'button-output-auto', outputId,
|
||||||
'{% trans "Auto-allocate stock items to this output" %}',
|
'{% trans "Auto-allocate stock items to this output" %}',
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (output) {
|
|
||||||
// Add a button to "complete" the particular build output
|
// Add a button to "complete" the particular build output
|
||||||
html += makeIconButton(
|
html += makeIconButton(
|
||||||
'fa-check icon-green', 'button-output-complete', outputId,
|
'fa-check icon-green', 'button-output-complete', outputId,
|
||||||
'{% trans "Complete build output" %}',
|
'{% trans "Complete build output" %}',
|
||||||
{
|
{
|
||||||
disabled: true
|
//disabled: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Add a button to "cancel" the particular build output (unallocate)
|
// Add a button to "cancel" the particular build output (unallocate)
|
||||||
html += makeIconButton(
|
html += makeIconButton(
|
||||||
@ -78,7 +71,6 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
|||||||
'{% trans "Unallocate stock from build output" %}',
|
'{% trans "Unallocate stock from build output" %}',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (output) {
|
|
||||||
// Add a button to "delete" the particular build output
|
// Add a button to "delete" the particular build output
|
||||||
html += makeIconButton(
|
html += makeIconButton(
|
||||||
'fa-trash-alt icon-red', 'button-output-delete', outputId,
|
'fa-trash-alt icon-red', 'button-output-delete', outputId,
|
||||||
@ -88,9 +80,6 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
|||||||
// Add a button to "destroy" the particular build output (mark as damaged, scrap)
|
// Add a button to "destroy" the particular build output (mark as damaged, scrap)
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
buildActions.html(html);
|
buildActions.html(html);
|
||||||
@ -103,13 +92,21 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
|||||||
data: {
|
data: {
|
||||||
output: outputId,
|
output: outputId,
|
||||||
},
|
},
|
||||||
reload: true,
|
success: reloadTable,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(panel).find(`#button-output-complete-${outputId}`).click(function() {
|
$(panel).find(`#button-output-complete-${outputId}`).click(function() {
|
||||||
// TODO
|
launchModalForm(
|
||||||
|
`/build/${buildId}/complete/`,
|
||||||
|
{
|
||||||
|
success: reloadTable,
|
||||||
|
data: {
|
||||||
|
output: outputId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(panel).find(`#button-output-unallocate-${outputId}`).click(function() {
|
$(panel).find(`#button-output-unallocate-${outputId}`).click(function() {
|
||||||
@ -155,18 +152,12 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
|
|
||||||
var outputId = null;
|
var outputId = null;
|
||||||
|
|
||||||
if (output) {
|
|
||||||
outputId = output.pk;
|
outputId = output.pk;
|
||||||
}
|
|
||||||
|
|
||||||
var table = options.table;
|
var table = options.table;
|
||||||
|
|
||||||
if (options.table == null) {
|
if (options.table == null) {
|
||||||
if (outputId != null) {
|
|
||||||
table = `#allocation-table-${outputId}`;
|
table = `#allocation-table-${outputId}`;
|
||||||
} else {
|
|
||||||
table = `#allocation-table-untracked`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadTable() {
|
function reloadTable() {
|
||||||
@ -177,14 +168,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
function requiredQuantity(row) {
|
function requiredQuantity(row) {
|
||||||
// Return the requied quantity for a given row
|
// Return the requied quantity for a given row
|
||||||
|
|
||||||
if (output) {
|
|
||||||
// Tracked stock allocated against a particular BuildOutput
|
|
||||||
return row.quantity * output.quantity;
|
return row.quantity * output.quantity;
|
||||||
} else {
|
|
||||||
// Untrack stock allocated against the build
|
|
||||||
return row.quantity * (buildInfo.quantity - buildInfo.completed);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sumAllocations(row) {
|
function sumAllocations(row) {
|
||||||
@ -300,7 +284,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
queryParams: {
|
queryParams: {
|
||||||
part: partId,
|
part: partId,
|
||||||
sub_part_detail: true,
|
sub_part_detail: true,
|
||||||
sub_part_trackable: outputId != null,
|
|
||||||
},
|
},
|
||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No BOM items found" %}';
|
return '{% trans "No BOM items found" %}';
|
||||||
@ -357,15 +340,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
var allocatedQuantity = sumAllocations(tableRow);
|
var allocatedQuantity = sumAllocations(tableRow);
|
||||||
|
|
||||||
// Is this line item fully allocated?
|
// Is this line item fully allocated?
|
||||||
if (output) {
|
|
||||||
if (allocatedQuantity >= (tableRow.quantity * output.quantity)) {
|
if (allocatedQuantity >= (tableRow.quantity * output.quantity)) {
|
||||||
allocatedLines += 1;
|
allocatedLines += 1;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (allocatedQuantity >= (tableRow.quantity * (buildInfo.quantity - buildInfo.completed))) {
|
|
||||||
allocatedLines += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the updated row back into the main table
|
// Push the updated row back into the main table
|
||||||
$(table).bootstrapTable('updateByUniqueId', key, tableRow, true);
|
$(table).bootstrapTable('updateByUniqueId', key, tableRow, true);
|
||||||
@ -506,6 +483,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
|
|
||||||
var html = imageHoverIcon(thumb) + renderLink(name, url);
|
var html = imageHoverIcon(thumb) + renderLink(name, url);
|
||||||
|
|
||||||
|
html += makePartIcons(row.sub_part_detail);
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -547,13 +526,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
var qA = rowA.quantity;
|
var qA = rowA.quantity;
|
||||||
var qB = rowB.quantity;
|
var qB = rowB.quantity;
|
||||||
|
|
||||||
if (output) {
|
|
||||||
qA *= output.quantity;
|
qA *= output.quantity;
|
||||||
qB *= output.quantity;
|
qB *= output.quantity;
|
||||||
} else {
|
|
||||||
qA *= buildInfo.quantity;
|
|
||||||
qB *= buildInfo.quantity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the case where both numerators are zero
|
// Handle the case where both numerators are zero
|
||||||
if ((aA == 0) && (aB == 0)) {
|
if ((aA == 0) && (aB == 0)) {
|
||||||
|
@ -61,6 +61,45 @@ function toggleStar(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function makePartIcons(part, options={}) {
|
||||||
|
/* Render a set of icons for the given part.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (part.trackable) {
|
||||||
|
html += makeIconBadge('fa-directions', '{% trans "Trackable part" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.virtual) {
|
||||||
|
html += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.is_template) {
|
||||||
|
html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.assembly) {
|
||||||
|
html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.starred) {
|
||||||
|
html += makeIconBadge('fa-star', '{% trans "Starred part" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.salable) {
|
||||||
|
html += makeIconBadge('fa-dollar-sign', title='{% trans "Salable part" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!part.active) {
|
||||||
|
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadPartVariantTable(table, partId, options={}) {
|
function loadPartVariantTable(table, partId, options={}) {
|
||||||
/* Load part variant table
|
/* Load part variant table
|
||||||
*/
|
*/
|
||||||
@ -340,40 +379,8 @@ function loadPartTable(table, url, options={}) {
|
|||||||
|
|
||||||
var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');
|
var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');
|
||||||
|
|
||||||
if (row.trackable) {
|
display += makePartIcons(row);
|
||||||
display += makeIconBadge('fa-directions', '{% trans "Trackable part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.virtual) {
|
|
||||||
display += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (row.is_template) {
|
|
||||||
display += makeIconBadge('fa-clone', '{% trans "Template part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.assembly) {
|
|
||||||
display += makeIconBadge('fa-tools', '{% trans "Assembled part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.starred) {
|
|
||||||
display += makeIconBadge('fa-star', '{% trans "Starred part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.salable) {
|
|
||||||
display += makeIconBadge('fa-dollar-sign', title='{% trans "Salable part" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
if (row.component) {
|
|
||||||
display = display + `<span class='fas fa-cogs label-right' title='Component part'></span>`;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!row.active) {
|
|
||||||
display += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
|
|
||||||
}
|
|
||||||
return display;
|
return display;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -908,7 +908,7 @@ function loadInstalledInTable(table, options) {
|
|||||||
url: "{% url 'api-bom-list' %}",
|
url: "{% url 'api-bom-list' %}",
|
||||||
queryParams: {
|
queryParams: {
|
||||||
part: options.part,
|
part: options.part,
|
||||||
trackable: true,
|
sub_part_trackable: true,
|
||||||
sub_part_detail: true,
|
sub_part_detail: true,
|
||||||
},
|
},
|
||||||
showColumns: false,
|
showColumns: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user