From 9dc41ba1228a91052f3bf35a0b17b4e36c20c954 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 17 Apr 2018 16:58:37 +1000 Subject: [PATCH 1/6] Fix requirements >= rather than == --- .../migrations/0004_auto_20180417_0657.py | 48 +++++++++++++++++++ InvenTree/build/models.py | 42 ++++++++++------ InvenTree/stock/models.py | 3 ++ requirements/base.txt | 18 +++---- 4 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 InvenTree/build/migrations/0004_auto_20180417_0657.py diff --git a/InvenTree/build/migrations/0004_auto_20180417_0657.py b/InvenTree/build/migrations/0004_auto_20180417_0657.py new file mode 100644 index 0000000000..23211a8d7c --- /dev/null +++ b/InvenTree/build/migrations/0004_auto_20180417_0657.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-17 06:57 +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', '0019_auto_20180416_1249'), + ('build', '0003_build_part'), + ] + + operations = [ + migrations.CreateModel( + name='BuildOutput', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)])), + ], + ), + migrations.RemoveField( + model_name='build', + name='part', + ), + migrations.RemoveField( + model_name='build', + name='quantity', + ), + migrations.AlterField( + model_name='build', + name='status', + field=models.PositiveIntegerField(choices=[(40, 'Cancelled'), (10, 'Pending'), (20, 'Allocated'), (50, 'Complete'), (30, 'Holding')], default=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AddField( + model_name='buildoutput', + name='build', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='outputs', to='build.Build'), + ), + migrations.AddField( + model_name='buildoutput', + name='part', + field=models.ForeignKey(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 a55abb15fc..960fce3edf 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.utils.translation import ugettext as _ + from django.db import models from django.core.validators import MinValueValidator -from InvenTree.helpers import ChoiceEnum - from part.models import Part class Build(models.Model): @@ -14,22 +14,34 @@ class Build(models.Model): Parts are then taken from stock """ - class BUILD_STATUS(ChoiceEnum): - # The build is 'pending' - no action taken yet - Pending = 10 + # Build status codes + PENDING = 10 + ALLOCATED = 20 + HOLDING = 30 + CANCELLED = 40 + COMPLETE = 50 - # The parts required for this build have been allocated - Allocated = 20 - - # The build has been cancelled (parts unallocated) - Cancelled = 30 - - # The build is complete! - Complete = 40 + BUILD_STATUS_CODES = { + PENDING : _("Pending"), + ALLOCATED : _("Allocated"), + HOLDING : _("Holding"), + CANCELLED : _("Cancelled"), + COMPLETE : _("Complete"), + } # Status of the build - status = models.PositiveIntegerField(default=BUILD_STATUS.Pending.value, - choices=BUILD_STATUS.choices()) + status = models.PositiveIntegerField(default=PENDING, + choices=BUILD_STATUS_CODES.items(), + validators=[MinValueValidator(0)]) + + +class BuildOutput(models.Model): + """ + A build output represents a single build part/quantity combination + """ + + build = models.ForeignKey(Build, on_delete=models.CASCADE, + related_name='outputs') part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='builds') diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 358d11e1d8..c3a5514730 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1,5 +1,8 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals + 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 diff --git a/requirements/base.txt b/requirements/base.txt index b39b8ebd07..9c9b4ed8b5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,9 +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-import-export==1.0.0 \ No newline at end of file +Django>=1.11 +pillow>=5.0.0 +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-import-export>=1.0.0 \ No newline at end of file From 45c5edee4d71294d8ba6cc7fdd823b857b595f08 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 17 Apr 2018 18:11:34 +1000 Subject: [PATCH 2/6] Added 'salable' field to Part model --- InvenTree/part/forms.py | 1 + .../part/migrations/0020_part_salable.py | 20 +++++++++++++++++++ InvenTree/part/models.py | 14 +++++++++---- InvenTree/part/templates/part/detail.html | 10 +++++++--- InvenTree/part/templates/yesnolabel.html | 5 +++++ 5 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 InvenTree/part/migrations/0020_part_salable.py create mode 100644 InvenTree/part/templates/yesnolabel.html diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index f986e1a0ae..1b35767a31 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -28,6 +28,7 @@ class EditPartForm(forms.ModelForm): 'buildable', 'trackable', 'purchaseable', + 'salable', ] diff --git a/InvenTree/part/migrations/0020_part_salable.py b/InvenTree/part/migrations/0020_part_salable.py new file mode 100644 index 0000000000..9d24532273 --- /dev/null +++ b/InvenTree/part/migrations/0020_part_salable.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-17 08:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0019_auto_20180416_1249'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='salable', + field=models.BooleanField(default=False, help_text='Can this part be sold to customers?'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 3d2fe90628..16c349cf11 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1,15 +1,18 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals + +import os + from django.db import models from django.db.models import Sum from django.core.validators import MinValueValidator -from InvenTree.models import InvenTreeTree - -import os - from django.db.models.signals import pre_delete from django.dispatch import receiver +from InvenTree.models import InvenTreeTree +# from stock.models import StockLocation + class PartCategory(InvenTreeTree): """ PartCategory provides hierarchical organization of Part objects. @@ -121,6 +124,9 @@ class Part(models.Model): # Is this part "purchaseable"? purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?') + # Can this part be sold to customers? + salable = models.BooleanField(default=False, help_text="Can this part be sold to customers?") + def __str__(self): if self.IPN: return "{name} ({ipn})".format( diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 1059b65f04..0e1c7aae34 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -29,15 +29,19 @@ Buildable - {{ part.buildable }} + {% include "yesnolabel.html" with value=part.buildable %} Trackable - {{ part.trackable }} + {% include "yesnolabel.html" with value=part.trackable %} Purchaseable - {{ part.purchaseable }} + {% include "yesnolabel.html" with value=part.purchaseable %} + + + Salable + {% include "yesnolabel.html" with value=part.salable %} {% if part.minimum_stock > 0 %} diff --git a/InvenTree/part/templates/yesnolabel.html b/InvenTree/part/templates/yesnolabel.html new file mode 100644 index 0000000000..cdc6070560 --- /dev/null +++ b/InvenTree/part/templates/yesnolabel.html @@ -0,0 +1,5 @@ +{% if value %} +Yes +{% else %} +No +{% endif %} \ No newline at end of file From 982803a0a7096c294053041440d11fd28f453674 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 17 Apr 2018 18:23:24 +1000 Subject: [PATCH 3/6] Updated Part model - Added 'default_location' - Added 'default_supplier' --- InvenTree/part/forms.py | 2 ++ .../migrations/0021_part_default_location.py | 22 +++++++++++++++ .../migrations/0022_auto_20180417_0819.py | 27 +++++++++++++++++++ InvenTree/part/models.py | 12 +++++++++ InvenTree/part/templates/part/detail.html | 12 +++++++++ InvenTree/stock/views.py | 6 ++++- 6 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 InvenTree/part/migrations/0021_part_default_location.py create mode 100644 InvenTree/part/migrations/0022_auto_20180417_0819.py diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 1b35767a31..dce4f007d7 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -24,6 +24,8 @@ class EditPartForm(forms.ModelForm): 'description', 'IPN', 'URL', + 'default_location', + 'default_supplier', 'minimum_stock', 'buildable', 'trackable', diff --git a/InvenTree/part/migrations/0021_part_default_location.py b/InvenTree/part/migrations/0021_part_default_location.py new file mode 100644 index 0000000000..0690abb280 --- /dev/null +++ b/InvenTree/part/migrations/0021_part_default_location.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-17 08:12 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0010_stockitem_build'), + ('part', '0020_part_salable'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='default_location', + field=models.ForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, to='stock.StockLocation'), + ), + ] diff --git a/InvenTree/part/migrations/0022_auto_20180417_0819.py b/InvenTree/part/migrations/0022_auto_20180417_0819.py new file mode 100644 index 0000000000..ed7c74db03 --- /dev/null +++ b/InvenTree/part/migrations/0022_auto_20180417_0819.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-17 08:19 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('supplier', '0007_auto_20180416_1253'), + ('part', '0021_part_default_location'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='default_supplier', + field=models.ForeignKey(blank=True, help_text='Default supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='supplier.SupplierPart'), + ), + migrations.AlterField( + model_name='part', + name='default_location', + field=models.ForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='stock.StockLocation'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 16c349cf11..d44b2ae6b5 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -106,6 +106,18 @@ class Part(models.Model): image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True) + default_location = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL, + blank=True, null=True, + help_text='Where is this item normally stored?', + related_name='default_parts') + + # Default supplier part + default_supplier = models.ForeignKey('supplier.SupplierPart', + on_delete=models.SET_NULL, + blank=True, null=True, + help_text='Default supplier part', + related_name='default_parts') + # Minimum "allowed" stock level minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text='Minimum allowed stock level') diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 0e1c7aae34..b45d62aa15 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -23,6 +23,18 @@ {% endif %} +{% if part.default_location %} + + Default Location + {{ part.default_location.pathstring }} + +{% endif %} +{% if part.default_supplier %} + + Default Supplier + {{ part.default_supplier.supplier.name }} | {{ part.default_supplier.SKU }} + +{% endif %} Units {{ part.units }} diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index cc715c0669..089953b16f 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -88,7 +88,11 @@ class StockItemCreate(CreateView): loc_id = self.request.GET.get('location', None) if part_id: - initials['part'] = get_object_or_404(Part, pk=part_id) + part = get_object_or_404(Part, pk=part_id) + if part: + initials['part'] = get_object_or_404(Part, pk=part_id) + initials['location'] = part.default_location + initials['supplier_part'] = part.default_supplier if loc_id: initials['location'] = get_object_or_404(StockLocation, pk=loc_id) From 256f8eb9243f0692893ada06d48c1dca6ee3b174 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 17 Apr 2018 18:29:40 +1000 Subject: [PATCH 4/6] Add 'batch' field to BuildOutput object This 'batch' output will be copied to any parts produced as part of this build --- InvenTree/build/admin.py | 8 +++++++- .../migrations/0005_buildoutput_batch.py | 20 +++++++++++++++++++ InvenTree/build/models.py | 7 +++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 InvenTree/build/migrations/0005_buildoutput_batch.py diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index dc9590890e..cf3d818636 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import Build +from .models import Build, BuildOutput class BuildAdmin(admin.ModelAdmin): @@ -11,4 +11,10 @@ class BuildAdmin(admin.ModelAdmin): list_display = ('status', ) +class BuildOutputAdmin(admin.ModelAdmin): + + list_display = ('build', 'part', 'batch', 'quantity', ) + + admin.site.register(Build, BuildAdmin) +admin.site.register(BuildOutput, BuildOutputAdmin) \ No newline at end of file diff --git a/InvenTree/build/migrations/0005_buildoutput_batch.py b/InvenTree/build/migrations/0005_buildoutput_batch.py new file mode 100644 index 0000000000..b53c83270e --- /dev/null +++ b/InvenTree/build/migrations/0005_buildoutput_batch.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-17 08:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0004_auto_20180417_0657'), + ] + + operations = [ + migrations.AddField( + model_name='buildoutput', + name='batch', + field=models.CharField(blank=True, help_text='Batch code for this build output', max_length=100), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 960fce3edf..4a266d0bf1 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -40,12 +40,19 @@ class BuildOutput(models.Model): A build output represents a single build part/quantity combination """ + batch = models.CharField(max_length=100, blank=True, + help_text='Batch code for this build output') + + # Reference to the build object of which this output is a part + # A build can have multiple outputs build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='outputs') + # A reference to the part being built part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='builds') + # How many parts to build? quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Number of parts to build') From 0b40197cd20d7fe18eccf8a01898bdb1ee1be6e7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 17 Apr 2018 20:25:43 +1000 Subject: [PATCH 5/6] Update 'Build' - Part model now has active_builds and inactive_builds properties - --- InvenTree/build/admin.py | 19 +++--- .../migrations/0006_auto_20180417_0933.py | 66 +++++++++++++++++++ .../migrations/0007_auto_20180417_1025.py | 21 ++++++ InvenTree/build/models.py | 54 ++++++++++----- InvenTree/part/models.py | 22 +++++++ InvenTree/part/templates/part/build.html | 29 ++++++-- InvenTree/part/templates/part/build_list.html | 20 ++++++ 7 files changed, 201 insertions(+), 30 deletions(-) create mode 100644 InvenTree/build/migrations/0006_auto_20180417_0933.py create mode 100644 InvenTree/build/migrations/0007_auto_20180417_1025.py create mode 100644 InvenTree/part/templates/part/build_list.html diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index cf3d818636..dfbefc45e1 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -3,18 +3,19 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import Build, BuildOutput +from .models import Build class BuildAdmin(admin.ModelAdmin): - list_display = ('status', ) - - -class BuildOutputAdmin(admin.ModelAdmin): - - list_display = ('build', 'part', 'batch', 'quantity', ) - + list_display = ('part', + 'status', + 'batch', + 'quantity', + 'creation_date', + 'completion_date', + 'title', + 'notes', + ) admin.site.register(Build, BuildAdmin) -admin.site.register(BuildOutput, BuildOutputAdmin) \ No newline at end of file diff --git a/InvenTree/build/migrations/0006_auto_20180417_0933.py b/InvenTree/build/migrations/0006_auto_20180417_0933.py new file mode 100644 index 0000000000..3ed5efb74e --- /dev/null +++ b/InvenTree/build/migrations/0006_auto_20180417_0933.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-17 09:33 +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', '0022_auto_20180417_0819'), + ('build', '0005_buildoutput_batch'), + ] + + operations = [ + migrations.RemoveField( + model_name='buildoutput', + name='build', + ), + migrations.RemoveField( + model_name='buildoutput', + name='part', + ), + migrations.AddField( + model_name='build', + name='batch', + field=models.CharField(blank=True, help_text='Batch code for this build output', max_length=100, null=True), + ), + migrations.AddField( + model_name='build', + name='completion_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='build', + name='creation_date', + field=models.DateField(auto_now=True), + ), + migrations.AddField( + model_name='build', + name='notes', + field=models.CharField(blank=True, max_length=500), + ), + migrations.AddField( + model_name='build', + name='part', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'), + preserve_default=False, + ), + migrations.AddField( + model_name='build', + name='quantity', + field=models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='build', + name='title', + field=models.CharField(default='Build title', help_text='Brief description of the build', max_length=100), + preserve_default=False, + ), + migrations.DeleteModel( + name='BuildOutput', + ), + ] diff --git a/InvenTree/build/migrations/0007_auto_20180417_1025.py b/InvenTree/build/migrations/0007_auto_20180417_1025.py new file mode 100644 index 0000000000..d50af03384 --- /dev/null +++ b/InvenTree/build/migrations/0007_auto_20180417_1025.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-17 10:25 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0006_auto_20180417_0933'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='status', + field=models.PositiveIntegerField(choices=[(40, 'Complete'), (10, 'Pending'), (20, 'Holding'), (30, 'Cancelled')], default=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 4a266d0bf1..ef4b495ced 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -15,44 +15,64 @@ class Build(models.Model): """ # Build status codes - PENDING = 10 - ALLOCATED = 20 - HOLDING = 30 - CANCELLED = 40 - COMPLETE = 50 + PENDING = 10 # Build is pending / active + HOLDING = 20 # Build is currently being held + CANCELLED = 30 # Build was cancelled + COMPLETE = 40 # Build is complete BUILD_STATUS_CODES = { PENDING : _("Pending"), - ALLOCATED : _("Allocated"), HOLDING : _("Holding"), CANCELLED : _("Cancelled"), COMPLETE : _("Complete"), } + batch = models.CharField(max_length=100, blank=True, null=True, + help_text='Batch code for this build output') + # Status of the build status = models.PositiveIntegerField(default=PENDING, choices=BUILD_STATUS_CODES.items(), validators=[MinValueValidator(0)]) -class BuildOutput(models.Model): - """ - A build output represents a single build part/quantity combination - """ + # Date the build model was 'created' + creation_date = models.DateField(auto_now=True, editable=False) - batch = models.CharField(max_length=100, blank=True, - help_text='Batch code for this build output') + # Date the build was 'completed' + completion_date = models.DateField(null=True, blank=True) - # Reference to the build object of which this output is a part - # A build can have multiple outputs - build = models.ForeignKey(Build, on_delete=models.CASCADE, - related_name='outputs') + # Brief build title + title = models.CharField(max_length=100, help_text='Brief description of the build') # A reference to the part being built + # Only 'buildable' parts can be selected part = models.ForeignKey(Part, on_delete=models.CASCADE, - related_name='builds') + related_name='builds', + limit_choices_to={'buildable': True}, + ) # How many parts to build? quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Number of parts to build') + + # Notes can be attached to each build output + notes = models.CharField(max_length=500, blank=True) + + @property + def is_active(self): + """ Is this build active? + An active build is either: + - Pending + - Holding + """ + + return self.status in [ + self.PENDING, + self.HOLDING + ] + + @property + def is_complete(self): + return self.status == self.COMPLETE diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d44b2ae6b5..e76e01ce06 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -183,6 +183,28 @@ class Part(models.Model): return total + @property + def active_builds(self): + """ Return a list of outstanding builds. + Builds marked as 'complete' or 'cancelled' are ignored + """ + + return [b for b in self.builds.all() if b.is_active] + + @property + def inactive_builds(self): + """ Return a list of inactive builds + """ + + return [b for b in self.builds.all() if not b.is_active] + + @property + def quantity_being_built(self): + """ Return the current number of parts currently being built + """ + + return sum([b.quantity for b in self.active_builds]) + @property def total_stock(self): """ Return the total stock quantity for this part. diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index 0c90630bc1..3aae16377f 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -4,10 +4,31 @@ {% include 'part/tabs.html' with tab='build' %} -

Build Part

+

Part Builds

-TODO -

-You can build {{ part.can_build }} of this part with current stock. + + + + + + + + +{% if part.active_builds|length > 0 %} + + + +{% include "part/build_list.html" with builds=part.active_builds %} +{% endif %} + +{% if part.inactive_builds|length > 0 %} + + + + +{% include "part/build_list.html" with builds=part.inactive_builds %} +{% endif %} + +
TitleQuantityStatusCompletion Date
Active Builds
Inactive Builds
{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/build_list.html b/InvenTree/part/templates/part/build_list.html new file mode 100644 index 0000000000..a42518964a --- /dev/null +++ b/InvenTree/part/templates/part/build_list.html @@ -0,0 +1,20 @@ +{% for build in builds %} + + {{ build.title }} + {{ build.quantity }} + + {% if build.status == build.PENDING %} + + {% elif build.status == build.HOLDING %} + + {% elif build.status == build.CANCELLED %} + + {% elif build.status == build.COMPLETE %} + + {% endif %} + {{ build.get_status_display }} + + + {{ build.completion_date }} + +{% endfor %} \ No newline at end of file From 11b9fb10d877719f6e4f0f185a830d20931e28b4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 17 Apr 2018 22:26:57 +1000 Subject: [PATCH 6/6] Parts now know how many builds they are allocated to - allocated_builds returns lists of active builds this part is allocated to - allocated_build_count returns the total number of this part allocated to builds - allocation_count returns total number of allocated parts (in the future this will also include those parts allocated to customer orders) --- InvenTree/part/models.py | 48 +++++++++++++++++-- InvenTree/part/templates/part/allocation.html | 29 +++++++++++ InvenTree/part/templates/part/build_list.html | 14 +----- .../part/templates/part/build_status.html | 11 +++++ InvenTree/part/templates/part/part_base.html | 41 +++++++++++----- InvenTree/part/templates/part/tabs.html | 5 +- InvenTree/part/templates/part/used_in.html | 2 + InvenTree/part/urls.py | 1 + 8 files changed, 123 insertions(+), 28 deletions(-) create mode 100644 InvenTree/part/templates/part/allocation.html create mode 100644 InvenTree/part/templates/part/build_status.html diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index e76e01ce06..2002165909 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -158,9 +158,11 @@ class Part(models.Model): 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 + total = self.total_stock + + total -= self.allocation_count + + return max(total, 0) @property def can_build(self): @@ -181,7 +183,7 @@ class Part(models.Model): if total is None or n < total: total = n - return total + return max(total, 0) @property def active_builds(self): @@ -205,6 +207,44 @@ class Part(models.Model): return sum([b.quantity for b in self.active_builds]) + @property + def allocated_builds(self): + """ Return list of builds to which this part is allocated + """ + + builds = [] + + for item in self.used_in.all(): + for build in item.part.active_builds: + builds.append(build) + + return builds + + @property + def allocated_build_count(self): + """ Return the total number of this that are allocated for builds + """ + + total = 0 + + for item in self.used_in.all(): + for build in item.part.active_builds: + n = build.quantity * item.quantity + total += n + + return total + + @property + def allocation_count(self): + """ Return true if any of this part is allocated + - To another build + - To a customer order + """ + + return sum([ + self.allocated_build_count, + ]) + @property def total_stock(self): """ Return the total stock quantity for this part. diff --git a/InvenTree/part/templates/part/allocation.html b/InvenTree/part/templates/part/allocation.html new file mode 100644 index 0000000000..35b8426815 --- /dev/null +++ b/InvenTree/part/templates/part/allocation.html @@ -0,0 +1,29 @@ +{% extends "part/part_base.html" %} + +{% block details %} + +{% include "part/tabs.html" with tab="allocation" %} + +

Part Allocation

+ +{% if part.allocated_build_count > 0 %} +

Allocated to Part Builds

+ + + + + + + +{% for build in part.allocated_builds %} + + + + + + +{% endfor %} +
BuildMakingAlloctedStatus
{{ build.title }}{{ build.part.name }}Quantity{% include "part/build_status.html" with build=build %}
+{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/build_list.html b/InvenTree/part/templates/part/build_list.html index a42518964a..8798323204 100644 --- a/InvenTree/part/templates/part/build_list.html +++ b/InvenTree/part/templates/part/build_list.html @@ -3,18 +3,8 @@ {{ build.title }} {{ build.quantity }} - {% if build.status == build.PENDING %} - - {% elif build.status == build.HOLDING %} - - {% elif build.status == build.CANCELLED %} - - {% elif build.status == build.COMPLETE %} - - {% endif %} - {{ build.get_status_display }} - + {% include "part/build_status.html" with build=build %} - {{ build.completion_date }} + {% if build.completion_date %}{{ build.completion_date }}{% endif %} {% endfor %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/build_status.html b/InvenTree/part/templates/part/build_status.html new file mode 100644 index 0000000000..1702ceccae --- /dev/null +++ b/InvenTree/part/templates/part/build_status.html @@ -0,0 +1,11 @@ +{% if build.status == build.PENDING %} + +{% elif build.status == build.HOLDING %} + +{% elif build.status == build.CANCELLED %} + +{% elif build.status == build.COMPLETE %} + +{% endif %} +{{ build.get_status_display }} + \ No newline at end of file diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 69840829e3..a9f66a76b9 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -22,11 +22,6 @@ {% if part.description %}

{{ part.description }}

{% endif %} - - - -
- {% if part.IPN %} @@ -39,15 +34,22 @@ {% endif %} + + + + +
+

Stock Status - {{ part.available_stock }} available

+
IPN{{ part.URL }}
- + @@ -62,6 +64,23 @@ {% endif %} + {% if part.quantity_being_built > 0 %} + + + + + {% endif %} + {% endif %} + {% if part.allocation_count > 0 %} + + + {% if part.allocation_count > part.total_stock %} + + {% endif %}
Available StockIn Stock - {% if part.available_stock == 0 %} - {{ part.available_stock }} - {% elif part.available_stock < part.minimum_stock %} - {{ part.available_stock }} + {% if part.stock == 0 %} + {{ part.total_stock }} + {% elif part.stock < part.minimum_stock %} + {{ part.total_stock }} {% else %} - {{ part.available_stock }} + {{ part.total_stock }} {% endif %}
Underway{{ part.quantity_being_built }}
Allocated{{ part.allocation_count }} + {% else %} + {{ part.allocation_count }} {{ part.total_stock }} + {% endif %} +
diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index ce1cb7645b..da0b1c9ec2 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -7,7 +7,10 @@ {% if part.used_in_count > 0 %} Used In{% if part.used_in_count > 0 %}{{ part.used_in_count }}{% endif %} {% endif %} - Stock {{ part.available_stock }} + Stock {{ part.total_stock }} + {% if part.allocation_count > 0 %} + Allocated {{ part.allocation_count }} + {% endif %} {% if part.purchaseable %} Suppliers {{ part.supplier_count }} diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html index 86a57f0b69..4a691aac23 100644 --- a/InvenTree/part/templates/part/used_in.html +++ b/InvenTree/part/templates/part/used_in.html @@ -9,11 +9,13 @@ + {% for item in part.used_in.all %} + {% endfor %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index ab95b2f43f..97888dc20b 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -44,6 +44,7 @@ part_detail_urls = [ 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'), # Any other URLs go to the part detail page
PartUses Description
{{ item.part.name }}{{ item.quantity }} {{ item.part.description }}