Starting to implement BOM management

- Each part can be made of other parts
- Disable tracking and project apps for now
- Project will change (eventually) to work order
- Part parameters have been disabled (for now)
This commit is contained in:
Oliver 2018-04-12 16:27:26 +10:00
parent 47e99d5f35
commit ed61ebe5b7
18 changed files with 284 additions and 140 deletions

View File

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

View File

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

View File

8
InvenTree/bom/admin.py Normal file
View File

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

6
InvenTree/bom/apps.py Normal file
View File

@ -0,0 +1,6 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class BomConfig(AppConfig):
name = 'bom'

37
InvenTree/bom/models.py Normal file
View File

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

View File

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

6
InvenTree/bom/tests.py Normal file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
# Create your tests here.

12
InvenTree/bom/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.conf.urls import url
from . import views
bom_urls = [
# Bom Item detail
url(r'^(?P<pk>[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())
]

35
InvenTree/bom/views.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,17 @@ part_cat_urls = [
url(r'^$', views.PartCategoryList.as_view())
]
part_urls = [
# Individual part
url(r'^(?P<pk>[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<pk>[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<pk>[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()),
]

View File

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

View File

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

View File

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