mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
040f719b68
@ -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
|
||||
|
@ -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)),
|
||||
|
@ -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<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),
|
||||
|
||||
url(r'^.*$', CompanyList.as_view(), name='api-company-list'),
|
||||
]
|
||||
|
@ -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/');
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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<pk>\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<pk>\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<pk>\d+)/?', BomDetail.as_view(), name='api-bom-detail'),
|
||||
|
||||
# Catch-all
|
||||
url(r'^.*$', BomList.as_view(), name='api-bom-list'),
|
||||
]
|
||||
|
@ -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"
|
||||
|
@ -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',
|
||||
|
@ -7,11 +7,16 @@
|
||||
<h3>Part Suppliers</h3>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<button class="btn btn-success float-right" id='supplier-create'>New Supplier Part</button>
|
||||
<button class="btn btn-success" id='supplier-create'>New Supplier Part</button>
|
||||
<div id='opt-dropdown' class="dropdown" style='float: right;'>
|
||||
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
||||
<span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href='#' id='supplier-part-delete' title='Delete supplier parts'>Delete</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
@ -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 %}
|
@ -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',
|
||||
},
|
||||
{
|
||||
|
@ -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 = "<button class='btn btn-success bom-edit-button btn-sm' type='button' url='" + row.url + "edit'>Edit</button>";
|
||||
var bDelt = "<button class='btn btn-danger bom-delete-button btn-sm' type='button' url='" + row.url + "delete'>Delete</button>";
|
||||
var bEdit = "<button class='btn btn-success bom-edit-button btn-sm' type='button' url='/part/bom/" + row.pk + "/edit'>Edit</button>";
|
||||
var bDelt = "<button class='btn btn-danger bom-delete-button btn-sm' type='button' url='/part/bom/" + row.pk + "/delete'>Delete</button>";
|
||||
|
||||
return "<div class='btn-group'>" + bEdit + bDelt + "</div>";
|
||||
}
|
||||
@ -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 = "<span class='label label-success'>" + value + "</span>";
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -1,5 +1,14 @@
|
||||
{% if form.non_field_errors %}
|
||||
<div class='alert alert-danger' role='alert' style='display: block;'>
|
||||
<b>Error Submitting Form:</b>
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
|
||||
{% crispy form %}
|
||||
</form>
|
Loading…
Reference in New Issue
Block a user