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 django.utils.translation import ugettext as _
|
||||
from django import forms
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
|
||||
from django import forms
|
||||
from .models import Build, BuildItem, BuildOrderAttachment
|
||||
from stock.models import StockLocation
|
||||
|
||||
from stock.models import StockLocation, StockItem
|
||||
|
||||
|
||||
class EditBuildForm(HelperForm):
|
||||
@ -106,18 +107,18 @@ class UnallocateBuildForm(HelperForm):
|
||||
class AutoAllocateForm(HelperForm):
|
||||
""" 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(
|
||||
required=False,
|
||||
widget=forms.HiddenInput()
|
||||
# Keep track of which build output we are interested in
|
||||
output = forms.ModelChoiceField(
|
||||
queryset=StockItem.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Build
|
||||
fields = [
|
||||
'confirm',
|
||||
'output_id',
|
||||
'output',
|
||||
]
|
||||
|
||||
|
||||
@ -136,20 +137,25 @@ class CompleteBuildForm(HelperForm):
|
||||
help_text=_('Location of completed parts'),
|
||||
)
|
||||
|
||||
serial_numbers = forms.CharField(
|
||||
label=_('Serial numbers'),
|
||||
confirm_incomplete = forms.BooleanField(
|
||||
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:
|
||||
model = Build
|
||||
fields = [
|
||||
'serial_numbers',
|
||||
'location',
|
||||
'confirm'
|
||||
'output',
|
||||
'confirm',
|
||||
'confirm_incomplete',
|
||||
]
|
||||
|
||||
|
||||
|
@ -24,7 +24,6 @@ from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
from InvenTree.helpers import increment, getSetting, normalize
|
||||
from InvenTree.helpers import ExtractSerialNumbers
|
||||
from InvenTree.validators import validate_build_order_reference
|
||||
from InvenTree.models import InvenTreeAttachment
|
||||
|
||||
@ -183,6 +182,16 @@ class Build(MPTTModel):
|
||||
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
|
||||
def remaining(self):
|
||||
"""
|
||||
@ -195,9 +204,13 @@ class Build(MPTTModel):
|
||||
def output_count(self):
|
||||
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
|
||||
@ -228,7 +241,7 @@ class Build(MPTTModel):
|
||||
Return all the "completed" build outputs
|
||||
"""
|
||||
|
||||
outputs = self.getBuildOutputs(complete=True)
|
||||
outputs = self.get_build_outputs(complete=True)
|
||||
|
||||
# TODO - Ordering?
|
||||
|
||||
@ -240,7 +253,7 @@ class Build(MPTTModel):
|
||||
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?
|
||||
|
||||
@ -278,49 +291,6 @@ class Build(MPTTModel):
|
||||
|
||||
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
|
||||
def cancelBuild(self, user):
|
||||
""" Mark the Build as CANCELLED
|
||||
@ -368,59 +338,49 @@ class Build(MPTTModel):
|
||||
Iterate through each item in the BOM
|
||||
"""
|
||||
|
||||
# Only look at the "untracked" BOM items
|
||||
# Tracked BOM items must be handled separately
|
||||
untracked_bom_items = self.part.bom_items.filter(sub_part__trackable=False)
|
||||
for bom_item in self.bom_items:
|
||||
|
||||
for item in untracked_bom_items.prefetch_related('sub_part'):
|
||||
part = bom_item.sub_part
|
||||
|
||||
# How many parts are still required for this build?
|
||||
#q_required = item.quantity * self.remaining
|
||||
q_required = self.getUnallocatedQuantity(item.sub_part)
|
||||
# Skip any parts which are already fully allocated
|
||||
if self.isPartFullyAllocated(part, output):
|
||||
continue
|
||||
|
||||
# Grab a list of StockItem objects which are "in stock"
|
||||
stock = StockModels.StockItem.objects.filter(
|
||||
StockModels.StockItem.IN_STOCK_FILTER
|
||||
)
|
||||
# How many parts are required to complete the output?
|
||||
required = self.unallocatedQuantity(part, output)
|
||||
|
||||
# Filter by part reference
|
||||
stock = stock.filter(part=item.sub_part)
|
||||
|
||||
# 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]
|
||||
)
|
||||
# Grab a list of stock items which are available
|
||||
stock_items = self.availableStockItems(part, output)
|
||||
|
||||
# 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()])
|
||||
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!
|
||||
if stock.count() == 1:
|
||||
stock_item = stock[0]
|
||||
if stock_items.count() == 1:
|
||||
stock_item = stock_items[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)
|
||||
# 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,
|
||||
install_into=output
|
||||
)
|
||||
|
||||
if len(build_items) > 0:
|
||||
continue
|
||||
|
||||
# Are there any parts available?
|
||||
# How many items are actually available?
|
||||
if stock_item.quantity > 0:
|
||||
|
||||
# Only take as many as are available
|
||||
if stock_item.quantity < q_required:
|
||||
q_required = stock_item.quantity
|
||||
if stock_item.quantity < required:
|
||||
required = stock_item.quantity
|
||||
|
||||
allocation = {
|
||||
'stock_item': stock_item,
|
||||
'quantity': q_required,
|
||||
'quantity': required,
|
||||
}
|
||||
|
||||
allocations.append(allocation)
|
||||
@ -472,7 +432,7 @@ class Build(MPTTModel):
|
||||
output.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def auto_allocate(self, output=None):
|
||||
def autoAllocate(self, output):
|
||||
"""
|
||||
Run auto-allocation routine to allocate StockItems to this Build.
|
||||
|
||||
@ -496,151 +456,160 @@ class Build(MPTTModel):
|
||||
build_item = BuildItem(
|
||||
build=self,
|
||||
stock_item=item['stock_item'],
|
||||
quantity=item['quantity'])
|
||||
quantity=item['quantity'],
|
||||
install_into=output,
|
||||
)
|
||||
|
||||
build_item.save()
|
||||
|
||||
@transaction.atomic
|
||||
def completeBuild(self, location, serial_numbers, user):
|
||||
""" Mark the Build as COMPLETE
|
||||
def completeBuildOutput(self, output, user, **kwargs):
|
||||
"""
|
||||
Complete a particular build output
|
||||
|
||||
- Takes allocated items from stock
|
||||
- Delete pending BuildItem objects
|
||||
- Remove allocated StockItems
|
||||
- Mark the output as complete
|
||||
"""
|
||||
|
||||
# Complete the build allocation for each BuildItem
|
||||
for build_item in self.allocated_stock.all().prefetch_related('stock_item'):
|
||||
build_item.complete_allocation(user)
|
||||
# List the allocated BuildItem objects for the given output
|
||||
allocated_items = output.items_to_install.all()
|
||||
|
||||
# Check that the stock-item has been assigned to this build, and remove the builditem from the database
|
||||
if build_item.stock_item.build_order == self:
|
||||
for build_item in allocated_items:
|
||||
|
||||
# Complete the allocation of stock for that item
|
||||
build_item.completeAllocation(user)
|
||||
|
||||
# Remove the build item from the database
|
||||
build_item.delete()
|
||||
|
||||
notes = 'Built {q} on {now}'.format(
|
||||
q=self.quantity,
|
||||
now=str(datetime.now().date())
|
||||
# Ensure that the output is updated correctly
|
||||
output.build = self
|
||||
output.is_building = False
|
||||
|
||||
output.save()
|
||||
|
||||
output.addTransactionNote(
|
||||
_('Completed build output'),
|
||||
user,
|
||||
system=True
|
||||
)
|
||||
|
||||
# Generate the build outputs
|
||||
if self.part.trackable and serial_numbers:
|
||||
# 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
|
||||
# Increase the completed quantity for this build
|
||||
self.completed += output.quantity
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
def isFullyAllocated(self):
|
||||
def requiredQuantity(self, part, output):
|
||||
"""
|
||||
Return True if this build has been fully allocated.
|
||||
"""
|
||||
|
||||
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.
|
||||
Get the quantity of a part required to complete the particular build output.
|
||||
|
||||
Args:
|
||||
part: The 'Part' archetype reference
|
||||
output: A particular build output (StockItem) (or None to specify the entire build)
|
||||
part: The Part object
|
||||
output - The particular build output (StockItem)
|
||||
"""
|
||||
|
||||
# Extract the BOM line item from the database
|
||||
try:
|
||||
item = PartModels.BomItem.objects.get(part=self.part.id, sub_part=part.id)
|
||||
q = item.quantity
|
||||
except PartModels.BomItem.DoesNotExist:
|
||||
q = 0
|
||||
bom_item = PartModels.BomItem.objects.get(part=self.part.pk, sub_part=part.pk)
|
||||
quantity = bom_item.quantity
|
||||
except (PartModels.BomItem.DoesNotExist):
|
||||
quantity = 0
|
||||
|
||||
if output:
|
||||
return q * output.quantity
|
||||
quantity *= output.quantity
|
||||
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:
|
||||
part: The 'Part' archetype reference
|
||||
output: A particular build output (StockItem) (or None to specify the entire build)
|
||||
part - The part object
|
||||
output - Build output (StockItem).
|
||||
"""
|
||||
|
||||
allocations = BuildItem.objects.filter(
|
||||
build=self.id,
|
||||
stock_item__part=part.id
|
||||
build=self,
|
||||
stock_item__part=part,
|
||||
install_into=output,
|
||||
)
|
||||
|
||||
# Optionally, filter by the specified build output StockItem
|
||||
if output is not None:
|
||||
allocations = allocations.filter(
|
||||
install_into=output
|
||||
)
|
||||
return allocations
|
||||
|
||||
def allocatedQuantity(self, part, 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))
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
Return the total unallocated (remaining) quantity of a part against a particular output.
|
||||
"""
|
||||
|
||||
required = self.getRequiredQuantity(part, output=output)
|
||||
allocated = self.getAllocatedQuantity(part, output=output)
|
||||
required = self.requiredQuantity(part, output)
|
||||
allocated = self.allocatedQuantity(part, output)
|
||||
|
||||
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
|
||||
def required_parts(self):
|
||||
""" Returns a dict of parts required to build this part (BOM) """
|
||||
@ -658,28 +627,31 @@ class Build(MPTTModel):
|
||||
|
||||
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)
|
||||
|
||||
if output:
|
||||
# Exclude items which are already allocated to the particular build output
|
||||
|
||||
to_exclude = BuildItem.objects.filter(
|
||||
# Exclude any items which have already been allocated
|
||||
allocated = BuildItem.objects.filter(
|
||||
build=self,
|
||||
stock_item__part=part,
|
||||
install_into=output
|
||||
install_into=output,
|
||||
)
|
||||
|
||||
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
|
||||
@ -690,16 +662,6 @@ class Build(MPTTModel):
|
||||
|
||||
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
|
||||
def is_active(self):
|
||||
""" 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')
|
||||
|
||||
# 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')
|
||||
|
||||
# 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):
|
||||
pass
|
||||
|
||||
if len(errors) > 0:
|
||||
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
|
||||
|
||||
@ -843,10 +797,13 @@ class BuildItem(models.Model):
|
||||
self.stock_item = item
|
||||
self.save()
|
||||
|
||||
# TODO - If the item__part object is not trackable, delete the stock item here
|
||||
|
||||
item.build_order = self.build
|
||||
if item.part.trackable:
|
||||
# If the part is trackable, install into the build output
|
||||
item.belongs_to = self.install_into
|
||||
item.save()
|
||||
else:
|
||||
# Part is *not* trackable, so just delete it
|
||||
item.delete()
|
||||
|
||||
build = models.ForeignKey(
|
||||
Build,
|
||||
|
@ -11,6 +11,9 @@ InvenTree | Allocate Parts
|
||||
|
||||
{% include "build/tabs.html" with tab='allocate' %}
|
||||
|
||||
<div class='btn-group' role='group'>
|
||||
|
||||
</div>
|
||||
|
||||
<h4>{% trans "Incomplete Build Ouputs" %}</h4>
|
||||
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
||||
@ -19,9 +22,7 @@ InvenTree | Allocate Parts
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h4>{% trans "Assigned Stock" %}</h4>
|
||||
|
||||
<!---
|
||||
<div id='build-item-toolbar'>
|
||||
{% if build.status == BuildStatus.PENDING %}
|
||||
<div class='btn-group'>
|
||||
@ -31,8 +32,7 @@ InvenTree | Allocate Parts
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='build-item-list' data-toolbar='#build-item-toolbar'></table>
|
||||
-->
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -46,14 +46,6 @@ InvenTree | Allocate Parts
|
||||
part: {{ build.part.pk }},
|
||||
};
|
||||
|
||||
loadBuildOutputAllocationTable(
|
||||
buildInfo,
|
||||
null,
|
||||
{
|
||||
table: '#build-item-list',
|
||||
}
|
||||
);
|
||||
|
||||
{% for item in build.incomplete_outputs %}
|
||||
// Get the build output as a javascript object
|
||||
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
||||
|
@ -7,18 +7,9 @@
|
||||
|
||||
<div class='alert alert-block alert-info'>
|
||||
<b>{% trans "Automatically Allocate Stock" %}</b><br>
|
||||
{% trans "Where the following conditions are met, stock will be automatically allocated to this build" %}:<br>
|
||||
<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>
|
||||
{% trans "The following stock items will be allocated to the specified build output" %}
|
||||
</div>
|
||||
|
||||
{% if allocations %}
|
||||
|
||||
<table class='table table-striped table-condensed'>
|
||||
<tr>
|
||||
<th></th>
|
||||
|
@ -1,36 +1,47 @@
|
||||
{% extends "modal_form.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
<h4>{% trans "Build" %} - {{ build }}</h4>
|
||||
|
||||
{% if build.isFullyAllocated %}
|
||||
{% if fully_allocated %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
<h4>{% trans "Build order allocation is complete" %}</h4>
|
||||
<h4>{% trans "Stock allocation is complete" %}</h4>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
<h4>{% trans "Warning: Build order allocation is not complete" %}</h4>
|
||||
{% trans "Build Order has not been fully allocated. Ensure that all Stock Items have been allocated to the Build" %}
|
||||
<h4>{% trans "Stock allocation is incomplete" %}</h4>
|
||||
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<div class='alert alert-block alert-success'>
|
||||
<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 panel-info'>
|
||||
<div class='panel-heading'>
|
||||
{% trans "The following items will be created" %}
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% 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>
|
||||
|
||||
|
@ -114,19 +114,6 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</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 %}
|
||||
<tr>
|
||||
<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_2), 250)
|
||||
|
||||
self.assertTrue(self.build.can_build)
|
||||
self.assertFalse(self.build.is_complete)
|
||||
|
||||
# Delete some stock and see if the build can still be completed
|
||||
self.stock_2_1.delete()
|
||||
self.assertFalse(self.build.can_build)
|
||||
|
||||
def test_build_item_clean(self):
|
||||
# Ensure that dodgy BuildItem objects cannot be created
|
||||
|
@ -95,6 +95,9 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
role_required = 'build.change'
|
||||
|
||||
def get_initial(self):
|
||||
"""
|
||||
Initial values for the form.
|
||||
"""
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
@ -102,62 +105,73 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
output = self.get_param('output')
|
||||
|
||||
if output:
|
||||
initials['output_id'] = output
|
||||
try:
|
||||
output = StockItem.objects.get(pk=output)
|
||||
initials['output'] = output
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
""" Get the context data for form rendering. """
|
||||
"""
|
||||
Get the context data for form rendering.
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
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:
|
||||
output = StockItem.objects.get(pk=output_id)
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
output = None
|
||||
|
||||
valid = False
|
||||
if output:
|
||||
context['output'] = output
|
||||
context['allocations'] = build.getAutoAllocations(output)
|
||||
|
||||
if confirm:
|
||||
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'))
|
||||
context['build'] = build
|
||||
|
||||
data = {
|
||||
'form_valid': valid,
|
||||
return context
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -292,37 +306,60 @@ class BuildComplete(AjaxUpdateView):
|
||||
model = Build
|
||||
form_class = forms.CompleteBuildForm
|
||||
context_object_name = "build"
|
||||
ajax_form_title = _("Complete Build")
|
||||
ajax_form_title = _("Complete Build Output")
|
||||
ajax_template_name = "build/complete.html"
|
||||
role_required = 'build.change'
|
||||
|
||||
def get_form(self):
|
||||
""" Get the form object.
|
||||
|
||||
If the part is trackable, include a field for serial numbers.
|
||||
"""
|
||||
build = self.get_object()
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
if not build.part.trackable:
|
||||
form.fields.pop('serial_numbers')
|
||||
else:
|
||||
# Extract the build output object
|
||||
output = None
|
||||
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
|
||||
|
||||
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):
|
||||
""" Get initial form data for the CompleteBuild form
|
||||
|
||||
- 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()
|
||||
|
||||
if build.part.default_location is not None:
|
||||
@ -332,94 +369,77 @@ class BuildComplete(AjaxUpdateView):
|
||||
except StockLocation.DoesNotExist:
|
||||
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
|
||||
|
||||
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 = Build.objects.get(id=self.kwargs['pk'])
|
||||
build = self.get_object()
|
||||
|
||||
context = {}
|
||||
|
||||
# Build object
|
||||
context['build'] = build
|
||||
|
||||
# Items to be removed from stock
|
||||
taking = BuildItem.objects.filter(build=build.id)
|
||||
context['taking'] = taking
|
||||
form = self.get_form()
|
||||
|
||||
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
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Handle POST request. Mark the build as COMPLETE
|
||||
def post_save(self, build, form, **kwargs):
|
||||
|
||||
- If the form validation passes, the Build objects completeBuild() method is called
|
||||
- Otherwise, the form is passed back to the client
|
||||
"""
|
||||
data = form.cleaned_data
|
||||
|
||||
build = self.get_object()
|
||||
location = data.get('location', None)
|
||||
output = data.get('output', None)
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
confirm = str2bool(request.POST.get('confirm', False))
|
||||
|
||||
loc_id = request.POST.get('location', None)
|
||||
|
||||
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())
|
||||
# Complete the build output
|
||||
build.completeBuildOutput(
|
||||
output,
|
||||
self.request.user,
|
||||
location=location,
|
||||
)
|
||||
|
||||
def get_data(self):
|
||||
""" Provide feedback data back to the form """
|
||||
return {
|
||||
'info': _('Build marked as COMPLETE')
|
||||
'success': _('Build output completed')
|
||||
}
|
||||
|
||||
|
||||
@ -589,19 +609,6 @@ class BuildCreate(AjaxCreateView):
|
||||
_('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):
|
||||
""" View for editing a Build object """
|
||||
@ -684,11 +691,13 @@ class BuildItemCreate(AjaxCreateView):
|
||||
|
||||
return ctx
|
||||
|
||||
def validate(self, request, form, data):
|
||||
def validate(self, build_item, form, **kwargs):
|
||||
"""
|
||||
Extra validation steps as required
|
||||
"""
|
||||
|
||||
data = form.cleaned_data
|
||||
|
||||
stock_item = data.get('stock_item', None)
|
||||
quantity = data.get('quantity', None)
|
||||
|
||||
@ -702,7 +711,9 @@ class BuildItemCreate(AjaxCreateView):
|
||||
available = stock_item.unallocated_quantity()
|
||||
if quantity > available:
|
||||
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):
|
||||
""" Create Form for making / editing new Part object """
|
||||
@ -758,7 +769,7 @@ class BuildItemCreate(AjaxCreateView):
|
||||
form.fields['install_into'].widget = HiddenInput()
|
||||
|
||||
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
|
||||
|
||||
self.available_stock = form.fields['stock_item'].queryset.all()
|
||||
@ -819,7 +830,7 @@ class BuildItemCreate(AjaxCreateView):
|
||||
|
||||
# Work out how much stock is required
|
||||
if build and part:
|
||||
required_quantity = build.getUnallocatedQuantity(part, output=output)
|
||||
required_quantity = build.unallocatedQuantity(part, output)
|
||||
else:
|
||||
required_quantity = None
|
||||
|
||||
|
@ -905,6 +905,18 @@ class Part(MPTTModel):
|
||||
def has_bom(self):
|
||||
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
|
||||
def bom_count(self):
|
||||
""" Return the number of items contained in the BOM for this part """
|
||||
@ -1188,7 +1200,7 @@ class Part(MPTTModel):
|
||||
parameter.save()
|
||||
|
||||
@transaction.atomic
|
||||
def deepCopy(self, other, **kwargs):
|
||||
def deep_copy(self, other, **kwargs):
|
||||
""" Duplicates non-field data from another part.
|
||||
Does not alter the normal fields of this part,
|
||||
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)
|
||||
|
||||
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):
|
||||
|
||||
|
@ -360,7 +360,7 @@ class MakePartVariant(AjaxCreateView):
|
||||
parameters_copy = str2bool(request.POST.get('parameters_copy', False))
|
||||
|
||||
# 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)
|
||||
|
||||
@ -473,7 +473,7 @@ class PartDuplicate(AjaxCreateView):
|
||||
original = self.get_part_to_copy()
|
||||
|
||||
if original:
|
||||
part.deepCopy(original, bom=bom_copy, parameters=parameters_copy)
|
||||
part.deep_copy(original, bom=bom_copy, parameters=parameters_copy)
|
||||
|
||||
try:
|
||||
data['url'] = part.get_absolute_url()
|
||||
|
@ -175,7 +175,7 @@ class StockItem(MPTTModel):
|
||||
if add_note:
|
||||
# This StockItem is being saved for the first time
|
||||
self.addTransactionNote(
|
||||
'Created stock item',
|
||||
_('Created stock item'),
|
||||
user,
|
||||
notes="Created new stock item for part '{p}'".format(p=str(self.part)),
|
||||
system=True
|
||||
@ -200,12 +200,6 @@ class StockItem(MPTTModel):
|
||||
|
||||
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 self.serial is not None:
|
||||
# Query to look for duplicate serial numbers
|
||||
|
@ -152,17 +152,7 @@ function loadBomTable(table, options) {
|
||||
|
||||
var sub_part = row.sub_part_detail;
|
||||
|
||||
if (sub_part.trackable) {
|
||||
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" %}');
|
||||
}
|
||||
html += makePartIcons(row.sub_part_detail);
|
||||
|
||||
// Display an extra icon if this part is an assembly
|
||||
if (sub_part.assembly) {
|
||||
@ -171,10 +161,6 @@ function loadBomTable(table, options) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -36,11 +36,8 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
/* Generate action buttons for a build output.
|
||||
*/
|
||||
|
||||
var outputId = 'untracked';
|
||||
|
||||
if (output) {
|
||||
outputId = output.pk;
|
||||
}
|
||||
var buildId = buildInfo.pk;
|
||||
var outputId = output.pk;
|
||||
|
||||
var panel = `#allocation-panel-${outputId}`;
|
||||
|
||||
@ -54,23 +51,19 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
// Add a button to "auto allocate" against the build
|
||||
if (!output) {
|
||||
html += makeIconButton(
|
||||
'fa-magic icon-blue', 'button-output-auto', outputId,
|
||||
'{% trans "Auto-allocate stock items to this output" %}',
|
||||
);
|
||||
}
|
||||
|
||||
if (output) {
|
||||
// Add a button to "complete" the particular build output
|
||||
html += makeIconButton(
|
||||
'fa-check icon-green', 'button-output-complete', outputId,
|
||||
'{% trans "Complete build output" %}',
|
||||
{
|
||||
disabled: true
|
||||
//disabled: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add a button to "cancel" the particular build output (unallocate)
|
||||
html += makeIconButton(
|
||||
@ -78,7 +71,6 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
'{% trans "Unallocate stock from build output" %}',
|
||||
);
|
||||
|
||||
if (output) {
|
||||
// Add a button to "delete" the particular build output
|
||||
html += makeIconButton(
|
||||
'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)
|
||||
// TODO
|
||||
|
||||
}
|
||||
|
||||
|
||||
html += '</div>';
|
||||
|
||||
buildActions.html(html);
|
||||
@ -103,13 +92,21 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
data: {
|
||||
output: outputId,
|
||||
},
|
||||
reload: true,
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(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() {
|
||||
@ -155,18 +152,12 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
var outputId = null;
|
||||
|
||||
if (output) {
|
||||
outputId = output.pk;
|
||||
}
|
||||
|
||||
var table = options.table;
|
||||
|
||||
if (options.table == null) {
|
||||
if (outputId != null) {
|
||||
table = `#allocation-table-${outputId}`;
|
||||
} else {
|
||||
table = `#allocation-table-untracked`;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
@ -177,14 +168,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
function requiredQuantity(row) {
|
||||
// Return the requied quantity for a given row
|
||||
|
||||
if (output) {
|
||||
// Tracked stock allocated against a particular BuildOutput
|
||||
return row.quantity * output.quantity;
|
||||
} else {
|
||||
// Untrack stock allocated against the build
|
||||
return row.quantity * (buildInfo.quantity - buildInfo.completed);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function sumAllocations(row) {
|
||||
@ -300,7 +284,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
queryParams: {
|
||||
part: partId,
|
||||
sub_part_detail: true,
|
||||
sub_part_trackable: outputId != null,
|
||||
},
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No BOM items found" %}';
|
||||
@ -357,15 +340,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
var allocatedQuantity = sumAllocations(tableRow);
|
||||
|
||||
// Is this line item fully allocated?
|
||||
if (output) {
|
||||
if (allocatedQuantity >= (tableRow.quantity * output.quantity)) {
|
||||
allocatedLines += 1;
|
||||
}
|
||||
} else {
|
||||
if (allocatedQuantity >= (tableRow.quantity * (buildInfo.quantity - buildInfo.completed))) {
|
||||
allocatedLines += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Push the updated row back into the main table
|
||||
$(table).bootstrapTable('updateByUniqueId', key, tableRow, true);
|
||||
@ -506,6 +483,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
var html = imageHoverIcon(thumb) + renderLink(name, url);
|
||||
|
||||
html += makePartIcons(row.sub_part_detail);
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
@ -547,13 +526,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
var qA = rowA.quantity;
|
||||
var qB = rowB.quantity;
|
||||
|
||||
if (output) {
|
||||
qA *= output.quantity;
|
||||
qB *= output.quantity;
|
||||
} else {
|
||||
qA *= buildInfo.quantity;
|
||||
qB *= buildInfo.quantity;
|
||||
}
|
||||
|
||||
// Handle the case where both numerators are zero
|
||||
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={}) {
|
||||
/* Load part variant table
|
||||
*/
|
||||
@ -340,40 +379,8 @@ function loadPartTable(table, url, options={}) {
|
||||
|
||||
var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');
|
||||
|
||||
if (row.trackable) {
|
||||
display += makeIconBadge('fa-directions', '{% trans "Trackable part" %}');
|
||||
}
|
||||
display += makePartIcons(row);
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
@ -908,7 +908,7 @@ function loadInstalledInTable(table, options) {
|
||||
url: "{% url 'api-bom-list' %}",
|
||||
queryParams: {
|
||||
part: options.part,
|
||||
trackable: true,
|
||||
sub_part_trackable: true,
|
||||
sub_part_detail: true,
|
||||
},
|
||||
showColumns: false,
|
||||
|
Loading…
Reference in New Issue
Block a user