mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
835144c87f
@ -48,9 +48,8 @@ INSTALLED_APPS = [
|
||||
|
||||
# InvenTree apps
|
||||
'part.apps.PartConfig',
|
||||
'supplier.apps.SupplierConfig',
|
||||
'stock.apps.StockConfig',
|
||||
'track.apps.TrackConfig',
|
||||
'supplier.apps.SupplierConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -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')]
|
||||
|
@ -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)
|
||||
|
||||
|
@ -9,7 +9,6 @@
|
||||
<li><a href="/part/">Parts</a></li>
|
||||
<li><a href="/stock/">Stock</a></li>
|
||||
<li><a href="/supplier/">Suppliers</a></li>
|
||||
<li><a href="/track/">Tracking</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
@ -2,6 +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):
|
||||
@ -12,5 +13,10 @@ 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)
|
||||
|
@ -44,6 +44,10 @@ class EditStockItemForm(forms.ModelForm):
|
||||
'part',
|
||||
'supplier_part',
|
||||
'location',
|
||||
'belongs_to',
|
||||
'serial',
|
||||
'batch',
|
||||
'quantity',
|
||||
'status'
|
||||
'status',
|
||||
'customer'
|
||||
]
|
||||
|
81
InvenTree/stock/migrations/0007_auto_20180416_0853.py
Normal file
81
InvenTree/stock/migrations/0007_auto_20180416_0853.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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,8 +121,9 @@ class StockItem(models.Model):
|
||||
|
||||
infinite = models.BooleanField(default=False)
|
||||
|
||||
# History of this item
|
||||
history = HistoricalRecords()
|
||||
@property
|
||||
def has_tracking_info(self):
|
||||
return self.tracking_info.all().count() > 0
|
||||
|
||||
@transaction.atomic
|
||||
def stocktake(self, count, user):
|
||||
@ -147,3 +172,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()
|
||||
|
@ -11,10 +11,35 @@
|
||||
<td>Part</td>
|
||||
<td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.name }}</td>
|
||||
</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>
|
||||
<td>Location</td>
|
||||
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
|
||||
</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>
|
||||
<td>Quantity</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
@ -47,6 +72,21 @@
|
||||
{% endif %}
|
||||
</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'>
|
||||
<a href="{% url 'stock-item-edit' item.id %}">
|
||||
<button class='btn btn-info'>Edit Stock Item</button>
|
||||
|
@ -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)
|
@ -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
|
@ -1,7 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TrackConfig(AppConfig):
|
||||
name = 'track'
|
@ -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'
|
||||
]
|
@ -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')]),
|
||||
),
|
||||
]
|
@ -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(),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -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)
|
@ -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__'
|
@ -1,5 +0,0 @@
|
||||
{% extends "create_edit_obj.html" %}
|
||||
|
||||
{% block obj_title %}
|
||||
Create a new tracked part
|
||||
{% endblock %}
|
@ -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 %}
|
@ -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 %}
|
@ -1,5 +0,0 @@
|
||||
{% extends "create_edit_obj.html" %}
|
||||
|
||||
{% block obj_title %}
|
||||
Edit tracked part information
|
||||
{% endblock %}
|
@ -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 %}
|
@ -1,5 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% endblock %}
|
@ -1,3 +0,0 @@
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -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'),
|
||||
]
|
@ -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())
|
1
Makefile
1
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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user