mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'origin/master' into scheduling
This commit is contained in:
commit
e162432fde
2
.github/workflows/docker_stable.yaml
vendored
2
.github/workflows/docker_stable.yaml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
push: true
|
||||
target: production
|
||||
build-args:
|
||||
branch: stable
|
||||
branch=stable
|
||||
tags: inventree/inventree:stable
|
||||
- name: Image Digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
2
.github/workflows/docker_tag.yaml
vendored
2
.github/workflows/docker_tag.yaml
vendored
@ -34,5 +34,5 @@ jobs:
|
||||
push: true
|
||||
target: production
|
||||
build-args:
|
||||
tag: ${{ github.event.release.tag_name }}
|
||||
tag=${{ github.event.release.tag_name }}
|
||||
tags: inventree/inventree:${{ github.event.release.tag_name }}
|
||||
|
@ -475,7 +475,6 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||
continue
|
||||
else:
|
||||
errors.append(_("Invalid group: {g}").format(g=group))
|
||||
continue
|
||||
|
||||
# plus signals either
|
||||
# 1: 'start+': expected number of serials, starting at start
|
||||
@ -500,7 +499,6 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||
# no case
|
||||
else:
|
||||
errors.append(_("Invalid group: {g}").format(g=group))
|
||||
continue
|
||||
|
||||
# Group should be a number
|
||||
elif group:
|
||||
|
@ -620,7 +620,7 @@ class DataFileExtractSerializer(serializers.Serializer):
|
||||
# Check for missing required columns
|
||||
if required:
|
||||
if name not in self.columns:
|
||||
raise serializers.ValidationError(_("Missing required column") + f": '{name}'")
|
||||
raise serializers.ValidationError(_(f"Missing required column: '{name}'"))
|
||||
|
||||
for col in self.columns:
|
||||
|
||||
@ -629,7 +629,7 @@ class DataFileExtractSerializer(serializers.Serializer):
|
||||
|
||||
# Check for duplicated columns
|
||||
if col in cols_seen:
|
||||
raise serializers.ValidationError(_("Duplicate column") + f": '{col}'")
|
||||
raise serializers.ValidationError(_(f"Duplicate column: '{col}'"))
|
||||
|
||||
cols_seen.add(col)
|
||||
|
||||
|
@ -659,6 +659,7 @@ LANGUAGES = [
|
||||
('es-mx', _('Spanish (Mexican)')),
|
||||
('fr', _('French')),
|
||||
('he', _('Hebrew')),
|
||||
('hu', _('Hungarian')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
('ko', _('Korean')),
|
||||
|
@ -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')
|
||||
|
||||
|
@ -9,7 +9,7 @@ import re
|
||||
import common.models
|
||||
|
||||
# InvenTree software version
|
||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 26
|
||||
|
@ -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
|
||||
@ -236,6 +236,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
auto_allocate = serializers.BooleanField(
|
||||
required=False,
|
||||
default=False,
|
||||
allow_null=True,
|
||||
label=_('Auto Allocate Serial Numbers'),
|
||||
help_text=_('Automatically allocate required items with matching serial numbers'),
|
||||
)
|
||||
@ -403,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"
|
||||
@ -414,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,
|
||||
)
|
||||
|
||||
|
||||
@ -435,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)
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/hu/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/hu/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
9652
InvenTree/locale/hu/LC_MESSAGES/django.po
Normal file
9652
InvenTree/locale/hu/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
|
@ -789,21 +789,20 @@ class BomImportExtractSerializer(DataFileExtractSerializer):
|
||||
pass
|
||||
|
||||
# No direct match, where else can we look?
|
||||
if part is None:
|
||||
if part_name or part_ipn:
|
||||
queryset = Part.objects.all()
|
||||
if part is None and (part_name or part_ipn):
|
||||
queryset = Part.objects.all()
|
||||
|
||||
if part_name:
|
||||
queryset = queryset.filter(name=part_name)
|
||||
if part_name:
|
||||
queryset = queryset.filter(name=part_name)
|
||||
|
||||
if part_ipn:
|
||||
queryset = queryset.filter(IPN=part_ipn)
|
||||
if part_ipn:
|
||||
queryset = queryset.filter(IPN=part_ipn)
|
||||
|
||||
if queryset.exists():
|
||||
if queryset.count() == 1:
|
||||
part = queryset.first()
|
||||
else:
|
||||
row['errors']['part'] = _('Multiple matching parts found')
|
||||
if queryset.exists():
|
||||
if queryset.count() == 1:
|
||||
part = queryset.first()
|
||||
else:
|
||||
row['errors']['part'] = _('Multiple matching parts found')
|
||||
|
||||
if part is None:
|
||||
row['errors']['part'] = _('No matching part found')
|
||||
|
@ -41,7 +41,7 @@ class BomUploadTest(InvenTreeAPITestCase):
|
||||
assembly=False,
|
||||
)
|
||||
|
||||
def post_bom(self, filename, file_data, part=None, clear_existing=None, expected_code=None, content_type='text/plain'):
|
||||
def post_bom(self, filename, file_data, clear_existing=None, expected_code=None, content_type='text/plain'):
|
||||
|
||||
bom_file = SimpleUploadedFile(
|
||||
filename,
|
||||
@ -49,9 +49,6 @@ class BomUploadTest(InvenTreeAPITestCase):
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
if part is None:
|
||||
part = self.part.pk
|
||||
|
||||
if clear_existing is None:
|
||||
clear_existing = False
|
||||
|
||||
@ -189,7 +186,7 @@ class BomUploadTest(InvenTreeAPITestCase):
|
||||
|
||||
self.assertIn('No part column specified', str(response.data))
|
||||
|
||||
response = self.post(
|
||||
self.post(
|
||||
url,
|
||||
{
|
||||
'rows': rows,
|
||||
|
@ -516,10 +516,8 @@ class StockList(generics.ListCreateAPIView):
|
||||
data['location'] = location.pk
|
||||
|
||||
# An expiry date was *not* specified - try to infer it!
|
||||
if 'expiry_date' not in data:
|
||||
|
||||
if part.default_expiry > 0:
|
||||
data['expiry_date'] = datetime.now().date() + timedelta(days=part.default_expiry)
|
||||
if 'expiry_date' not in data and part.default_expiry > 0:
|
||||
data['expiry_date'] = datetime.now().date() + timedelta(days=part.default_expiry)
|
||||
|
||||
# Attempt to extract serial numbers from submitted data
|
||||
serials = None
|
||||
|
@ -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 %}
|
||||
|
@ -393,7 +393,7 @@ class StockItemTest(StockAPITestCase):
|
||||
self.assertEqual(trackable_part.get_stock_count(), 0)
|
||||
|
||||
# This should fail, incorrect serial number count
|
||||
response = self.post(
|
||||
self.post(
|
||||
self.list_url,
|
||||
data={
|
||||
'part': trackable_part.pk,
|
||||
|
@ -28,6 +28,6 @@
|
||||
</button>
|
||||
</form>
|
||||
<br>
|
||||
<a href="{% url 'settings' %}" class="btn btn-secondary w-100 btn-sm">{% trans "back to settings" %}</a>
|
||||
<a href="{% url 'settings' %}" class="btn btn-secondary w-100 btn-sm">{% trans "Back to settings" %}</a>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -37,6 +37,6 @@
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<a href="{% url 'settings' %}" class="btn btn-secondary w-100 btn-sm mt-3">{% trans "back to settings" %}</a>
|
||||
<a href="{% url 'settings' %}" class="btn btn-secondary w-100 btn-sm mt-3">{% trans "Back to settings" %}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -893,6 +893,9 @@ function handleModalForm(url, options) {
|
||||
// Re-enable the modal
|
||||
modalEnable(modal, true);
|
||||
if ('form_valid' in response) {
|
||||
// Get visibility option of error message
|
||||
var hideErrorMessage = (options.hideErrorMessage === undefined) ? true : options.hideErrorMessage;
|
||||
|
||||
// Form data was validated correctly
|
||||
if (response.form_valid) {
|
||||
$(modal).modal('hide');
|
||||
@ -901,7 +904,7 @@ function handleModalForm(url, options) {
|
||||
// Form was returned, invalid!
|
||||
|
||||
// Disable error message with option or response
|
||||
if (!options.hideErrorMessage && !response.hideErrorMessage) {
|
||||
if (!hideErrorMessage && !response.hideErrorMessage) {
|
||||
var warningDiv = $(modal).find('#form-validation-warning');
|
||||
warningDiv.css('display', 'block');
|
||||
}
|
||||
|
@ -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