2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

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

View File

@ -4,12 +4,40 @@
{% include 'part/tabs.html' with tab='detail' %} {% 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... <div class='container-fluid'>
<br>
<a href="{% url 'part-edit' part.id %}"><button class="btn btn-info">Edit Part</button></a> <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> <a href="{% url 'part-delete' part.id %}"><button class="btn btn-danger">Delete Part</button></a>
</div>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

@ -1,15 +1,25 @@
<ul class="nav nav-tabs"> <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 '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 %} {% if part.bomItemCount > 0 %}
<li{% ifequal tab 'build' %} class "active"{% endifequal %}><a href="#">Build</a></li> <li{% ifequal tab 'build' %} class "active"{% endifequal %}><a href="#">Build</a></li>
{% endif %} {% endif %}
{% if part.usedInCount > 0 %} {% 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 %} {% 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 '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 %} {% 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 %} {% endif %}
</ul> </ul>

View File

@ -11,12 +11,18 @@ Part tracking for {{ part.name }}
<th>Serial</th> <th>Serial</th>
<th>Status</th> <th>Status</th>
</tr> </tr>
{% for track in part.serials.all %} {% for track in part.tracked_parts.all %}
<tr> <tr>
<td>{{ track.serial }}</td> <td><a href="{% url 'track-detail' track.id %}">{{ track.serial }}</a></td>
<td>{{ track.status }}</td> <td>{{ track.get_status_display }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </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 %} {% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<div class="navigation"> <div class="navigation">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="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 %} {% if location %}
{% for path_item in location.parentpath %} {% for path_item in location.parentpath %}
<li class='breadcrumb-item'><a href="{% url 'stock-location-detail' path_item.id %}">{{ path_item.name }}</a></li> <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"> <div class="col-sm-6">
<h3>{{ supplier.name }}</h3> <h3>{{ supplier.name }}</h3>
<p>{{ supplier.description }}</p> <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> <button class="btn btn-info">Edit supplier details</button>
</a></p> </a>
<p><a href="{% url 'supplier-delete' supplier.id %}"> <a href="{% url 'supplier-delete' supplier.id %}">
<button class="btn btn-danger">Delete supplier</button> <button class="btn btn-danger">Delete supplier</button>
</a></p> </a>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<table class="table"> <table class="table">
@ -71,5 +71,6 @@
<a href="{% url 'supplier-part-create' %}?supplier={{ supplier.id }}"> <a href="{% url 'supplier-part-create' %}?supplier={{ supplier.id }}">
<button class="btn btn-success">New Supplier Part</button> <button class="btn btn-success">New Supplier Part</button>
</a> </a>
</div>
{% endblock %} {% endblock %}

View File

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

View File

@ -5,6 +5,7 @@ from django.urls import reverse
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView, DeleteView, CreateView from django.views.generic.edit import UpdateView, DeleteView, CreateView
from part.models import Part
from .models import Supplier, SupplierPart from .models import Supplier, SupplierPart
from .forms import EditSupplierForm from .forms import EditSupplierForm
@ -76,9 +77,16 @@ class SupplierPartCreate(CreateView):
initials = super(SupplierPartCreate, self).get_initial().copy() initials = super(SupplierPartCreate, self).get_initial().copy()
supplier_id = self.request.GET.get('supplier', None) supplier_id = self.request.GET.get('supplier', None)
part_id = self.request.GET.get('part', None)
if supplier_id: if supplier_id:
initials['supplier'] = get_object_or_404(Supplier, pk=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 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 and tracking all events in the life of a part
""" """
def get_absolute_url(self):
return "/track/{id}/".format(id=self.id)
class Meta: class Meta:
# Cannot have multiple parts with same serial number # Cannot have multiple parts with same serial number
unique_together = ('part', 'serial') 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 %} {% block content %}
Part: <a href="{% url 'part-detail' part.part.id %}">{{ part.part.name }}</a><br> <h3>Part tracking information</h3>
Serial number: {{ part.serial }}
<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 %} {% if part.tracking_info.all|length > 0 %}
<p>Tracking information:</p> <p>Tracking information:</p>
@ -23,4 +47,13 @@ Serial number: {{ part.serial }}
</ul> </ul>
{% endif %} {% 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 %} {% endblock %}

View File

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

View File

@ -30,4 +30,10 @@
</div> </div>
{% endif %} {% endif %}
<div class='container-fluid'>
<a href="{% url 'track-create' %}">
<button class="btn btn-success">New Tracked Part</button>
</a>
</div>
{% endblock %} {% endblock %}

View File

@ -24,6 +24,9 @@ unique_api_urls = [
""" """
track_detail_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'), url('^.*$', views.TrackDetail.as_view(), name='track-detail'),
] ]
@ -31,8 +34,9 @@ tracking_urls = [
# Detail view # Detail view
url(r'^(?P<pk>\d+)/', include(track_detail_urls)), url(r'^(?P<pk>\d+)/', include(track_detail_urls)),
# List ALL tracked items # Create a new tracking item
url('', views.TrackIndex.as_view(), name='track-index'), 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 import DetailView, ListView
from django.views.generic.edit import UpdateView, DeleteView, CreateView from django.views.generic.edit import UpdateView, DeleteView, CreateView
from part.models import Part
from .models import UniquePart, PartTrackingInfo from .models import UniquePart, PartTrackingInfo
from .forms import EditTrackedPartForm
class TrackIndex(ListView): class TrackIndex(ListView):
model = UniquePart model = UniquePart
@ -23,3 +25,39 @@ class TrackDetail(DetailView):
template_name = 'track/detail.html' template_name = 'track/detail.html'
context_object_name='part' 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())