mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 20:46:47 +00:00
Merge pull request #2674 from inventree/0.6.x
[WIP] 0.6.1 Stable Release
This commit is contained in:
commit
f237e7743d
@ -258,6 +258,7 @@ class StockHistoryCode(StatusCode):
|
|||||||
# Build order codes
|
# Build order codes
|
||||||
BUILD_OUTPUT_CREATED = 50
|
BUILD_OUTPUT_CREATED = 50
|
||||||
BUILD_OUTPUT_COMPLETED = 55
|
BUILD_OUTPUT_COMPLETED = 55
|
||||||
|
BUILD_CONSUMED = 57
|
||||||
|
|
||||||
# Sales order codes
|
# Sales order codes
|
||||||
|
|
||||||
@ -298,6 +299,7 @@ class StockHistoryCode(StatusCode):
|
|||||||
|
|
||||||
BUILD_OUTPUT_CREATED: _('Build order output created'),
|
BUILD_OUTPUT_CREATED: _('Build order output created'),
|
||||||
BUILD_OUTPUT_COMPLETED: _('Build order output completed'),
|
BUILD_OUTPUT_COMPLETED: _('Build order output completed'),
|
||||||
|
BUILD_CONSUMED: _('Consumed by build order'),
|
||||||
|
|
||||||
RECEIVED_AGAINST_PURCHASE_ORDER: _('Received against purchase order')
|
RECEIVED_AGAINST_PURCHASE_ORDER: _('Received against purchase order')
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import re
|
|||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
# InvenTree software version
|
# InvenTree software version
|
||||||
INVENTREE_SW_VERSION = "0.6.0"
|
INVENTREE_SW_VERSION = "0.6.1"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 26
|
INVENTREE_API_VERSION = 26
|
||||||
|
@ -30,8 +30,6 @@ from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
|||||||
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
|
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
|
||||||
from InvenTree.validators import validate_build_order_reference
|
from InvenTree.validators import validate_build_order_reference
|
||||||
|
|
||||||
import common.models
|
|
||||||
|
|
||||||
import InvenTree.fields
|
import InvenTree.fields
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
@ -385,9 +383,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
Returns the BOM items for the part referenced by this BuildOrder
|
Returns the BOM items for the part referenced by this BuildOrder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.part.bom_items.all().prefetch_related(
|
return self.part.get_bom_items()
|
||||||
'sub_part'
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tracked_bom_items(self):
|
def tracked_bom_items(self):
|
||||||
@ -479,8 +475,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
outputs = self.get_build_outputs(complete=True)
|
outputs = self.get_build_outputs(complete=True)
|
||||||
|
|
||||||
# TODO - Ordering?
|
|
||||||
|
|
||||||
return outputs
|
return outputs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -491,8 +485,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
outputs = self.get_build_outputs(complete=False)
|
outputs = self.get_build_outputs(complete=False)
|
||||||
|
|
||||||
# TODO - Order by how "complete" they are?
|
|
||||||
|
|
||||||
return outputs
|
return outputs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -563,7 +555,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
if self.remaining > 0:
|
if self.remaining > 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not self.areUntrackedPartsFullyAllocated():
|
if not self.are_untracked_parts_allocated():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# No issues!
|
# No issues!
|
||||||
@ -584,7 +576,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
# Remove untracked allocated stock
|
# Remove untracked allocated stock
|
||||||
self.subtractUntrackedStock(user)
|
self.subtract_allocated_stock(user)
|
||||||
|
|
||||||
# Ensure that there are no longer any BuildItem objects
|
# Ensure that there are no longer any BuildItem objects
|
||||||
# which point to thisFcan Build Order
|
# which point to thisFcan Build Order
|
||||||
@ -768,7 +760,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
output.delete()
|
output.delete()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def subtractUntrackedStock(self, user):
|
def subtract_allocated_stock(self, user):
|
||||||
"""
|
"""
|
||||||
Called when the Build is marked as "complete",
|
Called when the Build is marked as "complete",
|
||||||
this function removes the allocated untracked items from stock.
|
this function removes the allocated untracked items from stock.
|
||||||
@ -831,7 +823,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def requiredQuantity(self, part, output):
|
def required_quantity(self, bom_item, output=None):
|
||||||
"""
|
"""
|
||||||
Get the quantity of a part required to complete the particular build output.
|
Get the quantity of a part required to complete the particular build output.
|
||||||
|
|
||||||
@ -840,12 +832,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
output - The particular build output (StockItem)
|
output - The particular build output (StockItem)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Extract the BOM line item from the database
|
quantity = bom_item.quantity
|
||||||
try:
|
|
||||||
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:
|
if output:
|
||||||
quantity *= output.quantity
|
quantity *= output.quantity
|
||||||
@ -854,32 +841,32 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
def allocatedItems(self, part, output):
|
def allocated_bom_items(self, bom_item, output=None):
|
||||||
"""
|
"""
|
||||||
Return all BuildItem objects which allocate stock of <part> to <output>
|
Return all BuildItem objects which allocate stock of <bom_item> to <output>
|
||||||
|
|
||||||
|
Note that the bom_item may allow variants, or direct substitutes,
|
||||||
|
making things difficult.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
part - The part object
|
bom_item - The BomItem object
|
||||||
output - Build output (StockItem).
|
output - Build output (StockItem).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Remember, if 'variant' stock is allowed to be allocated, it becomes more complicated!
|
|
||||||
variants = part.get_descendants(include_self=True)
|
|
||||||
|
|
||||||
allocations = BuildItem.objects.filter(
|
allocations = BuildItem.objects.filter(
|
||||||
build=self,
|
build=self,
|
||||||
stock_item__part__pk__in=[p.pk for p in variants],
|
bom_item=bom_item,
|
||||||
install_into=output,
|
install_into=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
return allocations
|
return allocations
|
||||||
|
|
||||||
def allocatedQuantity(self, part, output):
|
def allocated_quantity(self, bom_item, output=None):
|
||||||
"""
|
"""
|
||||||
Return the total quantity of given part allocated to a given build output.
|
Return the total quantity of given part allocated to a given build output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allocations = self.allocatedItems(part, output)
|
allocations = self.allocated_bom_items(bom_item, output)
|
||||||
|
|
||||||
allocated = allocations.aggregate(
|
allocated = allocations.aggregate(
|
||||||
q=Coalesce(
|
q=Coalesce(
|
||||||
@ -891,24 +878,24 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
return allocated['q']
|
return allocated['q']
|
||||||
|
|
||||||
def unallocatedQuantity(self, part, output):
|
def unallocated_quantity(self, bom_item, output=None):
|
||||||
"""
|
"""
|
||||||
Return the total unallocated (remaining) quantity of a part against a particular output.
|
Return the total unallocated (remaining) quantity of a part against a particular output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
required = self.requiredQuantity(part, output)
|
required = self.required_quantity(bom_item, output)
|
||||||
allocated = self.allocatedQuantity(part, output)
|
allocated = self.allocated_quantity(bom_item, output)
|
||||||
|
|
||||||
return max(required - allocated, 0)
|
return max(required - allocated, 0)
|
||||||
|
|
||||||
def isPartFullyAllocated(self, part, output):
|
def is_bom_item_allocated(self, bom_item, output=None):
|
||||||
"""
|
"""
|
||||||
Returns True if the part has been fully allocated to the particular build output
|
Test if the supplied BomItem has been fully allocated!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.unallocatedQuantity(part, output) == 0
|
return self.unallocated_quantity(bom_item, output) == 0
|
||||||
|
|
||||||
def isFullyAllocated(self, output, verbose=False):
|
def is_fully_allocated(self, output):
|
||||||
"""
|
"""
|
||||||
Returns True if the particular build output is fully allocated.
|
Returns True if the particular build output is fully allocated.
|
||||||
"""
|
"""
|
||||||
@ -919,53 +906,24 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
else:
|
else:
|
||||||
bom_items = self.tracked_bom_items
|
bom_items = self.tracked_bom_items
|
||||||
|
|
||||||
fully_allocated = True
|
|
||||||
|
|
||||||
for bom_item in bom_items:
|
for bom_item in bom_items:
|
||||||
part = bom_item.sub_part
|
|
||||||
|
|
||||||
if not self.isPartFullyAllocated(part, output):
|
if not self.is_bom_item_allocated(bom_item, output):
|
||||||
fully_allocated = False
|
return False
|
||||||
|
|
||||||
if verbose:
|
|
||||||
print(f"Part {part} is not fully allocated for output {output}")
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
# All parts must be fully allocated!
|
# All parts must be fully allocated!
|
||||||
return fully_allocated
|
return True
|
||||||
|
|
||||||
def areUntrackedPartsFullyAllocated(self):
|
def are_untracked_parts_allocated(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the un-tracked parts are fully allocated for this BuildOrder
|
Returns True if the un-tracked parts are fully allocated for this BuildOrder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.isFullyAllocated(None)
|
return self.is_fully_allocated(None)
|
||||||
|
|
||||||
def allocatedParts(self, output):
|
def unallocated_bom_items(self, output):
|
||||||
"""
|
"""
|
||||||
Return a list of parts which have been fully allocated against a particular output
|
Return a list of bom items which have *not* been fully allocated against a particular output
|
||||||
"""
|
|
||||||
|
|
||||||
allocated = []
|
|
||||||
|
|
||||||
# If output is not specified, we are talking about "untracked" items
|
|
||||||
if output is None:
|
|
||||||
bom_items = self.untracked_bom_items
|
|
||||||
else:
|
|
||||||
bom_items = self.tracked_bom_items
|
|
||||||
|
|
||||||
for bom_item in 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 = []
|
unallocated = []
|
||||||
@ -977,10 +935,9 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
bom_items = self.tracked_bom_items
|
bom_items = self.tracked_bom_items
|
||||||
|
|
||||||
for bom_item in bom_items:
|
for bom_item in bom_items:
|
||||||
part = bom_item.sub_part
|
|
||||||
|
|
||||||
if not self.isPartFullyAllocated(part, output):
|
if not self.is_bom_item_allocated(bom_item, output):
|
||||||
unallocated.append(part)
|
unallocated.append(bom_item)
|
||||||
|
|
||||||
return unallocated
|
return unallocated
|
||||||
|
|
||||||
@ -1008,57 +965,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
def availableStockItems(self, part, output):
|
|
||||||
"""
|
|
||||||
Returns stock items which are available for allocation to this build.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
part - Part object
|
|
||||||
output - The particular build output
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Grab initial query for items which are "in stock" and match the part
|
|
||||||
items = StockModels.StockItem.objects.filter(
|
|
||||||
StockModels.StockItem.IN_STOCK_FILTER
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if variants are allowed for this part
|
|
||||||
try:
|
|
||||||
bom_item = PartModels.BomItem.objects.get(part=self.part, sub_part=part)
|
|
||||||
allow_part_variants = bom_item.allow_variants
|
|
||||||
except PartModels.BomItem.DoesNotExist:
|
|
||||||
allow_part_variants = False
|
|
||||||
|
|
||||||
if allow_part_variants:
|
|
||||||
parts = part.get_descendants(include_self=True)
|
|
||||||
items = items.filter(part__pk__in=[p.pk for p in parts])
|
|
||||||
|
|
||||||
else:
|
|
||||||
items = items.filter(part=part)
|
|
||||||
|
|
||||||
# Exclude any items which have already been allocated
|
|
||||||
allocated = BuildItem.objects.filter(
|
|
||||||
build=self,
|
|
||||||
stock_item__part=part,
|
|
||||||
install_into=output,
|
|
||||||
)
|
|
||||||
|
|
||||||
items = items.exclude(
|
|
||||||
id__in=[item.stock_item.id for item in allocated.all()]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Limit query to stock items which are "downstream" of the source location
|
|
||||||
if self.take_from is not None:
|
|
||||||
items = items.filter(
|
|
||||||
location__in=[loc for loc in self.take_from.getUniqueChildren()]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Exclude expired stock items
|
|
||||||
if not common.models.InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_BUILD'):
|
|
||||||
items = items.exclude(StockModels.StockItem.EXPIRED_FILTER)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
@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:
|
||||||
@ -1257,7 +1163,12 @@ class BuildItem(models.Model):
|
|||||||
if item.part.trackable:
|
if item.part.trackable:
|
||||||
# Split the allocated stock if there are more available than allocated
|
# Split the allocated stock if there are more available than allocated
|
||||||
if item.quantity > self.quantity:
|
if item.quantity > self.quantity:
|
||||||
item = item.splitStock(self.quantity, None, user)
|
item = item.splitStock(
|
||||||
|
self.quantity,
|
||||||
|
None,
|
||||||
|
user,
|
||||||
|
code=StockHistoryCode.BUILD_CONSUMED,
|
||||||
|
)
|
||||||
|
|
||||||
# Make sure we are pointing to the new item
|
# Make sure we are pointing to the new item
|
||||||
self.stock_item = item
|
self.stock_item = item
|
||||||
@ -1268,7 +1179,11 @@ class BuildItem(models.Model):
|
|||||||
item.save()
|
item.save()
|
||||||
else:
|
else:
|
||||||
# Simply remove the items from stock
|
# Simply remove the items from stock
|
||||||
item.take_stock(self.quantity, user)
|
item.take_stock(
|
||||||
|
self.quantity,
|
||||||
|
user,
|
||||||
|
code=StockHistoryCode.BUILD_CONSUMED
|
||||||
|
)
|
||||||
|
|
||||||
def getStockItemThumbnail(self):
|
def getStockItemThumbnail(self):
|
||||||
"""
|
"""
|
||||||
|
@ -160,7 +160,7 @@ class BuildOutputSerializer(serializers.Serializer):
|
|||||||
if to_complete:
|
if to_complete:
|
||||||
|
|
||||||
# The build output must have all tracked parts allocated
|
# The build output must have all tracked parts allocated
|
||||||
if not build.isFullyAllocated(output):
|
if not build.is_fully_allocated(output):
|
||||||
raise ValidationError(_("This build output is not fully allocated"))
|
raise ValidationError(_("This build output is not fully allocated"))
|
||||||
|
|
||||||
return output
|
return output
|
||||||
@ -236,6 +236,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
auto_allocate = serializers.BooleanField(
|
auto_allocate = serializers.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
default=False,
|
default=False,
|
||||||
|
allow_null=True,
|
||||||
label=_('Auto Allocate Serial Numbers'),
|
label=_('Auto Allocate Serial Numbers'),
|
||||||
help_text=_('Automatically allocate required items with matching serial numbers'),
|
help_text=_('Automatically allocate required items with matching serial numbers'),
|
||||||
)
|
)
|
||||||
@ -403,6 +404,10 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
data = self.validated_data
|
data = self.validated_data
|
||||||
|
|
||||||
|
location = data['location']
|
||||||
|
status = data['status']
|
||||||
|
notes = data.get('notes', '')
|
||||||
|
|
||||||
outputs = data.get('outputs', [])
|
outputs = data.get('outputs', [])
|
||||||
|
|
||||||
# Mark the specified build outputs as "complete"
|
# Mark the specified build outputs as "complete"
|
||||||
@ -414,8 +419,9 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
build.complete_build_output(
|
build.complete_build_output(
|
||||||
output,
|
output,
|
||||||
request.user,
|
request.user,
|
||||||
status=data['status'],
|
location=location,
|
||||||
notes=data.get('notes', '')
|
status=status,
|
||||||
|
notes=notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -435,7 +441,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
if not build.areUntrackedPartsFullyAllocated() and not value:
|
if not build.are_untracked_parts_allocated() and not value:
|
||||||
raise ValidationError(_('Required stock has not been fully allocated'))
|
raise ValidationError(_('Required stock has not been fully allocated'))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
@ -125,7 +125,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% trans "Required build quantity has not yet been completed" %}
|
{% trans "Required build quantity has not yet been completed" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not build.areUntrackedPartsFullyAllocated %}
|
{% if not build.are_untracked_parts_allocated %}
|
||||||
<div class='alert alert-block alert-warning'>
|
<div class='alert alert-block alert-warning'>
|
||||||
{% trans "Stock has not been fully allocated to this Build Order" %}
|
{% trans "Stock has not been fully allocated to this Build Order" %}
|
||||||
</div>
|
</div>
|
||||||
@ -234,7 +234,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
completeBuildOrder({{ build.pk }}, {
|
completeBuildOrder({{ build.pk }}, {
|
||||||
allocated: {% if build.areUntrackedPartsFullyAllocated %}true{% else %}false{% endif %},
|
allocated: {% if build.are_untracked_parts_allocated %}true{% else %}false{% endif %},
|
||||||
completed: {% if build.remaining == 0 %}true{% else %}false{% endif %},
|
completed: {% if build.remaining == 0 %}true{% else %}false{% endif %},
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -192,7 +192,7 @@
|
|||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% if build.has_untracked_bom_items %}
|
{% if build.has_untracked_bom_items %}
|
||||||
{% if build.active %}
|
{% if build.active %}
|
||||||
{% if build.areUntrackedPartsFullyAllocated %}
|
{% if build.are_untracked_parts_allocated %}
|
||||||
<div class='alert alert-block alert-success'>
|
<div class='alert alert-block alert-success'>
|
||||||
{% trans "Untracked stock has been fully allocated for this Build Order" %}
|
{% trans "Untracked stock has been fully allocated for this Build Order" %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,20 +62,20 @@ class BuildTest(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create BOM item links for the parts
|
# Create BOM item links for the parts
|
||||||
BomItem.objects.create(
|
self.bom_item_1 = BomItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
sub_part=self.sub_part_1,
|
sub_part=self.sub_part_1,
|
||||||
quantity=5
|
quantity=5
|
||||||
)
|
)
|
||||||
|
|
||||||
BomItem.objects.create(
|
self.bom_item_2 = BomItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
sub_part=self.sub_part_2,
|
sub_part=self.sub_part_2,
|
||||||
quantity=3
|
quantity=3
|
||||||
)
|
)
|
||||||
|
|
||||||
# sub_part_3 is trackable!
|
# sub_part_3 is trackable!
|
||||||
BomItem.objects.create(
|
self.bom_item_3 = BomItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
sub_part=self.sub_part_3,
|
sub_part=self.sub_part_3,
|
||||||
quantity=2
|
quantity=2
|
||||||
@ -147,15 +147,15 @@ class BuildTest(TestCase):
|
|||||||
|
|
||||||
# None of the build outputs have been completed
|
# None of the build outputs have been completed
|
||||||
for output in self.build.get_build_outputs().all():
|
for output in self.build.get_build_outputs().all():
|
||||||
self.assertFalse(self.build.isFullyAllocated(output))
|
self.assertFalse(self.build.is_fully_allocated(output))
|
||||||
|
|
||||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1))
|
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_1, self.output_1))
|
||||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2, self.output_2))
|
||||||
|
|
||||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 15)
|
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1, self.output_1), 15)
|
||||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_2), 35)
|
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1, self.output_2), 35)
|
||||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_1), 9)
|
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2, self.output_1), 9)
|
||||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_2), 21)
|
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2, self.output_2), 21)
|
||||||
|
|
||||||
self.assertFalse(self.build.is_complete)
|
self.assertFalse(self.build.is_complete)
|
||||||
|
|
||||||
@ -226,7 +226,7 @@ class BuildTest(TestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
self.assertTrue(self.build.is_fully_allocated(self.output_1))
|
||||||
|
|
||||||
# Partially allocate tracked stock against build output 2
|
# Partially allocate tracked stock against build output 2
|
||||||
self.allocate_stock(
|
self.allocate_stock(
|
||||||
@ -236,7 +236,7 @@ class BuildTest(TestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertFalse(self.build.isFullyAllocated(self.output_2))
|
self.assertFalse(self.build.is_fully_allocated(self.output_2))
|
||||||
|
|
||||||
# Partially allocate untracked stock against build
|
# Partially allocate untracked stock against build
|
||||||
self.allocate_stock(
|
self.allocate_stock(
|
||||||
@ -247,9 +247,9 @@ class BuildTest(TestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertFalse(self.build.isFullyAllocated(None, verbose=True))
|
self.assertFalse(self.build.is_fully_allocated(None))
|
||||||
|
|
||||||
unallocated = self.build.unallocatedParts(None)
|
unallocated = self.build.unallocated_bom_items(None)
|
||||||
|
|
||||||
self.assertEqual(len(unallocated), 2)
|
self.assertEqual(len(unallocated), 2)
|
||||||
|
|
||||||
@ -260,19 +260,19 @@ class BuildTest(TestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertFalse(self.build.isFullyAllocated(None, verbose=True))
|
self.assertFalse(self.build.is_fully_allocated(None))
|
||||||
|
|
||||||
unallocated = self.build.unallocatedParts(None)
|
unallocated = self.build.unallocated_bom_items(None)
|
||||||
|
|
||||||
self.assertEqual(len(unallocated), 1)
|
self.assertEqual(len(unallocated), 1)
|
||||||
|
|
||||||
self.build.unallocateStock()
|
self.build.unallocateStock()
|
||||||
|
|
||||||
unallocated = self.build.unallocatedParts(None)
|
unallocated = self.build.unallocated_bom_items(None)
|
||||||
|
|
||||||
self.assertEqual(len(unallocated), 2)
|
self.assertEqual(len(unallocated), 2)
|
||||||
|
|
||||||
self.assertFalse(self.build.areUntrackedPartsFullyAllocated())
|
self.assertFalse(self.build.are_untracked_parts_allocated())
|
||||||
|
|
||||||
# Now we "fully" allocate the untracked untracked items
|
# Now we "fully" allocate the untracked untracked items
|
||||||
self.allocate_stock(
|
self.allocate_stock(
|
||||||
@ -283,7 +283,7 @@ class BuildTest(TestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(self.build.areUntrackedPartsFullyAllocated())
|
self.assertTrue(self.build.are_untracked_parts_allocated())
|
||||||
|
|
||||||
def test_cancel(self):
|
def test_cancel(self):
|
||||||
"""
|
"""
|
||||||
@ -331,9 +331,9 @@ class BuildTest(TestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(self.build.isFullyAllocated(None, verbose=True))
|
self.assertTrue(self.build.is_fully_allocated(None))
|
||||||
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
self.assertTrue(self.build.is_fully_allocated(self.output_1))
|
||||||
self.assertTrue(self.build.isFullyAllocated(self.output_2))
|
self.assertTrue(self.build.is_fully_allocated(self.output_2))
|
||||||
|
|
||||||
self.build.complete_build_output(self.output_1, None)
|
self.build.complete_build_output(self.output_1, None)
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# Generated by Django 3.0.7 on 2020-11-10 11:25
|
# Generated by Django 3.0.7 on 2020-11-10 11:25
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
@ -7,6 +9,9 @@ from django.db import migrations, connection
|
|||||||
from company.models import SupplierPriceBreak
|
from company.models import SupplierPriceBreak
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def migrate_currencies(apps, schema_editor):
|
def migrate_currencies(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
Migrate from the 'old' method of handling currencies,
|
Migrate from the 'old' method of handling currencies,
|
||||||
@ -19,7 +24,7 @@ def migrate_currencies(apps, schema_editor):
|
|||||||
for the SupplierPriceBreak model, to a new django-money compatible currency.
|
for the SupplierPriceBreak model, to a new django-money compatible currency.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print("Updating currency references for SupplierPriceBreak model...")
|
logger.info("Updating currency references for SupplierPriceBreak model...")
|
||||||
|
|
||||||
# A list of available currency codes
|
# A list of available currency codes
|
||||||
currency_codes = CURRENCIES.keys()
|
currency_codes = CURRENCIES.keys()
|
||||||
|
@ -1453,7 +1453,9 @@ class Part(MPTTModel):
|
|||||||
By default, will include inherited BOM items
|
By default, will include inherited BOM items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
|
queryset = BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
|
||||||
|
|
||||||
|
return queryset.prefetch_related('sub_part')
|
||||||
|
|
||||||
def get_installed_part_options(self, include_inherited=True, include_variants=True):
|
def get_installed_part_options(self, include_inherited=True, include_variants=True):
|
||||||
"""
|
"""
|
||||||
|
@ -122,7 +122,13 @@
|
|||||||
<h4>{% trans "Sales Order Allocations" %}</h4>
|
<h4>{% trans "Sales Order Allocations" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<table class='table table-striped table-condensed' id='sales-order-allocation-table'></table>
|
|
||||||
|
<div id='sales-order-allocation-button-toolbar'>
|
||||||
|
<div class='btn-group' role='group'>
|
||||||
|
{% include "filter_list.html" with id="salesorderallocation" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class='table table-striped table-condensed' id='sales-order-allocation-table' data-toolbar='#sales-order-allocation-button-toolbar'></table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -342,7 +348,12 @@
|
|||||||
<h4>{% trans "Build Order Allocations" %}</h4>
|
<h4>{% trans "Build Order Allocations" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<table class='table table-striped table-condensed' id='build-order-allocation-table'></table>
|
<div id='build-allocation-button-toolbar'>
|
||||||
|
<div class='btn-group' role='group'>
|
||||||
|
{% include "filter_list.html" with id="buildorderallocation" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class='table table-striped table-condensed' id='build-order-allocation-table' data-toolbar='#build-allocation-button-toolbar'></table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -722,6 +733,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Load the BOM table data in the pricing view
|
// Load the BOM table data in the pricing view
|
||||||
|
{% if part.has_bom and roles.sales_order.view %}
|
||||||
loadBomTable($("#bom-pricing-table"), {
|
loadBomTable($("#bom-pricing-table"), {
|
||||||
editable: false,
|
editable: false,
|
||||||
bom_url: "{% url 'api-bom-list' %}",
|
bom_url: "{% url 'api-bom-list' %}",
|
||||||
@ -729,6 +741,7 @@
|
|||||||
parent_id: {{ part.id }} ,
|
parent_id: {{ part.id }} ,
|
||||||
sub_part_detail: true,
|
sub_part_detail: true,
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
onPanelLoad("purchase-orders", function() {
|
onPanelLoad("purchase-orders", function() {
|
||||||
loadPartPurchaseOrderTable(
|
loadPartPurchaseOrderTable(
|
||||||
|
@ -59,13 +59,13 @@
|
|||||||
<ul class='dropdown-menu'>
|
<ul class='dropdown-menu'>
|
||||||
<li>
|
<li>
|
||||||
<a class='dropdown-item' href='#' id='part-count'>
|
<a class='dropdown-item' href='#' id='part-count'>
|
||||||
<span class='fas fa-clipboard-list'></span>
|
<span class='fas fa-check-circle icon-green'></span>
|
||||||
{% trans "Count part stock" %}
|
{% trans "Count part stock" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class='dropdown-item' href='#' id='part-move'>
|
<a class='dropdown-item' href='#' id='part-move'>
|
||||||
<span class='fas fa-exchange-alt'></span>
|
<span class='fas fa-exchange-alt icon-blue'></span>
|
||||||
{% trans "Transfer part stock" %}
|
{% trans "Transfer part stock" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -5,11 +5,13 @@ import logging
|
|||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from maintenance_mode.core import set_maintenance_mode
|
from maintenance_mode.core import set_maintenance_mode
|
||||||
|
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
from plugin.helpers import check_git_version, log_error
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
@ -34,3 +36,8 @@ class PluginAppConfig(AppConfig):
|
|||||||
# drop out of maintenance
|
# drop out of maintenance
|
||||||
# makes sure we did not have an error in reloading and maintenance is still active
|
# makes sure we did not have an error in reloading and maintenance is still active
|
||||||
set_maintenance_mode(False)
|
set_maintenance_mode(False)
|
||||||
|
|
||||||
|
# check git version
|
||||||
|
registry.git_is_modern = check_git_version()
|
||||||
|
if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage
|
||||||
|
log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
|
||||||
|
@ -94,21 +94,46 @@ def get_git_log(path):
|
|||||||
"""
|
"""
|
||||||
Get dict with info of the last commit to file named in path
|
Get dict with info of the last commit to file named in path
|
||||||
"""
|
"""
|
||||||
path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:]
|
from plugin import registry
|
||||||
command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path]
|
|
||||||
output = None
|
output = None
|
||||||
try:
|
if registry.git_is_modern:
|
||||||
output = str(subprocess.check_output(command, cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')[1:-1]
|
path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:]
|
||||||
if output:
|
command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path]
|
||||||
output = output.split('\n')
|
try:
|
||||||
except subprocess.CalledProcessError: # pragma: no cover
|
output = str(subprocess.check_output(command, cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')[1:-1]
|
||||||
pass
|
if output:
|
||||||
|
output = output.split('\n')
|
||||||
|
except subprocess.CalledProcessError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
output = 7 * [''] # pragma: no cover
|
output = 7 * [''] # pragma: no cover
|
||||||
|
|
||||||
return {'hash': output[0], 'author': output[1], 'mail': output[2], 'date': output[3], 'message': output[4], 'verified': output[5], 'key': output[6]}
|
return {'hash': output[0], 'author': output[1], 'mail': output[2], 'date': output[3], 'message': output[4], 'verified': output[5], 'key': output[6]}
|
||||||
|
|
||||||
|
|
||||||
|
def check_git_version():
|
||||||
|
"""returns if the current git version supports modern features"""
|
||||||
|
|
||||||
|
# get version string
|
||||||
|
try:
|
||||||
|
output = str(subprocess.check_output(['git', '--version'], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')
|
||||||
|
except subprocess.CalledProcessError: # pragma: no cover
|
||||||
|
return False
|
||||||
|
|
||||||
|
# process version string
|
||||||
|
try:
|
||||||
|
version = output[12:-1].split(".")
|
||||||
|
if len(version) > 1 and version[0] == '2':
|
||||||
|
if len(version) > 2 and int(version[1]) >= 22:
|
||||||
|
return True
|
||||||
|
except ValueError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class GitStatus:
|
class GitStatus:
|
||||||
"""
|
"""
|
||||||
Class for resolving git gpg singing state
|
Class for resolving git gpg singing state
|
||||||
|
@ -52,6 +52,7 @@ class PluginsRegistry:
|
|||||||
# flags
|
# flags
|
||||||
self.is_loading = False
|
self.is_loading = False
|
||||||
self.apps_loading = True # Marks if apps were reloaded yet
|
self.apps_loading = True # Marks if apps were reloaded yet
|
||||||
|
self.git_is_modern = True # Is a modern version of git available
|
||||||
|
|
||||||
# integration specific
|
# integration specific
|
||||||
self.installed_apps = [] # Holds all added plugin_paths
|
self.installed_apps = [] # Holds all added plugin_paths
|
||||||
|
@ -63,6 +63,43 @@ class StockLocation(InvenTreeTree):
|
|||||||
help_text=_('Select Owner'),
|
help_text=_('Select Owner'),
|
||||||
related_name='stock_locations')
|
related_name='stock_locations')
|
||||||
|
|
||||||
|
def get_location_owner(self):
|
||||||
|
"""
|
||||||
|
Get the closest "owner" for this location.
|
||||||
|
|
||||||
|
Start at this location, and traverse "up" the location tree until we find an owner
|
||||||
|
"""
|
||||||
|
|
||||||
|
for loc in self.get_ancestors(include_self=True, ascending=True):
|
||||||
|
if loc.owner is not None:
|
||||||
|
return loc.owner
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_ownership(self, user):
|
||||||
|
"""
|
||||||
|
Check if the user "owns" (is one of the owners of) the location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Superuser accounts automatically "own" everything
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
ownership_enabled = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
|
||||||
|
if not ownership_enabled:
|
||||||
|
# Location ownership function is not enabled, so return True
|
||||||
|
return True
|
||||||
|
|
||||||
|
owner = self.get_location_owner()
|
||||||
|
|
||||||
|
if owner is None:
|
||||||
|
# No owner set, for this location or any location above
|
||||||
|
# So, no ownership checks to perform!
|
||||||
|
return True
|
||||||
|
|
||||||
|
return user in owner.get_related_owners(include_group=True)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
@ -614,6 +651,48 @@ class StockItem(MPTTModel):
|
|||||||
help_text=_('Select Owner'),
|
help_text=_('Select Owner'),
|
||||||
related_name='stock_items')
|
related_name='stock_items')
|
||||||
|
|
||||||
|
def get_item_owner(self):
|
||||||
|
"""
|
||||||
|
Return the closest "owner" for this StockItem.
|
||||||
|
|
||||||
|
- If the item has an owner set, return that
|
||||||
|
- If the item is "in stock", check the StockLocation
|
||||||
|
- Otherwise, return None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.owner is not None:
|
||||||
|
return self.owner
|
||||||
|
|
||||||
|
if self.in_stock and self.location is not None:
|
||||||
|
loc_owner = self.location.get_location_owner()
|
||||||
|
|
||||||
|
if loc_owner:
|
||||||
|
return loc_owner
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_ownership(self, user):
|
||||||
|
"""
|
||||||
|
Check if the user "owns" (or is one of the owners of) the item
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Superuser accounts automatically "own" everything
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
ownership_enabled = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
|
||||||
|
if not ownership_enabled:
|
||||||
|
# Location ownership function is not enabled, so return True
|
||||||
|
return True
|
||||||
|
|
||||||
|
owner = self.get_item_owner()
|
||||||
|
|
||||||
|
if owner is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return user in owner.get_related_owners(include_group=True)
|
||||||
|
|
||||||
def is_stale(self):
|
def is_stale(self):
|
||||||
"""
|
"""
|
||||||
Returns True if this Stock item is "stale".
|
Returns True if this Stock item is "stale".
|
||||||
@ -830,7 +909,6 @@ class StockItem(MPTTModel):
|
|||||||
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:
|
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:
|
||||||
|
|
||||||
- Has installed stock items
|
- Has installed stock items
|
||||||
- Has a serial number and is tracked
|
|
||||||
- Is installed inside another StockItem
|
- Is installed inside another StockItem
|
||||||
- It has been assigned to a SalesOrder
|
- It has been assigned to a SalesOrder
|
||||||
- It has been assigned to a BuildOrder
|
- It has been assigned to a BuildOrder
|
||||||
@ -839,9 +917,6 @@ class StockItem(MPTTModel):
|
|||||||
if self.installed_item_count() > 0:
|
if self.installed_item_count() > 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.part.trackable and self.serial is not None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.sales_order is not None:
|
if self.sales_order is not None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1311,6 +1386,7 @@ class StockItem(MPTTModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
notes = kwargs.get('notes', '')
|
notes = kwargs.get('notes', '')
|
||||||
|
code = kwargs.get('code', StockHistoryCode.SPLIT_FROM_PARENT)
|
||||||
|
|
||||||
# Do not split a serialized part
|
# Do not split a serialized part
|
||||||
if self.serialized:
|
if self.serialized:
|
||||||
@ -1352,7 +1428,7 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
# Add a new tracking item for the new stock item
|
# Add a new tracking item for the new stock item
|
||||||
new_stock.add_tracking_entry(
|
new_stock.add_tracking_entry(
|
||||||
StockHistoryCode.SPLIT_FROM_PARENT,
|
code,
|
||||||
user,
|
user,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
deltas={
|
deltas={
|
||||||
@ -1530,7 +1606,7 @@ class StockItem(MPTTModel):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def take_stock(self, quantity, user, notes=''):
|
def take_stock(self, quantity, user, notes='', code=StockHistoryCode.STOCK_REMOVE):
|
||||||
"""
|
"""
|
||||||
Remove items from stock
|
Remove items from stock
|
||||||
"""
|
"""
|
||||||
@ -1550,7 +1626,7 @@ class StockItem(MPTTModel):
|
|||||||
if self.updateQuantity(self.quantity - quantity):
|
if self.updateQuantity(self.quantity - quantity):
|
||||||
|
|
||||||
self.add_tracking_entry(
|
self.add_tracking_entry(
|
||||||
StockHistoryCode.STOCK_REMOVE,
|
code,
|
||||||
user,
|
user,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
deltas={
|
deltas={
|
||||||
|
@ -18,18 +18,11 @@
|
|||||||
<h4>{% trans "Stock Tracking Information" %}</h4>
|
<h4>{% trans "Stock Tracking Information" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
{% if user_owns_item and roles.stock.change and not item.is_building %}
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners item.owner as owners %}
|
|
||||||
{% endif %}
|
|
||||||
<!-- Check permissions and owner -->
|
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners %}
|
|
||||||
{% if roles.stock.change and not item.is_building %}
|
|
||||||
<button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>
|
<button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Entry" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Entry" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,21 +59,14 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- Stock adjustment menu -->
|
<!-- Stock adjustment menu -->
|
||||||
<!-- Check permissions and owner -->
|
{% if user_owns_item %}
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners item.owner as owners %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
|
||||||
{% if roles.stock.change and not item.is_building %}
|
{% if roles.stock.change and not item.is_building %}
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
|
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
|
||||||
<ul class='dropdown-menu' role='menu'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
{% if not item.serialized %}
|
{% if not item.serialized %}
|
||||||
{% if item.in_stock %}
|
{% if item.in_stock %}
|
||||||
<li><a class='dropdown-item' href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-clipboard-list'></span> {% trans "Count stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Count stock" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not item.customer %}
|
{% if not item.customer %}
|
||||||
<li><a class='dropdown-item' href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
|
||||||
@ -219,24 +212,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners item.owner as owners %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class='info-messages'>
|
<div class='info-messages'>
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners item.owner as owners %}
|
|
||||||
|
|
||||||
{% if not user in owners and not user.is_superuser %}
|
|
||||||
<div class='alert alert-block alert-info'>
|
|
||||||
{% trans "You are not in the list of owners of this item. This stock item cannot be edited." %}<br>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if item.is_building %}
|
{% if item.is_building %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
{% trans "This stock item is in production and cannot be edited." %}<br>
|
{% trans "This stock item is in production and cannot be edited." %}<br>
|
||||||
@ -409,14 +386,28 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-vial'></span></td>
|
<td><span class='fas fa-vial'></span></td>
|
||||||
<td>{% trans "Tests" %}</td>
|
<td>{% trans "Tests" %}</td>
|
||||||
<td>{{ item.requiredTestStatus.passed }} / {{ item.requiredTestStatus.total }}</td>
|
<td>
|
||||||
|
{{ item.requiredTestStatus.passed }} / {{ item.requiredTestStatus.total }}
|
||||||
|
{% if item.passedAllRequiredTests %}
|
||||||
|
<span class='fas fa-check-circle float-right icon-green'></span>
|
||||||
|
{% else %}
|
||||||
|
<span class='fas fa-times-circle float-right icon-red'></span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.owner %}
|
{% if ownership_enabled and item_owner %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-users'></span></td>
|
<td><span class='fas fa-users'></span></td>
|
||||||
<td>{% trans "Owner" %}</td>
|
<td>{% trans "Owner" %}</td>
|
||||||
<td>{{ item.owner }}</td>
|
<td>
|
||||||
|
{{ item_owner }}
|
||||||
|
{% if not user_owns_item %}
|
||||||
|
<span class='badge rounded-pill bg-warning badge-right' title='{% trans "You are not in the list of owners of this item. This stock item cannot be edited." %}'>
|
||||||
|
{% trans "Read only" %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
|
|
||||||
<!-- Admin view -->
|
<!-- Admin view -->
|
||||||
{% if location and user.is_staff and roles.stock_location.change %}
|
{% if location and user.is_staff and roles.stock_location.change %}
|
||||||
{% url 'admin:stock_stocklocation_change' location.pk as url %}
|
{% url 'admin:stock_stocklocation_change' location.pk as url %}
|
||||||
@ -38,7 +39,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- Check permissions and owner -->
|
<!-- Check permissions and owner -->
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
{% if user_owns_location %}
|
||||||
{% if roles.stock.change %}
|
{% if roles.stock.change %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
|
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
|
||||||
@ -74,13 +75,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser or not location %}
|
{% if user_owns_location and roles.stock_location.add %}
|
||||||
{% if roles.stock_location.add %}
|
|
||||||
<button class='btn btn-success' id='location-create' type='button' title='{% trans "Create new stock location" %}'>
|
<button class='btn btn-success' id='location-create' type='button' title='{% trans "Create new stock location" %}'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Location" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Location" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details_left %}
|
{% block details_left %}
|
||||||
@ -106,23 +105,23 @@
|
|||||||
<td><em>{% trans "Top level stock location" %}</em></td>
|
<td><em>{% trans "Top level stock location" %}</em></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if ownership_enabled and location_owner %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-users'></span></td>
|
||||||
|
<td>{% trans "Location Owner" %}</td>
|
||||||
|
<td>
|
||||||
|
{{ location_owner }}
|
||||||
|
{% if not user_owns_location %}
|
||||||
|
<span class='badge rounded-pill bg-warning badge-right' title='{% trans "You are not in the list of owners of this location. This stock location cannot be edited." %}'>
|
||||||
|
{% trans "Read only" %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{% endblock details_left %}
|
{% endblock details_left %}
|
||||||
|
|
||||||
{% block details_below %}
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners location.owner as owners %}
|
|
||||||
|
|
||||||
{% if location and not user in owners and not user.is_superuser %}
|
|
||||||
<div class='alert alert-block alert-info'>
|
|
||||||
{% trans "You are not in the list of owners of this location. This stock location cannot be edited." %}<br>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock details_below %}
|
|
||||||
|
|
||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
{% if location %}
|
{% if location %}
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
|
@ -63,6 +63,11 @@ class StockIndex(InvenTreeRoleMixin, ListView):
|
|||||||
context['loc_count'] = StockLocation.objects.count()
|
context['loc_count'] = StockLocation.objects.count()
|
||||||
context['stock_count'] = StockItem.objects.count()
|
context['stock_count'] = StockItem.objects.count()
|
||||||
|
|
||||||
|
# No 'ownership' checks are necessary for the top-level StockLocation view
|
||||||
|
context['user_owns_location'] = True
|
||||||
|
context['location_owner'] = None
|
||||||
|
context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +81,16 @@ class StockLocationDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
queryset = StockLocation.objects.all()
|
queryset = StockLocation.objects.all()
|
||||||
model = StockLocation
|
model = StockLocation
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
context['location_owner'] = context['location'].get_location_owner()
|
||||||
|
context['user_owns_location'] = context['location'].check_ownership(self.request.user)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
@ -126,6 +141,10 @@ class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
# We only support integer serial number progression
|
# We only support integer serial number progression
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
data['item_owner'] = self.object.get_item_owner()
|
||||||
|
data['user_owns_item'] = self.object.check_ownership(self.request.user)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
@ -1554,11 +1554,11 @@ function locationDetail(row, showLink=true) {
|
|||||||
} else if (row.belongs_to) {
|
} else if (row.belongs_to) {
|
||||||
// StockItem is installed inside a different StockItem
|
// StockItem is installed inside a different StockItem
|
||||||
text = `{% trans "Installed in Stock Item" %} ${row.belongs_to}`;
|
text = `{% trans "Installed in Stock Item" %} ${row.belongs_to}`;
|
||||||
url = `/stock/item/${row.belongs_to}/installed/`;
|
url = `/stock/item/${row.belongs_to}/?display=installed-items`;
|
||||||
} else if (row.customer) {
|
} else if (row.customer) {
|
||||||
// StockItem has been assigned to a customer
|
// StockItem has been assigned to a customer
|
||||||
text = '{% trans "Shipped to customer" %}';
|
text = '{% trans "Shipped to customer" %}';
|
||||||
url = `/company/${row.customer}/assigned-stock/`;
|
url = `/company/${row.customer}/?display=assigned-stock`;
|
||||||
} else if (row.sales_order) {
|
} else if (row.sales_order) {
|
||||||
// StockItem has been assigned to a sales order
|
// StockItem has been assigned to a sales order
|
||||||
text = '{% trans "Assigned to Sales Order" %}';
|
text = '{% trans "Assigned to Sales Order" %}';
|
||||||
|
@ -278,7 +278,7 @@ $.fn.inventreeTable = function(options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(`Could not get list of visible columns for column '${tableName}'`);
|
console.log(`Could not get list of visible columns for table '${tableName}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,16 +46,16 @@
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if roles.stock.change %}
|
{% if roles.stock.change %}
|
||||||
<li><a class='dropdown-item' href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
|
<li><a class='dropdown-item' href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li>
|
<li><a class='dropdown-item' href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
|
<li><a class='dropdown-item' href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Count stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-merge' title='{% trans "Merge selected stock items" %}'><span class='fas fa-object-group'></span> {% trans "Merge stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-merge' title='{% trans "Merge selected stock items" %}'><span class='fas fa-object-group'></span> {% trans "Merge stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roles.stock.delete %}
|
{% if roles.stock.delete %}
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete stock" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -451,7 +451,7 @@ def update_group_roles(group, debug=False):
|
|||||||
group.permissions.add(permission)
|
group.permissions.add(permission)
|
||||||
|
|
||||||
if debug: # pragma: no cover
|
if debug: # pragma: no cover
|
||||||
print(f"Adding permission {perm} to group {group.name}")
|
logger.info(f"Adding permission {perm} to group {group.name}")
|
||||||
|
|
||||||
# Remove any extra permissions from the group
|
# Remove any extra permissions from the group
|
||||||
for perm in permissions_to_delete:
|
for perm in permissions_to_delete:
|
||||||
@ -466,7 +466,7 @@ def update_group_roles(group, debug=False):
|
|||||||
group.permissions.remove(permission)
|
group.permissions.remove(permission)
|
||||||
|
|
||||||
if debug: # pragma: no cover
|
if debug: # pragma: no cover
|
||||||
print(f"Removing permission {perm} from group {group.name}")
|
logger.info(f"Removing permission {perm} from group {group.name}")
|
||||||
|
|
||||||
# Enable all action permissions for certain children models
|
# Enable all action permissions for certain children models
|
||||||
# if parent model has 'change' permission
|
# if parent model has 'change' permission
|
||||||
@ -488,7 +488,7 @@ def update_group_roles(group, debug=False):
|
|||||||
permission = get_permission_object(child_perm)
|
permission = get_permission_object(child_perm)
|
||||||
if permission:
|
if permission:
|
||||||
group.permissions.add(permission)
|
group.permissions.add(permission)
|
||||||
print(f"Adding permission {child_perm} to group {group.name}")
|
logger.info(f"Adding permission {child_perm} to group {group.name}")
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets')
|
@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user