mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add pages for part tracking
- Edit / Delete / Create tracking info - Improvements to many pages
This commit is contained in:
parent
55b533d3ef
commit
8e6de1b832
@ -28,6 +28,7 @@ class EditPartForm(forms.ModelForm):
|
||||
'URL',
|
||||
'minimum_stock',
|
||||
'trackable',
|
||||
'purchaseable',
|
||||
]
|
||||
|
||||
|
||||
|
20
InvenTree/part/migrations/0017_part_purchaseable.py
Normal file
20
InvenTree/part/migrations/0017_part_purchaseable.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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.
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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>
|
@ -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 %}
|
@ -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>
|
||||
|
@ -48,4 +48,5 @@ class EditStockItemForm(forms.ModelForm):
|
||||
'supplier_part',
|
||||
'location',
|
||||
'quantity',
|
||||
'status'
|
||||
]
|
@ -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():
|
||||
|
@ -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 %}
|
@ -37,7 +37,7 @@
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>{{ item.status }}</td>
|
||||
<td>{{ item.get_status_display }}</td>
|
||||
</tr>
|
||||
{% if item.notes %}
|
||||
<tr>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -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
28
InvenTree/track/forms.py
Normal 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'
|
||||
]
|
@ -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')
|
||||
|
5
InvenTree/track/templates/track/create.html
Normal file
5
InvenTree/track/templates/track/create.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "create_edit_obj.html" %}
|
||||
|
||||
{% block obj_title %}
|
||||
Create a new tracked part
|
||||
{% endblock %}
|
11
InvenTree/track/templates/track/delete.html
Normal file
11
InvenTree/track/templates/track/delete.html
Normal 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 %}
|
@ -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 %}
|
@ -1,5 +1,5 @@
|
||||
{% extends "base.html"% }
|
||||
|
||||
{% block content %}
|
||||
{% extends "create_edit_obj.html" %}
|
||||
|
||||
{% block obj_title %}
|
||||
Edit tracked part information
|
||||
{% endblock %}
|
@ -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 %}
|
@ -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'),
|
||||
]
|
@ -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())
|
Loading…
Reference in New Issue
Block a user