2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 20:16:44 +00:00
This commit is contained in:
James Newlands 2018-04-17 00:03:09 +10:00
commit 0a2c48eda6
86 changed files with 1129 additions and 706 deletions

View File

@ -7,6 +7,7 @@ from rest_framework.exceptions import ValidationError
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
class Company(models.Model): class Company(models.Model):
""" Abstract model representing an external company """ Abstract model representing an external company
""" """
@ -89,6 +90,10 @@ class InvenTreeTree(models.Model):
return unique return unique
@property
def has_children(self):
return self.children.count() > 0
@property @property
def children(self): def children(self):
contents = ContentType.objects.get_for_model(type(self)) contents = ContentType.objects.get_for_model(type(self))
@ -185,7 +190,7 @@ class InvenTreeTree(models.Model):
@receiver(pre_delete, sender=InvenTreeTree, dispatch_uid='tree_pre_delete_log') @receiver(pre_delete, sender=InvenTreeTree, dispatch_uid='tree_pre_delete_log')
def before_delete_tree_item(sender, intance, using, **kwargs): def before_delete_tree_item(sender, instance, using, **kwargs):
# Update each tree item below this one # Update each tree item below this one
for child in instance.children.all(): for child in instance.children.all():

View File

@ -36,6 +36,7 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'simple_history', 'simple_history',
'crispy_forms', 'crispy_forms',
'import_export',
# Core django modules # Core django modules
'django.contrib.admin', 'django.contrib.admin',
@ -47,9 +48,8 @@ INSTALLED_APPS = [
# InvenTree apps # InvenTree apps
'part.apps.PartConfig', 'part.apps.PartConfig',
'supplier.apps.SupplierConfig',
'stock.apps.StockConfig', 'stock.apps.StockConfig',
'track.apps.TrackConfig', 'supplier.apps.SupplierConfig',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -68,7 +68,7 @@ ROOT_URLCONF = 'InvenTree.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -81,6 +81,8 @@ TEMPLATES = [
}, },
] ]
print(os.path.join(BASE_DIR, 'templates'))
REST_FRAMEWORK = { REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'InvenTree.utils.api_exception_handler' 'EXCEPTION_HANDLER': 'InvenTree.utils.api_exception_handler'
} }
@ -145,4 +147,8 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# crispy forms use the bootstrap templates
CRISPY_TEMPLATE_PACK = 'bootstrap' CRISPY_TEMPLATE_PACK = 'bootstrap'
# Use database transactions when importing / exporting data
IMPORT_EXPORT_USE_TRANSACTIONS = True

View File

@ -1,17 +1,14 @@
from django.conf.urls import url, include from django.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
from rest_framework.documentation import include_docs_urls
from part.urls import part_api_urls, part_cat_api_urls from part.urls import part_api_urls, part_cat_api_urls
from part.urls import bom_api_urls from part.urls import bom_api_urls
from part.urls import part_urls from part.urls import part_urls
from stock.urls import stock_api_urls, stock_api_loc_urls from stock.urls import stock_api_urls, stock_api_loc_urls
from stock.urls import stock_urls from stock.urls import stock_urls
#from supplier.urls import supplier_api_urls, supplier_api_part_urls # from supplier.urls import supplier_api_urls, supplier_api_part_urls
from supplier.urls import supplier_urls from supplier.urls import supplier_urls
from django.conf import settings from django.conf import settings
@ -19,10 +16,8 @@ from django.conf.urls.static import static
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from track.urls import tracking_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 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 from users.urls import user_urls
@ -37,28 +32,28 @@ apipatterns = [
# Part URLs # Part URLs
url(r'^part/', include(part_api_urls)), url(r'^part/', include(part_api_urls)),
url(r'^part-category/', include(part_cat_api_urls)), url(r'^part-category/', include(part_cat_api_urls)),
#url(r'^part-param/', include(part_param_urls)), # url(r'^part-param/', include(part_param_urls)),
#url(r'^part-param-template/', include(part_param_template_urls)), # url(r'^part-param-template/', include(part_param_template_urls)),
# Part BOM URLs # Part BOM URLs
url(r'^bom/', include(bom_api_urls)), url(r'^bom/', include(bom_api_urls)),
# Supplier URLs # Supplier URLs
#url(r'^supplier/', include(supplier_api_urls)), # url(r'^supplier/', include(supplier_api_urls)),
#url(r'^supplier-part/', include(supplier_api_part_urls)), # url(r'^supplier-part/', include(supplier_api_part_urls)),
#url(r'^price-break/', include(price_break_urls)), # url(r'^price-break/', include(price_break_urls)),
#url(r'^manufacturer/', include(manu_urls)), # url(r'^manufacturer/', include(manu_urls)),
#url(r'^customer/', include(cust_urls)), # url(r'^customer/', include(cust_urls)),
# Tracking URLs # Tracking URLs
#url(r'^track/', include(part_track_urls)), # url(r'^track/', include(part_track_urls)),
#url(r'^unique-part/', include(unique_urls)), # url(r'^unique-part/', include(unique_urls)),
# Project URLs # Project URLs
#url(r'^project/', include(prj_urls)), # url(r'^project/', include(prj_urls)),
#url(r'^project-category/', include(prj_cat_urls)), # url(r'^project-category/', include(prj_cat_urls)),
#url(r'^project-part/', include(prj_part_urls)), # url(r'^project-part/', include(prj_part_urls)),
#url(r'^project-run/', include(prj_run_urls)), # url(r'^project-run/', include(prj_run_urls)),
# User URLs # User URLs
url(r'^user/', include(user_urls)), url(r'^user/', include(user_urls)),
@ -67,13 +62,12 @@ apipatterns = [
urlpatterns = [ urlpatterns = [
# API URL # API URL
#url(r'^api/', include(apipatterns)), # url(r'^api/', include(apipatterns)),
#url(r'^api-doc/', include_docs_urls(title='InvenTree API')), # url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
url(r'^part/', include(part_urls)), url(r'^part/', include(part_urls)),
url(r'^stock/', include(stock_urls)), url(r'^stock/', include(stock_urls)),
url(r'^supplier/', include(supplier_urls)), url(r'^supplier/', include(supplier_urls)),
url(r'^track/', include(tracking_urls)),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
@ -87,4 +81,4 @@ if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Send any unknown URLs to the parts page # Send any unknown URLs to the parts page
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='part/', permanent=False), name='part-index')] urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/part/', permanent=False), name='part-index')]

View File

@ -1,24 +1,29 @@
from django.contrib import admin from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import PartCategory, Part from .models import PartCategory, Part
from .models import BomItem from .models import BomItem
from .models import PartAttachment from .models import PartAttachment
class PartAdmin(admin.ModelAdmin):
list_display = ('name', 'IPN', 'description', 'stock', 'category') class PartAdmin(ImportExportModelAdmin):
list_display = ('name', 'IPN', 'description', 'total_stock', 'category')
class PartCategoryAdmin(admin.ModelAdmin): class PartCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'pathstring', 'description') list_display = ('name', 'pathstring', 'description')
class BomItemAdmin(admin.ModelAdmin):
list_display=('part', 'sub_part', 'quantity') class BomItemAdmin(ImportExportModelAdmin):
list_display = ('part', 'sub_part', 'quantity')
class PartAttachmentAdmin(admin.ModelAdmin): class PartAttachmentAdmin(admin.ModelAdmin):
list_display = ('part', 'attachment') list_display = ('part', 'attachment')
""" """
class ParameterTemplateAdmin(admin.ModelAdmin): class ParameterTemplateAdmin(admin.ModelAdmin):
list_display = ('name', 'units', 'format') list_display = ('name', 'units', 'format')
@ -33,6 +38,6 @@ admin.site.register(PartCategory, PartCategoryAdmin)
admin.site.register(BomItem, BomItemAdmin) admin.site.register(BomItem, BomItemAdmin)
admin.site.register(PartAttachment, PartAttachmentAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin)
#admin.site.register(PartParameter, ParameterAdmin) # admin.site.register(PartParameter, ParameterAdmin)
#admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) # admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
#admin.site.register(CategoryParameterLink) # admin.site.register(CategoryParameterLink)

View File

@ -8,6 +8,9 @@ from django_filters.rest_framework import FilterSet, DjangoFilterBackend
from .models import PartCategory, Part, BomItem from .models import PartCategory, Part, BomItem
from InvenTree.models import FilterChildren
class PartDetail(generics.RetrieveUpdateDestroyAPIView): class PartDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
@ -69,6 +72,7 @@ class PartParamDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
""" """
class PartFilter(FilterSet): class PartFilter(FilterSet):
class Meta: class Meta:
@ -174,6 +178,7 @@ class PartTemplateList(generics.ListCreateAPIView):
""" """
class BomItemDetail(generics.RetrieveUpdateDestroyAPIView): class BomItemDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
@ -190,9 +195,6 @@ class BomItemFilter(FilterSet):
class BomItemList(generics.ListCreateAPIView): class BomItemList(generics.ListCreateAPIView):
#def get_queryset(self):
# params = self.request.
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
serializer_class = BomItemSerializer serializer_class = BomItemSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

View File

@ -2,7 +2,7 @@ from django import forms
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit from crispy_forms.layout import Submit
from .models import Part, PartCategory from .models import Part, PartCategory, BomItem
class EditPartForm(forms.ModelForm): class EditPartForm(forms.ModelForm):
@ -12,9 +12,7 @@ class EditPartForm(forms.ModelForm):
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_id = 'id-edit-part-form' self.helper.form_id = 'id-edit-part-form'
#self.helper.form_class = 'blueForms'
self.helper.form_method = 'post' self.helper.form_method = 'post'
#self.helper.form_action = 'submit'
self.helper.add_input(Submit('submit', 'Submit')) self.helper.add_input(Submit('submit', 'Submit'))
@ -27,7 +25,9 @@ class EditPartForm(forms.ModelForm):
'IPN', 'IPN',
'URL', 'URL',
'minimum_stock', 'minimum_stock',
'buildable',
'trackable', 'trackable',
'purchaseable',
] ]
@ -38,9 +38,7 @@ class EditCategoryForm(forms.ModelForm):
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_id = 'id-edit-part-form' self.helper.form_id = 'id-edit-part-form'
#self.helper.form_class = 'blueForms'
self.helper.form_method = 'post' self.helper.form_method = 'post'
#self.helper.form_action = 'submit'
self.helper.add_input(Submit('submit', 'Submit')) self.helper.add_input(Submit('submit', 'Submit'))
@ -51,3 +49,23 @@ class EditCategoryForm(forms.ModelForm):
'name', 'name',
'description' 'description'
] ]
class EditBomItemForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(EditBomItemForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'id-edit-part-form'
self.helper.form_method = 'post'
self.helper.add_input(Submit('submit', 'Submit'))
class Meta:
model = BomItem
fields = [
'part',
'sub_part',
'quantity'
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-15 14:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0016_auto_20180415_0316'),
]
operations = [
migrations.AddField(
model_name='part',
name='purchaseable',
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 12:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0017_part_purchaseable'),
]
operations = [
migrations.AddField(
model_name='part',
name='buildable',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 12:49
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0018_part_buildable'),
]
operations = [
migrations.AlterField(
model_name='part',
name='IPN',
field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100),
),
migrations.AlterField(
model_name='part',
name='URL',
field=models.URLField(blank=True, help_text='Link to extenal URL'),
),
migrations.AlterField(
model_name='part',
name='buildable',
field=models.BooleanField(default=False, help_text='Can this part be built from other parts?'),
),
migrations.AlterField(
model_name='part',
name='category',
field=models.ForeignKey(blank=True, help_text='Part category', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='parts', to='part.PartCategory'),
),
migrations.AlterField(
model_name='part',
name='description',
field=models.CharField(help_text='Part description', max_length=250),
),
migrations.AlterField(
model_name='part',
name='minimum_stock',
field=models.PositiveIntegerField(default=0, help_text='Minimum allowed stock level', validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='part',
name='name',
field=models.CharField(help_text='Part name (must be unique)', max_length=100, unique=True),
),
migrations.AlterField(
model_name='part',
name='purchaseable',
field=models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?'),
),
migrations.AlterField(
model_name='part',
name='trackable',
field=models.BooleanField(default=False, help_text='Does this part have tracking for unique items?'),
),
]

View File

@ -1,5 +1,4 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@ -23,7 +22,6 @@ class PartCategory(InvenTreeTree):
verbose_name = "Part Category" verbose_name = "Part Category"
verbose_name_plural = "Part Categories" verbose_name_plural = "Part Categories"
@property @property
def partcount(self): def partcount(self):
""" Return the total part count under this category """ Return the total part count under this category
@ -37,11 +35,10 @@ class PartCategory(InvenTreeTree):
return count return count
"""
@property @property
def parts(self): def has_parts(self):
return self.part_set.all() return self.parts.count() > 0
"""
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
def before_delete_part_category(sender, instance, using, **kwargs): def before_delete_part_category(sender, instance, using, **kwargs):
@ -85,36 +82,44 @@ class Part(models.Model):
return '/part/{id}/'.format(id=self.id) return '/part/{id}/'.format(id=self.id)
# Short name of the part # Short name of the part
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True, help_text='Part name (must be unique)')
# Longer description of the part (optional) # Longer description of the part (optional)
description = models.CharField(max_length=250) description = models.CharField(max_length=250, help_text='Part description')
# Internal Part Number (optional) # Internal Part Number (optional)
# Potentially multiple parts map to the same internal IPN (variants?) # Potentially multiple parts map to the same internal IPN (variants?)
# So this does not have to be unique # So this does not have to be unique
IPN = models.CharField(max_length=100, blank=True) IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
# Provide a URL for an external link # Provide a URL for an external link
URL = models.URLField(blank=True) URL = models.URLField(blank=True, help_text='Link to extenal URL')
# Part category - all parts must be assigned to a category # Part category - all parts must be assigned to a category
category = models.ForeignKey(PartCategory, related_name='parts', category = models.ForeignKey(PartCategory, related_name='parts',
null=True, blank=True, null=True, blank=True,
on_delete=models.DO_NOTHING) on_delete=models.DO_NOTHING,
help_text='Part category')
image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True) image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True)
# Minimum "allowed" stock level # Minimum "allowed" stock level
minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)]) minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text='Minimum allowed stock level')
# Units of quantity for this part. Default is "pcs" # Units of quantity for this part. Default is "pcs"
units = models.CharField(max_length=20, default="pcs", blank=True) units = models.CharField(max_length=20, default="pcs", blank=True)
# Can this part be built?
buildable = models.BooleanField(default=False, help_text='Can this part be built from other parts?')
# Is this part "trackable"? # Is this part "trackable"?
# Trackable parts can have unique instances which are assigned serial numbers # Trackable parts can have unique instances
# which are assigned serial numbers (or batch numbers)
# and can have their movements tracked # and can have their movements tracked
trackable = models.BooleanField(default=False) trackable = models.BooleanField(default=False, help_text='Does this part have tracking for unique items?')
# Is this part "purchaseable"?
purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?')
def __str__(self): def __str__(self):
if self.IPN: if self.IPN:
@ -127,10 +132,41 @@ class Part(models.Model):
class Meta: class Meta:
verbose_name = "Part" verbose_name = "Part"
verbose_name_plural = "Parts" verbose_name_plural = "Parts"
#unique_together = (("name", "category"),)
@property @property
def stock(self): def available_stock(self):
"""
Return the total available stock.
This subtracts stock which is already allocated
"""
# TODO - For now, just return total stock count
# TODO - In future must take account of allocated stock
return self.total_stock
@property
def can_build(self):
""" Return the number of units that can be build with available stock
"""
# If this part does NOT have a BOM, result is simply the currently available stock
if not self.has_bom:
return self.available_stock
total = None
# Calculate the minimum number of parts that can be built using each sub-part
for item in self.bom_items.all():
stock = item.sub_part.available_stock
n = int(1.0 * stock / item.quantity)
if total is None or n < total:
total = n
return total
@property
def total_stock(self):
""" Return the total stock quantity for this part. """ Return the total stock quantity for this part.
Part may be stored in multiple locations Part may be stored in multiple locations
""" """
@ -143,13 +179,21 @@ class Part(models.Model):
return result['total'] return result['total']
@property @property
def bomItemCount(self): def has_bom(self):
return self.bom_items.all().count() return self.bom_count > 0
@property @property
def usedInCount(self): def bom_count(self):
return self.used_in.all().count() return self.bom_items.count()
@property
def used_in_count(self):
return self.used_in.count()
@property
def supplier_count(self):
# Return the number of supplier parts available for this part
return self.supplier_parts.count()
""" """
@property @property
@ -171,9 +215,10 @@ class Part(models.Model):
return projects return projects
""" """
def attach_file(instance, filename): def attach_file(instance, filename):
base='part_files' base = 'part_files'
# TODO - For a new PartAttachment object, PK is NULL!! # TODO - For a new PartAttachment object, PK is NULL!!
@ -182,25 +227,27 @@ def attach_file(instance, filename):
return os.path.join(base, fn) return os.path.join(base, fn)
class PartAttachment(models.Model): class PartAttachment(models.Model):
""" A PartAttachment links a file to a part """ A PartAttachment links a file to a part
Parts can have multiple files such as datasheets, etc Parts can have multiple files such as datasheets, etc
""" """
part = models.ForeignKey(Part, on_delete=models.CASCADE, part = models.ForeignKey(Part, on_delete=models.CASCADE,
related_name='attachments') related_name='attachments')
attachment = models.FileField(upload_to=attach_file, null=True, blank=True) attachment = models.FileField(upload_to=attach_file, null=True, blank=True)
class BomItem(models.Model): class BomItem(models.Model):
""" A BomItem links a part to its component items. """ A BomItem links a part to its component items.
A part can have a BOM (bill of materials) which defines A part can have a BOM (bill of materials) which defines
which parts are required (and in what quatity) to make it which parts are required (and in what quatity) to make it
""" """
def get_absolute_url(self):
return '/part/bom/{id}/'.format(id=self.id)
# A link to the parent part # A link to the parent part
# Each part will get a reverse lookup field 'bom_items' # Each part will get a reverse lookup field 'bom_items'
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items') part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items')
@ -212,7 +259,6 @@ class BomItem(models.Model):
# Quantity required # Quantity required
quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)]) quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)])
class Meta: class Meta:
verbose_name = "BOM Item" verbose_name = "BOM Item"

View File

@ -3,6 +3,7 @@ from rest_framework import serializers
from .models import Part, PartCategory from .models import Part, PartCategory
from .models import BomItem from .models import BomItem
class BomItemSerializer(serializers.ModelSerializer): class BomItemSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -12,6 +13,7 @@ class BomItemSerializer(serializers.ModelSerializer):
'sub_part', 'sub_part',
'quantity') 'quantity')
""" """
class PartParameterSerializer(serializers.HyperlinkedModelSerializer): class PartParameterSerializer(serializers.HyperlinkedModelSerializer):
" Serializer for a PartParameter " Serializer for a PartParameter
@ -27,7 +29,7 @@ class PartParameterSerializer(serializers.HyperlinkedModelSerializer):
'units') 'units')
""" """
#class PartSerializer(serializers.HyperlinkedModelSerializer):
class PartSerializer(serializers.ModelSerializer): class PartSerializer(serializers.ModelSerializer):
""" Serializer for complete detail information of a part. """ Serializer for complete detail information of a part.
Used when displaying all details of a single component. Used when displaying all details of a single component.
@ -56,6 +58,7 @@ class PartCategorySerializer(serializers.HyperlinkedModelSerializer):
'parent', 'parent',
'pathstring') 'pathstring')
""" """
class PartTemplateSerializer(serializers.HyperlinkedModelSerializer): class PartTemplateSerializer(serializers.HyperlinkedModelSerializer):

View File

@ -12,7 +12,6 @@ Deletion title goes here
<p><b>This is a permanent action and cannot be undone.</b></p> <p><b>This is a permanent action and cannot be undone.</b></p>
{% block del_body %} {% block del_body %}
Deletion body goes here
{% endblock %} {% endblock %}
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}

View File

@ -9,7 +9,6 @@
<li><a href="/part/">Parts</a></li> <li><a href="/part/">Parts</a></li>
<li><a href="/stock/">Stock</a></li> <li><a href="/stock/">Stock</a></li>
<li><a href="/supplier/">Suppliers</a></li> <li><a href="/supplier/">Suppliers</a></li>
<li><a href="/track/">Tracking</a></li>
</ul> </ul>
</div> </div>
</nav> </nav>

View File

@ -0,0 +1,5 @@
{% extends 'create_edit_obj.html' %}
{% block obj_title %}
Create a new BOM item
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "delete_obj.html" %}
{% block del_title %}
Are you sure you want to delete this BOM item?
{% endblock %}
{% block del_body %}
Deleting this entry will remove the BOM row from the following part:
<ul class='list-group'>
<li class='list-group-item'>
<b>{{ item.part.name }}</b> - <i>{{ item.part.description }}</i>
</li>
</ul>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<h3>BOM Item</h3>
<table class="table table-striped">
<tr><td>Parent</td><td><a href="{% url 'part-bom' item.part.id %}">{{ item.part.name }}</a></td></tr>
<tr><td>Child</td><td><a href="{% url 'part-used-in' item.sub_part.id %}">{{ item.sub_part.name }}</a></td></tr>
<tr><td>Quantity</td><td>{{ item.quantity }}</td></tr>
</table>
<div class='container-fluid'>
<a href="{% url 'bom-item-edit' item.id %}"><button class="btn btn-info">Edit BOM item</button></a>
<a href="{% url 'bom-item-delete' item.id %}"><button class="btn btn-danger">Delete BOM item</button></a>
</div>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends 'create_edit_obj.html' %}
{% block obj_title %}
Edit details for BOM item
{% endblock %}

View File

@ -4,21 +4,31 @@
{% include 'part/tabs.html' with tab='bom' %} {% include 'part/tabs.html' with tab='bom' %}
<h3>Bill of Materials</h3>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Part</th> <th>Part</th>
<th>Description</th> <th>Description</th>
<th>Quantity</th> <th>Quantity</th>
<th>Edit</th>
</tr> </tr>
{% for bom_item in part.bom_items.all %} {% for bom_item in part.bom_items.all %}
{% with sub_part=bom_item.sub_part %} {% with sub_part=bom_item.sub_part %}
<tr> <tr>
<td><a href="{% url 'part-detail' sub_part.id %}">{{ sub_part.name }}</a></td> <td><a href="{% url 'part-detail' sub_part.id %}">{{ sub_part.name }}</a></td>
<td>{{ sub_part.description }}</td> <td>{{ sub_part.description }}</td>
<td>{{ bom_item.quantity }}</td> <td>{{ bom_item.quantity }}</span></td>
<td><a href="{% url 'bom-item-detail' bom_item.id %}">Edit</a></td>
</tr> </tr>
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
</table> </table>
<div class='container-fluid'>
<a href="{% url 'bom-item-create' %}?parent={{ part.id }}">
<button class='btn btn-success'>Add BOM Item</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "part/part_base.html" %}
{% block details %}
{% include 'part/tabs.html' with tab='build' %}
<h3>Build Part</h3>
TODO
<br><br>
You can build {{ part.can_build }} of this part with current stock.
{% endblock %}

View File

@ -18,7 +18,7 @@ Are you sure you want to delete category '{{ category.name }}'?
<ul class='list-group'> <ul class='list-group'>
{% for cat in category.children.all %} {% for cat in category.children.all %}
<li class='list-group-item'>{{ cat.name }} - {{ cat.description }}</li> <li class='list-group-item'><b>{{ cat.name }}</b> - <i>{{ cat.description }}</i></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
@ -33,7 +33,7 @@ Are you sure you want to delete category '{{ category.name }}'?
</p> </p>
<ul class='list-group'> <ul class='list-group'>
{% for part in category.parts.all %} {% for part in category.parts.all %}
<li class='list-group-item'>{{ part.name }} - {{ part.description }}</li> <li class='list-group-item'><b>{{ part.name }}</b> - <i>{{ part.description }}</i></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View File

@ -11,9 +11,15 @@
<i>{{ category.description }}</i> <i>{{ category.description }}</i>
</p> </p>
{% if category.has_children %}
<h4>Subcategories</h4>
{% include "part/category_subcategories.html" with children=category.children.all %} {% include "part/category_subcategories.html" with children=category.children.all %}
{% endif %}
{% if category.has_parts %}
<h4>Parts</h4>
{% include "part/category_parts.html" with parts=category.parts.all %} {% include "part/category_parts.html" with parts=category.parts.all %}
{% endif %}
<div class='container-fluid'> <div class='container-fluid'>
<a href="{% url 'category-create' %}?category={{ category.id }}"> <a href="{% url 'category-create' %}?category={{ category.id }}">

View File

@ -1,5 +1,3 @@
{% if parts|length > 0 %}
Parts:
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Part</th> <th>Part</th>
@ -12,4 +10,3 @@ Parts:
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endif %}

View File

@ -1,5 +1,3 @@
{% if children|length > 0 %}
Subcategories:
<ul class="list-group"> <ul class="list-group">
{% for child in children %} {% for child in children %}
<li class="list-group-item"> <li class="list-group-item">
@ -11,4 +9,3 @@ Subcategories:
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %}

View File

@ -7,8 +7,8 @@
{% block del_body %} {% block del_body %}
{% if part.usedInCount > 0 %} {% if part.used_in_count %}
<p>This part is used in BOMs for {{ part.usedInCount }} other parts. If you delete this part, the BOMs for the following parts will be updated: <p>This part is used in BOMs for {{ part.used_in_count }} other parts. If you delete this part, the BOMs for the following parts will be updated:
<ul class="list-group"> <ul class="list-group">
{% for child in part.used_in.all %} {% for child in part.used_in.all %}
<li class='list-group-item'>{{ child.part.name }} - {{ child.part.description }}</li> <li class='list-group-item'>{{ child.part.name }} - {{ child.part.description }}</li>

View File

@ -4,12 +4,52 @@
{% include 'part/tabs.html' with tab='detail' %} {% include 'part/tabs.html' with tab='detail' %}
<h3>Part Details</h3>
Part details go here... <table class='table table-striped'>
<br> <tr>
<td>Part name</td>
<td>{{ part.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ part.decription }}</td>
</tr>
<tr>
<td>Category</td>
<td>
{% if part.category %}
<a href="{% url 'category-detail' part.category.id %}">{{ part.category.name }}</a>
{% endif %}
</td>
</tr>
<tr>
<td>Units</td>
<td>{{ part.units }}</td>
</tr>
<tr>
<td>Buildable</td>
<td>{{ part.buildable }}</td>
</tr>
<tr>
<td>Trackable</td>
<td>{{ part.trackable }}</td>
</tr>
<tr>
<td>Purchaseable</td>
<td>{{ part.purchaseable }}</td>
</tr>
{% if part.minimum_stock > 0 %}
<tr>
<td>Minimum Stock</td>
<td>{{ part.minimum_stock }}</td>
</tr>
{% endif %}
</table>
<div class='container-fluid'>
<a href="{% url 'part-edit' part.id %}"><button class="btn btn-info">Edit Part</button></a> <a href="{% url 'part-edit' part.id %}"><button class="btn btn-info">Edit Part</button></a>
<a href="{% url 'part-delete' part.id %}"><button class="btn btn-danger">Delete Part</button></a> <a href="{% url 'part-delete' part.id %}"><button class="btn btn-danger">Delete Part</button></a>
</div>
{% endblock %} {% endblock %}

View File

@ -5,9 +5,15 @@
{% include "part/cat_link.html" with category=category %} {% include "part/cat_link.html" with category=category %}
{% if children.all|length > 0 %}
<h4>Part Categories</h4>
{% include "part/category_subcategories.html" with children=children %} {% include "part/category_subcategories.html" with children=children %}
{% endif %}
{% if parts.all|length > 0%}
<h4>Top Level Parts</h4>
{% include "part/category_parts.html" with parts=parts %} {% include "part/category_parts.html" with parts=parts %}
{% endif %}
<div class='container-fluid'> <div class='container-fluid'>
<a href="{% url 'category-create' %}"> <a href="{% url 'category-create' %}">

View File

@ -6,7 +6,9 @@
{% include "part/cat_link.html" with category=part.category %} {% include "part/cat_link.html" with category=part.category %}
<div class="media"> <div class="row">
<div class="col-sm-6">
<div class="media">
<div class="media-left"> <div class="media-left">
<img class="part-thumb" <img class="part-thumb"
{% if part.image %} {% if part.image %}
@ -15,24 +17,63 @@
src="{% static 'img/blank_image.png' %}" src="{% static 'img/blank_image.png' %}"
{% endif %}/> {% endif %}/>
</div> </div>
<div class="media-body"> <div class="media-body">
<h4>{{ part.name }}</h4> <h4>{{ part.name }}</h4>
{% if part.description %} {% if part.description %}
<p><i>{{ part.description }}</i></p> <p><i>{{ part.description }}</i></p>
{% endif %} {% endif %}
</div>
</div>
</div>
<div class="col-sm-6">
<table class="table table-striped">
{% if part.IPN %} {% if part.IPN %}
<p><b>IPN:</b> {{ part.IPN }}</p> <tr>
<td>IPN</td>
<td>{{ part.IPN }}</td>
</tr>
{% endif %} {% endif %}
{% if part.URL %} {% if part.URL %}
<p>{% include 'url.html' with url=part.URL %}</p> <tr>
<td>URL</td>
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr>
{% endif %} {% endif %}
<tr>
<td>Available Stock</td>
<td>
{% if part.available_stock == 0 %}
<span class='label label-danger'>{{ part.available_stock }}</span>
{% elif part.available_stock < part.minimum_stock %}
<span class='label label-warning'>{{ part.available_stock }}</span>
{% else %}
{{ part.available_stock }}
{% endif %}
</td>
</tr>
{% if part.buildable %}
<tr>
<td>Can Build</td>
<td>
{% if part.can_build == 0 %}
<span class='label label-danger'>0</span>
{% else %}
{{ part.can_build }}
{% endif %}
</td>
</tr>
{% endif %}
</table>
</div> </div>
</div> </div>
<hr>
<div class='container-fluid'>
{% block details %} {% block details %}
<!-- Specific part details go here... --> <!-- Specific part details go here... -->
{% endblock %} {% endblock %}
</div>
{% endblock %} {% endblock %}

View File

@ -4,12 +4,11 @@
{% include 'part/tabs.html' with tab='stock' %} {% include 'part/tabs.html' with tab='stock' %}
<br> <h3>Part Stock</h3>
Total in stock: {{ part.stock }}
<br>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Link</th>
<th>Quantity</th> <th>Quantity</th>
<th>Location</th> <th>Location</th>
<th>Supplier part</th> <th>Supplier part</th>
@ -18,8 +17,9 @@ Total in stock: {{ part.stock }}
</tr> </tr>
{% for stock in part.locations.all %} {% for stock in part.locations.all %}
<tr> <tr>
<td><a href="{% url 'stock-item-detail' stock.id %}">Click</a></td>
<td>{{ stock.quantity }}</td> <td>{{ stock.quantity }}</td>
<td><a href="/stock/list/?location={{ stock.location.id }}">{{ stock.location.name }}</a></td> <td><a href="{% url 'stock-location-detail' stock.location.id %}">{{ stock.location.name }}</a></td>
<td> <td>
{% if stock.supplier_part %} {% if stock.supplier_part %}
<a href="{% url 'supplier-part-detail' stock.supplier_part.id %}"> <a href="{% url 'supplier-part-detail' stock.supplier_part.id %}">
@ -31,6 +31,12 @@ Total in stock: {{ part.stock }}
<td>{{ stock.notes }}</td> <td>{{ stock.notes }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table
<div class='container-fluid'>
<a href="{% url 'stock-item-create' %}?part={{ part.id }}">
<button class='btn btn-success'>Add new Stock Item</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -4,17 +4,23 @@
{% include 'part/tabs.html' with tab='suppliers' %} {% include 'part/tabs.html' with tab='suppliers' %}
{% if part.supplier_parts.all|length > 0 %} <h3>Part Suppliers</h3>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Supplier</th>
<th>SKU</th> <th>SKU</th>
<th>Supplier</th>
<th>MPN</th>
<th>URL</th> <th>URL</th>
</tr> </tr>
{% for spart in part.supplier_parts.all %} {% for spart in part.supplier_parts.all %}
<tr> <tr>
<td><a href="{% url 'supplier-detail' spart.supplier.id %}">{{ spart.supplier.name }}</a></td>
<td><a href="{% url 'supplier-part-detail' spart.id %}">{{ spart.SKU }}</a></td> <td><a href="{% url 'supplier-part-detail' spart.id %}">{{ spart.SKU }}</a></td>
<td><a href="{% url 'supplier-detail' spart.supplier.id %}">{{ spart.supplier.name }}</a></td>
<td>
{% if spart.manufacturer %}{{ spart.manufacturer.name }}{% endif %}
{% if spart.MPN %} | {{ spart.MPN }}{% endif %}
</td>
<td> <td>
{% if spart.URL %} {% if spart.URL %}
<a href="{{ spart.URL }}">{{ spart.URL }}</a> <a href="{{ spart.URL }}">{{ spart.URL }}</a>
@ -23,8 +29,11 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% else %}
There are no suppliers defined for this part, sorry! <div class='container-fluid'>
{% endif %} <a href="{% url 'supplier-part-create' %}?part={{ part.id }}">
<button class="btn btn-success">New Supplier Part</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -1,15 +1,23 @@
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}><a href="{% url 'part-detail' part.id %}">Details</a></li> <li{% ifequal tab 'detail' %} class="active"{% endifequal %}><a href="{% url 'part-detail' part.id %}">Details</a></li>
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}><a href="{% url 'part-bom' part.id %}">BOM <span class="badge">{{ part.bomItemCount }}</span></a></li> {% if part.buildable %}
{% if part.bomItemCount > 0 %} <li{% ifequal tab 'bom' %} class="active"{% endifequal %}><a href="{% url 'part-bom' part.id %}">BOM<span class="badge">{{ part.bom_count }}</span></a></li>
<li{% ifequal tab 'build' %} class "active"{% endifequal %}><a href="#">Build</a></li> <li{% ifequal tab 'build' %} class="active"{% endifequal %}><a href="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.can_build }}</span></a></li>
{% endif %} {% endif %}
{% if part.usedInCount > 0 %} {% if part.used_in_count > 0 %}
<li{% ifequal tab 'used' %} class="active"{% endifequal %}><a href="{% url 'part-used-in' part.id %}">Used In <span class="badge">{{ part.usedInCount }}</span></a></li> <li{% ifequal tab 'used' %} class="active"{% endifequal %}><a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
{% endif %}
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}><a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.available_stock }}</span></a></li>
{% if part.purchaseable %}
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}><a href="{% url 'part-suppliers' part.id %}">Suppliers
<span class="badge">{{ part.supplier_count }}<span>
</a></li>
{% endif %} {% endif %}
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}><a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.stock }}</span></a></li>
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}><a href="{% url 'part-suppliers' part.id %}">Suppliers <span class="badge">{{ part.supplier_parts.all|length }}<span></a></li>
{% if part.trackable %} {% if part.trackable %}
<li{% ifequal tab 'track' %} class="active"{% endifequal %}><a href="{% url 'part-track' part.id %}">Tracking <span class="badge">{{ part.serials.all|length }}</span></a></li> <li{% ifequal tab 'track' %} class="active"{% endifequal %}><a href="{% url 'part-track' part.id %}">Tracking
{% if parts.serials.all|length > 0 %}
<span class="badge">{{ part.serials.all|length }}</span>
{% endif %}
</a></li>
{% endif %} {% endif %}
</ul> </ul>

View File

@ -11,12 +11,18 @@ Part tracking for {{ part.name }}
<th>Serial</th> <th>Serial</th>
<th>Status</th> <th>Status</th>
</tr> </tr>
{% for track in part.serials.all %} {% for track in part.tracked_parts.all %}
<tr> <tr>
<td>{{ track.serial }}</td> <td><a href="{% url 'track-detail' track.id %}">{{ track.serial }}</a></td>
<td>{{ track.status }}</td> <td>{{ track.get_status_display }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<div class='container-fluid'>
<a href="{% url 'track-create' %}?part={{ part.id }}">
<button class="btn btn-success">New Tracked Part</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -4,7 +4,7 @@
{% include 'part/tabs.html' with tab='used' %} {% include 'part/tabs.html' with tab='used' %}
This part is used to make the following parts: <h3>Used In</h3>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
@ -13,7 +13,7 @@ This part is used to make the following parts:
</tr> </tr>
{% for item in part.used_in.all %} {% for item in part.used_in.all %}
<tr> <tr>
<td><a href="{% url 'part-detail' item.part.id %}">{{ item.part.name }}</a></td> <td><a href="{% url 'part-bom' item.part.id %}">{{ item.part.name }}</a></td>
<td>{{ item.part.description }}</td> <td>{{ item.part.description }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -41,12 +41,12 @@ part_detail_urls = [
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'),
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
# Any other URLs go to the part detail page # Any other URLs go to the part detail page
#url(r'^.*$', views.detail, name='part-detail'),
url(r'^.*$', views.PartDetail.as_view(), name='part-detail'), url(r'^.*$', views.PartDetail.as_view(), name='part-detail'),
] ]
@ -57,6 +57,13 @@ part_category_urls = [
url('^.*$', views.CategoryDetail.as_view(), name='category-detail'), url('^.*$', views.CategoryDetail.as_view(), name='category-detail'),
] ]
part_bom_urls = [
url(r'^edit/?', views.BomItemEdit.as_view(), name='bom-item-edit'),
url('^delete/?', views.BomItemDelete.as_view(), name='bom-item-delete'),
url(r'^.*$', views.BomItemDetail.as_view(), name='bom-item-detail'),
]
# URL list for part web interface # URL list for part web interface
part_urls = [ part_urls = [
@ -66,20 +73,23 @@ part_urls = [
# Create a new part # Create a new part
url(r'^new/?', views.PartCreate.as_view(), name='part-create'), url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
# Individual # Create a new BOM item
url(r'^bom/new/?', views.BomItemCreate.as_view(), name='bom-item-create'),
# Individual part
url(r'^(?P<pk>\d+)/', include(part_detail_urls)), url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
# Part category # Part category
url(r'^category/(?P<pk>\d+)/', include(part_category_urls)), url(r'^category/(?P<pk>\d+)/', include(part_category_urls)),
url(r'^bom/(?P<pk>\d+)/', include(part_bom_urls)),
# Top level part list (display top level parts and categories) # Top level part list (display top level parts and categories)
url('', views.PartIndex.as_view(), name='part-index'), url('', views.PartIndex.as_view(), name='part-index'),
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='part-index'), url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='part-index'),
] ]
""" """
part_param_urls = [ part_param_urls = [
# Detail of a single part parameter # Detail of a single part parameter
@ -99,5 +109,3 @@ part_param_template_urls = [
url(r'^$', views.PartTemplateList.as_view()) url(r'^$', views.PartTemplateList.as_view())
] ]
""" """

View File

@ -1,14 +1,13 @@
from InvenTree.models import FilterChildren
from .models import PartCategory, Part
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView, DeleteView, CreateView from django.views.generic.edit import UpdateView, DeleteView, CreateView
from .forms import EditPartForm, EditCategoryForm from .forms import EditPartForm, EditCategoryForm, EditBomItemForm
from .models import PartCategory, Part, BomItem
class PartIndex(ListView): class PartIndex(ListView):
model = Part model = Part
@ -113,7 +112,7 @@ class CategoryDelete(DeleteView):
model = PartCategory model = PartCategory
template_name = 'part/category_delete.html' template_name = 'part/category_delete.html'
context_object_name = 'category' context_object_name = 'category'
success_url ='/part/' success_url = '/part/'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if 'confirm' in request.POST: if 'confirm' in request.POST:
@ -146,3 +145,47 @@ class CategoryCreate(CreateView):
initials['parent'] = get_object_or_404(PartCategory, pk=parent_id) initials['parent'] = get_object_or_404(PartCategory, pk=parent_id)
return initials return initials
class BomItemDetail(DetailView):
context_object_name = 'item'
queryset = BomItem.objects.all()
template_name = 'part/bom-detail.html'
class BomItemCreate(CreateView):
model = BomItem
form_class = EditBomItemForm
template_name = 'part/bom-create.html'
def get_initial(self):
# Look for initial values
initials = super(BomItemCreate, self).get_initial().copy()
# Parent part for this item?
parent_id = self.request.GET.get('parent', None)
if parent_id:
initials['part'] = get_object_or_404(Part, pk=parent_id)
return initials
class BomItemEdit(UpdateView):
model = BomItem
form_class = EditBomItemForm
template_name = 'part/bom-edit.html'
class BomItemDelete(DeleteView):
model = BomItem
template_name = 'part/bom-delete.html'
context_object_name = 'item'
success_url = '/part'
def post(self, request, *args, **kwargs):
if 'confirm' in request.POST:
return super(BomItemDelete, self).post(request, *args, **kwargs)
else:
return HttpResponseRedirect(self.get_object().get_absolute_url())

View File

@ -7,6 +7,7 @@
background-color: #777; background-color: #777;
color: #fff; color: #fff;
border-radius: 5px; border-radius: 5px;
margin-left: 10px;
} }
.part-thumb { .part-thumb {

View File

@ -2,6 +2,7 @@ from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin from simple_history.admin import SimpleHistoryAdmin
from .models import StockLocation, StockItem from .models import StockLocation, StockItem
from .models import StockItemTracking
class LocationAdmin(admin.ModelAdmin): class LocationAdmin(admin.ModelAdmin):
@ -12,5 +13,10 @@ class StockItemAdmin(SimpleHistoryAdmin):
list_display = ('part', 'quantity', 'location', 'status', 'updated') list_display = ('part', 'quantity', 'location', 'status', 'updated')
class StockTrackingAdmin(admin.ModelAdmin):
list_display = ('item', 'date', 'title')
admin.site.register(StockLocation, LocationAdmin) admin.site.register(StockLocation, LocationAdmin)
admin.site.register(StockItem, StockItemAdmin) admin.site.register(StockItem, StockItemAdmin)
admin.site.register(StockItemTracking, StockTrackingAdmin)

View File

@ -3,9 +3,6 @@ from django_filters import NumberFilter
from rest_framework import generics, permissions, response from rest_framework import generics, permissions, response
# from InvenTree.models import FilterChildren # from InvenTree.models import FilterChildren
from .models import StockLocation, StockItem from .models import StockLocation, StockItem
from .serializers import StockItemSerializer, StockQuantitySerializer from .serializers import StockItemSerializer, StockQuantitySerializer

View File

@ -14,7 +14,6 @@ class EditStockLocationForm(forms.ModelForm):
self.helper.form_id = 'id-edit-part-form' self.helper.form_id = 'id-edit-part-form'
self.helper.form_class = 'blueForms' self.helper.form_class = 'blueForms'
self.helper.form_method = 'post' self.helper.form_method = 'post'
#self.helper.form_action = 'submit'
self.helper.add_input(Submit('submit', 'Submit')) self.helper.add_input(Submit('submit', 'Submit'))
@ -36,16 +35,20 @@ class EditStockItemForm(forms.ModelForm):
self.helper.form_id = 'id-edit-part-form' self.helper.form_id = 'id-edit-part-form'
self.helper.form_class = 'blueForms' self.helper.form_class = 'blueForms'
self.helper.form_method = 'post' self.helper.form_method = 'post'
#self.helper.form_action = 'submit'
self.helper.add_input(Submit('submit', 'Submit')) self.helper.add_input(Submit('submit', 'Submit'))
class Meta: class Meta:
model = StockItem model = StockItem
fields = [ fields = [
'part', 'part',
'supplier_part', 'supplier_part',
'location', 'location',
'belongs_to',
'serial',
'batch',
'quantity', 'quantity',
'status',
'customer',
'URL',
] ]

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 08:53
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0006_auto_20180415_1011'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('stock', '0006_auto_20180415_0302'),
]
operations = [
migrations.CreateModel(
name='StockItemTracking',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(auto_now_add=True)),
('title', models.CharField(max_length=250)),
('description', models.CharField(blank=True, max_length=1024)),
],
),
migrations.RemoveField(
model_name='historicalstockitem',
name='history_user',
),
migrations.RemoveField(
model_name='historicalstockitem',
name='location',
),
migrations.RemoveField(
model_name='historicalstockitem',
name='part',
),
migrations.RemoveField(
model_name='historicalstockitem',
name='stocktake_user',
),
migrations.RemoveField(
model_name='historicalstockitem',
name='supplier_part',
),
migrations.AddField(
model_name='stockitem',
name='batch',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='stockitem',
name='belongs_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owned_parts', to='stock.StockItem'),
),
migrations.AddField(
model_name='stockitem',
name='customer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stockitems', to='supplier.Customer'),
),
migrations.AddField(
model_name='stockitem',
name='serial',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.DeleteModel(
name='HistoricalStockItem',
),
migrations.AddField(
model_name='stockitemtracking',
name='item',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracking_info', to='stock.StockItem'),
),
migrations.AddField(
model_name='stockitemtracking',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 11:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0007_auto_20180416_0853'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='URL',
field=models.URLField(blank=True, max_length=125),
),
]

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 12:53
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0019_auto_20180416_1249'),
('stock', '0008_stockitem_url'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='batch',
field=models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100),
),
migrations.AlterField(
model_name='stockitem',
name='belongs_to',
field=models.ForeignKey(blank=True, help_text='Is this item installed in another item?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owned_parts', to='stock.StockItem'),
),
migrations.AlterField(
model_name='stockitem',
name='customer',
field=models.ForeignKey(blank=True, help_text='Item assigned to customer?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stockitems', to='supplier.Customer'),
),
migrations.AlterField(
model_name='stockitem',
name='location',
field=models.ForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='items', to='stock.StockLocation'),
),
migrations.AlterField(
model_name='stockitem',
name='serial',
field=models.PositiveIntegerField(blank=True, help_text='Serial number for this item', null=True),
),
migrations.AlterUniqueTogether(
name='stockitem',
unique_together=set([('part', 'serial')]),
),
]

View File

@ -3,9 +3,9 @@ from django.utils.translation import ugettext as _
from django.db import models, transaction from django.db import models, transaction
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.contrib.auth.models import User from django.contrib.auth.models import User
from simple_history.models import HistoricalRecords
from supplier.models import SupplierPart from supplier.models import SupplierPart
from supplier.models import Customer
from part.models import Part from part.models import Part
from InvenTree.models import InvenTreeTree from InvenTree.models import InvenTreeTree
@ -14,16 +14,23 @@ from datetime import datetime
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
class StockLocation(InvenTreeTree): class StockLocation(InvenTreeTree):
""" Organization tree for StockItem objects """ Organization tree for StockItem objects
A "StockLocation" can be considered a warehouse, or storage location A "StockLocation" can be considered a warehouse, or storage location
Stock locations can be heirarchical as required Stock locations can be heirarchical as required
""" """
def get_absolute_url(self):
return '/stock/location/{id}/'.format(id=self.id)
@property @property
def items(self): def items(self):
stock_list = self.stockitem_set.all() return self.stockitem_set.all()
return stock_list
@property
def has_items(self):
return self.items.count() > 0
@receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log') @receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log')
@ -31,19 +38,72 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
# Update each part in the stock location # Update each part in the stock location
for item in instance.items.all(): for item in instance.items.all():
# If this location has a parent, move the child stock items to the parent
if instance.parent:
item.location = instance.parent item.location = instance.parent
item.save() item.save()
# No parent location? Delete the stock items
else:
item.delete()
# Update each child category
for child in instance.children.all():
child.parent = instance.parent
child.save()
class StockItem(models.Model): class StockItem(models.Model):
"""
A 'StockItem' instance represents a quantity of physical instances of a part.
It may exist in a StockLocation, or as part of a sub-assembly installed into another StockItem
StockItems may be tracked using batch or serial numbers.
If a serial number is assigned, then StockItem cannot have a quantity other than 1
"""
def get_absolute_url(self):
return '/stock/item/{id}/'.format(id=self.id)
class Meta:
unique_together = [
('part', 'serial'),
]
# The 'master' copy of the part of which this stock item is an instance
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='locations') part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='locations')
# The 'supplier part' used in this instance. May be null if no supplier parts are defined the master part
supplier_part = models.ForeignKey(SupplierPart, blank=True, null=True, on_delete=models.SET_NULL) supplier_part = models.ForeignKey(SupplierPart, blank=True, null=True, on_delete=models.SET_NULL)
# Where the part is stored. If the part has been used to build another stock item, the location may not make sense
location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING, location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,
related_name='items', blank=True, null=True) related_name='items', blank=True, null=True,
help_text='Where is this stock item located?')
# If this StockItem belongs to another StockItem (e.g. as part of a sub-assembly)
belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING,
related_name='owned_parts', blank=True, null=True,
help_text='Is this item installed in another item?')
# The StockItem may be assigned to a particular customer
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL,
related_name='stockitems', blank=True, null=True,
help_text='Item assigned to customer?')
# Optional serial number
serial = models.PositiveIntegerField(blank=True, null=True,
help_text='Serial number for this item')
# Optional URL to link to external resource
URL = models.URLField(max_length=125, blank=True)
# Optional batch information
batch = models.CharField(max_length=100, blank=True,
help_text='Batch code for this stock item')
# Quantity of this stock item. Value may be overridden by other settings
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)]) quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)])
# Last time this item was updated (set automagically)
updated = models.DateField(auto_now=True) updated = models.DateField(auto_now=True)
# last time the stock was checked / counted # last time the stock was checked / counted
@ -78,8 +138,9 @@ class StockItem(models.Model):
infinite = models.BooleanField(default=False) infinite = models.BooleanField(default=False)
# History of this item @property
history = HistoricalRecords() def has_tracking_info(self):
return self.tracking_info.count() > 0
@transaction.atomic @transaction.atomic
def stocktake(self, count, user): def stocktake(self, count, user):
@ -128,3 +189,34 @@ class StockItem(models.Model):
n=self.quantity, n=self.quantity,
part=self.part.name, part=self.part.name,
loc=self.location.name) loc=self.location.name)
@property
def is_trackable(self):
return self.part.trackable
class StockItemTracking(models.Model):
""" Stock tracking entry
"""
# Stock item
item = models.ForeignKey(StockItem, on_delete=models.CASCADE,
related_name='tracking_info')
# Date this entry was created (cannot be edited)
date = models.DateField(auto_now_add=True, editable=False)
# Short-form title for this tracking entry
title = models.CharField(max_length=250)
# Optional longer description
description = models.CharField(max_length=1024, blank=True)
# Which user created this tracking entry?
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
# TODO
# image = models.ImageField(upload_to=func, max_length=255, null=True, blank=True)
# TODO
# file = models.FileField()

View File

@ -5,6 +5,7 @@
{% include "stock/loc_link.html" with location=None %} {% include "stock/loc_link.html" with location=None %}
{% if locations.all|length > 0 %} {% if locations.all|length > 0 %}
<h4>Storage Locations</h4>
{% include "stock/location_list.html" with locations=locations %} {% include "stock/location_list.html" with locations=locations %}
{% endif %} {% endif %}
@ -12,4 +13,10 @@
{% include "stock/stock_table.html" with items=items %} {% include "stock/stock_table.html" with items=items %}
{% endif %} {% endif %}
<div class='container-fluid'>
<a href="{% url 'stock-location-create' %}">
<button class="btn btn-success">New Stock Location</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -4,19 +4,52 @@
{% include "stock/loc_link.html" with location=item.location %} {% include "stock/loc_link.html" with location=item.location %}
<h3>Stock entry details</h3>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<td>Part</td> <td>Part</td>
<td><a href="{% url 'part-detail' item.part.id %}">{{ item.part.name }}</td> <td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.name }}</td>
</tr> </tr>
{% if item.belongs_to %}
<tr>
<td>Belongs To</td>
<td><a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a></td>
</tr>
{% elif item.location %}
<tr> <tr>
<td>Location</td> <td>Location</td>
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td> <td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
</tr> </tr>
{% endif %}
{% if item.serial %}
<tr>
<td>Serial</td>
<td>{{ item.serial }}</td>
</tr>
{% endif %}
{% if item.batch %}
<tr>
<td>Batch</td>
<td>{{ item.batch }}</td>
</tr>
{% endif %}
{% if item.customer %}
<tr>
<td>Customer</td>
<td>{{ item.customer.name }}</td>
</tr>
{% endif %}
<tr> <tr>
<td>Quantity</td> <td>Quantity</td>
<td>{{ item.quantity }}</td> <td>{{ item.quantity }}</td>
</tr> </tr>
{% if item.URL %}
<tr>
<td>URL</td>
<td><a href="{{ item.URL }}">{{ item.URL }}</a></td>
</tr>
{% endif %}
{% if item.supplier_part %} {% if item.supplier_part %}
<tr> <tr>
<td>Supplier Part</td> <td>Supplier Part</td>
@ -35,7 +68,7 @@
{% endif %} {% endif %}
<tr> <tr>
<td>Status</td> <td>Status</td>
<td>{{ item.status }}</td> <td>{{ item.get_status_display }}</td>
</tr> </tr>
{% if item.notes %} {% if item.notes %}
<tr> <tr>
@ -45,4 +78,28 @@
{% endif %} {% endif %}
</table> </table>
{% if item.has_tracking_info %}
<h3>Stock Tracking</h3>
<ul class='list-group'>
{% for track in item.tracking_info.all %}
<li class='list-group-item'>
<b>{{ track.title }}</b>
{% if track.description %}
<br><br>{{ track.description }}</i>
{% endif %}
<span class='badge'>{{ track.date }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
<div class='container-fluid'>
<a href="{% url 'stock-item-edit' item.id %}">
<button class='btn btn-info'>Edit Stock Item</button>
</a>
<a href="{% url 'stock-item-delete' item.id %}">
<button class='btn btn-danger'>Delete Stock Item</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Create a new stock item
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "delete_obj.html" %}
{% block del_title %}
Are you sure you want to delete this stock item?
{% endblock %}
{% block del_body %}
This will remove <b>{{ item.quantity }}</b> units of <b>'{{ item.part.name }}'</b> from stock.
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Edit stock item for part '{{ item.part.name }}'
{% endblock %}

View File

@ -1,7 +1,7 @@
<div class="navigation"> <div class="navigation">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item{% if location is None %} active" aria-current="page{% endif %}"><a href="/stock/">Parts</a></li> <li class="breadcrumb-item{% if location is None %} active" aria-current="page{% endif %}"><a href="/stock/">Stock</a></li>
{% if location %} {% if location %}
{% for path_item in location.parentpath %} {% for path_item in location.parentpath %}
<li class='breadcrumb-item'><a href="{% url 'stock-location-detail' path_item.id %}">{{ path_item.name }}</a></li> <li class='breadcrumb-item'><a href="{% url 'stock-location-detail' path_item.id %}">{{ path_item.name }}</a></li>

View File

@ -4,14 +4,35 @@
{% include "stock/loc_link.html" with location=location %} {% include "stock/loc_link.html" with location=location %}
<p> <h3>{{ location.name }}</h3>
<b>{{ location.name }}</b><br> <p>{{ location.description }}</p>
<i>{{ location.description }}</i>
</p>
{% if location.has_children %}
<h4>Sub Locations</h4>
{% include "stock/location_list.html" with locations=location.children %} {% include "stock/location_list.html" with locations=location.children %}
{% endif %}
{% if location.has_items %}
<h4>Stock Items</h4>
{% include "stock/stock_table.html" with items=location.items %} {% include "stock/stock_table.html" with items=location.items %}
{% endif %}
<div class='container-fluid'>
<a href="{% url 'stock-location-create' %}?location={{ location.id }}">
<button class='btn btn-success'>New Stock Location</button>
</a>
<a href="{% url 'stock-item-create' %}?location={{ location.id }}">
<button class='btn btn-success'>New Stock Item</button>
</a>
<a href="{% url 'stock-location-edit' location.id %}">
<button class="btn btn-info">Edit Location</button>
</a>
<a href="{% url 'stock-location-delete' location.id %}">
<button class="btn btn-danger">Delete Location</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Create a new stock location
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends 'delete_obj.html' %}
{% block del_title %}
Are you sure you want to delete stock location '{{ location.name }}'?
{% endblock %}
{% block del_body %}
{% if location.children.all|length > 0 %}
<p>This location contains {{ location.children.all|length }} child locations.<br>
If this location is deleted, these child locations will be moved to
{% if location.parent %}
the '{{ location.parent.name }}' location.
{% else %}
the top level 'Stock' category.
{% endif %}
</p>
<ul class='list-group'>
{% for loc in location.children.all %}
<li class='list-group-item'><b>{{ loc.name }}</b> - <i>{{ loc.description}}</i></li>
{% endfor %}
</ul>
{% endif %}
{% if location.items.all|length > 0 %}
<p>This location contains {{ location.items.all|length }} stock items.<br>
{% if location.parent %}
If this location is deleted, these items will be moved to the '{{ location.parent.name }}' location.
{% else %}
If this location is deleted, these items will be deleted!
{% endif %}
</p>
<ul class='list-group'>
{% for item in location.items.all %}
<li class='list-group-item'><b>{{ item.part.name }}</b> - <i>{{ item.part.description }}</i><span class='badge'>{{ item.quantity }}</span></li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Edit stock location '{{ location.name }}'
{% endblock %}

View File

@ -1,4 +1,3 @@
Storage locations:
<ul class="list-group"> <ul class="list-group">
{% for child in locations.all %} {% for child in locations.all %}
<li class="list-group-item"><a href="{% url 'stock-location-detail' child.id %}">{{ child.name }}</a></li> <li class="list-group-item"><a href="{% url 'stock-location-detail' child.id %}">{{ child.name }}</a></li>

View File

@ -8,7 +8,7 @@
</tr> </tr>
{% for item in items.all %} {% for item in items.all %}
<tr> <tr>
<td><a href="{% url 'part-detail' item.part.id %}">{{ item.part.name }}</a></td> <td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.name }}</a></td>
<td>{{ item.quantity }}</td> <td>{{ item.quantity }}</td>
<td>{{ item.status }}</td> <td>{{ item.status }}</td>
<td>{{ item.stocktake_date }}</td> <td>{{ item.stocktake_date }}</td>

View File

@ -1,5 +1,4 @@
from django.conf.urls import url, include from django.conf.urls import url, include
from django.views.generic.base import RedirectView
from . import views from . import views
from . import api from . import api
@ -52,10 +51,10 @@ stock_urls = [
url(r'^location/new/', views.StockLocationCreate.as_view(), name='stock-location-create'), url(r'^location/new/', views.StockLocationCreate.as_view(), name='stock-location-create'),
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
# Individual stock items # Individual stock items
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)), url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
url(r'^item/new/', views.StockItemCreate.as_view(), name='stock-item-create'),
url(r'^.*$', views.StockIndex.as_view(), name='stock-index'), url(r'^.*$', views.StockIndex.as_view(), name='stock-index'),
] ]

View File

@ -1,16 +1,16 @@
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView, DeleteView, CreateView from django.views.generic.edit import UpdateView, DeleteView, CreateView
from part.models import Part
from .models import StockItem, StockLocation from .models import StockItem, StockLocation
from .forms import EditStockLocationForm from .forms import EditStockLocationForm
from .forms import EditStockItemForm from .forms import EditStockItemForm
class StockIndex(ListView): class StockIndex(ListView):
model = StockItem model = StockItem
template_name = 'stock/index.html' template_name = 'stock/index.html'
@ -29,6 +29,7 @@ class StockIndex(ListView):
return context return context
class StockLocationDetail(DetailView): class StockLocationDetail(DetailView):
context_object_name = 'location' context_object_name = 'location'
template_name = 'stock/location.html' template_name = 'stock/location.html'
@ -46,35 +47,60 @@ class StockItemDetail(DetailView):
class StockLocationEdit(UpdateView): class StockLocationEdit(UpdateView):
model = StockLocation model = StockLocation
form_class = EditStockLocationForm form_class = EditStockLocationForm
template_name = '/stock/location-edit.html' template_name = 'stock/location_edit.html'
context_object_name = 'location' context_object_name = 'location'
class StockItemEdit(UpdateView): class StockItemEdit(UpdateView):
model = StockItem model = StockItem
form_class = EditStockItemForm form_class = EditStockItemForm
template_name = '/stock/item-edit.html' template_name = 'stock/item_edit.html'
context_object_name = 'item' context_object_name = 'item'
class StockLocationCreate(CreateView): class StockLocationCreate(CreateView):
model = StockLocation model = StockLocation
form_class = EditStockLocationForm form_class = EditStockLocationForm
template_name = '/stock/location-create.html' template_name = 'stock/location_create.html'
context_object_name = 'location' context_object_name = 'location'
def get_initial(self):
initials = super(StockLocationCreate, self).get_initial().copy()
loc_id = self.request.GET.get('location', None)
if loc_id:
initials['parent'] = get_object_or_404(StockLocation, pk=loc_id)
return initials
class StockItemCreate(CreateView): class StockItemCreate(CreateView):
model = StockItem model = StockItem
form_class = EditStockItemForm form_class = EditStockItemForm
template_name = '/stock/item-create.html' template_name = 'stock/item_create.html'
context_object_name = 'item' context_object_name = 'item'
def get_initial(self):
initials = super(StockItemCreate, self).get_initial().copy()
part_id = self.request.GET.get('part', None)
loc_id = self.request.GET.get('location', None)
if part_id:
initials['part'] = get_object_or_404(Part, pk=part_id)
if loc_id:
initials['location'] = get_object_or_404(StockLocation, pk=loc_id)
return initials
class StockLocationDelete(DeleteView): class StockLocationDelete(DeleteView):
model = StockLocation model = StockLocation
success_url = '/stock/' success_url = '/stock'
template_name = '/stock/location-delete.html' template_name = 'stock/location_delete.html'
context_object_name = 'location'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if 'confirm' in request.POST: if 'confirm' in request.POST:
@ -84,9 +110,10 @@ class StockLocationDelete(DeleteView):
class StockItemDelete(DeleteView): class StockItemDelete(DeleteView):
model = StockLocation model = StockItem
success_url = '/stock/' success_url = '/stock/'
template_name = '/stock/item-delete.html' template_name = 'stock/item_delete.html'
context_object_name = 'item'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if 'confirm' in request.POST: if 'confirm' in request.POST:

View File

@ -1,13 +1,18 @@
from django.contrib import admin from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import Supplier, SupplierPart, Customer, Manufacturer from .models import Supplier, SupplierPart, Customer, Manufacturer
class CompanyAdmin(admin.ModelAdmin): class CompanyAdmin(ImportExportModelAdmin):
list_display = ('name', 'website', 'contact') list_display = ('name', 'website', 'contact')
class SupplierPartAdmin(ImportExportModelAdmin):
list_display = ('part', 'supplier', 'SKU')
admin.site.register(Customer, CompanyAdmin) admin.site.register(Customer, CompanyAdmin)
admin.site.register(Supplier, CompanyAdmin) admin.site.register(Supplier, CompanyAdmin)
admin.site.register(Manufacturer, CompanyAdmin) admin.site.register(Manufacturer, CompanyAdmin)
admin.site.register(SupplierPart) admin.site.register(SupplierPart, SupplierPartAdmin)

View File

@ -14,7 +14,6 @@ class EditSupplierForm(forms.ModelForm):
self.helper.form_id = 'id-edit-part-form' self.helper.form_id = 'id-edit-part-form'
self.helper.form_class = 'blueForms' self.helper.form_class = 'blueForms'
self.helper.form_method = 'post' self.helper.form_method = 'post'
#self.helper.form_action = 'submit'
self.helper.add_input(Submit('submit', 'Submit')) self.helper.add_input(Submit('submit', 'Submit'))
@ -40,7 +39,6 @@ class EditSupplierPartForm(forms.ModelForm):
self.helper.form_id = 'id-edit-part-form' self.helper.form_id = 'id-edit-part-form'
self.helper.form_class = 'blueForms' self.helper.form_class = 'blueForms'
self.helper.form_method = 'post' self.helper.form_method = 'post'
#self.helper.form_action = 'submit'
self.helper.add_input(Submit('submit', 'Submit')) self.helper.add_input(Submit('submit', 'Submit'))

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 12:53
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0006_auto_20180415_1011'),
]
operations = [
migrations.AlterField(
model_name='supplierpart',
name='MPN',
field=models.CharField(blank=True, help_text='Manufacturer part number', max_length=100),
),
migrations.AlterField(
model_name='supplierpart',
name='SKU',
field=models.CharField(help_text='Supplier stock keeping unit', max_length=100),
),
migrations.AlterField(
model_name='supplierpart',
name='manufacturer',
field=models.ForeignKey(blank=True, help_text='Manufacturer', null=True, on_delete=django.db.models.deletion.SET_NULL, to='supplier.Manufacturer'),
),
migrations.AlterField(
model_name='supplierpart',
name='part',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'),
),
]

View File

@ -14,6 +14,14 @@ class Supplier(Company):
def get_absolute_url(self): def get_absolute_url(self):
return "/supplier/{id}/".format(id=self.id) return "/supplier/{id}/".format(id=self.id)
@property
def part_count(self):
return self.parts.count()
@property
def has_parts(self):
return self.part_count > 0
class Manufacturer(Company): class Manufacturer(Company):
""" Represents a manfufacturer """ Represents a manfufacturer
@ -42,19 +50,20 @@ class SupplierPart(models.Model):
# Link to an actual part # Link to an actual part
# The part will have a field 'supplier_parts' which links to the supplier part options # 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.SET_NULL, part = models.ForeignKey(Part, on_delete=models.CASCADE,
related_name='supplier_parts') related_name='supplier_parts')
supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE, supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE,
related_name = 'parts') related_name='parts')
SKU = models.CharField(max_length=100, help_text='Supplier stock keeping unit')
SKU = models.CharField(max_length=100) manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.SET_NULL, help_text='Manufacturer')
manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.SET_NULL) MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
MPN = models.CharField(max_length=100, blank=True)
URL = models.URLField(blank=True) URL = models.URLField(blank=True)
description = models.CharField(max_length=250, blank=True) description = models.CharField(max_length=250, blank=True)
# Default price for a single unit # Default price for a single unit

View File

@ -5,12 +5,12 @@ Are you sure you want to delete supplier '{{ supplier.name }}'?
{% endblock %} {% endblock %}
{% block del_body %} {% block del_body %}
{% if supplier.parts.all|length > 0 %} {% if supplier.part_count > 0 %}
<p>There are {{ supplier.parts.all|length }} parts sourced from this supplier.<br> <p>There are {{ supplier.part_count }} parts sourced from this supplier.<br>
If this supplier is deleted, these child categories will also be deleted.</p> If this supplier is deleted, these supplier part entries will also be deleted.</p>
<ul class='list-group'> <ul class='list-group'>
{% for part in supplier.parts.all %} {% for part in supplier.parts.all %}
<li class='list-group-item'><b>{{ part.SKU }}</b><i>Part - {{ part.part.name }}</i></li> <li class='list-group-item'><b>{{ part.SKU }}</b><br><i>Part - {{ part.part.name }}</i></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View File

@ -6,12 +6,7 @@
<div class="col-sm-6"> <div class="col-sm-6">
<h3>{{ supplier.name }}</h3> <h3>{{ supplier.name }}</h3>
<p>{{ supplier.description }}</p> <p>{{ supplier.description }}</p>
<p><a href="{% url 'supplier-edit' supplier.id %}"> <p>{{ supplier.notes }}</p>
<button class="btn btn-info">Edit supplier details</button>
</a></p>
<p><a href="{% url 'supplier-delete' supplier.id %}">
<button class="btn btn-danger">Delete supplier</button>
</a></p>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<table class="table"> <table class="table">
@ -48,21 +43,24 @@
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>SKU</th> <th>SKU</th>
<th>Part</th> <th>Description</th>
<th>Manufacturer</th> <th>Parent Part</th>
<th>MPN</th> <th>MPN</th>
<th>URL</th> <th>URL</th>
</tr> </tr>
{% for part in supplier.parts.all %} {% for part in supplier.parts.all %}
<tr> <tr>
<td><a href="{% url 'supplier-part-detail' part.id %}">{{ part.SKU }}</a></td> <td><a href="{% url 'supplier-part-detail' part.id %}">{{ part.SKU }}</a></td>
<td>{{ part.description }}</td>
<td> <td>
{% if part.part %} {% if part.part %}
<a href="{% url 'part-detail' part.part.id %}">{{ part.part.name }}</a> <a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.name }}</a>
{% endif %} {% endif %}
</td> </td>
<td>Manufacturer name goes here</td> <td>
<td>MPN goes here</td> {% if part.manufacturer %}{{ part.manufacturer.name }}{% endif %}
{% if part.MPN %} | {{ part.MPN }}{% endif %}
</td>
<td>{{ part.URL }}</td> <td>{{ part.URL }}</td>
{% endfor %} {% endfor %}
</table> </table>
@ -71,5 +69,12 @@
<a href="{% url 'supplier-part-create' %}?supplier={{ supplier.id }}"> <a href="{% url 'supplier-part-create' %}?supplier={{ supplier.id }}">
<button class="btn btn-success">New Supplier Part</button> <button class="btn btn-success">New Supplier Part</button>
</a> </a>
<a href="{% url 'supplier-edit' supplier.id %}">
<button class="btn btn-info">Edit supplier details</button>
</a>
<a href="{% url 'supplier-delete' supplier.id %}">
<button class="btn btn-danger">Delete supplier</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -8,10 +8,10 @@
<tr><td>SKU</td><td>{{ part.SKU }}</tr></tr> <tr><td>SKU</td><td>{{ part.SKU }}</tr></tr>
<tr><td>Supplier</td><td><a href="{% url 'supplier-detail' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr> <tr><td>Supplier</td><td><a href="{% url 'supplier-detail' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<tr> <tr>
<td>Part</td> <td>Parent Part</td>
<td> <td>
{% if part.part %} {% if part.part %}
<a href="{% url 'part-detail' part.part.id %}">{{ part.part.name }}</a> <a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.name }}</a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@ -22,19 +22,21 @@
<tr><td>Description</td><td>{{ part.description }}</td></tr> <tr><td>Description</td><td>{{ part.description }}</td></tr>
{% endif %} {% endif %}
{% if part.manufacturer %} {% if part.manufacturer %}
<tr><td>Manufacturer</td><td>TODO</td></tr> <tr><td>Manufacturer</td><td>{% if part.manufacturer %}{{ part.manufacturer.name }}{% endif %}</td></tr>
<tr><td>MPN</td><td>TODO</td></tr> <tr><td>MPN</td><td>{{ part.MPN }}</td></tr>
{% endif %} {% endif %}
</table> </table>
<br> <br>
<p><a href="{% url 'supplier-part-edit' part.id %}"> <div class='container-fluid'>
<button class="btn btn-info">Edit supplier details</button> <a href="{% url 'supplier-part-edit' part.id %}">
</a></p> <button class="btn btn-info">Edit supplier part</button>
<p><a href="{% url 'supplier-part-delete' part.id %}"> </a>
<a href="{% url 'supplier-part-delete' part.id %}">
<button class="btn btn-danger">Delete supplier part</button> <button class="btn btn-danger">Delete supplier part</button>
</a></p> </a>
</div>
{% endblock %} {% endblock %}

View File

@ -2,7 +2,6 @@ from django.conf.urls import url, include
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from . import views from . import views
from . import api
""" """
cust_urls = [ cust_urls = [

View File

@ -1,15 +1,16 @@
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView, DeleteView, CreateView from django.views.generic.edit import UpdateView, DeleteView, CreateView
from part.models import Part
from .models import Supplier, SupplierPart from .models import Supplier, SupplierPart
from .forms import EditSupplierForm from .forms import EditSupplierForm
from .forms import EditSupplierPartForm from .forms import EditSupplierPartForm
class SupplierIndex(ListView): class SupplierIndex(ListView):
model = Supplier model = Supplier
template_name = 'supplier/index.html' template_name = 'supplier/index.html'
@ -76,9 +77,16 @@ class SupplierPartCreate(CreateView):
initials = super(SupplierPartCreate, self).get_initial().copy() initials = super(SupplierPartCreate, self).get_initial().copy()
supplier_id = self.request.GET.get('supplier', None) supplier_id = self.request.GET.get('supplier', None)
part_id = self.request.GET.get('part', None)
if supplier_id: if supplier_id:
initials['supplier'] = get_object_or_404(Supplier, pk=supplier_id) initials['supplier'] = get_object_or_404(Supplier, pk=supplier_id)
# TODO
# self.fields['supplier'].disabled = True
if part_id:
initials['part'] = get_object_or_404(Part, pk=part_id)
# TODO
# self.fields['part'].disabled = True
return initials return initials

View File

@ -1,16 +0,0 @@
from django.contrib import admin
from .models import UniquePart, PartTrackingInfo
class UniquePartAdmin(admin.ModelAdmin):
list_display = ('part', 'serial', 'status', 'creation_date')
class PartTrackingAdmin(admin.ModelAdmin):
list_display = ('part', 'date', 'title')
admin.site.register(UniquePart, UniquePartAdmin)
admin.site.register(PartTrackingInfo, PartTrackingAdmin)

View File

@ -1,97 +0,0 @@
from django_filters.rest_framework import FilterSet, DjangoFilterBackend
from django_filters import NumberFilter
from rest_framework import generics, permissions
from .models import UniquePart, PartTrackingInfo
from .serializers import UniquePartSerializer, PartTrackingInfoSerializer
class UniquePartDetail(generics.RetrieveUpdateDestroyAPIView):
"""
get:
Return a single UniquePart
post:
Update a UniquePart
delete:
Remove a UniquePart
"""
queryset = UniquePart.objects.all()
serializer_class = UniquePartSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class UniquePartFilter(FilterSet):
# Filter based on serial number
min_sn = NumberFilter(name='serial', lookup_expr='gte')
max_sn = NumberFilter(name='serial', lookup_expr='lte')
class Meta:
model = UniquePart
fields = ['serial', 'part', 'customer']
class UniquePartList(generics.ListCreateAPIView):
"""
get:
Return a list of all UniqueParts
(with optional query filter)
post:
Create a new UniquePart
"""
queryset = UniquePart.objects.all()
serializer_class = UniquePartSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
filter_backends = (DjangoFilterBackend,)
filter_class = UniquePartFilter
class PartTrackingDetail(generics.RetrieveUpdateDestroyAPIView):
"""
get:
Return a single PartTrackingInfo object
post:
Update a PartTrackingInfo object
delete:
Remove a PartTrackingInfo object
"""
queryset = PartTrackingInfo.objects.all()
serializer_class = PartTrackingInfoSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class PartTrackingFilter(FilterSet):
class Meta:
model = PartTrackingInfo
fields = ['part']
class PartTrackingList(generics.ListCreateAPIView):
"""
get:
Return a list of all PartTrackingInfo objects
(with optional query filter)
post:
Create a new PartTrackingInfo object
"""
queryset = PartTrackingInfo.objects.all()
serializer_class = PartTrackingInfoSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
filter_backends = (DjangoFilterBackend,)
filter_class = PartTrackingFilter

View File

@ -1,7 +0,0 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class TrackConfig(AppConfig):
name = 'track'

View File

@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-12 05:02
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('part', '0001_initial'),
('supplier', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PartTrackingInfo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(auto_now_add=True)),
('notes', models.CharField(max_length=500)),
],
),
migrations.CreateModel(
name='UniquePart',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('creation_date', models.DateField(auto_now_add=True)),
('serial', models.IntegerField()),
('status', models.IntegerField(choices=[(0, 'In progress'), (40, 'Damaged'), (10, 'In stock'), (50, 'Destroyed'), (20, 'Shipped'), (30, 'Returned')], default=0)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='supplier.Customer')),
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='part.Part')),
],
),
migrations.AddField(
model_name='parttrackinginfo',
name='part',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracking_info', to='track.UniquePart'),
),
migrations.AlterUniqueTogether(
name='uniquepart',
unique_together=set([('part', 'serial')]),
),
]

View File

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-13 14:40
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('track', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='uniquepart',
name='part',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='serials', to='part.Part'),
),
migrations.AlterField(
model_name='uniquepart',
name='serial',
field=models.PositiveIntegerField(),
),
]

View File

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-15 01:47
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('track', '0002_auto_20180413_1440'),
]
operations = [
migrations.AddField(
model_name='parttrackinginfo',
name='title',
field=models.CharField(default='tracking information', max_length=250),
preserve_default=False,
),
migrations.AlterField(
model_name='parttrackinginfo',
name='notes',
field=models.CharField(blank=True, max_length=1024),
),
migrations.AlterField(
model_name='uniquepart',
name='status',
field=models.IntegerField(choices=[(0, 'In progress'), (35, 'Repaired'), (40, 'Damaged'), (10, 'In stock'), (50, 'Destroyed'), (20, 'Shipped'), (30, 'Returned')], default=0),
),
]

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-15 01:50
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('track', '0003_auto_20180415_0147'),
]
operations = [
migrations.AddField(
model_name='parttrackinginfo',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,73 +0,0 @@
from __future__ import unicode_literals
from rest_framework.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.db import models
from django.contrib.auth.models import User
from supplier.models import Customer
from part.models import Part
class UniquePart(models.Model):
""" A unique instance of a Part object.
Used for tracking parts based on serial numbers,
and tracking all events in the life of a part
"""
class Meta:
# Cannot have multiple parts with same serial number
unique_together = ('part', 'serial')
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='serials')
creation_date = models.DateField(auto_now_add=True,
editable=False)
serial = models.PositiveIntegerField()
# createdBy = models.ForeignKey(User)
customer = models.ForeignKey(Customer, blank=True, null=True)
# Part status types
PART_IN_PROGRESS = 0
PART_IN_STOCK = 10
PART_SHIPPED = 20
PART_RETURNED = 30
PART_REPAIRED = 35
PART_DAMAGED = 40
PART_DESTROYED = 50
PART_STATUS_CODES = {
PART_IN_PROGRESS: _("In progress"),
PART_IN_STOCK: _("In stock"),
PART_SHIPPED: _("Shipped"),
PART_RETURNED: _("Returned"),
PART_REPAIRED: _("Repaired"),
PART_DAMAGED: _("Damaged"),
PART_DESTROYED: _("Destroyed")
}
status = models.IntegerField(default=PART_IN_PROGRESS, choices=PART_STATUS_CODES.items())
def __str__(self):
return "{pn} - # {sn}".format(pn = self.part.name,
sn = self.serial)
class PartTrackingInfo(models.Model):
""" Single data-point in the life of a UniquePart
Each time something happens to the UniquePart,
a new PartTrackingInfo object should be created.
"""
part = models.ForeignKey(UniquePart, on_delete=models.CASCADE, related_name='tracking_info')
date = models.DateField(auto_now_add=True, editable=False)
title = models.CharField(max_length=250)
notes = models.CharField(max_length=1024, blank=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)

View File

@ -1,23 +0,0 @@
from rest_framework import serializers
from .models import UniquePart, PartTrackingInfo
class UniquePartSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = UniquePart
fields = ['url',
'part',
'creation_date',
'serial',
# 'createdBy',
'customer',
'status']
class PartTrackingInfoSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PartTrackingInfo
fields = '__all__'

View File

@ -1,26 +0,0 @@
{% extends "base.html" %}
{% block content %}
Part: <a href="{% url 'part-detail' part.part.id %}">{{ part.part.name }}</a><br>
Serial number: {{ part.serial }}
{% if part.tracking_info.all|length > 0 %}
<p>Tracking information:</p>
<ul class='list-group'>
{% for info in part.tracking_info.all %}
<li class='list-group-item'>
<div class='panel panel-default'>
<div class='panel-heading'>
{{ info.title }}<span class="badge">{{ info.date }}</span>
</div>
{% if info.notes %}
<div class='panel-body'>{{ info.notes }}</div>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -1,5 +0,0 @@
{% extends "base.html"% }
{% block content %}
{% endblock %}

View File

@ -1,33 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h3>Part Tracking</h3>
<ul class='list-group'>
{% for part in parts.all %}
<li class='list-group-item'>
<a href="{% url 'track-detail' part.id %}">
{{ part.part.name }} - SN {{ part.serial }}
</a>
</li>
{% endfor %}
</ul>
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endif %}
{% endblock %}

View File

@ -1,5 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% endblock %}

View File

@ -1,3 +0,0 @@
# from django.test import TestCase
# Create your tests here.

View File

@ -1,38 +0,0 @@
from django.conf.urls import url, include
from django.views.generic.base import RedirectView
from . import views
"""
TODO - Implement JSON API for part serial number tracking
part_track_api_urls = [
url(r'^(?P<pk>[0-9]+)/?$', api.PartTrackingDetail.as_view(), name='parttrackinginfo-detail'),
url(r'^\?.*/?$', api.PartTrackingList.as_view()),
url(r'^$', api.PartTrackingList.as_view())
]
unique_api_urls = [
# Detail for a single unique part
url(r'^(?P<pk>[0-9]+)/?$', api.UniquePartDetail.as_view(), name='uniquepart-detail'),
# List all unique parts, with optional filters
url(r'^\?.*/?$', api.UniquePartList.as_view()),
url(r'^$', api.UniquePartList.as_view()),
]
"""
track_detail_urls = [
url('^.*$', views.TrackDetail.as_view(), name='track-detail'),
]
tracking_urls = [
# Detail view
url(r'^(?P<pk>\d+)/', include(track_detail_urls)),
# List ALL tracked items
url('', views.TrackIndex.as_view(), name='track-index'),
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='track-index'),
]

View File

@ -1,25 +0,0 @@
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView, DeleteView, CreateView
from .models import UniquePart, PartTrackingInfo
class TrackIndex(ListView):
model = UniquePart
template_name = 'track/index.html'
context_object_name = 'parts'
paginate_by = 50
def get_queryset(self):
return UniquePart.objects.order_by('part__name', 'serial')
class TrackDetail(DetailView):
queryset = UniquePart.objects.all()
template_name = 'track/detail.html'
context_object_name='part'

View File

@ -18,7 +18,6 @@ migrate:
python InvenTree/manage.py makemigrations part python InvenTree/manage.py makemigrations part
python InvenTree/manage.py makemigrations stock python InvenTree/manage.py makemigrations stock
python InvenTree/manage.py makemigrations supplier python InvenTree/manage.py makemigrations supplier
python InvenTree/manage.py makemigrations track
python InvenTree/manage.py migrate --run-syncdb python InvenTree/manage.py migrate --run-syncdb
python InvenTree/manage.py check python InvenTree/manage.py check

View File

@ -1,7 +1,9 @@
Django==1.11 Django==1.11
pillow==3.1.2
djangorestframework==3.6.2 djangorestframework==3.6.2
django_filter==1.0.2 django_filter==1.0.2
django-simple-history==1.8.2 django-simple-history==1.8.2
coreapi==2.3.0 coreapi==2.3.0
pygments==2.2.0 pygments==2.2.0
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2
django-import-export==1.0.0