diff --git a/InvenTree/part/migrations/0019_auto_20180416_1249.py b/InvenTree/part/migrations/0019_auto_20180416_1249.py new file mode 100644 index 0000000000..fb2717a7b6 --- /dev/null +++ b/InvenTree/part/migrations/0019_auto_20180416_1249.py @@ -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?'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index f0e1b7a436..3edd260f9b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -84,43 +84,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) + 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 (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) + purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?') def __str__(self): if self.IPN: diff --git a/InvenTree/part/templates/delete_obj.html b/InvenTree/part/templates/delete_obj.html index 2b85af7061..3cf123330d 100644 --- a/InvenTree/part/templates/delete_obj.html +++ b/InvenTree/part/templates/delete_obj.html @@ -12,7 +12,6 @@ Deletion title goes here

This is a permanent action and cannot be undone.

{% block del_body %} -Deletion body goes here {% endblock %}
{% csrf_token %} diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 31ae8919bd..0e9e28b6b1 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -4,18 +4,22 @@ {% include 'part/tabs.html' with tab='bom' %} +

Bill of Materials

+ + {% for bom_item in part.bom_items.all %} {% with sub_part=bom_item.sub_part %} - + + {% endwith %} {% endfor %} diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html new file mode 100644 index 0000000000..0c90630bc1 --- /dev/null +++ b/InvenTree/part/templates/part/build.html @@ -0,0 +1,13 @@ +{% extends "part/part_base.html" %} + +{% block details %} + +{% include 'part/tabs.html' with tab='build' %} + +

Build Part

+ +TODO +

+You can build {{ part.can_build }} of this part with current stock. + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 766a9bbf71..1059b65f04 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -4,9 +4,11 @@ {% include 'part/tabs.html' with tab='detail' %} +

Part Details

+
Part Description QuantityEdit
{{ sub_part.name }} {{ sub_part.description }}{{ bom_item.quantity }}{{ bom_item.sub_part.available_stock }}{{ bom_item.quantity }}Edit
- + @@ -37,6 +39,12 @@ +{% if part.minimum_stock > 0 %} + + + + +{% endif %}
NamePart name {{ part.name }}
Purchaseable {{ part.purchaseable }}
Minimum Stock{{ part.minimum_stock }}
diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html index 2d04fd9a14..85f899fb38 100644 --- a/InvenTree/part/templates/part/stock.html +++ b/InvenTree/part/templates/part/stock.html @@ -4,6 +4,8 @@ {% include 'part/tabs.html' with tab='stock' %} +

Part Stock

+ diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html index 5fbbee9bd8..1a832688c1 100644 --- a/InvenTree/part/templates/part/supplier.html +++ b/InvenTree/part/templates/part/supplier.html @@ -4,6 +4,8 @@ {% include 'part/tabs.html' with tab='suppliers' %} +

Part Suppliers

+
Link
diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 89a58ad82a..ce1cb7645b 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -2,7 +2,7 @@ Details {% if part.buildable %} BOM{{ part.bom_count }} - Build{{ part.can_build }} + Build{{ part.can_build }} {% endif %} {% if part.used_in_count > 0 %} Used In{% if part.used_in_count > 0 %}{{ part.used_in_count }}{% endif %} diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html index df6bdb1f24..86a57f0b69 100644 --- a/InvenTree/part/templates/part/used_in.html +++ b/InvenTree/part/templates/part/used_in.html @@ -4,6 +4,8 @@ {% include 'part/tabs.html' with tab='used' %} +

Used In

+
SKU
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 6c9af81bbb..eec6e744e8 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -41,6 +41,7 @@ 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'), diff --git a/InvenTree/stock/migrations/0009_auto_20180416_1253.py b/InvenTree/stock/migrations/0009_auto_20180416_1253.py new file mode 100644 index 0000000000..c1c8bab1c4 --- /dev/null +++ b/InvenTree/stock/migrations/0009_auto_20180416_1253.py @@ -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')]), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index d4aac549a5..cb86e76ff0 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -60,6 +60,12 @@ class StockItem(models.Model): 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') @@ -68,23 +74,29 @@ class StockItem(models.Model): # 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) + 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) + 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) + 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) + 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)]) diff --git a/InvenTree/supplier/migrations/0007_auto_20180416_1253.py b/InvenTree/supplier/migrations/0007_auto_20180416_1253.py new file mode 100644 index 0000000000..bf75d0fba9 --- /dev/null +++ b/InvenTree/supplier/migrations/0007_auto_20180416_1253.py @@ -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'), + ), + ] diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 5eec075edc..a9cf3f5453 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -42,18 +42,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') - SKU = models.CharField(max_length=100) + SKU = models.CharField(max_length=100, help_text='Supplier stock keeping unit') - manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.SET_NULL) - MPN = models.CharField(max_length=100, blank=True) + manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.SET_NULL, help_text='Manufacturer') + + 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 diff --git a/InvenTree/supplier/templates/supplier/partdetail.html b/InvenTree/supplier/templates/supplier/partdetail.html index 6531195486..dae2744c82 100644 --- a/InvenTree/supplier/templates/supplier/partdetail.html +++ b/InvenTree/supplier/templates/supplier/partdetail.html @@ -31,7 +31,7 @@
Part