Merge pull request #53 from inventree/stock-track

Stock track
This commit is contained in:
Oliver 2018-04-16 20:46:52 +10:00 committed by GitHub
commit 835144c87f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 191 additions and 639 deletions

View File

@ -48,9 +48,8 @@ INSTALLED_APPS = [
# InvenTree apps # InvenTree apps
'part.apps.PartConfig', 'part.apps.PartConfig',
'supplier.apps.SupplierConfig',
'stock.apps.StockConfig', 'stock.apps.StockConfig',
'track.apps.TrackConfig', 'supplier.apps.SupplierConfig',
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -16,8 +16,6 @@ from django.conf.urls.static import static
from django.views.generic.base import RedirectView 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 project.urls import prj_urls, prj_part_urls, prj_cat_urls, prj_run_urls
# from track.urls import unique_urls, part_track_urls # from track.urls import unique_urls, part_track_urls
@ -70,7 +68,6 @@ urlpatterns = [
url(r'^part/', include(part_urls)), url(r'^part/', include(part_urls)),
url(r'^stock/', include(stock_urls)), url(r'^stock/', include(stock_urls)),
url(r'^supplier/', include(supplier_urls)), url(r'^supplier/', include(supplier_urls)),
url(r'^track/', include(tracking_urls)),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), 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) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Send any unknown URLs to the parts page # 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')]

View File

@ -111,7 +111,8 @@ class Part(models.Model):
units = models.CharField(max_length=20, default="pcs", blank=True) units = models.CharField(max_length=20, default="pcs", blank=True)
# Is this part "trackable"? # 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 # and can have their movements tracked
trackable = models.BooleanField(default=False) trackable = models.BooleanField(default=False)

View File

@ -9,7 +9,6 @@
<li><a href="/part/">Parts</a></li> <li><a href="/part/">Parts</a></li>
<li><a href="/stock/">Stock</a></li> <li><a href="/stock/">Stock</a></li>
<li><a href="/supplier/">Suppliers</a></li> <li><a href="/supplier/">Suppliers</a></li>
<li><a href="/track/">Tracking</a></li>
</ul> </ul>
</div> </div>
</nav> </nav>

View File

@ -2,6 +2,7 @@ from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin from simple_history.admin import SimpleHistoryAdmin
from .models import StockLocation, StockItem from .models import StockLocation, StockItem
from .models import StockItemTracking
class LocationAdmin(admin.ModelAdmin): class LocationAdmin(admin.ModelAdmin):
@ -12,5 +13,10 @@ class StockItemAdmin(SimpleHistoryAdmin):
list_display = ('part', 'quantity', 'location', 'status', 'updated') list_display = ('part', 'quantity', 'location', 'status', 'updated')
class StockTrackingAdmin(admin.ModelAdmin):
list_display = ('item', 'date', 'title')
admin.site.register(StockLocation, LocationAdmin) admin.site.register(StockLocation, LocationAdmin)
admin.site.register(StockItem, StockItemAdmin) admin.site.register(StockItem, StockItemAdmin)
admin.site.register(StockItemTracking, StockTrackingAdmin)

View File

@ -44,6 +44,10 @@ class EditStockItemForm(forms.ModelForm):
'part', 'part',
'supplier_part', 'supplier_part',
'location', 'location',
'belongs_to',
'serial',
'batch',
'quantity', 'quantity',
'status' 'status',
'customer'
] ]

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 08:53
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0006_auto_20180415_1011'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('stock', '0006_auto_20180415_0302'),
]
operations = [
migrations.CreateModel(
name='StockItemTracking',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(auto_now_add=True)),
('title', models.CharField(max_length=250)),
('description', models.CharField(blank=True, max_length=1024)),
],
),
migrations.RemoveField(
model_name='historicalstockitem',
name='history_user',
),
migrations.RemoveField(
model_name='historicalstockitem',
name='location',
),
migrations.RemoveField(
model_name='historicalstockitem',
name='part',
),
migrations.RemoveField(
model_name='historicalstockitem',
name='stocktake_user',
),
migrations.RemoveField(
model_name='historicalstockitem',
name='supplier_part',
),
migrations.AddField(
model_name='stockitem',
name='batch',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='stockitem',
name='belongs_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owned_parts', to='stock.StockItem'),
),
migrations.AddField(
model_name='stockitem',
name='customer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stockitems', to='supplier.Customer'),
),
migrations.AddField(
model_name='stockitem',
name='serial',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.DeleteModel(
name='HistoricalStockItem',
),
migrations.AddField(
model_name='stockitemtracking',
name='item',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracking_info', to='stock.StockItem'),
),
migrations.AddField(
model_name='stockitemtracking',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -3,9 +3,9 @@ from django.utils.translation import ugettext as _
from django.db import models, transaction from django.db import models, transaction
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.contrib.auth.models import User from django.contrib.auth.models import User
from simple_history.models import HistoricalRecords
from supplier.models import SupplierPart from supplier.models import SupplierPart
from supplier.models import Customer
from part.models import Part from part.models import Part
from InvenTree.models import InvenTreeTree from InvenTree.models import InvenTreeTree
@ -50,19 +50,43 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
class StockItem(models.Model): 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): def get_absolute_url(self):
return '/stock/item/{id}/'.format(id=self.id) 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') 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) 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, location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING,
related_name='items', blank=True, null=True) 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)]) quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)])
# Last time this item was updated (set automagically)
updated = models.DateField(auto_now=True) updated = models.DateField(auto_now=True)
# last time the stock was checked / counted # last time the stock was checked / counted
@ -97,8 +121,9 @@ class StockItem(models.Model):
infinite = models.BooleanField(default=False) infinite = models.BooleanField(default=False)
# History of this item @property
history = HistoricalRecords() def has_tracking_info(self):
return self.tracking_info.all().count() > 0
@transaction.atomic @transaction.atomic
def stocktake(self, count, user): def stocktake(self, count, user):
@ -147,3 +172,30 @@ class StockItem(models.Model):
n=self.quantity, n=self.quantity,
part=self.part.name, part=self.part.name,
loc=self.location.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()

View File

@ -11,10 +11,35 @@
<td>Part</td> <td>Part</td>
<td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.name }}</td> <td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.name }}</td>
</tr> </tr>
{% if item.belongs_to %}
<tr>
<td>Belongs To</td>
<td><a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a></td>
</tr>
{% elif item.location %}
<tr> <tr>
<td>Location</td> <td>Location</td>
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td> <td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
</tr> </tr>
{% endif %}
{% if item.serial %}
<tr>
<td>Serial</td>
<td>{{ item.serial }}</td>
</tr>
{% endif %}
{% if item.batch %}
<tr>
<td>Batch</td>
<td>{{ item.batch }}</td>
</tr>
{% endif %}
{% if item.customer %}
<tr>
<td>Customer</td>
<td>{{ item.customer.name }}</td>
</tr>
{% endif %}
<tr> <tr>
<td>Quantity</td> <td>Quantity</td>
<td>{{ item.quantity }}</td> <td>{{ item.quantity }}</td>
@ -47,6 +72,21 @@
{% endif %} {% endif %}
</table> </table>
{% if item.has_tracking_info %}
<h3>Stock Tracking</h3>
<ul class='list-group'>
{% for track in item.tracking_info.all %}
<li class='list-group-item'>
<b>{{ track.title }}</b>
{% if track.description %}
<br><br>{{ track.description }}</i>
{% endif %}
<span class='badge'>{{ track.date }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
<div class='container-fluid'> <div class='container-fluid'>
<a href="{% url 'stock-item-edit' item.id %}"> <a href="{% url 'stock-item-edit' item.id %}">
<button class='btn btn-info'>Edit Stock Item</button> <button class='btn btn-info'>Edit Stock Item</button>

View File

@ -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)

View File

@ -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

View File

@ -1,7 +0,0 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class TrackConfig(AppConfig):
name = 'track'

View File

@ -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'
]

View File

@ -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')]),
),
]

View File

@ -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(),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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)

View File

@ -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__'

View File

@ -1,5 +0,0 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Create a new tracked part
{% endblock %}

View File

@ -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 <b>{{ track.part.name }} SN-{{ track.serial }}</b> will be deleted.
{% endblock %}

View File

@ -1,59 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h3>Part tracking information</h3>
<table class="table table-striped">
<tr>
<td>Part</td>
<td><a href="{% url 'part-track' part.part.id %}">{{ part.part.name }}</a></td>
</tr>
<tr>
<td>Serial Number</td>
<td>{{ part.serial }}</td>
</tr>
<tr>
<td>Creation Date</td>
<td>{{ part.creation_date }}</td>
</tr>
{% if part.URL %}
<tr>
<td>URL</td>
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr>
{% endif %}
{% if part.customer %}
<tr>
<td>Customer</td>
<td>{{ part.customer }}</td>
</tr>
{% endif %}
<tr>
<td>Status</td>
<td>{{ part.get_status_display }}</td>
</tr>
</table>
{% if part.tracking_info.all|length > 0 %}
<p>Tracking information:</p>
<ul class='list-group'>
{% for info in part.tracking_info.all %}
<li class='list-group-item'>
{{ info.title }}
{% if info.note %}<br><i>{{ info.notes }}</i>{% endif %}<span class="badge">{{ info.date }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
<div class='container-fluid'>
<a href="{% url 'track-edit' part.id %}">
<button class="btn btn-info">Edit</button>
</a>
<a href="{% url 'track-delete' part.id %}">
<button class="btn btn-danger">Delete</button>
</a>
</div>
{% endblock %}

View File

@ -1,5 +0,0 @@
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Edit tracked part information
{% endblock %}

View File

@ -1,39 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h3>Part Tracking</h3>
<ul class='list-group'>
{% for part in parts.all %}
<li class='list-group-item'>
<a href="{% url 'track-detail' part.id %}">
{{ part.part.name }} - SN {{ part.serial }}
</a>
</li>
{% endfor %}
</ul>
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endif %}
<div class='container-fluid'>
<a href="{% url 'track-create' %}">
<button class="btn btn-success">New Tracked Part</button>
</a>
</div>
{% endblock %}

View File

@ -1,5 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% endblock %}

View File

@ -1,3 +0,0 @@
# from django.test import TestCase
# Create your tests here.

View File

@ -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<pk>[0-9]+)/?$', api.PartTrackingDetail.as_view(), name='parttrackinginfo-detail'),
url(r'^\?.*/?$', api.PartTrackingList.as_view()),
url(r'^$', api.PartTrackingList.as_view())
]
unique_api_urls = [
# Detail for a single unique part
url(r'^(?P<pk>[0-9]+)/?$', api.UniquePartDetail.as_view(), name='uniquepart-detail'),
# List all unique parts, with optional filters
url(r'^\?.*/?$', api.UniquePartList.as_view()),
url(r'^$', api.UniquePartList.as_view()),
]
"""
track_detail_urls = [
url(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<pk>\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'),
]

View File

@ -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())

View File

@ -18,7 +18,6 @@ migrate:
python InvenTree/manage.py makemigrations part python InvenTree/manage.py makemigrations part
python InvenTree/manage.py makemigrations stock python InvenTree/manage.py makemigrations stock
python InvenTree/manage.py makemigrations supplier python InvenTree/manage.py makemigrations supplier
python InvenTree/manage.py makemigrations track
python InvenTree/manage.py migrate --run-syncdb python InvenTree/manage.py migrate --run-syncdb
python InvenTree/manage.py check python InvenTree/manage.py check