From 76ee150ca49ddc1ba6d1c6ff7a98581b21aa66a3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Apr 2018 20:08:04 +1000 Subject: [PATCH 1/3] Moved tracking information to StockItem --- InvenTree/InvenTree/settings.py | 3 +- InvenTree/InvenTree/urls.py | 5 +- InvenTree/part/models.py | 3 +- InvenTree/part/templates/navbar.html | 1 - InvenTree/stock/forms.py | 6 +- .../migrations/0007_auto_20180416_0853.py | 81 ++++++++++++++++ InvenTree/stock/models.py | 56 ++++++++++- InvenTree/stock/templates/stock/item.html | 25 +++++ InvenTree/track/__init__.py | 0 InvenTree/track/admin.py | 15 --- InvenTree/track/api.py | 97 ------------------- InvenTree/track/apps.py | 7 -- InvenTree/track/forms.py | 28 ------ InvenTree/track/migrations/0001_initial.py | 47 --------- .../migrations/0002_auto_20180413_1440.py | 26 ----- .../migrations/0003_auto_20180415_0147.py | 31 ------ .../migrations/0004_parttrackinginfo_user.py | 23 ----- .../track/migrations/0005_uniquepart_url.py | 20 ---- InvenTree/track/migrations/__init__.py | 0 InvenTree/track/models.py | 78 --------------- InvenTree/track/serializers.py | 23 ----- InvenTree/track/templates/track/create.html | 5 - InvenTree/track/templates/track/delete.html | 11 --- InvenTree/track/templates/track/detail.html | 59 ----------- InvenTree/track/templates/track/edit.html | 5 - InvenTree/track/templates/track/index.html | 39 -------- InvenTree/track/templates/track/new.html | 5 - InvenTree/track/tests.py | 3 - InvenTree/track/urls.py | 41 -------- InvenTree/track/views.py | 63 ------------ Makefile | 1 - 31 files changed, 167 insertions(+), 640 deletions(-) create mode 100644 InvenTree/stock/migrations/0007_auto_20180416_0853.py delete mode 100644 InvenTree/track/__init__.py delete mode 100644 InvenTree/track/admin.py delete mode 100644 InvenTree/track/api.py delete mode 100644 InvenTree/track/apps.py delete mode 100644 InvenTree/track/forms.py delete mode 100644 InvenTree/track/migrations/0001_initial.py delete mode 100644 InvenTree/track/migrations/0002_auto_20180413_1440.py delete mode 100644 InvenTree/track/migrations/0003_auto_20180415_0147.py delete mode 100644 InvenTree/track/migrations/0004_parttrackinginfo_user.py delete mode 100644 InvenTree/track/migrations/0005_uniquepart_url.py delete mode 100644 InvenTree/track/migrations/__init__.py delete mode 100644 InvenTree/track/models.py delete mode 100644 InvenTree/track/serializers.py delete mode 100644 InvenTree/track/templates/track/create.html delete mode 100644 InvenTree/track/templates/track/delete.html delete mode 100644 InvenTree/track/templates/track/detail.html delete mode 100644 InvenTree/track/templates/track/edit.html delete mode 100644 InvenTree/track/templates/track/index.html delete mode 100644 InvenTree/track/templates/track/new.html delete mode 100644 InvenTree/track/tests.py delete mode 100644 InvenTree/track/urls.py delete mode 100644 InvenTree/track/views.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index b1ce5ec324..bc458b112d 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -48,9 +48,8 @@ INSTALLED_APPS = [ # InvenTree apps 'part.apps.PartConfig', - 'supplier.apps.SupplierConfig', 'stock.apps.StockConfig', - 'track.apps.TrackConfig', + 'supplier.apps.SupplierConfig', ] MIDDLEWARE = [ diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 202aa32447..18eb9784ec 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -16,8 +16,6 @@ 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 @@ -70,7 +68,6 @@ urlpatterns = [ 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')), @@ -84,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')] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 9bd13a0d40..d9bdc50311 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -111,7 +111,8 @@ class Part(models.Model): units = models.CharField(max_length=20, default="pcs", blank=True) # 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) diff --git a/InvenTree/part/templates/navbar.html b/InvenTree/part/templates/navbar.html index b71ddf4ffc..db8cae6aa7 100644 --- a/InvenTree/part/templates/navbar.html +++ b/InvenTree/part/templates/navbar.html @@ -9,7 +9,6 @@
  • Parts
  • Stock
  • Suppliers
  • -
  • Tracking
  • \ No newline at end of file diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index e05cdfc4fa..51655b4182 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -44,6 +44,10 @@ class EditStockItemForm(forms.ModelForm): 'part', 'supplier_part', 'location', + 'belongs_to', + 'serial', + 'batch', 'quantity', - 'status' + 'status', + 'customer' ] diff --git a/InvenTree/stock/migrations/0007_auto_20180416_0853.py b/InvenTree/stock/migrations/0007_auto_20180416_0853.py new file mode 100644 index 0000000000..0aa701198a --- /dev/null +++ b/InvenTree/stock/migrations/0007_auto_20180416_0853.py @@ -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), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index c7ad5f0b9b..d6bd3ba867 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -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 @@ -50,19 +50,43 @@ def before_delete_stock_location(sender, instance, using, **kwargs): 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) + # 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) + # 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) + + # 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) + + # Optional serial number + serial = models.PositiveIntegerField(blank=True, null=True) + + # Optional batch information + batch = models.CharField(max_length=100, blank=True) + + # 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 @@ -97,9 +121,6 @@ class StockItem(models.Model): infinite = models.BooleanField(default=False) - # History of this item - history = HistoricalRecords() - @transaction.atomic def stocktake(self, count, user): """ Perform item stocktake. @@ -147,3 +168,30 @@ class StockItem(models.Model): n=self.quantity, part=self.part.name, loc=self.location.name) + + +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() diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 92fb3258cb..c7955f0d7f 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -11,10 +11,35 @@ Part {{ item.part.name }} +{% if item.belongs_to %} + + Belongs To + {{ item.belongs_to }} + +{% elif item.location %} Location {{ item.location.name }} +{% endif %} +{% if item.serial %} + + Serial + {{ item.serial }} + +{% endif %} +{% if item.batch %} + + Batch + {{ item.batch }} + +{% endif %} +{% if item.customer %} + + Customer + {{ item.customer.name }} + +{% endif %} Quantity {{ item.quantity }} diff --git a/InvenTree/track/__init__.py b/InvenTree/track/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/InvenTree/track/admin.py b/InvenTree/track/admin.py deleted file mode 100644 index 751ad5711c..0000000000 --- a/InvenTree/track/admin.py +++ /dev/null @@ -1,15 +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) diff --git a/InvenTree/track/api.py b/InvenTree/track/api.py deleted file mode 100644 index c36f710e0d..0000000000 --- a/InvenTree/track/api.py +++ /dev/null @@ -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 diff --git a/InvenTree/track/apps.py b/InvenTree/track/apps.py deleted file mode 100644 index 7873ab8bfe..0000000000 --- a/InvenTree/track/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import unicode_literals - -from django.apps import AppConfig - - -class TrackConfig(AppConfig): - name = 'track' diff --git a/InvenTree/track/forms.py b/InvenTree/track/forms.py deleted file mode 100644 index 7ac159a510..0000000000 --- a/InvenTree/track/forms.py +++ /dev/null @@ -1,28 +0,0 @@ -from django import forms -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Submit - -from .models import UniquePart - - -class EditTrackedPartForm(forms.ModelForm): - - def __init__(self, *args, **kwargs): - super(EditTrackedPartForm, self).__init__(*args, **kwargs) - self.helper = FormHelper() - - self.helper.form_id = 'id-edit-part-form' - self.helper.form_class = 'blueForms' - self.helper.form_method = 'post' - - self.helper.add_input(Submit('submit', 'Submit')) - - class Meta: - model = UniquePart - fields = [ - 'part', - 'serial', - 'URL', - 'customer', - 'status' - ] diff --git a/InvenTree/track/migrations/0001_initial.py b/InvenTree/track/migrations/0001_initial.py deleted file mode 100644 index 8f84a6b6a4..0000000000 --- a/InvenTree/track/migrations/0001_initial.py +++ /dev/null @@ -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')]), - ), - ] diff --git a/InvenTree/track/migrations/0002_auto_20180413_1440.py b/InvenTree/track/migrations/0002_auto_20180413_1440.py deleted file mode 100644 index bc3a2ed5ea..0000000000 --- a/InvenTree/track/migrations/0002_auto_20180413_1440.py +++ /dev/null @@ -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(), - ), - ] diff --git a/InvenTree/track/migrations/0003_auto_20180415_0147.py b/InvenTree/track/migrations/0003_auto_20180415_0147.py deleted file mode 100644 index ee74a6c253..0000000000 --- a/InvenTree/track/migrations/0003_auto_20180415_0147.py +++ /dev/null @@ -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), - ), - ] diff --git a/InvenTree/track/migrations/0004_parttrackinginfo_user.py b/InvenTree/track/migrations/0004_parttrackinginfo_user.py deleted file mode 100644 index 76ddb5e2a3..0000000000 --- a/InvenTree/track/migrations/0004_parttrackinginfo_user.py +++ /dev/null @@ -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), - ), - ] diff --git a/InvenTree/track/migrations/0005_uniquepart_url.py b/InvenTree/track/migrations/0005_uniquepart_url.py deleted file mode 100644 index 50ea92eaa6..0000000000 --- a/InvenTree/track/migrations/0005_uniquepart_url.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-04-15 15:21 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('track', '0004_parttrackinginfo_user'), - ] - - operations = [ - migrations.AddField( - model_name='uniquepart', - name='URL', - field=models.URLField(blank=True), - ), - ] diff --git a/InvenTree/track/migrations/__init__.py b/InvenTree/track/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/InvenTree/track/models.py b/InvenTree/track/models.py deleted file mode 100644 index 8df03994a1..0000000000 --- a/InvenTree/track/models.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import unicode_literals -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 - """ - - def get_absolute_url(self): - return "/track/{id}/".format(id=self.id) - - 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() - - # Provide a URL for an external link - URL = models.URLField(blank=True) - - # 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) diff --git a/InvenTree/track/serializers.py b/InvenTree/track/serializers.py deleted file mode 100644 index 34f15e227d..0000000000 --- a/InvenTree/track/serializers.py +++ /dev/null @@ -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__' diff --git a/InvenTree/track/templates/track/create.html b/InvenTree/track/templates/track/create.html deleted file mode 100644 index 0760c37871..0000000000 --- a/InvenTree/track/templates/track/create.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "create_edit_obj.html" %} - -{% block obj_title %} -Create a new tracked part -{% endblock %} diff --git a/InvenTree/track/templates/track/delete.html b/InvenTree/track/templates/track/delete.html deleted file mode 100644 index 13490c000c..0000000000 --- a/InvenTree/track/templates/track/delete.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "delete_obj.html" %} - -{% block del_title %} -Are you sure you want to delete tracking info for this part? -{% endblock %} - -{% block del_body %} - -All tracking information for part {{ track.part.name }} SN-{{ track.serial }} will be deleted. - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/track/templates/track/detail.html b/InvenTree/track/templates/track/detail.html deleted file mode 100644 index 6626cd4b49..0000000000 --- a/InvenTree/track/templates/track/detail.html +++ /dev/null @@ -1,59 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -

    Part tracking information

    - - - - - - - - - - - - - - - {% if part.URL %} - - - - - {% endif %} - {% if part.customer %} - - - - - {% endif %} - - - - -
    Part{{ part.part.name }}
    Serial Number{{ part.serial }}
    Creation Date{{ part.creation_date }}
    URL{{ part.URL }}
    Customer{{ part.customer }}
    Status{{ part.get_status_display }}
    - -{% if part.tracking_info.all|length > 0 %} -

    Tracking information:

    - -{% endif %} - -
    - - - - - - -
    - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/track/templates/track/edit.html b/InvenTree/track/templates/track/edit.html deleted file mode 100644 index 9f23c94e9d..0000000000 --- a/InvenTree/track/templates/track/edit.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "create_edit_obj.html" %} - -{% block obj_title %} -Edit tracked part information -{% endblock %} diff --git a/InvenTree/track/templates/track/index.html b/InvenTree/track/templates/track/index.html deleted file mode 100644 index 94ed748a4a..0000000000 --- a/InvenTree/track/templates/track/index.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -

    Part Tracking

    - - - -{% if is_paginated %} - -{% endif %} - -
    - - - -
    - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/track/templates/track/new.html b/InvenTree/track/templates/track/new.html deleted file mode 100644 index 9cd5dfea90..0000000000 --- a/InvenTree/track/templates/track/new.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/track/tests.py b/InvenTree/track/tests.py deleted file mode 100644 index a79ca8be56..0000000000 --- a/InvenTree/track/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/InvenTree/track/urls.py b/InvenTree/track/urls.py deleted file mode 100644 index 5f7332f417..0000000000 --- a/InvenTree/track/urls.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.conf.urls import url, include - -from . import views - -""" -TODO - Implement JSON API for part serial number tracking -part_track_api_urls = [ - url(r'^(?P[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[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(r'^edit/?', views.TrackEdit.as_view(), name='track-edit'), - url(r'^delete/?', views.TrackDelete.as_view(), name='track-delete'), - - url('^.*$', views.TrackDetail.as_view(), name='track-detail'), -] - -tracking_urls = [ - # Detail view - url(r'^(?P\d+)/', include(track_detail_urls)), - - # Create a new tracking item - url(r'^new/?', views.TrackCreate.as_view(), name='track-create'), - - # List ALL tracked items - url(r'^.*$', views.TrackIndex.as_view(), name='track-index'), -] diff --git a/InvenTree/track/views.py b/InvenTree/track/views.py deleted file mode 100644 index 5af7b57f77..0000000000 --- a/InvenTree/track/views.py +++ /dev/null @@ -1,63 +0,0 @@ -from django.shortcuts import get_object_or_404 -from django.http import HttpResponseRedirect - -from django.views.generic import DetailView, ListView -from django.views.generic.edit import UpdateView, DeleteView, CreateView - -from part.models import Part -from .models import UniquePart - -from .forms import EditTrackedPartForm - - -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' - - -class TrackCreate(CreateView): - model = UniquePart - form_class = EditTrackedPartForm - template_name = 'track/create.html' - context_object_name = 'part' - - def get_initial(self): - initials = super(TrackCreate, self).get_initial().copy() - - part_id = self.request.GET.get('part', None) - - if part_id: - initials['part'] = get_object_or_404(Part, pk=part_id) - - return initials - - -class TrackEdit(UpdateView): - model = UniquePart - form_class = EditTrackedPartForm - template_name = 'track/edit.html' - context_obect_name = 'part' - - -class TrackDelete(DeleteView): - model = UniquePart - success_url = '/track' - template_name = 'track/delete.html' - context_object_name = 'track' - - def post(self, request, *args, **kwargs): - if 'confirm' in request.POST: - return super(TrackDelete, self).post(request, *args, **kwargs) - else: - return HttpResponseRedirect(self.get_object().get_absolute_url()) diff --git a/Makefile b/Makefile index ac9cb51062..e2a51d7779 100644 --- a/Makefile +++ b/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 From 09b357976d7483ab0b5e44bf279d5999606b2265 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Apr 2018 20:17:58 +1000 Subject: [PATCH 2/3] Admin for part tracking - Added 'has_tracking_info' function for StockItem --- InvenTree/stock/admin.py | 6 +++++- InvenTree/stock/models.py | 4 ++++ InvenTree/stock/templates/stock/item.html | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 99d70ff9f4..a3e5c188f4 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -2,7 +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): list_display = ('name', 'pathstring', 'description') @@ -12,5 +12,9 @@ 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) \ No newline at end of file diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index d6bd3ba867..689a04523c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -121,6 +121,10 @@ class StockItem(models.Model): infinite = models.BooleanField(default=False) + @property + def has_tracking_info(self): + return self.tracking_info.all().count() > 0 + @transaction.atomic def stocktake(self, count, user): """ Perform item stocktake. diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index c7955f0d7f..7266e692f4 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -72,6 +72,21 @@ {% endif %} +{% if item.has_tracking_info %} +

    Stock Tracking

    +
      +{% for track in item.tracking_info.all %} +
    • + {{ track.title }} + {% if track.description %} +

      {{ track.description }} + {% endif %} + {{ track.date }} +
    • +{% endfor %} +
    +{% endif %} +