mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-20 22:06:28 +00:00
Merge remote-tracking branch 'origin/master' into bom_dev
This commit is contained in:
@ -190,6 +190,21 @@ class PartThumbs(generics.ListAPIView):
|
||||
return Response(data)
|
||||
|
||||
|
||||
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
|
||||
""" API endpoint for updating Part thumbnails"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = part_serializers.PartThumbSerializerUpdate
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend
|
||||
]
|
||||
|
||||
|
||||
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a single Part object """
|
||||
|
||||
@ -588,7 +603,20 @@ class BomList(generics.ListCreateAPIView):
|
||||
"""
|
||||
|
||||
serializer_class = part_serializers.BomItemSerializer
|
||||
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
|
||||
data = serializer.data
|
||||
|
||||
if request.is_ajax():
|
||||
return JsonResponse(data, safe=False)
|
||||
else:
|
||||
return Response(data)
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
# Do we wish to include extra detail?
|
||||
@ -607,8 +635,10 @@ class BomList(generics.ListCreateAPIView):
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
queryset = BomItem.objects.all()
|
||||
|
||||
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
||||
|
||||
return queryset
|
||||
@ -716,7 +746,10 @@ part_api_urls = [
|
||||
url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
|
||||
])),
|
||||
|
||||
url(r'^thumbs/', PartThumbs.as_view(), name='api-part-thumbs'),
|
||||
url(r'^thumbs/', include([
|
||||
url(r'^$', PartThumbs.as_view(), name='api-part-thumbs'),
|
||||
url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
|
||||
])),
|
||||
|
||||
url(r'^(?P<pk>\d+)/?', PartDetail.as_view(), name='api-part-detail'),
|
||||
|
||||
|
@ -40,7 +40,7 @@ def MakeBomTemplate(fmt):
|
||||
return DownloadFile(data, filename)
|
||||
|
||||
|
||||
def ExportBom(part, fmt='csv', cascade=False):
|
||||
def ExportBom(part, fmt='csv', cascade=False, max_levels=None):
|
||||
""" Export a BOM (Bill of Materials) for a given part.
|
||||
|
||||
Args:
|
||||
@ -59,8 +59,8 @@ def ExportBom(part, fmt='csv', cascade=False):
|
||||
# Add items at a given layer
|
||||
for item in items:
|
||||
|
||||
item.level = '-' * level
|
||||
|
||||
item.level = str(int(level))
|
||||
|
||||
# Avoid circular BOM references
|
||||
if item.pk in uids:
|
||||
continue
|
||||
@ -68,7 +68,8 @@ def ExportBom(part, fmt='csv', cascade=False):
|
||||
bom_items.append(item)
|
||||
|
||||
if item.sub_part.assembly:
|
||||
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
|
||||
if max_levels is None or level < max_levels:
|
||||
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
|
||||
|
||||
if cascade:
|
||||
# Cascading (multi-level) BOM
|
||||
|
@ -56,6 +56,8 @@ class BomExportForm(forms.Form):
|
||||
|
||||
cascading = forms.BooleanField(label=_("Cascading"), required=False, initial=False, help_text=_("Download cascading / multi-level BOM"))
|
||||
|
||||
levels = forms.IntegerField(label=_("Levels"), required=True, initial=0, help_text=_("Select maximum number of BOM levels to export (0 = all levels)"))
|
||||
|
||||
def get_choices(self):
|
||||
""" BOM export format choices """
|
||||
|
||||
|
18
InvenTree/part/migrations/0046_auto_20200804_0107.py
Normal file
18
InvenTree/part/migrations/0046_auto_20200804_0107.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.7 on 2020-08-04 01:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0045_auto_20200605_0932'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='partcategory',
|
||||
name='default_keywords',
|
||||
field=models.CharField(blank=True, help_text='Default keywords for parts in this category', max_length=250, null=True),
|
||||
),
|
||||
]
|
17
InvenTree/part/migrations/0047_auto_20200808_0715.py
Normal file
17
InvenTree/part/migrations/0047_auto_20200808_0715.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.0.7 on 2020-08-08 07:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0046_auto_20200804_0107'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='part',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Part', 'verbose_name_plural': 'Parts'},
|
||||
),
|
||||
]
|
@ -65,14 +65,14 @@ class PartCategory(InvenTreeTree):
|
||||
help_text=_('Default location for parts in this category')
|
||||
)
|
||||
|
||||
default_keywords = models.CharField(blank=True, max_length=250, help_text=_('Default keywords for parts in this category'))
|
||||
default_keywords = models.CharField(null=True, blank=True, max_length=250, help_text=_('Default keywords for parts in this category'))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('category-detail', kwargs={'pk': self.id})
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Part Category"
|
||||
verbose_name_plural = "Part Categories"
|
||||
verbose_name = _("Part Category")
|
||||
verbose_name_plural = _("Part Categories")
|
||||
|
||||
def get_parts(self, cascade=True):
|
||||
""" Return a queryset for all parts under this category.
|
||||
@ -239,6 +239,7 @@ class Part(MPTTModel):
|
||||
class Meta:
|
||||
verbose_name = _("Part")
|
||||
verbose_name_plural = _("Parts")
|
||||
ordering = ['name', ]
|
||||
|
||||
class MPTTMeta:
|
||||
# For legacy reasons the 'variant_of' field is used to indicate the MPTT parent
|
||||
@ -559,16 +560,17 @@ class Part(MPTTModel):
|
||||
|
||||
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name='parts_responible')
|
||||
|
||||
def format_barcode(self):
|
||||
def format_barcode(self, **kwargs):
|
||||
""" Return a JSON string for formatting a barcode for this Part object """
|
||||
|
||||
return helpers.MakeBarcode(
|
||||
"part",
|
||||
self.id,
|
||||
{
|
||||
"id": self.id,
|
||||
"name": self.full_name,
|
||||
"url": reverse('api-part-detail', kwargs={'pk': self.id}),
|
||||
}
|
||||
},
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@property
|
||||
@ -1490,7 +1492,7 @@ class BomItem(models.Model):
|
||||
pass
|
||||
|
||||
class Meta:
|
||||
verbose_name = "BOM Item"
|
||||
verbose_name = _("BOM Item")
|
||||
|
||||
# Prevent duplication of parent/child rows
|
||||
unique_together = ('part', 'sub_part')
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""
|
||||
JSON serializers for Part app
|
||||
"""
|
||||
import imghdr
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
@ -92,6 +93,27 @@ class PartThumbSerializer(serializers.Serializer):
|
||||
count = serializers.IntegerField(read_only=True)
|
||||
|
||||
|
||||
class PartThumbSerializerUpdate(InvenTreeModelSerializer):
|
||||
""" Serializer for updating Part thumbnail """
|
||||
|
||||
def validate_image(self, value):
|
||||
"""
|
||||
Check that file is an image.
|
||||
"""
|
||||
validate = imghdr.what(value)
|
||||
if not validate:
|
||||
raise serializers.ValidationError("File is not an image")
|
||||
return value
|
||||
|
||||
image = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = Part
|
||||
fields = [
|
||||
'image',
|
||||
]
|
||||
|
||||
|
||||
class PartBriefSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for Part (brief detail) """
|
||||
|
||||
@ -214,6 +236,9 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
||||
starred = serializers.SerializerMethodField()
|
||||
|
||||
# PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...)
|
||||
category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all())
|
||||
|
||||
# TODO - Include annotation for the following fields:
|
||||
# allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
|
||||
# bom_items = serializers.IntegerField(source='bom_count', read_only=True)
|
||||
@ -280,8 +305,13 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
price_range = serializers.CharField(read_only=True)
|
||||
|
||||
quantity = serializers.FloatField()
|
||||
|
||||
part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True))
|
||||
|
||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||
|
||||
sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(component=True))
|
||||
|
||||
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
|
||||
|
||||
validated = serializers.BooleanField(read_only=True, source='is_line_valid')
|
||||
@ -306,6 +336,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
queryset = queryset.prefetch_related('part')
|
||||
queryset = queryset.prefetch_related('part__category')
|
||||
queryset = queryset.prefetch_related('part__stock_items')
|
||||
|
||||
queryset = queryset.prefetch_related('sub_part')
|
||||
queryset = queryset.prefetch_related('sub_part__category')
|
||||
queryset = queryset.prefetch_related('sub_part__stock_items')
|
||||
|
@ -1392,10 +1392,22 @@ class BomDownload(AjaxView):
|
||||
|
||||
cascade = str2bool(request.GET.get('cascade', False))
|
||||
|
||||
levels = request.GET.get('levels', None)
|
||||
|
||||
if levels is not None:
|
||||
try:
|
||||
levels = int(levels)
|
||||
|
||||
if levels <= 0:
|
||||
levels = None
|
||||
|
||||
except ValueError:
|
||||
levels = None
|
||||
|
||||
if not IsValidBOMFormat(export_format):
|
||||
export_format = 'csv'
|
||||
|
||||
return ExportBom(part, fmt=export_format, cascade=cascade)
|
||||
return ExportBom(part, fmt=export_format, cascade=cascade, max_levels=levels)
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -1419,6 +1431,7 @@ class BomExport(AjaxView):
|
||||
# Extract POSTed form data
|
||||
fmt = request.POST.get('file_format', 'csv').lower()
|
||||
cascade = str2bool(request.POST.get('cascading', False))
|
||||
levels = request.POST.get('levels', None)
|
||||
|
||||
try:
|
||||
part = Part.objects.get(pk=self.kwargs['pk'])
|
||||
@ -1434,6 +1447,9 @@ class BomExport(AjaxView):
|
||||
url += '?file_format=' + fmt
|
||||
url += '&cascade=' + str(cascade)
|
||||
|
||||
if levels:
|
||||
url += '&levels=' + str(levels)
|
||||
|
||||
data = {
|
||||
'form_valid': part is not None,
|
||||
'url': url,
|
||||
|
Reference in New Issue
Block a user