diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 4488233196..21abfb8195 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -46,10 +46,11 @@ INSTALLED_APPS = [ # InvenTree apps 'part.apps.PartConfig', - 'project.apps.ProjectConfig', - 'stock.apps.StockConfig', + 'bom.apps.BomConfig', 'supplier.apps.SupplierConfig', - 'track.apps.TrackConfig' + 'stock.apps.StockConfig', + #'project.apps.ProjectConfig', + #'track.apps.TrackConfig', ] MIDDLEWARE = [ diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 2e808e3945..7038fcbb19 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -3,11 +3,14 @@ from django.contrib import admin from rest_framework.documentation import include_docs_urls -from part.urls import part_urls, part_cat_urls, part_param_urls, part_param_template_urls +from part.urls import part_urls, part_cat_urls +from bom.urls import bom_urls from stock.urls import stock_urls, stock_loc_urls -from project.urls import prj_urls, prj_part_urls, prj_cat_urls, prj_run_urls from supplier.urls import cust_urls, manu_urls, supplier_part_urls, price_break_urls, supplier_urls -from track.urls import unique_urls, part_track_urls + +#from project.urls import prj_urls, prj_part_urls, prj_cat_urls, prj_run_urls +#from track.urls import unique_urls, part_track_urls + from users.urls import user_urls admin.site.site_header = "InvenTree Admin" @@ -21,8 +24,11 @@ apipatterns = [ # Part URLs url(r'^part/', include(part_urls)), url(r'^part-category/', include(part_cat_urls)), - url(r'^part-param/', include(part_param_urls)), - url(r'^part-param-template/', include(part_param_template_urls)), + #url(r'^part-param/', include(part_param_urls)), + #url(r'^part-param-template/', include(part_param_template_urls)), + + # Part BOM URLs + url(r'^bom/', include(bom_urls)), # Supplier URLs url(r'^supplier/', include(supplier_urls)), @@ -32,14 +38,14 @@ apipatterns = [ url(r'^customer/', include(cust_urls)), # Tracking URLs - url(r'^track/', include(part_track_urls)), - url(r'^unique-part/', include(unique_urls)), + #url(r'^track/', include(part_track_urls)), + #url(r'^unique-part/', include(unique_urls)), # Project URLs - url(r'^project/', include(prj_urls)), - url(r'^project-category/', include(prj_cat_urls)), - url(r'^project-part/', include(prj_part_urls)), - url(r'^project-run/', include(prj_run_urls)), + #url(r'^project/', include(prj_urls)), + #url(r'^project-category/', include(prj_cat_urls)), + #url(r'^project-part/', include(prj_part_urls)), + #url(r'^project-run/', include(prj_run_urls)), # User URLs url(r'^user/', include(user_urls)), diff --git a/InvenTree/bom/__init__.py b/InvenTree/bom/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/bom/admin.py b/InvenTree/bom/admin.py new file mode 100644 index 0000000000..4c31fcbf5a --- /dev/null +++ b/InvenTree/bom/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import BomItem + +class BomItemAdmin(admin.ModelAdmin): + list_display=('part', 'sub_part', 'quantity') + +admin.site.register(BomItem, BomItemAdmin) \ No newline at end of file diff --git a/InvenTree/bom/apps.py b/InvenTree/bom/apps.py new file mode 100644 index 0000000000..e740d8d611 --- /dev/null +++ b/InvenTree/bom/apps.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + +class BomConfig(AppConfig): + name = 'bom' \ No newline at end of file diff --git a/InvenTree/bom/models.py b/InvenTree/bom/models.py new file mode 100644 index 0000000000..67b5a932e0 --- /dev/null +++ b/InvenTree/bom/models.py @@ -0,0 +1,37 @@ +from __future__ import unicode_literals +from django.utils.translation import ugettext as _ +from django.db import models +from django.db.models import Sum +from django.core.validators import MinValueValidator + +from part.models import Part + +class BomItem(models.Model): + """ A BomItem links a part to its component items. + A part can have a BOM (bill of materials) which defines + which parts are required (and in what quatity) to make it + """ + + # A link to the parent part + # Each part will get a reverse lookup field 'bom_items' + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items') + + # A link to the child item (sub-part) + # Each part will get a reverse lookup field 'used_in' + sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in') + + # Quantity required + quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)]) + + + class Meta: + verbose_name = "BOM Item" + + # Prevent duplication of parent/child rows + unique_together = ('part', 'sub_part') + + def __str__(self): + return "{par} -> {child} ({n})".format( + par=self.part.name, + child=self.sub_part.name, + n=self.quantity) diff --git a/InvenTree/bom/serializers.py b/InvenTree/bom/serializers.py new file mode 100644 index 0000000000..925f0f05d4 --- /dev/null +++ b/InvenTree/bom/serializers.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from .models import BomItem + + +class BomItemSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = BomItem + fields = ('url', + 'part', + 'sub_part', + 'quantity') \ No newline at end of file diff --git a/InvenTree/bom/tests.py b/InvenTree/bom/tests.py new file mode 100644 index 0000000000..5982e6bcd2 --- /dev/null +++ b/InvenTree/bom/tests.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.test import TestCase + +# Create your tests here. diff --git a/InvenTree/bom/urls.py b/InvenTree/bom/urls.py new file mode 100644 index 0000000000..495bbe553b --- /dev/null +++ b/InvenTree/bom/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url + +from . import views + +bom_urls = [ + # Bom Item detail + url(r'^(?P[0-9]+)/?$', views.BomItemDetail.as_view(), name='bomitem-detail'), + + # List of top-level categories + url(r'^\?*.*/?$', views.BomItemList.as_view()), + url(r'^$', views.BomItemList.as_view()) +] \ No newline at end of file diff --git a/InvenTree/bom/views.py b/InvenTree/bom/views.py new file mode 100644 index 0000000000..dd35ac3f2d --- /dev/null +++ b/InvenTree/bom/views.py @@ -0,0 +1,35 @@ +from django_filters.rest_framework import FilterSet, DjangoFilterBackend + +from rest_framework import generics, permissions + +from InvenTree.models import FilterChildren + +from .models import BomItem + +from .serializers import BomItemSerializer + + +class BomItemDetail(generics.RetrieveUpdateDestroyAPIView): + + queryset = BomItem.objects.all() + serializer_class = BomItemSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class BomItemFilter(FilterSet): + + class Meta: + model = BomItem + fields = ['part', 'sub_part'] + + +class BomItemList(generics.ListCreateAPIView): + + #def get_queryset(self): + # params = self.request. + + queryset = BomItem.objects.all() + serializer_class = BomItemSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + filter_backends = (DjangoFilterBackend,) + filter_class = BomItemFilter \ No newline at end of file diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index dd5ec73763..10b5b0d415 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -1,7 +1,6 @@ from django.contrib import admin -from .models import PartCategory, Part, PartParameter, PartParameterTemplate, CategoryParameterLink - +from .models import PartCategory, Part class PartAdmin(admin.ModelAdmin): @@ -12,18 +11,18 @@ class PartCategoryAdmin(admin.ModelAdmin): list_display = ('name', 'path', 'description') - +""" class ParameterTemplateAdmin(admin.ModelAdmin): list_display = ('name', 'units', 'format') class ParameterAdmin(admin.ModelAdmin): list_display = ('part', 'template', 'value') - +""" admin.site.register(Part, PartAdmin) admin.site.register(PartCategory, PartCategoryAdmin) -admin.site.register(PartParameter, ParameterAdmin) -admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) -admin.site.register(CategoryParameterLink) +#admin.site.register(PartParameter, ParameterAdmin) +#admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) +#admin.site.register(CategoryParameterLink) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 099d5ab6cd..6c2df81e3e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -21,7 +21,10 @@ class PartCategory(InvenTreeTree): class Part(models.Model): - """ Represents a """ + """ Represents an abstract part + Parts can be "stocked" in multiple warehouses, + and can be combined to form other parts + """ # Short name of the part name = models.CharField(max_length=100) @@ -92,84 +95,4 @@ class Part(models.Model): return projects -class PartParameterTemplate(models.Model): - """ A PartParameterTemplate pre-defines a parameter field, - ready to be copied for use with a given Part. - A PartParameterTemplate can be optionally associated with a PartCategory - """ - name = models.CharField(max_length=20, unique=True) - units = models.CharField(max_length=10, blank=True) - # Parameter format - PARAM_NUMERIC = 10 - PARAM_TEXT = 20 - PARAM_BOOL = 30 - - PARAM_TYPE_CODES = { - PARAM_NUMERIC: _("Numeric"), - PARAM_TEXT: _("Text"), - PARAM_BOOL: _("Bool") - } - - format = models.PositiveIntegerField( - default=PARAM_NUMERIC, - choices=PARAM_TYPE_CODES.items(), - validators=[MinValueValidator(0)]) - - def __str__(self): - return "{name} ({units})".format( - name=self.name, - units=self.units) - - class Meta: - verbose_name = "Parameter Template" - verbose_name_plural = "Parameter Templates" - - -class CategoryParameterLink(models.Model): - """ Links a PartParameterTemplate to a PartCategory - """ - category = models.ForeignKey(PartCategory, on_delete=models.CASCADE) - template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE) - - def __str__(self): - return "{name} - {cat}".format( - name=self.template.name, - cat=self.category) - - class Meta: - verbose_name = "Category Parameter" - verbose_name_plural = "Category Parameters" - unique_together = ('category', 'template') - - -class PartParameter(models.Model): - """ PartParameter is associated with a single part - """ - - part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') - template = models.ForeignKey(PartParameterTemplate) - - # Value data - value = models.CharField(max_length=50, blank=True) - min_value = models.CharField(max_length=50, blank=True) - max_value = models.CharField(max_length=50, blank=True) - - def __str__(self): - return "{name} : {val}{units}".format( - name=self.template.name, - val=self.value, - units=self.template.units) - - @property - def units(self): - return self.template.units - - @property - def name(self): - return self.template.name - - class Meta: - verbose_name = "Part Parameter" - verbose_name_plural = "Part Parameters" - unique_together = ('part', 'template') diff --git a/InvenTree/part/param_todo.py b/InvenTree/part/param_todo.py new file mode 100644 index 0000000000..e597bedf57 --- /dev/null +++ b/InvenTree/part/param_todo.py @@ -0,0 +1,89 @@ +""" +TODO - Implement part parameters, and templates + +See code below +""" + + + +class PartParameterTemplate(models.Model): + """ A PartParameterTemplate pre-defines a parameter field, + ready to be copied for use with a given Part. + A PartParameterTemplate can be optionally associated with a PartCategory + """ + name = models.CharField(max_length=20, unique=True) + units = models.CharField(max_length=10, blank=True) + + # Parameter format + PARAM_NUMERIC = 10 + PARAM_TEXT = 20 + PARAM_BOOL = 30 + + PARAM_TYPE_CODES = { + PARAM_NUMERIC: _("Numeric"), + PARAM_TEXT: _("Text"), + PARAM_BOOL: _("Bool") + } + + format = models.PositiveIntegerField( + default=PARAM_NUMERIC, + choices=PARAM_TYPE_CODES.items(), + validators=[MinValueValidator(0)]) + + def __str__(self): + return "{name} ({units})".format( + name=self.name, + units=self.units) + + class Meta: + verbose_name = "Parameter Template" + verbose_name_plural = "Parameter Templates" + + +class CategoryParameterLink(models.Model): + """ Links a PartParameterTemplate to a PartCategory + """ + category = models.ForeignKey(PartCategory, on_delete=models.CASCADE) + template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE) + + def __str__(self): + return "{name} - {cat}".format( + name=self.template.name, + cat=self.category) + + class Meta: + verbose_name = "Category Parameter" + verbose_name_plural = "Category Parameters" + unique_together = ('category', 'template') + + +class PartParameter(models.Model): + """ PartParameter is associated with a single part + """ + + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') + template = models.ForeignKey(PartParameterTemplate) + + # Value data + value = models.CharField(max_length=50, blank=True) + min_value = models.CharField(max_length=50, blank=True) + max_value = models.CharField(max_length=50, blank=True) + + def __str__(self): + return "{name} : {val}{units}".format( + name=self.template.name, + val=self.value, + units=self.template.units) + + @property + def units(self): + return self.template.units + + @property + def name(self): + return self.template.name + + class Meta: + verbose_name = "Part Parameter" + verbose_name_plural = "Part Parameters" + unique_together = ('part', 'template') diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 1b4bd9a428..d1ef66f3e2 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -1,11 +1,11 @@ from rest_framework import serializers -from .models import Part, PartCategory, PartParameter, PartParameterTemplate - +from .models import Part, PartCategory +""" class PartParameterSerializer(serializers.HyperlinkedModelSerializer): - """ Serializer for a PartParameter - """ + " Serializer for a PartParameter + " class Meta: model = PartParameter @@ -15,7 +15,7 @@ class PartParameterSerializer(serializers.HyperlinkedModelSerializer): 'name', 'value', 'units') - +""" class PartSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for complete detail information of a part. @@ -44,7 +44,7 @@ class PartCategorySerializer(serializers.HyperlinkedModelSerializer): 'parent', 'path') - +""" class PartTemplateSerializer(serializers.HyperlinkedModelSerializer): class Meta: @@ -53,3 +53,4 @@ class PartTemplateSerializer(serializers.HyperlinkedModelSerializer): 'name', 'units', 'format') +""" \ No newline at end of file diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 1ed26cac2b..9237aa8f51 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -12,6 +12,17 @@ part_cat_urls = [ url(r'^$', views.PartCategoryList.as_view()) ] +part_urls = [ + + # Individual part + url(r'^(?P[0-9]+)/?$', views.PartDetail.as_view(), name='part-detail'), + + # List parts with optional filters + url(r'^\?.*/?$', views.PartList.as_view()), + url(r'^$', views.PartList.as_view()), +] + +""" part_param_urls = [ # Detail of a single part parameter url(r'^(?P[0-9]+)/?$', views.PartParamDetail.as_view(), name='partparameter-detail'), @@ -29,13 +40,6 @@ part_param_template_urls = [ url(r'^\?.*/?$', views.PartTemplateList.as_view()), url(r'^$', views.PartTemplateList.as_view()) ] +""" -part_urls = [ - # Individual part - url(r'^(?P[0-9]+)/?$', views.PartDetail.as_view(), name='part-detail'), - - # List parts with optional filters - url(r'^\?.*/?$', views.PartList.as_view()), - url(r'^$', views.PartList.as_view()), -] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d2a1e9f55d..54dc39c1a2 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -3,11 +3,12 @@ from django_filters.rest_framework import FilterSet, DjangoFilterBackend from rest_framework import generics, permissions from InvenTree.models import FilterChildren -from .models import PartCategory, Part, PartParameter, PartParameterTemplate +from .models import PartCategory, Part + from .serializers import PartSerializer from .serializers import PartCategorySerializer -from .serializers import PartParameterSerializer -from .serializers import PartTemplateSerializer +#from .serializers import PartParameterSerializer +#from .serializers import PartTemplateSerializer class PartDetail(generics.RetrieveUpdateDestroyAPIView): @@ -28,22 +29,22 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) +""" class PartParamFilter(FilterSet): class Meta: model = PartParameter fields = ['part'] - class PartParamList(generics.ListCreateAPIView): - """ + " get: Return a list of all part parameters (with optional filters) post: Create a new part parameter - """ + "" queryset = PartParameter.objects.all() serializer_class = PartParameterSerializer @@ -53,7 +54,7 @@ class PartParamList(generics.ListCreateAPIView): class PartParamDetail(generics.RetrieveUpdateDestroyAPIView): - """ + "" get: Detail view of a single PartParameter @@ -64,12 +65,12 @@ class PartParamDetail(generics.RetrieveUpdateDestroyAPIView): delete: Remove a PartParameter from the database - """ + " queryset = PartParameter.objects.all() serializer_class = PartParameterSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - +""" class PartFilter(FilterSet): @@ -138,8 +139,9 @@ class PartCategoryList(generics.ListCreateAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) +""" class PartTemplateDetail(generics.RetrieveUpdateDestroyAPIView): - """ + "" get: Return detail on a single PartParameterTemplate object @@ -150,7 +152,7 @@ class PartTemplateDetail(generics.RetrieveUpdateDestroyAPIView): delete: Remove a PartParameterTemplate object - """ + "" queryset = PartParameterTemplate.objects.all() serializer_class = PartTemplateSerializer @@ -158,7 +160,7 @@ class PartTemplateDetail(generics.RetrieveUpdateDestroyAPIView): class PartTemplateList(generics.ListCreateAPIView): - """ + "" get: Return a list of all PartParameterTemplate objects @@ -167,8 +169,10 @@ class PartTemplateList(generics.ListCreateAPIView): post: Create a new PartParameterTemplate object - """ + "" queryset = PartParameterTemplate.objects.all() serializer_class = PartTemplateSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + +""" diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index ea7cc09c71..005b6aa44a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -14,6 +14,8 @@ from datetime import datetime class StockLocation(InvenTreeTree): """ Organization tree for StockItem objects + A "StockLocation" can be considered a warehouse, or storage location + Stock locations can be heirarchical as required """ @property @@ -36,33 +38,27 @@ class StockItem(models.Model): review_needed = models.BooleanField(default=False) # Stock status types - ITEM_IN_STOCK = 10 - ITEM_INCOMING = 15 - ITEM_IN_PROGRESS = 20 - ITEM_COMPLETE = 25 + ITEM_OK = 10 ITEM_ATTENTION = 50 ITEM_DAMAGED = 55 ITEM_DESTROYED = 60 ITEM_STATUS_CODES = { - ITEM_IN_STOCK: _("In stock"), - ITEM_INCOMING: _("Incoming"), - ITEM_IN_PROGRESS: _("In progress"), - ITEM_COMPLETE: _("Complete"), + ITEM_OK: _("OK"), ITEM_ATTENTION: _("Attention needed"), ITEM_DAMAGED: _("Damaged"), ITEM_DESTROYED: _("Destroyed") } status = models.PositiveIntegerField( - default=ITEM_IN_STOCK, + default=ITEM_OK, choices=ITEM_STATUS_CODES.items(), validators=[MinValueValidator(0)]) notes = models.CharField(max_length=100, blank=True) # If stock item is incoming, an (optional) ETA field - expected_arrival = models.DateField(null=True, blank=True) + # expected_arrival = models.DateField(null=True, blank=True) infinite = models.BooleanField(default=False) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 13393bb4ad..31f4c67d98 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -35,7 +35,11 @@ class SupplierPart(models.Model): class Meta: unique_together = ('part', 'supplier', 'SKU') - part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE) + # Link to an actual part +# The part will have a field 'supplier_parts' which links to the supplier part options + part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE, + related_name='supplier_parts') + supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) SKU = models.CharField(max_length=100)