UI improvements

This commit is contained in:
Oliver 2018-04-16 23:09:45 +10:00
parent b6b4189c49
commit a67d5b58db
16 changed files with 213 additions and 23 deletions

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 12:49
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0018_part_buildable'),
]
operations = [
migrations.AlterField(
model_name='part',
name='IPN',
field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100),
),
migrations.AlterField(
model_name='part',
name='URL',
field=models.URLField(blank=True, help_text='Link to extenal URL'),
),
migrations.AlterField(
model_name='part',
name='buildable',
field=models.BooleanField(default=False, help_text='Can this part be built from other parts?'),
),
migrations.AlterField(
model_name='part',
name='category',
field=models.ForeignKey(blank=True, help_text='Part category', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='parts', to='part.PartCategory'),
),
migrations.AlterField(
model_name='part',
name='description',
field=models.CharField(help_text='Part description', max_length=250),
),
migrations.AlterField(
model_name='part',
name='minimum_stock',
field=models.PositiveIntegerField(default=0, help_text='Minimum allowed stock level', validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='part',
name='name',
field=models.CharField(help_text='Part name (must be unique)', max_length=100, unique=True),
),
migrations.AlterField(
model_name='part',
name='purchaseable',
field=models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?'),
),
migrations.AlterField(
model_name='part',
name='trackable',
field=models.BooleanField(default=False, help_text='Does this part have tracking for unique items?'),
),
]

View File

@ -84,43 +84,44 @@ class Part(models.Model):
return '/part/{id}/'.format(id=self.id)
# Short name of the part
name = models.CharField(max_length=100, unique=True)
name = models.CharField(max_length=100, unique=True, help_text='Part name (must be unique)')
# Longer description of the part (optional)
description = models.CharField(max_length=250)
description = models.CharField(max_length=250, help_text='Part description')
# Internal Part Number (optional)
# Potentially multiple parts map to the same internal IPN (variants?)
# So this does not have to be unique
IPN = models.CharField(max_length=100, blank=True)
IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
# Provide a URL for an external link
URL = models.URLField(blank=True)
URL = models.URLField(blank=True, help_text='Link to extenal URL')
# Part category - all parts must be assigned to a category
category = models.ForeignKey(PartCategory, related_name='parts',
null=True, blank=True,
on_delete=models.DO_NOTHING)
on_delete=models.DO_NOTHING,
help_text='Part category')
image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True)
# Minimum "allowed" stock level
minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)])
minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text='Minimum allowed stock level')
# Units of quantity for this part. Default is "pcs"
units = models.CharField(max_length=20, default="pcs", blank=True)
# Can this part be built?
buildable = models.BooleanField(default=False)
buildable = models.BooleanField(default=False, help_text='Can this part be built from other parts?')
# Is this part "trackable"?
# 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)
trackable = models.BooleanField(default=False, help_text='Does this part have tracking for unique items?')
# Is this part "purchaseable"?
purchaseable = models.BooleanField(default=True)
purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?')
def __str__(self):
if self.IPN:

View File

@ -12,7 +12,6 @@ Deletion title goes here
<p><b>This is a permanent action and cannot be undone.</b></p>
{% block del_body %}
Deletion body goes here
{% endblock %}
<form action="" method="post">{% csrf_token %}

View File

@ -4,18 +4,22 @@
{% include 'part/tabs.html' with tab='bom' %}
<h3>Bill of Materials</h3>
<table class="table table-striped">
<tr>
<th>Part</th>
<th>Description</th>
<th>Quantity</th>
<th>Edit</th>
</tr>
{% for bom_item in part.bom_items.all %}
{% with sub_part=bom_item.sub_part %}
<tr>
<td><a href="{% url 'part-detail' sub_part.id %}">{{ sub_part.name }}</a></td>
<td>{{ sub_part.description }}</td>
<td>{{ bom_item.quantity }}<span class='badge'>{{ bom_item.sub_part.available_stock }}</span></td>
<td>{{ bom_item.quantity }}</span></td>
<td><a href="{% url 'bom-item-detail' bom_item.id %}">Edit</a></td>
</tr>
{% endwith %}
{% endfor %}

View File

@ -0,0 +1,13 @@
{% extends "part/part_base.html" %}
{% block details %}
{% include 'part/tabs.html' with tab='build' %}
<h3>Build Part</h3>
TODO
<br><br>
You can build {{ part.can_build }} of this part with current stock.
{% endblock %}

View File

@ -4,9 +4,11 @@
{% include 'part/tabs.html' with tab='detail' %}
<h3>Part Details</h3>
<table class='table table-striped'>
<tr>
<td>Name</td>
<td>Part name</td>
<td>{{ part.name }}</td>
</tr>
<tr>
@ -37,6 +39,12 @@
<td>Purchaseable</td>
<td>{{ part.purchaseable }}</td>
</tr>
{% if part.minimum_stock > 0 %}
<tr>
<td>Minimum Stock</td>
<td>{{ part.minimum_stock }}</td>
</tr>
{% endif %}
</table>
<div class='container-fluid'>

View File

@ -4,6 +4,8 @@
{% include 'part/tabs.html' with tab='stock' %}
<h3>Part Stock</h3>
<table class="table table-striped">
<tr>
<th>Link</th>

View File

@ -4,6 +4,8 @@
{% include 'part/tabs.html' with tab='suppliers' %}
<h3>Part Suppliers</h3>
<table class="table table-striped">
<tr>
<th>SKU</th>

View File

@ -2,7 +2,7 @@
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}><a href="{% url 'part-detail' part.id %}">Details</a></li>
{% if part.buildable %}
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}><a href="{% url 'part-bom' part.id %}">BOM<span class="badge">{{ part.bom_count }}</span></a></li>
<li{% ifequal tab 'build' %} class "active"{% endifequal %}><a href="#">Build<span class='badge'>{{ part.can_build }}</span></a></li>
<li{% ifequal tab 'build' %} class="active"{% endifequal %}><a href="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.can_build }}</span></a></li>
{% endif %}
{% if part.used_in_count > 0 %}
<li{% ifequal tab 'used' %} class="active"{% endifequal %}><a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>

View File

@ -4,6 +4,8 @@
{% include 'part/tabs.html' with tab='used' %}
<h3>Used In</h3>
<table class="table table-striped">
<tr>
<th>Part</th>

View File

@ -41,6 +41,7 @@ part_detail_urls = [
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'),
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 12:53
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0019_auto_20180416_1249'),
('stock', '0008_stockitem_url'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='batch',
field=models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100),
),
migrations.AlterField(
model_name='stockitem',
name='belongs_to',
field=models.ForeignKey(blank=True, help_text='Is this item installed in another item?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owned_parts', to='stock.StockItem'),
),
migrations.AlterField(
model_name='stockitem',
name='customer',
field=models.ForeignKey(blank=True, help_text='Item assigned to customer?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stockitems', to='supplier.Customer'),
),
migrations.AlterField(
model_name='stockitem',
name='location',
field=models.ForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='items', to='stock.StockLocation'),
),
migrations.AlterField(
model_name='stockitem',
name='serial',
field=models.PositiveIntegerField(blank=True, help_text='Serial number for this item', null=True),
),
migrations.AlterUniqueTogether(
name='stockitem',
unique_together=set([('part', 'serial')]),
),
]

View File

@ -60,6 +60,12 @@ class StockItem(models.Model):
def get_absolute_url(self):
return '/stock/item/{id}/'.format(id=self.id)
class Meta:
unique_together = [
('part', 'serial'),
]
# 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')
@ -68,23 +74,29 @@ class StockItem(models.Model):
# 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)
related_name='items', blank=True, null=True,
help_text='Where is this stock item located?')
# 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)
related_name='owned_parts', blank=True, null=True,
help_text='Is this item installed in another item?')
# 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)
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL,
related_name='stockitems', blank=True, null=True,
help_text='Item assigned to customer?')
# Optional serial number
serial = models.PositiveIntegerField(blank=True, null=True)
serial = models.PositiveIntegerField(blank=True, null=True,
help_text='Serial number for this item')
# Optional URL to link to external resource
URL = models.URLField(max_length=125, blank=True)
# Optional batch information
batch = models.CharField(max_length=100, blank=True)
batch = models.CharField(max_length=100, blank=True,
help_text='Batch code for this stock item')
# Quantity of this stock item. Value may be overridden by other settings
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)])

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-16 12:53
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0006_auto_20180415_1011'),
]
operations = [
migrations.AlterField(
model_name='supplierpart',
name='MPN',
field=models.CharField(blank=True, help_text='Manufacturer part number', max_length=100),
),
migrations.AlterField(
model_name='supplierpart',
name='SKU',
field=models.CharField(help_text='Supplier stock keeping unit', max_length=100),
),
migrations.AlterField(
model_name='supplierpart',
name='manufacturer',
field=models.ForeignKey(blank=True, help_text='Manufacturer', null=True, on_delete=django.db.models.deletion.SET_NULL, to='supplier.Manufacturer'),
),
migrations.AlterField(
model_name='supplierpart',
name='part',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='part.Part'),
),
]

View File

@ -42,18 +42,20 @@ class SupplierPart(models.Model):
# Link to an actual part
# The part will have a field 'supplier_parts' which links to the supplier part options
part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.SET_NULL,
part = models.ForeignKey(Part, on_delete=models.CASCADE,
related_name='supplier_parts')
supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE,
related_name='parts')
SKU = models.CharField(max_length=100)
SKU = models.CharField(max_length=100, help_text='Supplier stock keeping unit')
manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.SET_NULL)
MPN = models.CharField(max_length=100, blank=True)
manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.SET_NULL, help_text='Manufacturer')
MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
URL = models.URLField(blank=True)
description = models.CharField(max_length=250, blank=True)
# Default price for a single unit

View File

@ -31,7 +31,7 @@
<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 part</button>
</a>
<a href="{% url 'supplier-part-delete' part.id %}">
<button class="btn btn-danger">Delete supplier part</button>