2
0
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:
eeintech
2020-08-17 12:05:54 -05:00
51 changed files with 2862 additions and 1438 deletions

View File

@ -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'),

View File

@ -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

View File

@ -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 """

View 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),
),
]

View 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'},
),
]

View File

@ -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')

View File

@ -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')

View File

@ -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,