Add pages for part tracking

- Edit / Delete / Create tracking info
- Improvements to many pages
This commit is contained in:
Oliver 2018-04-16 00:30:57 +10:00
parent 55b533d3ef
commit 8e6de1b832
26 changed files with 302 additions and 58 deletions

View File

@ -28,6 +28,7 @@ class EditPartForm(forms.ModelForm):
'URL',
'minimum_stock',
'trackable',
'purchaseable',
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-15 14:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0016_auto_20180415_0316'),
]
operations = [
migrations.AddField(
model_name='part',
name='purchaseable',
field=models.BooleanField(default=True),
),
]

View File

@ -115,6 +115,9 @@ class Part(models.Model):
# and can have their movements tracked
trackable = models.BooleanField(default=False)
# Is this part "purchaseable"?
purchaseable = models.BooleanField(default=True)
def __str__(self):
if self.IPN:
return "{name} ({ipn})".format(
@ -128,6 +131,10 @@ class Part(models.Model):
verbose_name_plural = "Parts"
#unique_together = (("name", "category"),)
@property
def tracked_parts(self):
return self.serials.order_by('serial')
@property
def stock(self):
""" Return the total stock quantity for this part.

View File

@ -4,12 +4,40 @@
{% include 'part/tabs.html' with tab='detail' %}
<table class='table table-striped'>
<tr>
<td>Name</td>
<td>{{ part.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ part.decription }}</td>
</tr>
<tr>
<td>Category</td>
<td>
{% if part.category %}
<a href="{% url 'category-detail' part.category.id %}">{{ part.category.name }}</a>
{% endif %}
</td>
</tr>
<tr>
<td>Units</td>
<td>{{ part.units }}</td>
</tr>
<tr>
<td>Trackable</td>
<td>{{ part.trackable }}</td>
</tr>
<tr>
<td>Purchaseable</td>
<td>{{ part.purchaseable }}</td>
</tr>
</table>
Part details go here...
<br>
<div class='container-fluid'>
<a href="{% url 'part-edit' part.id %}"><button class="btn btn-info">Edit Part</button></a>
<a href="{% url 'part-delete' part.id %}"><button class="btn btn-danger">Delete Part</button></a>
</div>
{% endblock %}

View File

@ -6,33 +6,54 @@
{% include "part/cat_link.html" with category=part.category %}
<div class="media">
<div class="media-left">
<img class="part-thumb"
{% if part.image %}
src="{{ part.image.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
</div>
<div class="media-body">
<h4>{{ part.name }}</h4>
{% if part.description %}
<p><i>{{ part.description }}</i></p>
{% endif %}
{% if part.IPN %}
<p><b>IPN:</b> {{ part.IPN }}</p>
{% endif %}
{% if part.URL %}
<p>{% include 'url.html' with url=part.URL %}</p>
{% endif %}
<div class="row">
<div class="col-sm-6">
<div class="media">
<div class="media-left">
<img class="part-thumb"
{% if part.image %}
src="{{ part.image.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
</div>
<div class="media-body">
<h4>{{ part.name }}</h4>
{% if part.description %}
<p><i>{{ part.description }}</i></p>
{% endif %}
</div>
</div>
</div>
<div class="col-sm-6">
<table class="table table-striped">
{% if part.IPN %}
<tr>
<td>IPN</td>
<td>{{ part.IPN }}</td>
</tr>
{% endif %}
{% if part.URL %}
<tr>
<td>URL</td>
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr>
{% endif %}
<tr>
<td>Stock</td>
<td>{{ part.stock }}</td>
</tr>
</table>
</div>
</div>
<hr>
<div class='container-fluid'>
{% block details %}
<!-- Specific part details go here... -->
{% endblock %}
</div>
{% endblock %}

View File

@ -4,7 +4,6 @@
{% include 'part/tabs.html' with tab='suppliers' %}
{% if part.supplier_parts.all|length > 0 %}
<table class="table table-striped">
<tr>
<th>SKU</th>
@ -23,8 +22,11 @@
</tr>
{% endfor %}
</table>
{% else %}
There are no suppliers defined for this part, sorry!
{% endif %}
<div class='container-fluid'>
<a href="{% url 'supplier-part-create' %}?part={{ part.id }}">
<button class="btn btn-success">New Supplier Part</button>
</a>
</div>
{% endblock %}

View File

@ -1,15 +1,25 @@
<ul class="nav nav-tabs">
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}><a href="{% url 'part-detail' part.id %}">Details</a></li>
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}><a href="{% url 'part-bom' part.id %}">BOM <span class="badge">{{ part.bomItemCount }}</span></a></li>
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}><a href="{% url 'part-bom' part.id %}">BOM{% if part.bomItemCount > 0 %}<span class="badge">{{ part.bomItemCount }}</span>{% endif %}</a></li>
{% if part.bomItemCount > 0 %}
<li{% ifequal tab 'build' %} class "active"{% endifequal %}><a href="#">Build</a></li>
{% endif %}
{% if part.usedInCount > 0 %}
<li{% ifequal tab 'used' %} class="active"{% endifequal %}><a href="{% url 'part-used-in' part.id %}">Used In <span class="badge">{{ part.usedInCount }}</span></a></li>
<li{% ifequal tab 'used' %} class="active"{% endifequal %}><a href="{% url 'part-used-in' part.id %}">Used In{% if part.usedInCount > 0 %}<span class="badge">{{ part.usedInCount }}</span>{% endif %}</a></li>
{% endif %}
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}><a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.stock }}</span></a></li>
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}><a href="{% url 'part-suppliers' part.id %}">Suppliers <span class="badge">{{ part.supplier_parts.all|length }}<span></a></li>
{% if part.purchaseable %}
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}><a href="{% url 'part-suppliers' part.id %}">Suppliers
{% if part.supplier_parts.all|length > 0 %}
<span class="badge">{{ part.supplier_parts.all|length }}<span>
{% endif %}
</a></li>
{% endif %}
{% if part.trackable %}
<li{% ifequal tab 'track' %} class="active"{% endifequal %}><a href="{% url 'part-track' part.id %}">Tracking <span class="badge">{{ part.serials.all|length }}</span></a></li>
<li{% ifequal tab 'track' %} class="active"{% endifequal %}><a href="{% url 'part-track' part.id %}">Tracking
{% if parts.serials.all|length > 0 %}
<span class="badge">{{ part.serials.all|length }}</span>
{% endif %}
</a></li>
{% endif %}
</ul>

View File

@ -11,12 +11,18 @@ Part tracking for {{ part.name }}
<th>Serial</th>
<th>Status</th>
</tr>
{% for track in part.serials.all %}
{% for track in part.tracked_parts.all %}
<tr>
<td>{{ track.serial }}</td>
<td>{{ track.status }}</td>
<td><a href="{% url 'track-detail' track.id %}">{{ track.serial }}</a></td>
<td>{{ track.get_status_display }}</td>
</tr>
{% endfor %}
</table>
<div class='container-fluid'>
<a href="{% url 'track-create' %}?part={{ part.id }}">
<button class="btn btn-success">New Tracked Part</button>
</a>
</div>
{% endblock %}

View File

@ -4,8 +4,6 @@
{% include 'part/tabs.html' with tab='used' %}
This part is used to make the following parts:
<table class="table table-striped">
<tr>
<th>Part</th>

View File

@ -48,4 +48,5 @@ class EditStockItemForm(forms.ModelForm):
'supplier_part',
'location',
'quantity',
'status'
]

View File

@ -34,8 +34,13 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
# Update each part in the stock location
for item in instance.items.all():
item.location = instance.parent
item.save()
# If this location has a parent, move the child stock items to the parent
if instance.parent:
item.location = instance.parent
item.save()
# No parent location? Delete the stock items
else:
item.delete()
# Update each child category
for child in instance.children.all():

View File

@ -12,4 +12,10 @@
{% include "stock/stock_table.html" with items=items %}
{% endif %}
<div class='container-fluid'>
<a href="{% url 'stock-location-create' %}">
<button class="btn btn-success">New Stock Location</button>
</a>
</div>
{% endblock %}

View File

@ -37,7 +37,7 @@
{% endif %}
<tr>
<td>Status</td>
<td>{{ item.status }}</td>
<td>{{ item.get_status_display }}</td>
</tr>
{% if item.notes %}
<tr>

View File

@ -1,7 +1,7 @@
<div class="navigation">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item{% if location is None %} active" aria-current="page{% endif %}"><a href="/stock/">Parts</a></li>
<li class="breadcrumb-item{% if location is None %} active" aria-current="page{% endif %}"><a href="/stock/">Stock</a></li>
{% if location %}
{% for path_item in location.parentpath %}
<li class='breadcrumb-item'><a href="{% url 'stock-location-detail' path_item.id %}">{{ path_item.name }}</a></li>

View File

@ -6,12 +6,12 @@
<div class="col-sm-6">
<h3>{{ supplier.name }}</h3>
<p>{{ supplier.description }}</p>
<p><a href="{% url 'supplier-edit' supplier.id %}">
<a href="{% url 'supplier-edit' supplier.id %}">
<button class="btn btn-info">Edit supplier details</button>
</a></p>
<p><a href="{% url 'supplier-delete' supplier.id %}">
</a>
<a href="{% url 'supplier-delete' supplier.id %}">
<button class="btn btn-danger">Delete supplier</button>
</a></p>
</a>
</div>
<div class="col-sm-6">
<table class="table">
@ -71,5 +71,6 @@
<a href="{% url 'supplier-part-create' %}?supplier={{ supplier.id }}">
<button class="btn btn-success">New Supplier Part</button>
</a>
</div>
{% endblock %}

View File

@ -29,12 +29,14 @@
<br>
<p><a href="{% url 'supplier-part-edit' part.id %}">
<div class='container-fluid'>
<a href="{% url 'supplier-part-edit' part.id %}">
<button class="btn btn-info">Edit supplier details</button>
</a></p>
<p><a href="{% url 'supplier-part-delete' part.id %}">
</a>
<a href="{% url 'supplier-part-delete' part.id %}">
<button class="btn btn-danger">Delete supplier part</button>
</a></p>
</a>
</div>
{% endblock %}

View File

@ -5,6 +5,7 @@ from django.urls import reverse
from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView, DeleteView, CreateView
from part.models import Part
from .models import Supplier, SupplierPart
from .forms import EditSupplierForm
@ -76,9 +77,16 @@ class SupplierPartCreate(CreateView):
initials = super(SupplierPartCreate, self).get_initial().copy()
supplier_id = self.request.GET.get('supplier', None)
part_id = self.request.GET.get('part', None)
if supplier_id:
initials['supplier'] = get_object_or_404(Supplier, pk=supplier_id)
# TODO
# self.fields['supplier'].disabled = True
if part_id:
initials['part'] = get_object_or_404(Part, pk=part_id)
# TODO
# self.fields['part'].disabled = True
return initials

28
InvenTree/track/forms.py Normal file
View File

@ -0,0 +1,28 @@
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.form_action = 'submit'
self.helper.add_input(Submit('submit', 'Submit'))
class Meta:
model = UniquePart
fields = [
'part',
'serial',
'customer',
'status'
]

View File

@ -15,6 +15,9 @@ class UniquePart(models.Model):
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')

View File

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

View File

@ -0,0 +1,11 @@
{% 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

@ -2,8 +2,32 @@
{% block content %}
Part: <a href="{% url 'part-detail' part.part.id %}">{{ part.part.name }}</a><br>
Serial number: {{ part.serial }}
<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.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>
@ -23,4 +47,13 @@ Serial number: {{ part.serial }}
</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 +1,5 @@
{% extends "base.html"% }
{% block content %}
{% extends "create_edit_obj.html" %}
{% block obj_title %}
Edit tracked part information
{% endblock %}

View File

@ -30,4 +30,10 @@
</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

@ -24,6 +24,9 @@ unique_api_urls = [
"""
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'),
]
@ -31,8 +34,9 @@ tracking_urls = [
# Detail view
url(r'^(?P<pk>\d+)/', include(track_detail_urls)),
# List ALL tracked items
url('', views.TrackIndex.as_view(), name='track-index'),
# Create a new tracking item
url(r'^new/?', views.TrackCreate.as_view(), name='track-create'),
url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='track-index'),
# List ALL tracked items
url(r'^.*$', views.TrackIndex.as_view(), name='track-index'),
]

View File

@ -5,8 +5,10 @@ from django.urls import reverse
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, PartTrackingInfo
from .forms import EditTrackedPartForm
class TrackIndex(ListView):
model = UniquePart
@ -23,3 +25,39 @@ class TrackDetail(DetailView):
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())