mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree
This commit is contained in:
commit
0a2c48eda6
@ -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():
|
||||||
|
@ -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
|
||||||
|
@ -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')]
|
||||||
|
@ -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)
|
||||||
|
@ -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,)
|
||||||
|
@ -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'
|
||||||
|
]
|
||||||
|
20
InvenTree/part/migrations/0017_part_purchaseable.py
Normal file
20
InvenTree/part/migrations/0017_part_purchaseable.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
20
InvenTree/part/migrations/0018_part_buildable.py
Normal file
20
InvenTree/part/migrations/0018_part_buildable.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
62
InvenTree/part/migrations/0019_auto_20180416_1249.py
Normal file
62
InvenTree/part/migrations/0019_auto_20180416_1249.py
Normal 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?'),
|
||||||
|
),
|
||||||
|
]
|
@ -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"
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
5
InvenTree/part/templates/part/bom-create.html
Normal file
5
InvenTree/part/templates/part/bom-create.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'create_edit_obj.html' %}
|
||||||
|
|
||||||
|
{% block obj_title %}
|
||||||
|
Create a new BOM item
|
||||||
|
{% endblock %}
|
15
InvenTree/part/templates/part/bom-delete.html
Normal file
15
InvenTree/part/templates/part/bom-delete.html
Normal 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 %}
|
18
InvenTree/part/templates/part/bom-detail.html
Normal file
18
InvenTree/part/templates/part/bom-detail.html
Normal 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 %}
|
5
InvenTree/part/templates/part/bom-edit.html
Normal file
5
InvenTree/part/templates/part/bom-edit.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'create_edit_obj.html' %}
|
||||||
|
|
||||||
|
{% block obj_title %}
|
||||||
|
Edit details for BOM item
|
||||||
|
{% endblock %}
|
@ -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 %}
|
13
InvenTree/part/templates/part/build.html
Normal file
13
InvenTree/part/templates/part/build.html
Normal 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 %}
|
@ -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 %}
|
||||||
|
@ -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 }}">
|
||||||
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
||||||
|
@ -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 %}
|
@ -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' %}">
|
||||||
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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>
|
@ -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 %}
|
@ -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 %}
|
||||||
|
@ -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())
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
]
|
]
|
81
InvenTree/stock/migrations/0007_auto_20180416_0853.py
Normal file
81
InvenTree/stock/migrations/0007_auto_20180416_0853.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
20
InvenTree/stock/migrations/0008_stockitem_url.py
Normal file
20
InvenTree/stock/migrations/0008_stockitem_url.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
46
InvenTree/stock/migrations/0009_auto_20180416_1253.py
Normal file
46
InvenTree/stock/migrations/0009_auto_20180416_1253.py
Normal 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')]),
|
||||||
|
),
|
||||||
|
]
|
@ -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()
|
||||||
|
@ -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 %}
|
@ -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 %}
|
5
InvenTree/stock/templates/stock/item_create.html
Normal file
5
InvenTree/stock/templates/stock/item_create.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "create_edit_obj.html" %}
|
||||||
|
|
||||||
|
{% block obj_title %}
|
||||||
|
Create a new stock item
|
||||||
|
{% endblock %}
|
9
InvenTree/stock/templates/stock/item_delete.html
Normal file
9
InvenTree/stock/templates/stock/item_delete.html
Normal 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 %}
|
5
InvenTree/stock/templates/stock/item_edit.html
Normal file
5
InvenTree/stock/templates/stock/item_edit.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "create_edit_obj.html" %}
|
||||||
|
|
||||||
|
{% block obj_title %}
|
||||||
|
Edit stock item for part '{{ item.part.name }}'
|
||||||
|
{% endblock %}
|
@ -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>
|
||||||
|
@ -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 %}
|
5
InvenTree/stock/templates/stock/location_create.html
Normal file
5
InvenTree/stock/templates/stock/location_create.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "create_edit_obj.html" %}
|
||||||
|
|
||||||
|
{% block obj_title %}
|
||||||
|
Create a new stock location
|
||||||
|
{% endblock %}
|
41
InvenTree/stock/templates/stock/location_delete.html
Normal file
41
InvenTree/stock/templates/stock/location_delete.html
Normal 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 %}
|
5
InvenTree/stock/templates/stock/location_edit.html
Normal file
5
InvenTree/stock/templates/stock/location_edit.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "create_edit_obj.html" %}
|
||||||
|
|
||||||
|
{% block obj_title %}
|
||||||
|
Edit stock location '{{ location.name }}'
|
||||||
|
{% endblock %}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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'),
|
||||||
]
|
]
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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'))
|
||||||
|
|
||||||
|
36
InvenTree/supplier/migrations/0007_auto_20180416_1253.py
Normal file
36
InvenTree/supplier/migrations/0007_auto_20180416_1253.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
@ -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 %}
|
@ -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 = [
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class TrackConfig(AppConfig):
|
|
||||||
name = 'track'
|
|
@ -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')]),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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(),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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)
|
|
@ -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__'
|
|
@ -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 %}
|
|
@ -1,5 +0,0 @@
|
|||||||
{% extends "base.html"% }
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -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 %}
|
|
@ -1,5 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,3 +0,0 @@
|
|||||||
# from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -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'),
|
|
||||||
]
|
|
@ -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'
|
|
||||||
|
|
1
Makefile
1
Makefile
@ -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
|
||||||
|
|
||||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user