diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 57eddd53bd..4ab1fd694d 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from rest_framework import serializers -from rest_framework import generics from django.contrib.auth.models import User @@ -24,13 +23,18 @@ class UserSerializerBrief(serializers.ModelSerializer): ] -class DraftRUDView(generics.RetrieveAPIView, generics.UpdateAPIView, generics.DestroyAPIView): +class InvenTreeModelSerializer(serializers.ModelSerializer): + """ + Inherits the standard Django ModelSerializer class, + but also ensures that the underlying model class data are checked on validation. + """ - def perform_update(self, serializer): + def validate(self, data): + # Run any native validation checks first (may throw an ValidationError) + data = super(serializers.ModelSerializer, self).validate(data) - ctx_data = serializer._context['request'].data + # Now ensure the underlying model is correct + instance = self.Meta.model(**data) + instance.clean() - if ctx_data.get('_is_final', False) in [True, u'true', u'True', 1]: - super(generics.UpdateAPIView, self).perform_update(serializer) - else: - pass + return data diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index b5a0f328da..c33689be61 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -11,7 +11,7 @@ from stock.urls import stock_urls from build.urls import build_urls -from part.api import part_api_urls +from part.api import part_api_urls, bom_api_urls from company.api import company_api_urls from stock.api import stock_api_urls from build.api import build_api_urls @@ -30,6 +30,7 @@ admin.site.site_header = "InvenTree Admin" apipatterns = [ url(r'^part/', include(part_api_urls)), + url(r'^bom/', include(bom_api_urls)), url(r'^company/', include(company_api_urls)), url(r'^stock/', include(stock_api_urls)), url(r'^build/', include(build_api_urls)), diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 455a0e76e1..d90cfdb1ee 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -43,7 +43,19 @@ class CompanyList(generics.ListCreateAPIView): ordering = 'name' +class CompanyDetail(generics.RetrieveUpdateDestroyAPIView): + + queryset = Company.objects.all() + serializer_class = CompanySerializer + + permission_classes = [ + permissions.IsAuthenticatedOrReadOnly, + ] + + company_api_urls = [ + url(r'^(?P\d+)/?', CompanyDetail.as_view(), name='api-company-detail'), + url(r'^.*$', CompanyList.as_view(), name='api-company-list'), ] diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index c3d9f0a8b3..1db55c12d5 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -50,10 +50,10 @@ }, { sortable: true, - field: 'part', + field: 'part_name', title: 'Part', formatter: function(value, row, index, field) { - return renderLink(value.name, value.url + 'suppliers/'); + return renderLink(value, '/part/' + row.part + '/suppliers/'); } }, { diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 5601850e1c..96606c643a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -17,7 +17,6 @@ from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer from .serializers import CategorySerializer from InvenTree.views import TreeSerializer -from InvenTree.serializers import DraftRUDView class PartCategoryTree(TreeSerializer): @@ -56,7 +55,7 @@ class CategoryList(generics.ListCreateAPIView): ] -class PartDetail(DraftRUDView): +class PartDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Part.objects.all() serializer_class = PartSerializer @@ -125,7 +124,7 @@ class PartList(generics.ListCreateAPIView): ] -class BomList(generics.ListAPIView): +class BomList(generics.ListCreateAPIView): queryset = BomItem.objects.all() serializer_class = BomItemSerializer @@ -146,7 +145,17 @@ class BomList(generics.ListAPIView): ] -class SupplierPartList(generics.ListAPIView): +class BomDetail(generics.RetrieveUpdateDestroyAPIView): + + queryset = BomItem.objects.all() + serializer_class = BomItemSerializer + + permission_classes = [ + permissions.IsAuthenticatedOrReadOnly, + ] + + +class SupplierPartList(generics.ListCreateAPIView): queryset = SupplierPart.objects.all() serializer_class = SupplierPartSerializer @@ -167,6 +176,16 @@ class SupplierPartList(generics.ListAPIView): ] +class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView): + + queryset = SupplierPart.objects.all() + serializer_class = SupplierPartSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + read_only_fields = [ + ] + + class SupplierPriceBreakList(generics.ListCreateAPIView): queryset = SupplierPriceBreak.objects.all() @@ -189,16 +208,31 @@ cat_api_urls = [ url(r'^$', CategoryList.as_view(), name='api-part-category-list'), ] +supplier_part_api_urls = [ + + url(r'^(?P\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'), + + # Catch anything else + url(r'^.*$', SupplierPartList.as_view(), name='api-part-supplier-list'), +] + part_api_urls = [ url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'), url(r'^category/', include(cat_api_urls)), + url(r'^supplier/', include(supplier_part_api_urls)), url(r'^price-break/?', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'), - url(r'^supplier/?', SupplierPartList.as_view(), name='api-part-supplier-list'), - url(r'^bom/?', BomList.as_view(), name='api-bom-list'), url(r'^(?P\d+)/', PartDetail.as_view(), name='api-part-detail'), url(r'^.*$', PartList.as_view(), name='api-part-list'), ] + +bom_api_urls = [ + # BOM Item Detail + url('^(?P\d+)/?', BomDetail.as_view(), name='api-bom-detail'), + + # Catch-all + url(r'^.*$', BomList.as_view(), name='api-bom-list'), +] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 6c9dc4ec54..fa04b48c9b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -337,7 +337,7 @@ class BomItem(models.Model): """ def get_absolute_url(self): - return reverse('bom-detail', kwargs={'pk': self.id}) + return reverse('bom-item-detail', kwargs={'pk': self.id}) # A link to the parent part # Each part will get a reverse lookup field 'bom_items' @@ -359,11 +359,12 @@ class BomItem(models.Model): # A part cannot refer to itself in its BOM if self.part == self.sub_part: - raise ValidationError(_('A part cannot contain itself as a BOM item')) + raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')}) + # Test for simple recursion for item in self.sub_part.bom_items.all(): if self.part == item.sub_part: - raise ValidationError(_("Part '{p1}' is used in BOM for '{p2}' (recursive)".format(p1=str(self.part), p2=str(self.sub_part)))) + raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)".format(p1=str(self.part), p2=str(self.sub_part)))}) class Meta: verbose_name = "BOM Item" diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 378dbe4732..68066645bd 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from .models import Part, PartCategory, BomItem from .models import SupplierPart, SupplierPriceBreak -from company.serializers import CompanyBriefSerializer +from InvenTree.serializers import InvenTreeModelSerializer class CategorySerializer(serializers.ModelSerializer): @@ -67,20 +67,22 @@ class PartSerializer(serializers.ModelSerializer): ] -class BomItemSerializer(serializers.ModelSerializer): +class BomItemSerializer(InvenTreeModelSerializer): - url = serializers.CharField(source='get_absolute_url', read_only=True) + # url = serializers.CharField(source='get_absolute_url', read_only=True) - part = PartBriefSerializer(many=False, read_only=True) - sub_part = PartBriefSerializer(many=False, read_only=True) + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True) class Meta: model = BomItem fields = [ 'pk', - 'url', + # 'url', 'part', + 'part_detail', 'sub_part', + 'sub_part_detail', 'quantity', 'note', ] @@ -90,8 +92,9 @@ class SupplierPartSerializer(serializers.ModelSerializer): url = serializers.CharField(source='get_absolute_url', read_only=True) - part = PartBriefSerializer(many=False, read_only=True) - supplier = CompanyBriefSerializer(many=False, read_only=True) + part_name = serializers.CharField(source='part.name', read_only=True) + + supplier_name = serializers.CharField(source='supplier.name', read_only=True) class Meta: model = SupplierPart @@ -99,7 +102,9 @@ class SupplierPartSerializer(serializers.ModelSerializer): 'pk', 'url', 'part', + 'part_name', 'supplier', + 'supplier_name', 'SKU', 'manufacturer', 'MPN', diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html index b5426971eb..fba1ec38db 100644 --- a/InvenTree/part/templates/part/supplier.html +++ b/InvenTree/part/templates/part/supplier.html @@ -7,11 +7,16 @@

Part Suppliers

- + +
-
-
@@ -46,10 +51,10 @@ }, { sortable: true, - field: 'supplier', + field: 'supplier_name', title: 'Supplier', formatter: function(value, row, index, field) { - return renderLink(value.name, value.url); + return renderLink(value, '/company/' + row.supplier + '/'); } }, { @@ -74,4 +79,6 @@ url: "{% url 'api-part-supplier-list' %}" }); + linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']) + {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html index bff5eff8ed..bce5dfcffa 100644 --- a/InvenTree/part/templates/part/used_in.html +++ b/InvenTree/part/templates/part/used_in.html @@ -41,14 +41,14 @@ visible: false, }, { - field: 'part', + field: 'part_detail', title: 'Part', formatter: function(value, row, index, field) { - return renderLink(value.name, value.url); + return renderLink(value.name, value.url + 'bom/'); } }, { - field: 'part.description', + field: 'part_detail.description', title: 'Description', }, { diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 3d8db7d7e2..cc533f5201 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -87,7 +87,7 @@ function loadBomTable(table, options) { // Part column cols.push( { - field: 'sub_part', + field: 'sub_part_detail', title: 'Part', sortable: true, formatter: function(value, row, index, field) { @@ -99,7 +99,7 @@ function loadBomTable(table, options) { // Part description cols.push( { - field: 'sub_part.description', + field: 'sub_part_detail.description', title: 'Description', } ); @@ -127,8 +127,8 @@ function loadBomTable(table, options) { if (options.editable) { cols.push({ formatter: function(value, row, index, field) { - var bEdit = ""; - var bDelt = ""; + var bEdit = ""; + var bDelt = ""; return "
" + bEdit + bDelt + "
"; } @@ -137,14 +137,14 @@ function loadBomTable(table, options) { else { cols.push( { - field: 'sub_part.available_stock', + field: 'sub_part_detail.available_stock', title: 'Available', searchable: false, sortable: true, formatter: function(value, row, index, field) { var text = ""; - if (row.quantity < row.sub_part.available_stock) + if (row.quantity < row.sub_part_detail.available_stock) { text = "" + value + ""; } diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 52edd2945e..215a9e311f 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -13,7 +13,6 @@ from .serializers import LocationSerializer from .serializers import StockTrackingSerializer from InvenTree.views import TreeSerializer -from InvenTree.serializers import DraftRUDView from rest_framework.serializers import ValidationError from rest_framework.views import APIView @@ -26,7 +25,7 @@ class StockCategoryTree(TreeSerializer): model = StockLocation -class StockDetail(DraftRUDView): +class StockDetail(generics.RetrieveUpdateDestroyAPIView): """ get: diff --git a/InvenTree/templates/modal_form.html b/InvenTree/templates/modal_form.html index 44fd4c70f8..1318e4f238 100644 --- a/InvenTree/templates/modal_form.html +++ b/InvenTree/templates/modal_form.html @@ -1,5 +1,14 @@ +{% if form.non_field_errors %} + +{% endif %} +
{% csrf_token %} {% load crispy_forms_tags %} + + {% crispy form %}
\ No newline at end of file