From 1a2fb9e17042b0632fd89a3cbb67f3b0e64912d6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 22:27:36 +1000 Subject: [PATCH 01/21] Add 'has_variants' and 'variant_of' field for Part - StockItem cannot point to a part which is a template part --- .../migrations/0003_auto_20190525_2226.py | 24 +++++++++++++++++++ InvenTree/part/models.py | 12 ++++++++++ .../migrations/0002_auto_20190525_2226.py | 19 +++++++++++++++ InvenTree/stock/models.py | 14 ++++++++++- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 InvenTree/part/migrations/0003_auto_20190525_2226.py create mode 100644 InvenTree/stock/migrations/0002_auto_20190525_2226.py diff --git a/InvenTree/part/migrations/0003_auto_20190525_2226.py b/InvenTree/part/migrations/0003_auto_20190525_2226.py new file mode 100644 index 0000000000..3e805d460d --- /dev/null +++ b/InvenTree/part/migrations/0003_auto_20190525_2226.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2 on 2019-05-25 12:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0002_auto_20190520_2204'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='has_variants', + field=models.BooleanField(default=False, help_text='Is this part a template part?'), + ), + migrations.AddField( + model_name='part', + name='variant_of', + field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'active': True, 'has_variants': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.Part'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 385bae7bf4..d03c6030e3 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -192,6 +192,7 @@ class Part(models.Model): description: Longer form description of the part keywords: Optional keywords for improving part search results IPN: Internal part number (optional) + has_variants: If True, this part is a 'template' part and cannot be instantiated as a StockItem URL: Link to an external page with more information about this part (e.g. internal Wiki) image: Image of this part default_location: Where the item is normally stored (may be null) @@ -258,6 +259,17 @@ class Part(models.Model): variant = models.CharField(max_length=32, blank=True, help_text='Part variant or revision code') + has_variants = models.BooleanField(default=False, help_text='Is this part a template part?') + + variant_of = models.ForeignKey('part.Part', related_name='variants', + null=True, blank=True, + limit_choices_to={ + 'has_variants': True, + 'active': True, + }, + on_delete=models.SET_NULL, + help_text='Is this part a variant of another part?') + description = models.CharField(max_length=250, blank=False, help_text='Part description') keywords = models.CharField(max_length=250, blank=True, help_text='Part keywords to improve visibility in search results') diff --git a/InvenTree/stock/migrations/0002_auto_20190525_2226.py b/InvenTree/stock/migrations/0002_auto_20190525_2226.py new file mode 100644 index 0000000000..b8607c3ec7 --- /dev/null +++ b/InvenTree/stock/migrations/0002_auto_20190525_2226.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-05-25 12:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='part', + field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'has_variants': True}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 3680079b30..fdf88d5473 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -135,11 +135,18 @@ class StockItem(models.Model): }) if self.part is not None: + # A trackable part must have a serial number if self.part.trackable and not self.serial: raise ValidationError({ 'serial': _('Serial number must be set for trackable items') }) + # A template part cannot be instantiated as a StockItem + if self.part.has_variants: + raise ValidationError({ + 'part': _('Stock item cannot be created for a template Part') + }) + except Part.DoesNotExist: # This gets thrown if self.supplier_part is null # TODO - Find a test than can be perfomed... @@ -186,7 +193,12 @@ class StockItem(models.Model): } ) - part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='stock_items', help_text='Base part') + part = models.ForeignKey('part.Part', on_delete=models.CASCADE, + related_name='stock_items', help_text='Base part', + limit_choices_to={ + 'has_variants': True, + 'active': True, + }) supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL, help_text='Select a matching supplier part for this stock item') From bc778c145170a808d8c6f5a0a8223262e2e8ab6e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 22:43:47 +1000 Subject: [PATCH 02/21] Prevent a Part from both having variants and being a variant of something else --- InvenTree/part/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d03c6030e3..df948bb1c8 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -253,6 +253,15 @@ class Part(models.Model): else: return static('/img/blank_image.png') + def clean(self): + """ Perform cleaning operations for the Part model """ + + if self.has_variants and self.variant_of is not None: + raise ValidationError({ + 'variant_of': _("Part cannot be a variant of another part if it is already a template"), + 'has_variants': _("Part cannot be a template part if it is a variant of another part") + }) + name = models.CharField(max_length=100, blank=False, help_text='Part name', validators=[validators.validate_part_name] ) From d70110690bc61313dca7484b73361416424cdc27 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:09:04 +1000 Subject: [PATCH 03/21] Validate uniqueness for StockItems - If the Part is a variant of a template, ensure that the serial numbers are unique across all instances of the template - Prevent instantiation of a StockItem for a part which has variants --- .../migrations/0003_auto_20190525_2303.py | 19 ++++++++++++++ InvenTree/stock/models.py | 19 +++++++++++++- InvenTree/stock/views.py | 25 +++++++++++-------- 3 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 InvenTree/stock/migrations/0003_auto_20190525_2303.py diff --git a/InvenTree/stock/migrations/0003_auto_20190525_2303.py b/InvenTree/stock/migrations/0003_auto_20190525_2303.py new file mode 100644 index 0000000000..e8f7708cfe --- /dev/null +++ b/InvenTree/stock/migrations/0003_auto_20190525_2303.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-05-25 13:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0002_auto_20190525_2226'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='part', + field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'has_variants': False}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index fdf88d5473..46bab9ae73 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -115,6 +115,23 @@ class StockItem(models.Model): system=True ) + def validate_unique(self, exclude=None): + super(StockItem, self).validate_unique(exclude) + + # If the Part object is a variant (of a template part), + # ensure that the serial number is unique + # across all variants of the same template part + + + try: + if self.serial is not None and self.part.variant_of is not None: + if StockItem.objects.filter(part__variant_of=self.part.variant_of, serial=self.serial).exclude(id=self.id).exists(): + raise ValidationError({ + 'serial': _('A part with this serial number already exists for template part {part}'.format(part=self.part.variant_of)) + }) + except Part.DoesNotExist: + pass + def clean(self): """ Validate the StockItem object (separate to field validation) @@ -196,7 +213,7 @@ class StockItem(models.Model): part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='stock_items', help_text='Base part', limit_choices_to={ - 'has_variants': True, + 'has_variants': False, 'active': True, }) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index cd9fb615af..d729cb83fc 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -208,28 +208,31 @@ class StockItemCreate(AjaxCreateView): try: part = Part.objects.get(id=part_id) - parts = form.fields['supplier_part'].queryset - parts = parts.filter(part=part.id) + # Hide the 'part' field (as a valid part is selected) + form.fields['part'].widget = HiddenInput() + # If the part is NOT purchaseable, hide the supplier_part field if not part.purchaseable: form.fields['supplier_part'].widget = HiddenInput() - form.fields['supplier_part'].queryset = parts + else: + # Pre-select the allowable SupplierPart options + parts = form.fields['supplier_part'].queryset + parts = parts.filter(part=part.id) - # If there is one (and only one) supplier part available, pre-select it - all_parts = parts.all() - if len(all_parts) == 1: + form.fields['supplier_part'].queryset = parts - # TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate - form.fields['supplier_part'].initial = all_parts[0].id + # If there is one (and only one) supplier part available, pre-select it + all_parts = parts.all() + if len(all_parts) == 1: + + # TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate + form.fields['supplier_part'].initial = all_parts[0].id except Part.DoesNotExist: pass - # Hide the 'part' field - form.fields['part'].widget = HiddenInput() - # Otherwise if the user has selected a SupplierPart, we know what Part they meant! elif form['supplier_part'].value() is not None: pass From 0e684071fa9deb95298b203b29415801595b717f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:21:38 +1000 Subject: [PATCH 04/21] Display message if a part is a template or a variant --- InvenTree/part/templates/part/part_base.html | 22 +++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 8470677487..cc1ed3df9f 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -4,12 +4,24 @@ {% block content %} +{% if part.active == False %} +
+ This part is not active: +
+{% endif %} +{% if part.has_variants %} +
+ This part is a template part.
+ It is not a real part, but real parts can be based on this template. +
+{% endif %} +{% if part.variant_of %} +
+ This part is a variant of {{ part.variant_of.full_name }} +
+{% endif %} +
- {% if part.active == False %} -
- This part ({{ part.full_name }}) is not active: -
- {% endif %}
From 75d38489d7819fbfca14cfa6c505435261b85dff Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:26:46 +1000 Subject: [PATCH 05/21] Add ability to filter stock by variants for a templated part --- InvenTree/stock/api.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 0bff4a3873..e548c233b9 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -11,7 +11,7 @@ from django.urls import reverse from .models import StockLocation, StockItem from .models import StockItemTracking -from part.models import PartCategory +from part.models import Part, PartCategory from .serializers import StockItemSerializer, StockQuantitySerializer from .serializers import LocationSerializer @@ -263,11 +263,28 @@ class StockList(generics.ListCreateAPIView): we may wish to also request stock items from all child locations. """ + # Start with all objects + stock_list = StockItem.objects.all() + + # Does the client wish to filter by the Part ID? + part_id = self.request.query_params.get('part', None) + + if part_id: + try: + part = Part.objects.get(pk=part_id) + + # If the part is a Template part, select stock items for any "variant" parts under that template + if part.has_variants: + stock_list = stock_list.filter(part__in=[part.id for part in Part.objects.filter(variant_of=part_id)]) + else: + stock_list = stock_list.filter(part=part_id) + + except Part.DoesNotExist: + pass + # Does the client wish to filter by stock location? loc_id = self.request.query_params.get('location', None) - # Start with all objects - stock_list = StockItem.objects.all() if loc_id: try: @@ -312,7 +329,6 @@ class StockList(generics.ListCreateAPIView): ] filter_fields = [ - 'part', 'supplier_part', 'customer', 'belongs_to', From 39c46115983ac517b79d67e4691b92a169e74650 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:31:23 +1000 Subject: [PATCH 06/21] Add a tab for part variants --- InvenTree/part/templates/part/tabs.html | 5 +++++ InvenTree/part/templates/part/variants.html | 9 +++++++++ InvenTree/part/urls.py | 9 +++++---- 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 InvenTree/part/templates/part/variants.html diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index ff2cbbf4a0..855462c217 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -2,6 +2,11 @@ Details + {% if part.has_variants %} + + Variants {{ part.variants.count }} + + {% endif %} Stock {{ part.total_stock }} diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html new file mode 100644 index 0000000000..dd8effcc29 --- /dev/null +++ b/InvenTree/part/templates/part/variants.html @@ -0,0 +1,9 @@ +{% extends "part/part_base.html" %} +{% load static %} + +{% block details %} +{% include "part/tabs.html" with tab='variants' %} + +TODO + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 900ebe8127..c8582160bb 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -26,14 +26,15 @@ part_detail_urls = [ url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'), url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'), - url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), - url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), + url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'), + url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), + url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'), 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'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), + url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), + url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), From bbe66472ee44e2878c1cb5a14259f01836d165d3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:40:59 +1000 Subject: [PATCH 07/21] Display list of part variants --- InvenTree/part/templates/part/tabs.html | 2 +- InvenTree/part/templates/part/variants.html | 47 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 855462c217..a251c840de 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -25,7 +25,7 @@ Used In{% if part.used_in_count > 0 %}{{ part.used_in_count }}{% endif %} {% endif %} - {% if part.purchaseable %} + {% if part.purchaseable and part.has_variants == False %} Suppliers {{ part.supplier_count }} diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html index dd8effcc29..72427ca19c 100644 --- a/InvenTree/part/templates/part/variants.html +++ b/InvenTree/part/templates/part/variants.html @@ -4,6 +4,51 @@ {% block details %} {% include "part/tabs.html" with tab='variants' %} -TODO +
+
+

Part Variants

+
+
+
+
+
+ + + + + + + + + + + {% for variant in part.variants.all %} + + + + + + {% endfor %} + +
VariantDescriptionStock
+
+ + {% if variant.image %} + + {% endif %} +
+ {{ variant.full_name }}
{{ variant.description }}{{ variant.total_stock }}
+ +{% endblock %} + + +{% block js_ready %} +{{ block.super }} + + + $('#variant-table').bootstrapTable({ + search: true, + sortable: true, + }); {% endblock %} \ No newline at end of file From 3d57483f0e0da230b8bf32ab3596b4fa06eda7f1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:43:06 +1000 Subject: [PATCH 08/21] Add a 'new varian't button --- InvenTree/part/templates/part/variants.html | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html index 72427ca19c..8bcf458602 100644 --- a/InvenTree/part/templates/part/variants.html +++ b/InvenTree/part/templates/part/variants.html @@ -13,7 +13,15 @@

- +
+
+ {% if part.active %} + + {% endif %} +
+
+ +
From 08ac7e2a368dee9b9edcdbdf79bdc935b5cf84cc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:45:26 +1000 Subject: [PATCH 09/21] Rendering tweaks --- InvenTree/part/templates/part/part_base.html | 2 +- InvenTree/part/templates/part/variants.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index cc1ed3df9f..42bbbbf8fd 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -17,7 +17,7 @@ {% endif %} {% if part.variant_of %}
- This part is a variant of {{ part.variant_of.full_name }} + This part is a variant of {{ part.variant_of.full_name }}
{% endif %} diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html index 8bcf458602..cea712ea03 100644 --- a/InvenTree/part/templates/part/variants.html +++ b/InvenTree/part/templates/part/variants.html @@ -15,7 +15,7 @@
- {% if part.active %} + {% if part.has_variants and part.active %} {% endif %}
From 9c1c008f335d6cbdd7bd76eee8497a7c3ce1bd16 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:50:24 +1000 Subject: [PATCH 10/21] Show attachments for the Template part under attachments tab --- InvenTree/part/templates/part/attachments.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html index d0ccaf122d..3e28a31140 100644 --- a/InvenTree/part/templates/part/attachments.html +++ b/InvenTree/part/templates/part/attachments.html @@ -36,6 +36,20 @@
{% endfor %} +{% if part.variant_of and part.variant_of.attachments.count > 0 %} + + + +{% for attachment in part.variant_of.attachments.all %} + + + + + +{% endfor %} +{% endif %}
Variant
+ Attachments for template part {{ part.variant_of.full_name }} +
{{ attachment.basename }}{{ attachment.comment }}
{% endblock %} From c3d75deb16f552f62a16c5a81d55b1eca0bd644a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:55:16 +1000 Subject: [PATCH 11/21] More 'limit_choices_to' limitations for template parts --- .../migrations/0003_auto_20190525_2355.py | 19 +++++++++++++++++++ InvenTree/build/models.py | 1 + .../migrations/0004_auto_20190525_2354.py | 19 +++++++++++++++++++ InvenTree/company/models.py | 5 ++++- 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 InvenTree/build/migrations/0003_auto_20190525_2355.py create mode 100644 InvenTree/company/migrations/0004_auto_20190525_2354.py diff --git a/InvenTree/build/migrations/0003_auto_20190525_2355.py b/InvenTree/build/migrations/0003_auto_20190525_2355.py new file mode 100644 index 0000000000..fe127061bc --- /dev/null +++ b/InvenTree/build/migrations/0003_auto_20190525_2355.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-05-25 13:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0002_auto_20190520_2204'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='part', + field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'buildable': True, 'has_variants': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index b5f5aa10eb..d01c001ba8 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -50,6 +50,7 @@ class Build(models.Model): part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='builds', limit_choices_to={ + 'has_variants': False, 'buildable': True, 'active': True }, diff --git a/InvenTree/company/migrations/0004_auto_20190525_2354.py b/InvenTree/company/migrations/0004_auto_20190525_2354.py new file mode 100644 index 0000000000..d402cdeed5 --- /dev/null +++ b/InvenTree/company/migrations/0004_auto_20190525_2354.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-05-25 13:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0003_remove_supplierpart_minimum'), + ] + + operations = [ + migrations.AlterField( + model_name='supplierpart', + name='part', + field=models.ForeignKey(help_text='Select part', limit_choices_to={'has_variants': False, 'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index be59c2b66f..c62facb8ab 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -188,7 +188,10 @@ class SupplierPart(models.Model): part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='supplier_parts', - limit_choices_to={'purchaseable': True}, + limit_choices_to={ + 'purchaseable': True, + 'has_variants': False, + }, help_text='Select part', ) From c45a506a100e6454f7c8751b5d955dbf48bc762e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 23:58:31 +1000 Subject: [PATCH 12/21] Rename field part.has_variants to part.is_template --- .../migrations/0004_auto_20190525_2356.py | 19 +++++++++++++++ InvenTree/build/models.py | 2 +- .../migrations/0005_auto_20190525_2356.py | 19 +++++++++++++++ InvenTree/company/models.py | 2 +- .../migrations/0004_auto_20190525_2356.py | 24 +++++++++++++++++++ InvenTree/part/models.py | 10 ++++---- InvenTree/part/templates/part/part_base.html | 2 +- InvenTree/part/templates/part/tabs.html | 4 ++-- InvenTree/part/templates/part/variants.html | 2 +- InvenTree/stock/api.py | 2 +- .../migrations/0004_auto_20190525_2356.py | 19 +++++++++++++++ InvenTree/stock/models.py | 4 ++-- 12 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 InvenTree/build/migrations/0004_auto_20190525_2356.py create mode 100644 InvenTree/company/migrations/0005_auto_20190525_2356.py create mode 100644 InvenTree/part/migrations/0004_auto_20190525_2356.py create mode 100644 InvenTree/stock/migrations/0004_auto_20190525_2356.py diff --git a/InvenTree/build/migrations/0004_auto_20190525_2356.py b/InvenTree/build/migrations/0004_auto_20190525_2356.py new file mode 100644 index 0000000000..1a43e5bfc8 --- /dev/null +++ b/InvenTree/build/migrations/0004_auto_20190525_2356.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-05-25 13:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0003_auto_20190525_2355'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='part', + field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'buildable': True, 'is_template': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index d01c001ba8..ad482b8dc6 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -50,7 +50,7 @@ class Build(models.Model): part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='builds', limit_choices_to={ - 'has_variants': False, + 'is_template': False, 'buildable': True, 'active': True }, diff --git a/InvenTree/company/migrations/0005_auto_20190525_2356.py b/InvenTree/company/migrations/0005_auto_20190525_2356.py new file mode 100644 index 0000000000..fe02be0d84 --- /dev/null +++ b/InvenTree/company/migrations/0005_auto_20190525_2356.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-05-25 13:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0004_auto_20190525_2354'), + ] + + operations = [ + migrations.AlterField( + model_name='supplierpart', + name='part', + field=models.ForeignKey(help_text='Select part', limit_choices_to={'is_template': False, 'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index c62facb8ab..245264900d 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -190,7 +190,7 @@ class SupplierPart(models.Model): related_name='supplier_parts', limit_choices_to={ 'purchaseable': True, - 'has_variants': False, + 'is_template': False, }, help_text='Select part', ) diff --git a/InvenTree/part/migrations/0004_auto_20190525_2356.py b/InvenTree/part/migrations/0004_auto_20190525_2356.py new file mode 100644 index 0000000000..7b9a5fe6ac --- /dev/null +++ b/InvenTree/part/migrations/0004_auto_20190525_2356.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2 on 2019-05-25 13:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0003_auto_20190525_2226'), + ] + + operations = [ + migrations.RenameField( + model_name='part', + old_name='has_variants', + new_name='is_template', + ), + migrations.AlterField( + model_name='part', + name='variant_of', + field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'active': True, 'is_template': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.Part'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index df948bb1c8..1511b7a0d8 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -192,7 +192,7 @@ class Part(models.Model): description: Longer form description of the part keywords: Optional keywords for improving part search results IPN: Internal part number (optional) - has_variants: If True, this part is a 'template' part and cannot be instantiated as a StockItem + is_template: If True, this part is a 'template' part and cannot be instantiated as a StockItem URL: Link to an external page with more information about this part (e.g. internal Wiki) image: Image of this part default_location: Where the item is normally stored (may be null) @@ -256,10 +256,10 @@ class Part(models.Model): def clean(self): """ Perform cleaning operations for the Part model """ - if self.has_variants and self.variant_of is not None: + if self.is_template and self.variant_of is not None: raise ValidationError({ + 'is_template': _("Part cannot be a template part if it is a variant of another part"), 'variant_of': _("Part cannot be a variant of another part if it is already a template"), - 'has_variants': _("Part cannot be a template part if it is a variant of another part") }) name = models.CharField(max_length=100, blank=False, help_text='Part name', @@ -268,12 +268,12 @@ class Part(models.Model): variant = models.CharField(max_length=32, blank=True, help_text='Part variant or revision code') - has_variants = models.BooleanField(default=False, help_text='Is this part a template part?') + is_template = models.BooleanField(default=False, help_text='Is this part a template part?') variant_of = models.ForeignKey('part.Part', related_name='variants', null=True, blank=True, limit_choices_to={ - 'has_variants': True, + 'is_template': True, 'active': True, }, on_delete=models.SET_NULL, diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 42bbbbf8fd..748303df35 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -9,7 +9,7 @@ This part is not active:
{% endif %} -{% if part.has_variants %} +{% if part.is_template %}
This part is a template part.
It is not a real part, but real parts can be based on this template. diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index a251c840de..6dd4a0ab80 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -2,7 +2,7 @@ Details - {% if part.has_variants %} + {% if part.is_template %} Variants {{ part.variants.count }} @@ -25,7 +25,7 @@ Used In{% if part.used_in_count > 0 %}{{ part.used_in_count }}{% endif %} {% endif %} - {% if part.purchaseable and part.has_variants == False %} + {% if part.purchaseable and part.is_template == False %} Suppliers {{ part.supplier_count }} diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html index cea712ea03..1afcda25d3 100644 --- a/InvenTree/part/templates/part/variants.html +++ b/InvenTree/part/templates/part/variants.html @@ -15,7 +15,7 @@
- {% if part.has_variants and part.active %} + {% if part.is_template and part.active %} {% endif %}
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index e548c233b9..aec679a46f 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -274,7 +274,7 @@ class StockList(generics.ListCreateAPIView): part = Part.objects.get(pk=part_id) # If the part is a Template part, select stock items for any "variant" parts under that template - if part.has_variants: + if part.is_template: stock_list = stock_list.filter(part__in=[part.id for part in Part.objects.filter(variant_of=part_id)]) else: stock_list = stock_list.filter(part=part_id) diff --git a/InvenTree/stock/migrations/0004_auto_20190525_2356.py b/InvenTree/stock/migrations/0004_auto_20190525_2356.py new file mode 100644 index 0000000000..4b2f939eb2 --- /dev/null +++ b/InvenTree/stock/migrations/0004_auto_20190525_2356.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-05-25 13:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0003_auto_20190525_2303'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='part', + field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'is_template': False}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 46bab9ae73..5d297df946 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -159,7 +159,7 @@ class StockItem(models.Model): }) # A template part cannot be instantiated as a StockItem - if self.part.has_variants: + if self.part.is_template: raise ValidationError({ 'part': _('Stock item cannot be created for a template Part') }) @@ -213,7 +213,7 @@ class StockItem(models.Model): part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='stock_items', help_text='Base part', limit_choices_to={ - 'has_variants': False, + 'is_template': False, 'active': True, }) From 7881a67db4798e48e85369a0a17bcea37fa5e7d6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 May 2019 00:01:01 +1000 Subject: [PATCH 13/21] Calculate stock based on variant stock if part is a template --- InvenTree/part/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 1511b7a0d8..f7b577ccea 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -522,7 +522,10 @@ class Part(models.Model): Part may be stored in multiple locations """ - total = self.stock_entries.aggregate(total=Sum('quantity'))['total'] + if self.is_template: + total = sum([variant.total_stock for variant in self.variants.all()]) + else: + total = self.stock_entries.aggregate(total=Sum('quantity'))['total'] if total: return total From 75b21bdd8fe4599b23a892d3a36984b7c787f039 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 May 2019 00:20:03 +1000 Subject: [PATCH 14/21] Visual tweaks --- InvenTree/part/templates/part/stock.html | 6 ++++++ InvenTree/part/templates/part/variants.html | 3 ++- InvenTree/templates/stock_table.html | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html index 3b2bdec166..e9c22bf1d9 100644 --- a/InvenTree/part/templates/part/stock.html +++ b/InvenTree/part/templates/part/stock.html @@ -13,6 +13,12 @@

+{% if part.is_template %} +
+ Showing stock for all variants of {{ part.full_name }} +
+{% endif %} + {% include "stock_table.html" %} {% endblock %} diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html index 1afcda25d3..0cf9735364 100644 --- a/InvenTree/part/templates/part/variants.html +++ b/InvenTree/part/templates/part/variants.html @@ -39,7 +39,8 @@ {% endif %}
- {{ variant.full_name }} + {{ variant.full_name }} + {{ variant.description }} {{ variant.total_stock }} diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index f5411fabb8..fae7b04214 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -1,6 +1,8 @@
+ {% if part.is_template == False %} + {% endif %}

diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index 51d86f1c7a..1001ef11a1 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -124,7 +124,12 @@ function loadPartTable(table, url, options={}) { sortable: true, formatter: function(value, row, index, field) { + if (row.is_template) { + value = '' + value + ''; + } + var display = imageHoverIcon(row.image_url) + renderLink(value, row.url); + if (!row.active) { display = display + "INACTIVE"; } @@ -135,6 +140,14 @@ function loadPartTable(table, url, options={}) { sortable: true, field: 'description', title: 'Description', + formatter: function(value, row, index, field) { + + if (row.is_template) { + value = '' + value + ''; + } + + return value; + } }, { sortable: true, From f3b17b2164efff47139a0d0bd9377e5917b81b3c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 May 2019 00:32:18 +1000 Subject: [PATCH 17/21] Add some form fields (might have to remove later) --- InvenTree/part/forms.py | 1 + InvenTree/stock/forms.py | 1 + 2 files changed, 2 insertions(+) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 4b91d3bc22..cbc1cdaf8c 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -94,6 +94,7 @@ class EditPartForm(HelperForm): 'IPN', 'variant', 'is_template', + 'variant_of', 'description', 'keywords', 'URL', diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index ee8592bbb9..a05c8caef0 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -34,6 +34,7 @@ class CreateStockItemForm(HelperForm): 'location', 'quantity', 'batch', + 'serial', 'delete_on_deplete', 'status', 'notes', From d563e873edf8fa914c44aa4be7a44e1a1639e540 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 May 2019 00:35:52 +1000 Subject: [PATCH 18/21] Change some icons --- InvenTree/company/templates/company/partdetail.html | 2 +- InvenTree/part/templates/part/attachments.html | 2 +- InvenTree/static/script/inventree/bom.js | 2 +- InvenTree/static/script/inventree/build.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/company/templates/company/partdetail.html b/InvenTree/company/templates/company/partdetail.html index 812ceacf04..ab4a369469 100644 --- a/InvenTree/company/templates/company/partdetail.html +++ b/InvenTree/company/templates/company/partdetail.html @@ -84,7 +84,7 @@ InvenTree | {{ company.name }} - Parts diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html index 3e28a31140..493002d2f0 100644 --- a/InvenTree/part/templates/part/attachments.html +++ b/InvenTree/part/templates/part/attachments.html @@ -30,7 +30,7 @@ diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index b6e0c63a70..77dfbe740e 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -189,7 +189,7 @@ function loadBomTable(table, options) { if (options.editable) { cols.push({ formatter: function(value, row, index, field) { - var bEdit = ""; + var bEdit = ""; var bDelt = ""; return "
" + bEdit + bDelt + "
"; diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js index e3d1238e5e..0a9250ebb2 100644 --- a/InvenTree/static/script/inventree/build.js +++ b/InvenTree/static/script/inventree/build.js @@ -40,7 +40,7 @@ function loadAllocationTable(table, part_id, part, url, required, button) { formatter: function(value, row, index, field) { var html = value; - var bEdit = ""; + var bEdit = ""; var bDel = ""; html += "
" + bEdit + bDel + "
"; From 2138977b0744588716a5369984f094be6473e2a4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 May 2019 00:39:36 +1000 Subject: [PATCH 19/21] Include template attachments in attachment count --- InvenTree/part/models.py | 15 +++++++++++++++ InvenTree/part/templates/part/tabs.html | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index f7b577ccea..8a5a9125f3 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -771,6 +771,21 @@ class Part(models.Model): return data.export(file_format) + @property + def attachment_count(self): + """ Count the number of attachments for this part. + If the part is a variant of a template part, + include the number of attachments for the template part. + + """ + + n = self.attachments.count() + + if self.variant_of: + n += self.variant_of.attachments.count() + + return n + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 6dd4a0ab80..53a551f36c 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -40,7 +40,7 @@ {% endif %} - Attachments {% if part.attachments.all|length > 0 %}{{ part.attachments.all|length }}{% endif %} + Attachments {% if part.attachment_count > 0 %}{{ part.attachment_count }}{% endif %} From e5bb6284db0c873bd2c0472cd0dcbe0b4e88f944 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 May 2019 00:41:24 +1000 Subject: [PATCH 20/21] Add variant-of to part detail view --- InvenTree/part/templates/part/detail.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index b561d08ae5..068731da64 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -52,6 +52,12 @@ + {% if part.variant_of %} + + + + + {% endif %} {% if part.keywords %} From 41eb195940808901697f7922adaaef96a92b7ab4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 May 2019 00:42:40 +1000 Subject: [PATCH 21/21] Thanks, PEP --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8a5a9125f3..3b9a7aea60 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -774,7 +774,7 @@ class Part(models.Model): @property def attachment_count(self): """ Count the number of attachments for this part. - If the part is a variant of a template part, + If the part is a variant of a template part, include the number of attachments for the template part. """
{{ pb.quantity }} {{ pb.cost }}
- +
{{ attachment.comment }}
- +
Description {{ part.description }}
Variant Of{{ part.variant_of.full_name }}
Keywords