mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2673 from SchrodingersGat/build-order-allocating-substitutes
Build order allocating substitutes
This commit is contained in:
commit
cbb88806cd
@ -258,6 +258,7 @@ class StockHistoryCode(StatusCode):
|
||||
# Build order codes
|
||||
BUILD_OUTPUT_CREATED = 50
|
||||
BUILD_OUTPUT_COMPLETED = 55
|
||||
BUILD_CONSUMED = 57
|
||||
|
||||
# Sales order codes
|
||||
|
||||
@ -298,6 +299,7 @@ class StockHistoryCode(StatusCode):
|
||||
|
||||
BUILD_OUTPUT_CREATED: _('Build order output created'),
|
||||
BUILD_OUTPUT_COMPLETED: _('Build order output completed'),
|
||||
BUILD_CONSUMED: _('Consumed by build order'),
|
||||
|
||||
RECEIVED_AGAINST_PURCHASE_ORDER: _('Received against purchase order')
|
||||
|
||||
|
@ -30,8 +30,6 @@ from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
||||
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
|
||||
from InvenTree.validators import validate_build_order_reference
|
||||
|
||||
import common.models
|
||||
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
import InvenTree.tasks
|
||||
@ -479,8 +477,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
outputs = self.get_build_outputs(complete=True)
|
||||
|
||||
# TODO - Ordering?
|
||||
|
||||
return outputs
|
||||
|
||||
@property
|
||||
@ -491,8 +487,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
outputs = self.get_build_outputs(complete=False)
|
||||
|
||||
# TODO - Order by how "complete" they are?
|
||||
|
||||
return outputs
|
||||
|
||||
@property
|
||||
@ -563,7 +557,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
if self.remaining > 0:
|
||||
return False
|
||||
|
||||
if not self.areUntrackedPartsFullyAllocated():
|
||||
if not self.are_untracked_parts_allocated():
|
||||
return False
|
||||
|
||||
# No issues!
|
||||
@ -584,7 +578,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
self.save()
|
||||
|
||||
# Remove untracked allocated stock
|
||||
self.subtractUntrackedStock(user)
|
||||
self.subtract_allocated_stock(user)
|
||||
|
||||
# Ensure that there are no longer any BuildItem objects
|
||||
# which point to thisFcan Build Order
|
||||
@ -768,7 +762,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
output.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def subtractUntrackedStock(self, user):
|
||||
def subtract_allocated_stock(self, user):
|
||||
"""
|
||||
Called when the Build is marked as "complete",
|
||||
this function removes the allocated untracked items from stock.
|
||||
@ -831,7 +825,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
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.
|
||||
|
||||
@ -840,12 +834,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
output - The particular build output (StockItem)
|
||||
"""
|
||||
|
||||
# Extract the BOM line item from the database
|
||||
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
|
||||
quantity = bom_item.quantity
|
||||
|
||||
if output:
|
||||
quantity *= output.quantity
|
||||
@ -854,32 +843,32 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
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:
|
||||
part - The part object
|
||||
bom_item - The BomItem object
|
||||
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(
|
||||
build=self,
|
||||
stock_item__part__pk__in=[p.pk for p in variants],
|
||||
bom_item=bom_item,
|
||||
install_into=output,
|
||||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
allocations = self.allocatedItems(part, output)
|
||||
allocations = self.allocated_bom_items(bom_item, output)
|
||||
|
||||
allocated = allocations.aggregate(
|
||||
q=Coalesce(
|
||||
@ -891,24 +880,24 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
required = self.requiredQuantity(part, output)
|
||||
allocated = self.allocatedQuantity(part, output)
|
||||
required = self.required_quantity(bom_item, output)
|
||||
allocated = self.allocated_quantity(bom_item, output)
|
||||
|
||||
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.
|
||||
"""
|
||||
@ -919,53 +908,24 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
else:
|
||||
bom_items = self.tracked_bom_items
|
||||
|
||||
fully_allocated = True
|
||||
|
||||
for bom_item in bom_items:
|
||||
part = bom_item.sub_part
|
||||
|
||||
if not self.isPartFullyAllocated(part, output):
|
||||
fully_allocated = False
|
||||
|
||||
if verbose:
|
||||
print(f"Part {part} is not fully allocated for output {output}")
|
||||
else:
|
||||
break
|
||||
if not self.is_bom_item_allocated(bom_item, output):
|
||||
return False
|
||||
|
||||
# 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
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
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
|
||||
Return a list of bom items which have *not* been fully allocated against a particular output
|
||||
"""
|
||||
|
||||
unallocated = []
|
||||
@ -977,10 +937,9 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
bom_items = self.tracked_bom_items
|
||||
|
||||
for bom_item in bom_items:
|
||||
part = bom_item.sub_part
|
||||
|
||||
if not self.isPartFullyAllocated(part, output):
|
||||
unallocated.append(part)
|
||||
if not self.is_bom_item_allocated(bom_item, output):
|
||||
unallocated.append(bom_item)
|
||||
|
||||
return unallocated
|
||||
|
||||
@ -1008,57 +967,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
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
|
||||
def is_active(self):
|
||||
""" Is this build active? An active build is either:
|
||||
@ -1257,7 +1165,12 @@ class BuildItem(models.Model):
|
||||
if item.part.trackable:
|
||||
# Split the allocated stock if there are more available than allocated
|
||||
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
|
||||
self.stock_item = item
|
||||
@ -1268,7 +1181,11 @@ class BuildItem(models.Model):
|
||||
item.save()
|
||||
else:
|
||||
# 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):
|
||||
"""
|
||||
|
@ -160,7 +160,7 @@ class BuildOutputSerializer(serializers.Serializer):
|
||||
if to_complete:
|
||||
|
||||
# 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"))
|
||||
|
||||
return output
|
||||
@ -404,6 +404,10 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
location = data['location']
|
||||
status = data['status']
|
||||
notes = data.get('notes', '')
|
||||
|
||||
outputs = data.get('outputs', [])
|
||||
|
||||
# Mark the specified build outputs as "complete"
|
||||
@ -415,8 +419,9 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
build.complete_build_output(
|
||||
output,
|
||||
request.user,
|
||||
status=data['status'],
|
||||
notes=data.get('notes', '')
|
||||
location=location,
|
||||
status=status,
|
||||
notes=notes,
|
||||
)
|
||||
|
||||
|
||||
@ -436,7 +441,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
||||
|
||||
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'))
|
||||
|
||||
return value
|
||||
|
@ -125,7 +125,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
{% trans "Required build quantity has not yet been completed" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not build.areUntrackedPartsFullyAllocated %}
|
||||
{% if not build.are_untracked_parts_allocated %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "Stock has not been fully allocated to this Build Order" %}
|
||||
</div>
|
||||
@ -234,7 +234,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
{% else %}
|
||||
|
||||
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 %},
|
||||
});
|
||||
{% endif %}
|
||||
|
@ -192,7 +192,7 @@
|
||||
<div class='panel-content'>
|
||||
{% if build.has_untracked_bom_items %}
|
||||
{% if build.active %}
|
||||
{% if build.areUntrackedPartsFullyAllocated %}
|
||||
{% if build.are_untracked_parts_allocated %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Untracked stock has been fully allocated for this Build Order" %}
|
||||
</div>
|
||||
|
@ -62,20 +62,20 @@ class BuildTest(TestCase):
|
||||
)
|
||||
|
||||
# Create BOM item links for the parts
|
||||
BomItem.objects.create(
|
||||
self.bom_item_1 = BomItem.objects.create(
|
||||
part=self.assembly,
|
||||
sub_part=self.sub_part_1,
|
||||
quantity=5
|
||||
)
|
||||
|
||||
BomItem.objects.create(
|
||||
self.bom_item_2 = BomItem.objects.create(
|
||||
part=self.assembly,
|
||||
sub_part=self.sub_part_2,
|
||||
quantity=3
|
||||
)
|
||||
|
||||
# sub_part_3 is trackable!
|
||||
BomItem.objects.create(
|
||||
self.bom_item_3 = BomItem.objects.create(
|
||||
part=self.assembly,
|
||||
sub_part=self.sub_part_3,
|
||||
quantity=2
|
||||
@ -147,15 +147,15 @@ class BuildTest(TestCase):
|
||||
|
||||
# None of the build outputs have been completed
|
||||
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.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
||||
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_1, self.output_1))
|
||||
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.unallocatedQuantity(self.sub_part_1, self.output_2), 35)
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_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_1, self.output_1), 15)
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1, self.output_2), 35)
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2, self.output_1), 9)
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2, self.output_2), 21)
|
||||
|
||||
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
|
||||
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
|
||||
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)
|
||||
|
||||
@ -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.build.unallocateStock()
|
||||
|
||||
unallocated = self.build.unallocatedParts(None)
|
||||
unallocated = self.build.unallocated_bom_items(None)
|
||||
|
||||
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
|
||||
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):
|
||||
"""
|
||||
@ -331,9 +331,9 @@ class BuildTest(TestCase):
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(self.build.isFullyAllocated(None, verbose=True))
|
||||
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
||||
self.assertTrue(self.build.isFullyAllocated(self.output_2))
|
||||
self.assertTrue(self.build.is_fully_allocated(None))
|
||||
self.assertTrue(self.build.is_fully_allocated(self.output_1))
|
||||
self.assertTrue(self.build.is_fully_allocated(self.output_2))
|
||||
|
||||
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
|
||||
|
||||
import logging
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from moneyed import CURRENCIES
|
||||
@ -7,6 +9,9 @@ from django.db import migrations, connection
|
||||
from company.models import SupplierPriceBreak
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def migrate_currencies(apps, schema_editor):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
print("Updating currency references for SupplierPriceBreak model...")
|
||||
logger.info("Updating currency references for SupplierPriceBreak model...")
|
||||
|
||||
# A list of available currency codes
|
||||
currency_codes = CURRENCIES.keys()
|
||||
|
@ -1311,6 +1311,7 @@ class StockItem(MPTTModel):
|
||||
"""
|
||||
|
||||
notes = kwargs.get('notes', '')
|
||||
code = kwargs.get('code', StockHistoryCode.SPLIT_FROM_PARENT)
|
||||
|
||||
# Do not split a serialized part
|
||||
if self.serialized:
|
||||
@ -1352,7 +1353,7 @@ class StockItem(MPTTModel):
|
||||
|
||||
# Add a new tracking item for the new stock item
|
||||
new_stock.add_tracking_entry(
|
||||
StockHistoryCode.SPLIT_FROM_PARENT,
|
||||
code,
|
||||
user,
|
||||
notes=notes,
|
||||
deltas={
|
||||
@ -1530,7 +1531,7 @@ class StockItem(MPTTModel):
|
||||
return True
|
||||
|
||||
@transaction.atomic
|
||||
def take_stock(self, quantity, user, notes=''):
|
||||
def take_stock(self, quantity, user, notes='', code=StockHistoryCode.STOCK_REMOVE):
|
||||
"""
|
||||
Remove items from stock
|
||||
"""
|
||||
@ -1550,7 +1551,7 @@ class StockItem(MPTTModel):
|
||||
if self.updateQuantity(self.quantity - quantity):
|
||||
|
||||
self.add_tracking_entry(
|
||||
StockHistoryCode.STOCK_REMOVE,
|
||||
code,
|
||||
user,
|
||||
notes=notes,
|
||||
deltas={
|
||||
|
@ -409,7 +409,14 @@
|
||||
<tr>
|
||||
<td><span class='fas fa-vial'></span></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>
|
||||
{% endif %}
|
||||
{% if item.owner %}
|
||||
|
@ -1554,11 +1554,11 @@ function locationDetail(row, showLink=true) {
|
||||
} else if (row.belongs_to) {
|
||||
// StockItem is installed inside a different StockItem
|
||||
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) {
|
||||
// StockItem has been assigned to a customer
|
||||
text = '{% trans "Shipped to customer" %}';
|
||||
url = `/company/${row.customer}/assigned-stock/`;
|
||||
url = `/company/${row.customer}/?display=assigned-stock`;
|
||||
} else if (row.sales_order) {
|
||||
// StockItem has been assigned to a sales order
|
||||
text = '{% trans "Assigned to Sales Order" %}';
|
||||
|
@ -451,7 +451,7 @@ def update_group_roles(group, debug=False):
|
||||
group.permissions.add(permission)
|
||||
|
||||
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
|
||||
for perm in permissions_to_delete:
|
||||
@ -466,7 +466,7 @@ def update_group_roles(group, debug=False):
|
||||
group.permissions.remove(permission)
|
||||
|
||||
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
|
||||
# if parent model has 'change' permission
|
||||
@ -488,7 +488,7 @@ def update_group_roles(group, debug=False):
|
||||
permission = get_permission_object(child_perm)
|
||||
if 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')
|
||||
|
Loading…
Reference in New Issue
Block a user