mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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.dispatch import receiver
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
""" Abstract model representing an external company
|
||||
"""
|
||||
@ -89,6 +90,10 @@ class InvenTreeTree(models.Model):
|
||||
|
||||
return unique
|
||||
|
||||
@property
|
||||
def has_children(self):
|
||||
return self.children.count() > 0
|
||||
|
||||
@property
|
||||
def children(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')
|
||||
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
|
||||
for child in instance.children.all():
|
||||
|
@ -36,6 +36,7 @@ INSTALLED_APPS = [
|
||||
'rest_framework',
|
||||
'simple_history',
|
||||
'crispy_forms',
|
||||
'import_export',
|
||||
|
||||
# Core django modules
|
||||
'django.contrib.admin',
|
||||
@ -47,9 +48,8 @@ INSTALLED_APPS = [
|
||||
|
||||
# InvenTree apps
|
||||
'part.apps.PartConfig',
|
||||
'supplier.apps.SupplierConfig',
|
||||
'stock.apps.StockConfig',
|
||||
'track.apps.TrackConfig',
|
||||
'supplier.apps.SupplierConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -68,7 +68,7 @@ ROOT_URLCONF = 'InvenTree.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@ -81,6 +81,8 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
print(os.path.join(BASE_DIR, 'templates'))
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'EXCEPTION_HANDLER': 'InvenTree.utils.api_exception_handler'
|
||||
}
|
||||
@ -145,4 +147,8 @@ MEDIA_URL = '/media/'
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
# crispy forms use the bootstrap templates
|
||||
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.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 bom_api_urls
|
||||
from part.urls import part_urls
|
||||
|
||||
|
||||
from stock.urls import stock_api_urls, stock_api_loc_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 django.conf import settings
|
||||
@ -19,10 +16,8 @@ from django.conf.urls.static import static
|
||||
|
||||
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
|
||||
|
||||
@ -37,28 +32,28 @@ apipatterns = [
|
||||
# Part URLs
|
||||
url(r'^part/', include(part_api_urls)),
|
||||
url(r'^part-category/', include(part_cat_api_urls)),
|
||||
#url(r'^part-param/', include(part_param_urls)),
|
||||
#url(r'^part-param-template/', include(part_param_template_urls)),
|
||||
# url(r'^part-param/', include(part_param_urls)),
|
||||
# url(r'^part-param-template/', include(part_param_template_urls)),
|
||||
|
||||
# Part BOM URLs
|
||||
url(r'^bom/', include(bom_api_urls)),
|
||||
|
||||
# Supplier URLs
|
||||
#url(r'^supplier/', include(supplier_api_urls)),
|
||||
#url(r'^supplier-part/', include(supplier_api_part_urls)),
|
||||
#url(r'^price-break/', include(price_break_urls)),
|
||||
#url(r'^manufacturer/', include(manu_urls)),
|
||||
#url(r'^customer/', include(cust_urls)),
|
||||
# url(r'^supplier/', include(supplier_api_urls)),
|
||||
# url(r'^supplier-part/', include(supplier_api_part_urls)),
|
||||
# url(r'^price-break/', include(price_break_urls)),
|
||||
# url(r'^manufacturer/', include(manu_urls)),
|
||||
# url(r'^customer/', include(cust_urls)),
|
||||
|
||||
# Tracking URLs
|
||||
#url(r'^track/', include(part_track_urls)),
|
||||
#url(r'^unique-part/', include(unique_urls)),
|
||||
# url(r'^track/', include(part_track_urls)),
|
||||
# url(r'^unique-part/', include(unique_urls)),
|
||||
|
||||
# Project URLs
|
||||
#url(r'^project/', include(prj_urls)),
|
||||
#url(r'^project-category/', include(prj_cat_urls)),
|
||||
#url(r'^project-part/', include(prj_part_urls)),
|
||||
#url(r'^project-run/', include(prj_run_urls)),
|
||||
# url(r'^project/', include(prj_urls)),
|
||||
# url(r'^project-category/', include(prj_cat_urls)),
|
||||
# url(r'^project-part/', include(prj_part_urls)),
|
||||
# url(r'^project-run/', include(prj_run_urls)),
|
||||
|
||||
# User URLs
|
||||
url(r'^user/', include(user_urls)),
|
||||
@ -67,13 +62,12 @@ apipatterns = [
|
||||
urlpatterns = [
|
||||
|
||||
# API URL
|
||||
#url(r'^api/', include(apipatterns)),
|
||||
#url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
|
||||
# url(r'^api/', include(apipatterns)),
|
||||
# url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
|
||||
|
||||
url(r'^part/', include(part_urls)),
|
||||
url(r'^stock/', include(stock_urls)),
|
||||
url(r'^supplier/', include(supplier_urls)),
|
||||
url(r'^track/', include(tracking_urls)),
|
||||
|
||||
url(r'^admin/', admin.site.urls),
|
||||
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)
|
||||
|
||||
# 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 import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from .models import PartCategory, Part
|
||||
from .models import BomItem
|
||||
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):
|
||||
|
||||
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):
|
||||
list_display = ('part', 'attachment')
|
||||
|
||||
|
||||
"""
|
||||
class ParameterTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'units', 'format')
|
||||
@ -33,6 +38,6 @@ admin.site.register(PartCategory, PartCategoryAdmin)
|
||||
admin.site.register(BomItem, BomItemAdmin)
|
||||
admin.site.register(PartAttachment, PartAttachmentAdmin)
|
||||
|
||||
#admin.site.register(PartParameter, ParameterAdmin)
|
||||
#admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
|
||||
#admin.site.register(CategoryParameterLink)
|
||||
# admin.site.register(PartParameter, ParameterAdmin)
|
||||
# admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
|
||||
# admin.site.register(CategoryParameterLink)
|
||||
|
@ -8,6 +8,9 @@ from django_filters.rest_framework import FilterSet, DjangoFilterBackend
|
||||
|
||||
from .models import PartCategory, Part, BomItem
|
||||
|
||||
from InvenTree.models import FilterChildren
|
||||
|
||||
|
||||
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
|
||||
@ -69,6 +72,7 @@ class PartParamDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
"""
|
||||
|
||||
|
||||
class PartFilter(FilterSet):
|
||||
|
||||
class Meta:
|
||||
@ -174,6 +178,7 @@ class PartTemplateList(generics.ListCreateAPIView):
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class BomItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
queryset = BomItem.objects.all()
|
||||
@ -190,9 +195,6 @@ class BomItemFilter(FilterSet):
|
||||
|
||||
class BomItemList(generics.ListCreateAPIView):
|
||||
|
||||
#def get_queryset(self):
|
||||
# params = self.request.
|
||||
|
||||
queryset = BomItem.objects.all()
|
||||
serializer_class = BomItemSerializer
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
@ -2,7 +2,7 @@ from django import forms
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Submit
|
||||
|
||||
from .models import Part, PartCategory
|
||||
from .models import Part, PartCategory, BomItem
|
||||
|
||||
|
||||
class EditPartForm(forms.ModelForm):
|
||||
@ -12,9 +12,7 @@ class EditPartForm(forms.ModelForm):
|
||||
self.helper = FormHelper()
|
||||
|
||||
self.helper.form_id = 'id-edit-part-form'
|
||||
#self.helper.form_class = 'blueForms'
|
||||
self.helper.form_method = 'post'
|
||||
#self.helper.form_action = 'submit'
|
||||
|
||||
self.helper.add_input(Submit('submit', 'Submit'))
|
||||
|
||||
@ -27,7 +25,9 @@ class EditPartForm(forms.ModelForm):
|
||||
'IPN',
|
||||
'URL',
|
||||
'minimum_stock',
|
||||
'buildable',
|
||||
'trackable',
|
||||
'purchaseable',
|
||||
]
|
||||
|
||||
|
||||
@ -38,9 +38,7 @@ class EditCategoryForm(forms.ModelForm):
|
||||
self.helper = FormHelper()
|
||||
|
||||
self.helper.form_id = 'id-edit-part-form'
|
||||
#self.helper.form_class = 'blueForms'
|
||||
self.helper.form_method = 'post'
|
||||
#self.helper.form_action = 'submit'
|
||||
|
||||
self.helper.add_input(Submit('submit', 'Submit'))
|
||||
|
||||
@ -50,4 +48,24 @@ class EditCategoryForm(forms.ModelForm):
|
||||
'parent',
|
||||
'name',
|
||||
'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 django.utils.translation import ugettext as _
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.core.validators import MinValueValidator
|
||||
@ -23,7 +22,6 @@ class PartCategory(InvenTreeTree):
|
||||
verbose_name = "Part Category"
|
||||
verbose_name_plural = "Part Categories"
|
||||
|
||||
|
||||
@property
|
||||
def partcount(self):
|
||||
""" Return the total part count under this category
|
||||
@ -37,11 +35,10 @@ class PartCategory(InvenTreeTree):
|
||||
|
||||
return count
|
||||
|
||||
"""
|
||||
@property
|
||||
def parts(self):
|
||||
return self.part_set.all()
|
||||
"""
|
||||
def has_parts(self):
|
||||
return self.parts.count() > 0
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
|
||||
def before_delete_part_category(sender, instance, using, **kwargs):
|
||||
@ -85,36 +82,44 @@ class Part(models.Model):
|
||||
return '/part/{id}/'.format(id=self.id)
|
||||
|
||||
# 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)
|
||||
description = models.CharField(max_length=250)
|
||||
description = models.CharField(max_length=250, help_text='Part description')
|
||||
|
||||
# Internal Part Number (optional)
|
||||
# Potentially multiple parts map to the same internal IPN (variants?)
|
||||
# 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
|
||||
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
|
||||
category = models.ForeignKey(PartCategory, related_name='parts',
|
||||
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)
|
||||
|
||||
# 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 = 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"?
|
||||
# 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
|
||||
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):
|
||||
if self.IPN:
|
||||
@ -127,10 +132,41 @@ class Part(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Part"
|
||||
verbose_name_plural = "Parts"
|
||||
#unique_together = (("name", "category"),)
|
||||
|
||||
@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.
|
||||
Part may be stored in multiple locations
|
||||
"""
|
||||
@ -143,13 +179,21 @@ class Part(models.Model):
|
||||
return result['total']
|
||||
|
||||
@property
|
||||
def bomItemCount(self):
|
||||
return self.bom_items.all().count()
|
||||
|
||||
def has_bom(self):
|
||||
return self.bom_count > 0
|
||||
|
||||
@property
|
||||
def usedInCount(self):
|
||||
return self.used_in.all().count()
|
||||
def bom_count(self):
|
||||
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
|
||||
@ -171,9 +215,10 @@ class Part(models.Model):
|
||||
return projects
|
||||
"""
|
||||
|
||||
|
||||
def attach_file(instance, filename):
|
||||
|
||||
base='part_files'
|
||||
base = 'part_files'
|
||||
|
||||
# TODO - For a new PartAttachment object, PK is NULL!!
|
||||
|
||||
@ -182,25 +227,27 @@ def attach_file(instance, filename):
|
||||
|
||||
return os.path.join(base, fn)
|
||||
|
||||
|
||||
class PartAttachment(models.Model):
|
||||
""" A PartAttachment links a file to a part
|
||||
Parts can have multiple files such as datasheets, etc
|
||||
"""
|
||||
|
||||
|
||||
part = models.ForeignKey(Part, on_delete=models.CASCADE,
|
||||
related_name='attachments')
|
||||
|
||||
attachment = models.FileField(upload_to=attach_file, null=True, blank=True)
|
||||
|
||||
|
||||
|
||||
class BomItem(models.Model):
|
||||
""" A BomItem links a part to its component items.
|
||||
A part can have a BOM (bill of materials) which defines
|
||||
which parts are required (and in what quatity) to make it
|
||||
"""
|
||||
|
||||
def get_absolute_url(self):
|
||||
return '/part/bom/{id}/'.format(id=self.id)
|
||||
|
||||
# A link to the parent part
|
||||
# Each part will get a reverse lookup field 'bom_items'
|
||||
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='bom_items')
|
||||
@ -212,7 +259,6 @@ class BomItem(models.Model):
|
||||
# Quantity required
|
||||
quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)])
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "BOM Item"
|
||||
|
||||
|
@ -3,6 +3,7 @@ from rest_framework import serializers
|
||||
from .models import Part, PartCategory
|
||||
from .models import BomItem
|
||||
|
||||
|
||||
class BomItemSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
@ -12,6 +13,7 @@ class BomItemSerializer(serializers.ModelSerializer):
|
||||
'sub_part',
|
||||
'quantity')
|
||||
|
||||
|
||||
"""
|
||||
class PartParameterSerializer(serializers.HyperlinkedModelSerializer):
|
||||
" Serializer for a PartParameter
|
||||
@ -27,7 +29,7 @@ class PartParameterSerializer(serializers.HyperlinkedModelSerializer):
|
||||
'units')
|
||||
"""
|
||||
|
||||
#class PartSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class PartSerializer(serializers.ModelSerializer):
|
||||
""" Serializer for complete detail information of a part.
|
||||
Used when displaying all details of a single component.
|
||||
@ -56,6 +58,7 @@ class PartCategorySerializer(serializers.HyperlinkedModelSerializer):
|
||||
'parent',
|
||||
'pathstring')
|
||||
|
||||
|
||||
"""
|
||||
class PartTemplateSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
@ -65,4 +68,4 @@ class PartTemplateSerializer(serializers.HyperlinkedModelSerializer):
|
||||
'name',
|
||||
'units',
|
||||
'format')
|
||||
"""
|
||||
"""
|
||||
|
@ -12,7 +12,6 @@ Deletion title goes here
|
||||
<p><b>This is a permanent action and cannot be undone.</b></p>
|
||||
|
||||
{% block del_body %}
|
||||
Deletion body goes here
|
||||
{% endblock %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
|
@ -9,7 +9,6 @@
|
||||
<li><a href="/part/">Parts</a></li>
|
||||
<li><a href="/stock/">Stock</a></li>
|
||||
<li><a href="/supplier/">Suppliers</a></li>
|
||||
<li><a href="/track/">Tracking</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</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' %}
|
||||
|
||||
<h3>Bill of Materials</h3>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Part</th>
|
||||
<th>Description</th>
|
||||
<th>Quantity</th>
|
||||
<th>Edit</th>
|
||||
</tr>
|
||||
{% for bom_item in part.bom_items.all %}
|
||||
{% with sub_part=bom_item.sub_part %}
|
||||
<tr>
|
||||
<td><a href="{% url 'part-detail' sub_part.id %}">{{ sub_part.name }}</a></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>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</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 %}
|
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'>
|
||||
{% 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 %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
@ -33,7 +33,7 @@ Are you sure you want to delete category '{{ category.name }}'?
|
||||
</p>
|
||||
<ul class='list-group'>
|
||||
{% 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 %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
@ -11,9 +11,15 @@
|
||||
<i>{{ category.description }}</i>
|
||||
</p>
|
||||
|
||||
{% if category.has_children %}
|
||||
<h4>Subcategories</h4>
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
|
||||
<div class='container-fluid'>
|
||||
<a href="{% url 'category-create' %}?category={{ category.id }}">
|
||||
|
@ -1,5 +1,3 @@
|
||||
{% if parts|length > 0 %}
|
||||
Parts:
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Part</th>
|
||||
@ -11,5 +9,4 @@ Parts:
|
||||
<td>{{ part.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</table>
|
@ -1,5 +1,3 @@
|
||||
{% if children|length > 0 %}
|
||||
Subcategories:
|
||||
<ul class="list-group">
|
||||
{% for child in children %}
|
||||
<li class="list-group-item">
|
||||
@ -10,5 +8,4 @@ Subcategories:
|
||||
<span class='badge'>{{ child.partcount }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</ul>
|
@ -7,8 +7,8 @@
|
||||
|
||||
{% block del_body %}
|
||||
|
||||
{% if part.usedInCount > 0 %}
|
||||
<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:
|
||||
{% if part.used_in_count %}
|
||||
<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">
|
||||
{% for child in part.used_in.all %}
|
||||
<li class='list-group-item'>{{ child.part.name }} - {{ child.part.description }}</li>
|
||||
|
@ -4,12 +4,52 @@
|
||||
|
||||
{% include 'part/tabs.html' with tab='detail' %}
|
||||
|
||||
<h3>Part Details</h3>
|
||||
|
||||
Part details go here...
|
||||
<br>
|
||||
<table class='table table-striped'>
|
||||
<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-delete' part.id %}"><button class="btn btn-danger">Delete Part</button></a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -5,9 +5,15 @@
|
||||
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
|
||||
{% if parts.all|length > 0%}
|
||||
<h4>Top Level Parts</h4>
|
||||
{% include "part/category_parts.html" with parts=parts %}
|
||||
{% endif %}
|
||||
|
||||
<div class='container-fluid'>
|
||||
<a href="{% url 'category-create' %}">
|
||||
|
@ -6,33 +6,74 @@
|
||||
|
||||
{% include "part/cat_link.html" with category=part.category %}
|
||||
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<img class="part-thumb"
|
||||
{% if part.image %}
|
||||
src="{{ part.image.url }}"
|
||||
{% else %}
|
||||
src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}/>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h4>{{ part.name }}</h4>
|
||||
{% if part.description %}
|
||||
<p><i>{{ part.description }}</i></p>
|
||||
{% endif %}
|
||||
{% if part.IPN %}
|
||||
<p><b>IPN:</b> {{ part.IPN }}</p>
|
||||
{% endif %}
|
||||
{% if part.URL %}
|
||||
<p>{% include 'url.html' with url=part.URL %}</p>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<img class="part-thumb"
|
||||
{% if part.image %}
|
||||
src="{{ part.image.url }}"
|
||||
{% else %}
|
||||
src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}/>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h4>{{ part.name }}</h4>
|
||||
{% if part.description %}
|
||||
<p><i>{{ part.description }}</i></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-striped">
|
||||
{% if part.IPN %}
|
||||
<tr>
|
||||
<td>IPN</td>
|
||||
<td>{{ part.IPN }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.URL %}
|
||||
<tr>
|
||||
<td>URL</td>
|
||||
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
|
||||
</tr>
|
||||
{% 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>
|
||||
|
||||
<hr>
|
||||
<div class='container-fluid'>
|
||||
{% block details %}
|
||||
|
||||
<!-- Specific part details go here... -->
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -4,12 +4,11 @@
|
||||
|
||||
{% include 'part/tabs.html' with tab='stock' %}
|
||||
|
||||
<br>
|
||||
Total in stock: {{ part.stock }}
|
||||
<br>
|
||||
<h3>Part Stock</h3>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Link</th>
|
||||
<th>Quantity</th>
|
||||
<th>Location</th>
|
||||
<th>Supplier part</th>
|
||||
@ -18,8 +17,9 @@ Total in stock: {{ part.stock }}
|
||||
</tr>
|
||||
{% for stock in part.locations.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'stock-item-detail' stock.id %}">Click</a></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>
|
||||
{% if stock.supplier_part %}
|
||||
<a href="{% url 'supplier-part-detail' stock.supplier_part.id %}">
|
||||
@ -31,6 +31,12 @@ Total in stock: {{ part.stock }}
|
||||
<td>{{ stock.notes }}</td>
|
||||
</tr>
|
||||
{% 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 %}
|
@ -4,17 +4,23 @@
|
||||
|
||||
{% include 'part/tabs.html' with tab='suppliers' %}
|
||||
|
||||
{% if part.supplier_parts.all|length > 0 %}
|
||||
<h3>Part Suppliers</h3>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Supplier</th>
|
||||
<th>SKU</th>
|
||||
<th>Supplier</th>
|
||||
<th>MPN</th>
|
||||
<th>URL</th>
|
||||
</tr>
|
||||
{% for spart in part.supplier_parts.all %}
|
||||
<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-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>
|
||||
{% if spart.URL %}
|
||||
<a href="{{ spart.URL }}">{{ spart.URL }}</a>
|
||||
@ -23,8 +29,11 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
There are no suppliers defined for this part, sorry!
|
||||
{% endif %}
|
||||
|
||||
<div class='container-fluid'>
|
||||
<a href="{% url 'supplier-part-create' %}?part={{ part.id }}">
|
||||
<button class="btn btn-success">New Supplier Part</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -1,15 +1,23 @@
|
||||
<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 'bom' %} class="active"{% endifequal %}><a href="{% url 'part-bom' part.id %}">BOM <span class="badge">{{ part.bomItemCount }}</span></a></li>
|
||||
{% if part.bomItemCount > 0 %}
|
||||
<li{% ifequal tab 'build' %} class "active"{% endifequal %}><a href="#">Build</a></li>
|
||||
{% if part.buildable %}
|
||||
<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="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.can_build }}</span></a></li>
|
||||
{% endif %}
|
||||
{% if part.usedInCount > 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>
|
||||
{% if part.used_in_count > 0 %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
</ul>
|
@ -11,12 +11,18 @@ Part tracking for {{ part.name }}
|
||||
<th>Serial</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
{% for track in part.serials.all %}
|
||||
{% for track in part.tracked_parts.all %}
|
||||
<tr>
|
||||
<td>{{ track.serial }}</td>
|
||||
<td>{{ track.status }}</td>
|
||||
<td><a href="{% url 'track-detail' track.id %}">{{ track.serial }}</a></td>
|
||||
<td>{{ track.get_status_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</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 %}
|
@ -4,7 +4,7 @@
|
||||
|
||||
{% 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">
|
||||
<tr>
|
||||
@ -13,7 +13,7 @@ This part is used to make the following parts:
|
||||
</tr>
|
||||
{% for item in part.used_in.all %}
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -41,12 +41,12 @@ part_detail_urls = [
|
||||
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'^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'^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'),
|
||||
|
||||
# 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'),
|
||||
]
|
||||
|
||||
@ -57,6 +57,13 @@ part_category_urls = [
|
||||
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
|
||||
part_urls = [
|
||||
|
||||
@ -66,20 +73,23 @@ part_urls = [
|
||||
# Create a new part
|
||||
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)),
|
||||
|
||||
# Part category
|
||||
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)
|
||||
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 = [
|
||||
# Detail of a single part parameter
|
||||
@ -99,5 +109,3 @@ part_param_template_urls = [
|
||||
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.urls import reverse
|
||||
|
||||
from django.views.generic import DetailView, ListView
|
||||
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):
|
||||
model = Part
|
||||
@ -113,7 +112,7 @@ class CategoryDelete(DeleteView):
|
||||
model = PartCategory
|
||||
template_name = 'part/category_delete.html'
|
||||
context_object_name = 'category'
|
||||
success_url ='/part/'
|
||||
success_url = '/part/'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'confirm' in request.POST:
|
||||
@ -146,3 +145,47 @@ class CategoryCreate(CreateView):
|
||||
initials['parent'] = get_object_or_404(PartCategory, pk=parent_id)
|
||||
|
||||
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;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.part-thumb {
|
||||
|
@ -2,6 +2,7 @@ from django.contrib import admin
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
|
||||
from .models import StockLocation, StockItem
|
||||
from .models import StockItemTracking
|
||||
|
||||
|
||||
class LocationAdmin(admin.ModelAdmin):
|
||||
@ -12,5 +13,10 @@ class StockItemAdmin(SimpleHistoryAdmin):
|
||||
list_display = ('part', 'quantity', 'location', 'status', 'updated')
|
||||
|
||||
|
||||
class StockTrackingAdmin(admin.ModelAdmin):
|
||||
list_display = ('item', 'date', 'title')
|
||||
|
||||
|
||||
admin.site.register(StockLocation, LocationAdmin)
|
||||
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 InvenTree.models import FilterChildren
|
||||
from .models import StockLocation, StockItem
|
||||
from .serializers import StockItemSerializer, StockQuantitySerializer
|
||||
|
@ -14,7 +14,6 @@ class EditStockLocationForm(forms.ModelForm):
|
||||
self.helper.form_id = 'id-edit-part-form'
|
||||
self.helper.form_class = 'blueForms'
|
||||
self.helper.form_method = 'post'
|
||||
#self.helper.form_action = '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_class = 'blueForms'
|
||||
self.helper.form_method = 'post'
|
||||
#self.helper.form_action = 'submit'
|
||||
|
||||
self.helper.add_input(Submit('submit', 'Submit'))
|
||||
|
||||
|
||||
class Meta:
|
||||
model = StockItem
|
||||
fields = [
|
||||
'part',
|
||||
'supplier_part',
|
||||
'location',
|
||||
'belongs_to',
|
||||
'serial',
|
||||
'batch',
|
||||
'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.core.validators import MinValueValidator
|
||||
from django.contrib.auth.models import User
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
from supplier.models import SupplierPart
|
||||
from supplier.models import Customer
|
||||
from part.models import Part
|
||||
from InvenTree.models import InvenTreeTree
|
||||
|
||||
@ -14,16 +14,23 @@ from datetime import datetime
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
|
||||
class StockLocation(InvenTreeTree):
|
||||
""" Organization tree for StockItem objects
|
||||
A "StockLocation" can be considered a warehouse, or storage location
|
||||
Stock locations can be heirarchical as required
|
||||
"""
|
||||
|
||||
def get_absolute_url(self):
|
||||
return '/stock/location/{id}/'.format(id=self.id)
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
stock_list = self.stockitem_set.all()
|
||||
return stock_list
|
||||
return self.stockitem_set.all()
|
||||
|
||||
@property
|
||||
def has_items(self):
|
||||
return self.items.count() > 0
|
||||
|
||||
|
||||
@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
|
||||
for item in instance.items.all():
|
||||
item.location = instance.parent
|
||||
item.save()
|
||||
# If this location has a parent, move the child stock items to the parent
|
||||
if instance.parent:
|
||||
item.location = instance.parent
|
||||
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):
|
||||
"""
|
||||
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')
|
||||
|
||||
# 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)
|
||||
|
||||
# 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,
|
||||
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)])
|
||||
|
||||
# Last time this item was updated (set automagically)
|
||||
updated = models.DateField(auto_now=True)
|
||||
|
||||
# last time the stock was checked / counted
|
||||
@ -78,8 +138,9 @@ class StockItem(models.Model):
|
||||
|
||||
infinite = models.BooleanField(default=False)
|
||||
|
||||
# History of this item
|
||||
history = HistoricalRecords()
|
||||
@property
|
||||
def has_tracking_info(self):
|
||||
return self.tracking_info.count() > 0
|
||||
|
||||
@transaction.atomic
|
||||
def stocktake(self, count, user):
|
||||
@ -128,3 +189,34 @@ class StockItem(models.Model):
|
||||
n=self.quantity,
|
||||
part=self.part.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 %}
|
||||
|
||||
{% if locations.all|length > 0 %}
|
||||
<h4>Storage Locations</h4>
|
||||
{% include "stock/location_list.html" with locations=locations %}
|
||||
{% endif %}
|
||||
|
||||
@ -12,4 +13,10 @@
|
||||
{% include "stock/stock_table.html" with items=items %}
|
||||
{% endif %}
|
||||
|
||||
<div class='container-fluid'>
|
||||
<a href="{% url 'stock-location-create' %}">
|
||||
<button class="btn btn-success">New Stock Location</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -4,19 +4,52 @@
|
||||
|
||||
{% include "stock/loc_link.html" with location=item.location %}
|
||||
|
||||
<h3>Stock entry details</h3>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<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>
|
||||
{% 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>
|
||||
<td>Location</td>
|
||||
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
|
||||
</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>
|
||||
<td>Quantity</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
</tr>
|
||||
{% if item.URL %}
|
||||
<tr>
|
||||
<td>URL</td>
|
||||
<td><a href="{{ item.URL }}">{{ item.URL }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.supplier_part %}
|
||||
<tr>
|
||||
<td>Supplier Part</td>
|
||||
@ -35,7 +68,7 @@
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>{{ item.status }}</td>
|
||||
<td>{{ item.get_status_display }}</td>
|
||||
</tr>
|
||||
{% if item.notes %}
|
||||
<tr>
|
||||
@ -45,4 +78,28 @@
|
||||
{% endif %}
|
||||
</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 %}
|
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">
|
||||
<nav aria-label="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 %}
|
||||
{% for path_item in location.parentpath %}
|
||||
<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 %}
|
||||
|
||||
<p>
|
||||
<b>{{ location.name }}</b><br>
|
||||
<i>{{ location.description }}</i>
|
||||
</p>
|
||||
|
||||
<h3>{{ location.name }}</h3>
|
||||
<p>{{ location.description }}</p>
|
||||
|
||||
{% if location.has_children %}
|
||||
<h4>Sub Locations</h4>
|
||||
{% 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 %}
|
||||
{% 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 %}
|
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">
|
||||
{% for child in locations.all %}
|
||||
<li class="list-group-item"><a href="{% url 'stock-location-detail' child.id %}">{{ child.name }}</a></li>
|
||||
|
@ -8,7 +8,7 @@
|
||||
</tr>
|
||||
{% for item in items.all %}
|
||||
<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.status }}</td>
|
||||
<td>{{ item.stocktake_date }}</td>
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.conf.urls import url, include
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from . import views
|
||||
from . import api
|
||||
@ -52,10 +51,10 @@ stock_urls = [
|
||||
|
||||
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
|
||||
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'),
|
||||
]
|
||||
]
|
||||
|
@ -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.urls import reverse
|
||||
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.edit import UpdateView, DeleteView, CreateView
|
||||
|
||||
|
||||
from part.models import Part
|
||||
from .models import StockItem, StockLocation
|
||||
|
||||
from .forms import EditStockLocationForm
|
||||
from .forms import EditStockItemForm
|
||||
|
||||
|
||||
class StockIndex(ListView):
|
||||
model = StockItem
|
||||
template_name = 'stock/index.html'
|
||||
@ -29,6 +29,7 @@ class StockIndex(ListView):
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class StockLocationDetail(DetailView):
|
||||
context_object_name = 'location'
|
||||
template_name = 'stock/location.html'
|
||||
@ -46,35 +47,60 @@ class StockItemDetail(DetailView):
|
||||
class StockLocationEdit(UpdateView):
|
||||
model = StockLocation
|
||||
form_class = EditStockLocationForm
|
||||
template_name = '/stock/location-edit.html'
|
||||
template_name = 'stock/location_edit.html'
|
||||
context_object_name = 'location'
|
||||
|
||||
|
||||
class StockItemEdit(UpdateView):
|
||||
model = StockItem
|
||||
form_class = EditStockItemForm
|
||||
template_name = '/stock/item-edit.html'
|
||||
template_name = 'stock/item_edit.html'
|
||||
context_object_name = 'item'
|
||||
|
||||
|
||||
class StockLocationCreate(CreateView):
|
||||
model = StockLocation
|
||||
form_class = EditStockLocationForm
|
||||
template_name = '/stock/location-create.html'
|
||||
template_name = 'stock/location_create.html'
|
||||
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):
|
||||
model = StockItem
|
||||
form_class = EditStockItemForm
|
||||
template_name = '/stock/item-create.html'
|
||||
template_name = 'stock/item_create.html'
|
||||
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):
|
||||
model = StockLocation
|
||||
success_url = '/stock/'
|
||||
template_name = '/stock/location-delete.html'
|
||||
success_url = '/stock'
|
||||
template_name = 'stock/location_delete.html'
|
||||
context_object_name = 'location'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'confirm' in request.POST:
|
||||
@ -84,12 +110,13 @@ class StockLocationDelete(DeleteView):
|
||||
|
||||
|
||||
class StockItemDelete(DeleteView):
|
||||
model = StockLocation
|
||||
model = StockItem
|
||||
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):
|
||||
if 'confirm' in request.POST:
|
||||
return super(StockItemDelete, self).post(request, *args, **kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect(self.get_object().get_absolute_url())
|
||||
return HttpResponseRedirect(self.get_object().get_absolute_url())
|
||||
|
@ -1,13 +1,18 @@
|
||||
from django.contrib import admin
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from .models import Supplier, SupplierPart, Customer, Manufacturer
|
||||
|
||||
|
||||
class CompanyAdmin(admin.ModelAdmin):
|
||||
class CompanyAdmin(ImportExportModelAdmin):
|
||||
list_display = ('name', 'website', 'contact')
|
||||
|
||||
|
||||
class SupplierPartAdmin(ImportExportModelAdmin):
|
||||
list_display = ('part', 'supplier', 'SKU')
|
||||
|
||||
|
||||
admin.site.register(Customer, CompanyAdmin)
|
||||
admin.site.register(Supplier, 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_class = 'blueForms'
|
||||
self.helper.form_method = 'post'
|
||||
#self.helper.form_action = '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_class = 'blueForms'
|
||||
self.helper.form_method = 'post'
|
||||
#self.helper.form_action = 'submit'
|
||||
|
||||
self.helper.add_input(Submit('submit', 'Submit'))
|
||||
|
||||
@ -54,4 +52,4 @@ class EditSupplierPartForm(forms.ModelForm):
|
||||
'URL',
|
||||
'manufacturer',
|
||||
'MPN',
|
||||
]
|
||||
]
|
||||
|
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):
|
||||
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):
|
||||
""" Represents a manfufacturer
|
||||
@ -42,19 +50,20 @@ class SupplierPart(models.Model):
|
||||
|
||||
# Link to an actual part
|
||||
# The part will have a field 'supplier_parts' which links to the supplier part options
|
||||
part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.SET_NULL,
|
||||
part = models.ForeignKey(Part, on_delete=models.CASCADE,
|
||||
related_name='supplier_parts')
|
||||
|
||||
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)
|
||||
MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
|
||||
|
||||
URL = models.URLField(blank=True)
|
||||
|
||||
description = models.CharField(max_length=250, blank=True)
|
||||
|
||||
# Default price for a single unit
|
||||
|
@ -5,12 +5,12 @@ Are you sure you want to delete supplier '{{ supplier.name }}'?
|
||||
{% endblock %}
|
||||
|
||||
{% block del_body %}
|
||||
{% if supplier.parts.all|length > 0 %}
|
||||
<p>There are {{ supplier.parts.all|length }} parts sourced from this supplier.<br>
|
||||
If this supplier is deleted, these child categories will also be deleted.</p>
|
||||
{% if supplier.part_count > 0 %}
|
||||
<p>There are {{ supplier.part_count }} parts sourced from this supplier.<br>
|
||||
If this supplier is deleted, these supplier part entries will also be deleted.</p>
|
||||
<ul class='list-group'>
|
||||
{% 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 %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
@ -6,12 +6,7 @@
|
||||
<div class="col-sm-6">
|
||||
<h3>{{ supplier.name }}</h3>
|
||||
<p>{{ supplier.description }}</p>
|
||||
<p><a href="{% url 'supplier-edit' supplier.id %}">
|
||||
<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>
|
||||
<p>{{ supplier.notes }}</p>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table">
|
||||
@ -48,21 +43,24 @@
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Part</th>
|
||||
<th>Manufacturer</th>
|
||||
<th>Description</th>
|
||||
<th>Parent Part</th>
|
||||
<th>MPN</th>
|
||||
<th>URL</th>
|
||||
</tr>
|
||||
{% for part in supplier.parts.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'supplier-part-detail' part.id %}">{{ part.SKU }}</a></td>
|
||||
<td>{{ part.description }}</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td>Manufacturer name goes here</td>
|
||||
<td>MPN goes here</td>
|
||||
<td>
|
||||
{% if part.manufacturer %}{{ part.manufacturer.name }}{% endif %}
|
||||
{% if part.MPN %} | {{ part.MPN }}{% endif %}
|
||||
</td>
|
||||
<td>{{ part.URL }}</td>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@ -71,5 +69,12 @@
|
||||
<a href="{% url 'supplier-part-create' %}?supplier={{ supplier.id }}">
|
||||
<button class="btn btn-success">New Supplier Part</button>
|
||||
</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 %}
|
@ -8,10 +8,10 @@
|
||||
<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>Part</td>
|
||||
<td>Parent Part</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -22,19 +22,21 @@
|
||||
<tr><td>Description</td><td>{{ part.description }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.manufacturer %}
|
||||
<tr><td>Manufacturer</td><td>TODO</td></tr>
|
||||
<tr><td>MPN</td><td>TODO</td></tr>
|
||||
<tr><td>Manufacturer</td><td>{% if part.manufacturer %}{{ part.manufacturer.name }}{% endif %}</td></tr>
|
||||
<tr><td>MPN</td><td>{{ part.MPN }}</td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<p><a href="{% url 'supplier-part-edit' part.id %}">
|
||||
<button class="btn btn-info">Edit supplier details</button>
|
||||
</a></p>
|
||||
<p><a href="{% url 'supplier-part-delete' part.id %}">
|
||||
<div class='container-fluid'>
|
||||
<a href="{% url 'supplier-part-edit' part.id %}">
|
||||
<button class="btn btn-info">Edit supplier part</button>
|
||||
</a>
|
||||
<a href="{% url 'supplier-part-delete' part.id %}">
|
||||
<button class="btn btn-danger">Delete supplier part</button>
|
||||
</a></p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
@ -2,7 +2,6 @@ from django.conf.urls import url, include
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from . import views
|
||||
from . import api
|
||||
|
||||
"""
|
||||
cust_urls = [
|
||||
@ -75,4 +74,4 @@ supplier_urls = [
|
||||
|
||||
# Redirect any other patterns
|
||||
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='supplier-index'),
|
||||
]
|
||||
]
|
||||
|
@ -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.urls import reverse
|
||||
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.edit import UpdateView, DeleteView, CreateView
|
||||
|
||||
from part.models import Part
|
||||
from .models import Supplier, SupplierPart
|
||||
|
||||
from .forms import EditSupplierForm
|
||||
from .forms import EditSupplierPartForm
|
||||
|
||||
|
||||
class SupplierIndex(ListView):
|
||||
model = Supplier
|
||||
template_name = 'supplier/index.html'
|
||||
@ -76,9 +77,16 @@ class SupplierPartCreate(CreateView):
|
||||
initials = super(SupplierPartCreate, self).get_initial().copy()
|
||||
|
||||
supplier_id = self.request.GET.get('supplier', None)
|
||||
part_id = self.request.GET.get('part', None)
|
||||
|
||||
if 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
|
||||
|
||||
|
@ -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 stock
|
||||
python InvenTree/manage.py makemigrations supplier
|
||||
python InvenTree/manage.py makemigrations track
|
||||
python InvenTree/manage.py migrate --run-syncdb
|
||||
python InvenTree/manage.py check
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
Django==1.11
|
||||
pillow==3.1.2
|
||||
djangorestframework==3.6.2
|
||||
django_filter==1.0.2
|
||||
django-simple-history==1.8.2
|
||||
coreapi==2.3.0
|
||||
pygments==2.2.0
|
||||
django-crispy-forms==1.7.2
|
||||
django-crispy-forms==1.7.2
|
||||
django-import-export==1.0.0
|
Loading…
Reference in New Issue
Block a user