mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
0deb6e8bea
19
InvenTree/build/migrations/0011_auto_20190508_0748.py
Normal file
19
InvenTree/build/migrations/0011_auto_20190508_0748.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.2 on 2019-05-07 21:48
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('build', '0010_auto_20190505_2233'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='build',
|
||||||
|
name='status',
|
||||||
|
field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Allocated'), (30, 'Cancelled'), (40, 'Complete')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
|
||||||
|
),
|
||||||
|
]
|
@ -13,9 +13,11 @@ from django.core.exceptions import ValidationError
|
|||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
|
from django.db.models import Sum
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
from part.models import BomItem
|
||||||
|
|
||||||
|
|
||||||
class Build(models.Model):
|
class Build(models.Model):
|
||||||
@ -33,49 +35,6 @@ class Build(models.Model):
|
|||||||
notes: Text notes
|
notes: Text notes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
""" Called when the Build model is saved to the database.
|
|
||||||
|
|
||||||
If this is a new Build, try to allocate StockItem objects automatically.
|
|
||||||
|
|
||||||
- If there is only one StockItem for a Part, use that one.
|
|
||||||
- If there are multiple StockItem objects, leave blank and let the user decide
|
|
||||||
"""
|
|
||||||
|
|
||||||
allocate_parts = False
|
|
||||||
|
|
||||||
# If there is no PK yet, then this is the first time the Build has been saved
|
|
||||||
if not self.pk:
|
|
||||||
allocate_parts = True
|
|
||||||
|
|
||||||
# Save this Build first
|
|
||||||
super(Build, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
if allocate_parts:
|
|
||||||
for item in self.part.bom_items.all():
|
|
||||||
part = item.sub_part
|
|
||||||
# Number of parts required for this build
|
|
||||||
q_req = item.quantity * self.quantity
|
|
||||||
|
|
||||||
stock = StockItem.objects.filter(part=part)
|
|
||||||
|
|
||||||
if len(stock) == 1:
|
|
||||||
stock_item = stock[0]
|
|
||||||
|
|
||||||
# Are there any parts available?
|
|
||||||
if stock_item.quantity > 0:
|
|
||||||
# If there are not enough parts, reduce the amount we will take
|
|
||||||
if stock_item.quantity < q_req:
|
|
||||||
q_req = stock_item.quantity
|
|
||||||
|
|
||||||
# Allocate parts to this build
|
|
||||||
build_item = BuildItem(
|
|
||||||
build=self,
|
|
||||||
stock_item=stock_item,
|
|
||||||
quantity=q_req)
|
|
||||||
|
|
||||||
build_item.save()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Build {q} x {part}".format(q=self.quantity, part=str(self.part))
|
return "Build {q} x {part}".format(q=self.quantity, part=str(self.part))
|
||||||
|
|
||||||
@ -103,11 +62,13 @@ class Build(models.Model):
|
|||||||
|
|
||||||
# Build status codes
|
# Build status codes
|
||||||
PENDING = 10 # Build is pending / active
|
PENDING = 10 # Build is pending / active
|
||||||
|
ALLOCATED = 20 # Parts have been removed from stock
|
||||||
CANCELLED = 30 # Build was cancelled
|
CANCELLED = 30 # Build was cancelled
|
||||||
COMPLETE = 40 # Build is complete
|
COMPLETE = 40 # Build is complete
|
||||||
|
|
||||||
#: Build status codes
|
#: Build status codes
|
||||||
BUILD_STATUS_CODES = {PENDING: _("Pending"),
|
BUILD_STATUS_CODES = {PENDING: _("Pending"),
|
||||||
|
ALLOCATED: _("Allocated"),
|
||||||
CANCELLED: _("Cancelled"),
|
CANCELLED: _("Cancelled"),
|
||||||
COMPLETE: _("Complete"),
|
COMPLETE: _("Complete"),
|
||||||
}
|
}
|
||||||
@ -153,6 +114,68 @@ class Build(models.Model):
|
|||||||
self.status = self.CANCELLED
|
self.status = self.CANCELLED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def getAutoAllocations(self):
|
||||||
|
""" Return a list of parts which will be allocated
|
||||||
|
using the 'AutoAllocate' function.
|
||||||
|
|
||||||
|
For each item in the BOM for the attached Part:
|
||||||
|
|
||||||
|
- If there is a single StockItem, use that StockItem
|
||||||
|
- Take as many parts as available (up to the quantity required for the BOM)
|
||||||
|
- If there are multiple StockItems available, ignore (leave up to the user)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict object containing the StockItem objects to be allocated (and the quantities)
|
||||||
|
"""
|
||||||
|
|
||||||
|
allocations = {}
|
||||||
|
|
||||||
|
for item in self.part.bom_items.all():
|
||||||
|
|
||||||
|
# How many parts required for this build?
|
||||||
|
q_required = item.quantity * self.quantity
|
||||||
|
|
||||||
|
stock = StockItem.objects.filter(part=item.sub_part)
|
||||||
|
|
||||||
|
# Only one StockItem to choose from? Default to that one!
|
||||||
|
if len(stock) == 1:
|
||||||
|
stock_item = stock[0]
|
||||||
|
|
||||||
|
# Check that we have not already allocated this stock-item against this build
|
||||||
|
build_items = BuildItem.objects.filter(build=self, stock_item=stock_item)
|
||||||
|
|
||||||
|
if len(build_items) > 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Are there any parts available?
|
||||||
|
if stock_item.quantity > 0:
|
||||||
|
# Only take as many as are available
|
||||||
|
if stock_item.quantity < q_required:
|
||||||
|
q_required = stock_item.quantity
|
||||||
|
|
||||||
|
# Add the item to the allocations list
|
||||||
|
allocations[stock_item] = q_required
|
||||||
|
|
||||||
|
return allocations
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def autoAllocate(self):
|
||||||
|
""" Run auto-allocation routine to allocate StockItems to this Build.
|
||||||
|
|
||||||
|
See: getAutoAllocations()
|
||||||
|
"""
|
||||||
|
|
||||||
|
allocations = self.getAutoAllocations()
|
||||||
|
|
||||||
|
for item in allocations:
|
||||||
|
# Create a new allocation
|
||||||
|
build_item = BuildItem(
|
||||||
|
build=self,
|
||||||
|
stock_item=item,
|
||||||
|
quantity=allocations[item])
|
||||||
|
|
||||||
|
build_item.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def completeBuild(self, location, user):
|
def completeBuild(self, location, user):
|
||||||
""" Mark the Build as COMPLETE
|
""" Mark the Build as COMPLETE
|
||||||
@ -200,6 +223,41 @@ class Build(models.Model):
|
|||||||
self.status = self.COMPLETE
|
self.status = self.COMPLETE
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def getRequiredQuantity(self, part):
|
||||||
|
""" Calculate the quantity of <part> required to make this build.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = BomItem.objects.get(part=self.part.id, sub_part=part.id)
|
||||||
|
return item.quantity * self.quantity
|
||||||
|
except BomItem.DoesNotExist:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def getAllocatedQuantity(self, part):
|
||||||
|
""" Calculate the total number of <part> currently allocated to this build
|
||||||
|
"""
|
||||||
|
|
||||||
|
allocated = BuildItem.objects.filter(build=self.id, stock_item__part=part.id).aggregate(Sum('quantity'))
|
||||||
|
|
||||||
|
q = allocated['quantity__sum']
|
||||||
|
|
||||||
|
if q:
|
||||||
|
return int(q)
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def getUnallocatedQuantity(self, part):
|
||||||
|
""" Calculate the quantity of <part> which still needs to be allocated to this build.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
Part - the part to be tested
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The remaining allocated quantity
|
||||||
|
"""
|
||||||
|
|
||||||
|
return max(self.getRequiredQuantity(part) - self.getAllocatedQuantity(part), 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def required_parts(self):
|
def required_parts(self):
|
||||||
""" Returns a dict of parts required to build this part (BOM) """
|
""" Returns a dict of parts required to build this part (BOM) """
|
||||||
@ -208,7 +266,8 @@ class Build(models.Model):
|
|||||||
for item in self.part.bom_items.all():
|
for item in self.part.bom_items.all():
|
||||||
part = {'part': item.sub_part,
|
part = {'part': item.sub_part,
|
||||||
'per_build': item.quantity,
|
'per_build': item.quantity,
|
||||||
'quantity': item.quantity * self.quantity
|
'quantity': item.quantity * self.quantity,
|
||||||
|
'allocated': self.getAllocatedQuantity(item.sub_part)
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.append(part)
|
parts.append(part)
|
||||||
|
@ -6,17 +6,24 @@
|
|||||||
|
|
||||||
<h3>Allocate Parts for Build</h3>
|
<h3>Allocate Parts for Build</h3>
|
||||||
|
|
||||||
<h4><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></h4>
|
<div class='row'>
|
||||||
{{ build.quantity }} x {{ build.part.name }}
|
<div class='col-sm-6'>
|
||||||
|
|
||||||
|
<h4><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></h4>
|
||||||
|
{{ build.quantity }} x {{ build.part.name }}
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<div class='btn-group' style='float: right;'>
|
||||||
|
<button class='btn btn-warning' type='button' id='complete-build'>Complete Build</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
{% for bom_item in bom_items.all %}
|
{% for bom_item in bom_items.all %}
|
||||||
{% include "build/allocation_item.html" with item=bom_item build=build collapse_id=bom_item.id %}
|
{% include "build/allocation_item.html" with item=bom_item build=build collapse_id=bom_item.id %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class='btn btn-warning' type='button' id='complete-build'>Complete Build</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -1,9 +1,23 @@
|
|||||||
{% extends "collapse.html" %}
|
{% extends "collapse.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block collapse_title %}
|
{% block collapse_title %}
|
||||||
{{ item.sub_part.name }}
|
<div>
|
||||||
|
<div class='media-left'>
|
||||||
|
<img class='part-thumb' style='height: auto; width: 48px;'
|
||||||
|
{% if item.sub_part.image %}
|
||||||
|
src="{{ item.sub_part.image.url }}" alt='{{ item.sub_part.name }}'>
|
||||||
|
{% else %}
|
||||||
|
src="{% static 'img/blank_image.png' %}" alt='No image'>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class='media-body'>
|
||||||
|
{{ item.sub_part.name }}<br>
|
||||||
|
<small><i>{{ item.sub_part.description }}</i></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block collapse_heading %}
|
{% block collapse_heading %}
|
||||||
|
@ -12,6 +12,11 @@
|
|||||||
<th>Part</th>
|
<th>Part</th>
|
||||||
<th>Quantity</th>
|
<th>Quantity</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
{% if completed %}
|
||||||
|
<th>Completed</th>
|
||||||
|
{% else %}
|
||||||
|
<th>Created</th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -21,6 +26,11 @@
|
|||||||
<td><a href="{% url 'part-build' build.part.id %}">{{ build.part.name }}</a></td>
|
<td><a href="{% url 'part-build' build.part.id %}">{{ build.part.name }}</a></td>
|
||||||
<td>{{ build.quantity }}</td>
|
<td>{{ build.quantity }}</td>
|
||||||
<td>{% include "build_status.html" with build=build %}
|
<td>{% include "build_status.html" with build=build %}
|
||||||
|
{% if completed %}
|
||||||
|
<td>{{ build.completion_date }}<span class='badge'>{{ build.completed_by.username }}</span></td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ build.creation_date }}</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -85,7 +85,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Part</th>
|
<th>Part</th>
|
||||||
<th>Required</th>
|
<th>Required</th>
|
||||||
<th>Stock</th>
|
<th>Available</th>
|
||||||
|
<th>Allocated</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -94,6 +95,7 @@
|
|||||||
<td><a href="{% url 'part-detail' item.part.id %}">{{ item.part.name }}</a></td>
|
<td><a href="{% url 'part-detail' item.part.id %}">{{ item.part.name }}</a></td>
|
||||||
<td>{{ item.quantity }}</td>
|
<td>{{ item.quantity }}</td>
|
||||||
<td>{{ item.part.total_stock }}</td>
|
<td>{{ item.part.total_stock }}</td>
|
||||||
|
<td>{{ item.allocated }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -3,39 +3,26 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
<h4>Active Builds</h4>
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
<div id='active-build-toolbar'>
|
<h3>Part Builds</h3>
|
||||||
<div class='btn-group'>
|
</div>
|
||||||
<button class="btn btn-success" id='new-build'>Start New Build</button>
|
<div class='col-sm-6'>
|
||||||
|
<div class='container' id='active-build-toolbar' style='float: right;'>
|
||||||
|
<div class='btn-group' style='float: right;'>
|
||||||
|
<button type='button' class="btn btn-success" id='new-build'>Start New Build</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed build-table' id='build-table-{{collapse_id}}' data-toolbar='#active-build-toolbar'>
|
<hr>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Build</th>
|
|
||||||
<th>Part</th>
|
|
||||||
<th>Quantity</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for build in active %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></td>
|
|
||||||
<td><a href="{% url 'part-build' build.part.id %}">{{ build.part.name }}</a></td>
|
|
||||||
<td>{{ build.quantity }}</td>
|
|
||||||
<td>{% include "build_status.html" with build=build %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p></p>
|
{% include "build/build_list.html" with builds=active title="Active Builds" completed=False collapse_id='active' %}
|
||||||
{% include "build/build_list.html" with builds=completed title="Completed Builds" collapse_id="complete" %}
|
|
||||||
<p></p>
|
{% include "build/build_list.html" with builds=completed completed=True title="Completed Builds" collapse_id="complete" %}
|
||||||
{% include "build/build_list.html" with builds=cancelled title="Cancelled Builds" collapse_id="cancelled" %}
|
|
||||||
|
{% include "build/build_list.html" with builds=cancelled title="Cancelled Builds" completed=False collapse_id="cancelled" %}
|
||||||
|
|
||||||
{% include 'modals.html' %}
|
{% include 'modals.html' %}
|
||||||
|
|
||||||
@ -43,6 +30,9 @@
|
|||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
$("#collapse-item-active").collapse().show();
|
||||||
|
|
||||||
$("#new-build").click(function() {
|
$("#new-build").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'build-create' %}",
|
"{% url 'build-create' %}",
|
||||||
@ -74,7 +64,10 @@
|
|||||||
{
|
{
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -289,6 +289,16 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)])
|
query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)])
|
||||||
|
|
||||||
form.fields['stock_item'].queryset = query
|
form.fields['stock_item'].queryset = query
|
||||||
|
|
||||||
|
stocks = query.all()
|
||||||
|
# If there is only one item selected, select it
|
||||||
|
if len(stocks) == 1:
|
||||||
|
form.fields['stock_item'].initial = stocks[0].id
|
||||||
|
# There is no stock available
|
||||||
|
elif len(stocks) == 0:
|
||||||
|
# TODO - Add a message to the form describing the problem
|
||||||
|
pass
|
||||||
|
|
||||||
except Part.DoesNotExist:
|
except Part.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -303,10 +313,24 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
initials = super(AjaxCreateView, self).get_initial().copy()
|
initials = super(AjaxCreateView, self).get_initial().copy()
|
||||||
|
|
||||||
build_id = self.get_param('build')
|
build_id = self.get_param('build')
|
||||||
|
part_id = self.get_param('part')
|
||||||
|
|
||||||
|
if part_id:
|
||||||
|
try:
|
||||||
|
part = Part.objects.get(pk=part_id)
|
||||||
|
except Part.DoesNotExist:
|
||||||
|
part = None
|
||||||
|
|
||||||
if build_id:
|
if build_id:
|
||||||
try:
|
try:
|
||||||
initials['build'] = Build.objects.get(pk=build_id)
|
build = Build.objects.get(pk=build_id)
|
||||||
|
initials['build'] = build
|
||||||
|
|
||||||
|
# Try to work out how many parts to allocate
|
||||||
|
if part:
|
||||||
|
unallocated = build.getUnallocatedQuantity(part)
|
||||||
|
initials['quantity'] = unallocated
|
||||||
|
|
||||||
except Build.DoesNotExist:
|
except Build.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import os
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
def rename_company_image(instance, filename):
|
def rename_company_image(instance, filename):
|
||||||
@ -78,6 +79,14 @@ class Company(models.Model):
|
|||||||
""" Get the web URL for the detail view for this Company """
|
""" Get the web URL for the detail view for this Company """
|
||||||
return reverse('company-detail', kwargs={'pk': self.id})
|
return reverse('company-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
|
def get_image_url(self):
|
||||||
|
""" Return the URL of the image for this company """
|
||||||
|
|
||||||
|
if self.image:
|
||||||
|
return os.path.join(settings.MEDIA_URL, str(self.image.url))
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def part_count(self):
|
def part_count(self):
|
||||||
""" The number of parts supplied by this company """
|
""" The number of parts supplied by this company """
|
||||||
|
@ -51,10 +51,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'part_name',
|
field: 'part_detail.name',
|
||||||
title: 'Part',
|
title: 'Part',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return renderLink(value, '/part/' + row.part + '/suppliers/');
|
return imageHoverIcon(row.part_detail.image_url) + renderLink(value, '/part/' + row.part + '/suppliers/');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4,11 +4,21 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h3>Companies</h3>
|
<div class='row'>
|
||||||
<div id='button-toolbar'>
|
<div class='col-sm-6'>
|
||||||
<button style='float: right;' class="btn btn-success" id='new-company'>New Company</button>
|
<h3>Company List</h3>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<div class='container' id='active-build-toolbar' style='float: right;'>
|
||||||
|
<div class='btn-group' style='float: right;'>
|
||||||
|
<button type='button' class="btn btn-success" id='new-company'>New Company</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
|
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import tablib
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@ -120,8 +121,17 @@ class Part(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
|
""" Return the web URL for viewing this part """
|
||||||
return reverse('part-detail', kwargs={'pk': self.id})
|
return reverse('part-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
|
def get_image_url(self):
|
||||||
|
""" Return the URL of the image for this part """
|
||||||
|
|
||||||
|
if self.image:
|
||||||
|
return os.path.join(settings.MEDIA_URL, str(self.image.url))
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
# Short name of the part
|
# Short name of the part
|
||||||
name = models.CharField(max_length=100, unique=True, blank=False, help_text='Part name (must be unique)')
|
name = models.CharField(max_length=100, unique=True, blank=False, help_text='Part name (must be unique)')
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
|
|||||||
""" Serializer for Part (brief detail) """
|
""" Serializer for Part (brief detail) """
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
image_url = serializers.CharField(source='get_image_url', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Part
|
model = Part
|
||||||
@ -40,6 +41,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
|
|||||||
'pk',
|
'pk',
|
||||||
'url',
|
'url',
|
||||||
'name',
|
'name',
|
||||||
|
'image_url',
|
||||||
'description',
|
'description',
|
||||||
'available_stock',
|
'available_stock',
|
||||||
]
|
]
|
||||||
@ -51,6 +53,7 @@ class PartSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
image_url = serializers.CharField(source='get_image_url', read_only=True)
|
||||||
category_name = serializers.CharField(source='category_path', read_only=True)
|
category_name = serializers.CharField(source='category_path', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -60,6 +63,7 @@ class PartSerializer(serializers.ModelSerializer):
|
|||||||
'pk',
|
'pk',
|
||||||
'url', # Link to the part detail page
|
'url', # Link to the part detail page
|
||||||
'name',
|
'name',
|
||||||
|
'image_url',
|
||||||
'IPN',
|
'IPN',
|
||||||
'URL', # Link to an external URL (optional)
|
'URL', # Link to an external URL (optional)
|
||||||
'description',
|
'description',
|
||||||
@ -121,9 +125,10 @@ class SupplierPartSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
|
||||||
part_name = serializers.CharField(source='part.name', read_only=True)
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
|
|
||||||
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
||||||
|
supplier_logo = serializers.CharField(source='supplier.get_image_url', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
@ -131,9 +136,10 @@ class SupplierPartSerializer(serializers.ModelSerializer):
|
|||||||
'pk',
|
'pk',
|
||||||
'url',
|
'url',
|
||||||
'part',
|
'part',
|
||||||
'part_name',
|
'part_detail',
|
||||||
'supplier',
|
'supplier',
|
||||||
'supplier_name',
|
'supplier_name',
|
||||||
|
'supplier_logo',
|
||||||
'SKU',
|
'SKU',
|
||||||
'manufacturer',
|
'manufacturer',
|
||||||
'MPN',
|
'MPN',
|
||||||
|
@ -5,12 +5,19 @@
|
|||||||
|
|
||||||
{% include 'part/tabs.html' with tab='attachments' %}
|
{% include 'part/tabs.html' with tab='attachments' %}
|
||||||
|
|
||||||
<h4>Attachments</h4>
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
<div id='toolbar' class='btn-group'>
|
<h4>Part Attachments</h4>
|
||||||
<button type='button' class='btn btn-success' id='new-attachment'>Add Attachment</button>
|
</div>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<div class="btn-group" style="float: right;">
|
||||||
|
<button type='button' class='btn btn-success' id='new-attachment'>Add Attachment</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' data-toolbar='#toolbar' id='attachment-table'>
|
<table class='table table-striped table-condensed' data-toolbar='#toolbar' id='attachment-table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>File</th>
|
<th>File</th>
|
||||||
|
@ -119,81 +119,18 @@
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
$("#part-table").bootstrapTable({
|
loadPartTable(
|
||||||
sortable: true,
|
"#part-table",
|
||||||
search: true,
|
"{% url 'api-part-list' %}",
|
||||||
sortName: 'description',
|
{
|
||||||
idField: 'pk',
|
query: {
|
||||||
method: 'get',
|
|
||||||
pagination: true,
|
|
||||||
rememberOrder: true,
|
|
||||||
queryParams: function(p) {
|
|
||||||
return {
|
|
||||||
active: true,
|
|
||||||
{% if category %}
|
{% if category %}
|
||||||
category: {{ category.id }},
|
category: {{ category.id }},
|
||||||
include_child_categories: true,
|
include_child_categories: true,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
}
|
},
|
||||||
|
buttons: ['#part-options'],
|
||||||
},
|
},
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
checkbox: true,
|
|
||||||
title: 'Select',
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: 'ID',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
title: 'Part',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return renderLink(value, row.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'description',
|
|
||||||
title: 'Description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'category_name',
|
|
||||||
title: 'Category',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
if (row.category) {
|
|
||||||
return renderLink(row.category_name, "/part/category/" + row.category + "/");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'total_stock',
|
|
||||||
title: 'Stock',
|
|
||||||
searchable: false,
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
if (value) {
|
|
||||||
return renderLink(value, row.url + 'stock/');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return "<span class='label label-warning'>No stock</span>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
url: "{% url 'api-part-list' %}",
|
|
||||||
});
|
|
||||||
|
|
||||||
linkButtonsToSelection(
|
|
||||||
$("#part-table"),
|
|
||||||
['#part-options']
|
|
||||||
);
|
);
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -6,26 +6,24 @@
|
|||||||
|
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<h3>Part Details</h3>
|
<h4>Part Details</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<h3>
|
<div class="btn-group" style="float: right;">
|
||||||
<div class="dropdown" style="float: right;">
|
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
||||||
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
<span class="caret"></span></button>
|
||||||
<span class="caret"></span></button>
|
<ul class="dropdown-menu">
|
||||||
<ul class="dropdown-menu">
|
{% if part.active %}
|
||||||
{% if part.active %}
|
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
|
||||||
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
|
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
|
||||||
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
|
<li><a href='#' id='stocktake-part' title='Stocktake'>Stocktake</a></li>
|
||||||
<li><a href='#' id='stocktake-part' title='Stocktake'>Stocktake</a></li>
|
<hr>
|
||||||
<hr>
|
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
|
||||||
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
|
{% else %}
|
||||||
{% else %}
|
<li><a href="#" id='activate-part' title='Activate part'>Activate</a></li>
|
||||||
<li><a href="#" id='activate-part' title='Activate part'>Activate</a></li>
|
{% endif %}
|
||||||
{% endif %}
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -4,7 +4,14 @@
|
|||||||
|
|
||||||
{% include 'part/tabs.html' with tab='stock' %}
|
{% include 'part/tabs.html' with tab='stock' %}
|
||||||
|
|
||||||
<h3>Part Stock</h3>
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<h4>Part Stock</h4>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
<div id='button-toolbar'>
|
||||||
{% if part.active %}
|
{% if part.active %}
|
||||||
|
@ -4,7 +4,15 @@
|
|||||||
|
|
||||||
{% include 'part/tabs.html' with tab='suppliers' %}
|
{% include 'part/tabs.html' with tab='suppliers' %}
|
||||||
|
|
||||||
<h3>Part Suppliers</h3>
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<h4>Part Suppliers</h4>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
<div id='button-toolbar'>
|
||||||
<button class="btn btn-success" id='supplier-create'>New Supplier Part</button>
|
<button class="btn btn-success" id='supplier-create'>New Supplier Part</button>
|
||||||
@ -55,7 +63,7 @@
|
|||||||
field: 'supplier_name',
|
field: 'supplier_name',
|
||||||
title: 'Supplier',
|
title: 'Supplier',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return renderLink(value, '/company/' + row.supplier + '/');
|
return imageHoverIcon(row.supplier_logo) + renderLink(value, '/company/' + row.supplier + '/');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -38,3 +38,5 @@
|
|||||||
<a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachments.all|length > 0 %}<span class="badge">{{ part.attachments.all|length }}</span>{% endif %}</a>
|
<a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachments.all|length > 0 %}<span class="badge">{{ part.attachments.all|length }}</span>{% endif %}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<br>
|
@ -4,7 +4,17 @@
|
|||||||
|
|
||||||
{% include 'part/tabs.html' with tab='used' %}
|
{% include 'part/tabs.html' with tab='used' %}
|
||||||
|
|
||||||
<h3>Used In</h3>
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<h4>Used to Build</h4>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<div class="btn-group" style="float: right;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<table class="table table-striped table-condensed" id='used-table'>
|
<table class="table table-striped table-condensed" id='used-table'>
|
||||||
</table>
|
</table>
|
||||||
@ -33,7 +43,7 @@
|
|||||||
field: 'part_detail',
|
field: 'part_detail',
|
||||||
title: 'Part',
|
title: 'Part',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return renderLink(value.name, value.url + 'bom/');
|
return imageHoverIcon(row.part_detail.image_url) + renderLink(value.name, value.url + 'bom/');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -404,12 +404,30 @@ class CategoryEdit(AjaxUpdateView):
|
|||||||
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
|
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
context['category'] = PartCategory.objects.get(pk=self.kwargs['pk'])
|
context['category'] = self.get_object()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
""" Customize form data for PartCategory editing.
|
||||||
|
|
||||||
|
Limit the choices for 'parent' field to those which make sense
|
||||||
|
"""
|
||||||
|
|
||||||
|
form = super(AjaxUpdateView, self).get_form()
|
||||||
|
|
||||||
|
category = self.get_object()
|
||||||
|
|
||||||
|
# Remove any invalid choices for the parent category part
|
||||||
|
parent_choices = PartCategory.objects.all()
|
||||||
|
parent_choices = parent_choices.exclude(id__in=category.getUniqueChildren())
|
||||||
|
|
||||||
|
form.fields['parent'].queryset = parent_choices
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
class CategoryDelete(AjaxDeleteView):
|
class CategoryDelete(AjaxDeleteView):
|
||||||
""" Delete view to delete a PartCategory """
|
""" Delete view to delete a PartCategory """
|
||||||
|
@ -10,6 +10,32 @@
|
|||||||
color: #ffcc00;
|
color: #ffcc00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Part image icons with full-display on mouse hover */
|
||||||
|
|
||||||
|
.hover-img-thumb {
|
||||||
|
background: #eee;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid #cce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-img-large {
|
||||||
|
background: #eee;
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
border: 1px solid #555;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-icon:hover > .hover-img-large {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* dropzone class - for Drag-n-Drop file uploads */
|
/* dropzone class - for Drag-n-Drop file uploads */
|
||||||
.dropzone {
|
.dropzone {
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
@ -159,6 +185,14 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-group .panel {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading {
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,16 @@ function loadBomTable(table, options) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Part notes
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
field: 'note',
|
||||||
|
title: 'Notes',
|
||||||
|
searchable: true,
|
||||||
|
sortable: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (options.editable) {
|
if (options.editable) {
|
||||||
cols.push({
|
cols.push({
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
@ -124,6 +134,7 @@ function loadBomTable(table, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
cols.push(
|
cols.push(
|
||||||
{
|
{
|
||||||
@ -149,16 +160,6 @@ function loadBomTable(table, options) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part notes
|
|
||||||
cols.push(
|
|
||||||
{
|
|
||||||
field: 'note',
|
|
||||||
title: 'Notes',
|
|
||||||
searchable: true,
|
|
||||||
sortable: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Configure the table (bootstrap-table)
|
// Configure the table (bootstrap-table)
|
||||||
|
|
||||||
table.bootstrapTable({
|
table.bootstrapTable({
|
||||||
|
@ -118,3 +118,18 @@ function enableDragAndDrop(element, url, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imageHoverIcon(url) {
|
||||||
|
/* Render a small thumbnail icon for an image.
|
||||||
|
* On mouseover, display a full-size version of the image
|
||||||
|
*/
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<a class='hover-icon'>
|
||||||
|
<img class='hover-img-thumb' src='` + url + `'>
|
||||||
|
<img class='hover-img-large' src='` + url + `'>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
@ -73,3 +73,99 @@ function toggleStar(options) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadPartTable(table, url, options={}) {
|
||||||
|
/* Load part listing data into specified table.
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* - table: HTML reference to the table
|
||||||
|
* - url: Base URL for API query
|
||||||
|
* - options: object containing following (optional) fields
|
||||||
|
* allowInactive: If true, allow display of inactive parts
|
||||||
|
* query: extra query params for API request
|
||||||
|
* buttons: If provided, link buttons to selection status of this table
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Default query params
|
||||||
|
query = options.query;
|
||||||
|
|
||||||
|
if (!options.allowInactive) {
|
||||||
|
// Only display active parts
|
||||||
|
query.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(table).bootstrapTable({
|
||||||
|
url: url,
|
||||||
|
sortable: true,
|
||||||
|
search: true,
|
||||||
|
sortName: 'name',
|
||||||
|
method: 'get',
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 25,
|
||||||
|
rememberOrder: true,
|
||||||
|
formatNoMatches: function() { return "No parts found"; },
|
||||||
|
queryParams: function(p) {
|
||||||
|
return query;
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
checkbox: true,
|
||||||
|
title: 'Select',
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: 'Part',
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var display = imageHoverIcon(row.image_url) + renderLink(value, row.url);
|
||||||
|
if (!row.active) {
|
||||||
|
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>";
|
||||||
|
}
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'description',
|
||||||
|
title: 'Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'category_name',
|
||||||
|
title: 'Category',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
if (row.category) {
|
||||||
|
return renderLink(row.category_name, "/part/category/" + row.category + "/");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'total_stock',
|
||||||
|
title: 'Stock',
|
||||||
|
searchable: false,
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
if (value) {
|
||||||
|
return renderLink(value, row.url + 'stock/');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "<span class='label label-warning'>No stock</span>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.buttons) {
|
||||||
|
linkButtonsToSelection($(table), options.buttons);
|
||||||
|
}
|
||||||
|
}
|
@ -347,6 +347,7 @@ function loadStockTable(table, options) {
|
|||||||
search: true,
|
search: true,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
pagination: true,
|
pagination: true,
|
||||||
|
pageSize: 25,
|
||||||
rememberOrder: true,
|
rememberOrder: true,
|
||||||
queryParams: options.params,
|
queryParams: options.params,
|
||||||
columns: [
|
columns: [
|
||||||
@ -365,9 +366,14 @@ function loadStockTable(table, options) {
|
|||||||
title: 'Part',
|
title: 'Part',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return renderLink(value, row.part.url);
|
return imageHoverIcon(row.part.image_url) + renderLink(value, row.part.url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'part.description',
|
||||||
|
title: 'Description',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'location',
|
field: 'location',
|
||||||
title: 'Location',
|
title: 'Location',
|
||||||
|
@ -75,6 +75,24 @@ class StockLocationEdit(AjaxUpdateView):
|
|||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Stock Location'
|
ajax_form_title = 'Edit Stock Location'
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
""" Customize form data for StockLocation editing.
|
||||||
|
|
||||||
|
Limit the choices for 'parent' field to those which make sense.
|
||||||
|
"""
|
||||||
|
|
||||||
|
form = super(AjaxUpdateView, self).get_form()
|
||||||
|
|
||||||
|
location = self.get_object()
|
||||||
|
|
||||||
|
# Remove any invalid choices for the 'parent' field
|
||||||
|
parent_choices = StockLocation.objects.all()
|
||||||
|
parent_choices = parent_choices.exclude(id__in=location.getUniqueChildren())
|
||||||
|
|
||||||
|
form.fields['parent'].queryset = parent_choices
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
class StockLocationQRCode(QRCodeView):
|
class StockLocationQRCode(QRCodeView):
|
||||||
""" View for displaying a QR code for a StockLocation object """
|
""" View for displaying a QR code for a StockLocation object """
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>InvenTree</h3>
|
<h3>InvenTree</h3>
|
||||||
|
<hr>
|
||||||
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %}
|
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %}
|
||||||
|
|
||||||
{% if to_order %}
|
{% if to_order %}
|
||||||
|
@ -23,56 +23,14 @@
|
|||||||
$("#part-result-count").html("(found " + n + " results)");
|
$("#part-result-count").html("(found " + n + " results)");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#part-results-table").bootstrapTable({
|
loadPartTable("#part-results-table",
|
||||||
sortable: true,
|
"{% url 'api-part-list' %}",
|
||||||
search: true,
|
{
|
||||||
pagination: true,
|
query: {
|
||||||
formatNoMatches: function() { return "No parts found matching search query"; },
|
|
||||||
queryParams: function(p) {
|
|
||||||
return {
|
|
||||||
search: "{{ query }}",
|
search: "{{ query }}",
|
||||||
}
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: 'ID',
|
|
||||||
visible: false,
|
|
||||||
},
|
},
|
||||||
{
|
allowInactive: true,
|
||||||
field: 'name',
|
}
|
||||||
title: 'Name',
|
);
|
||||||
sortable: true,
|
|
||||||
searchable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return renderLink(value, row.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'IPN',
|
|
||||||
title: 'Internal Part Number',
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'description',
|
|
||||||
title: 'Description',
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'available_stock',
|
|
||||||
title: 'Stock',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
if (value) {
|
|
||||||
return renderLink(value, row.url + 'stock/');
|
|
||||||
} else {
|
|
||||||
return renderLink('No stock', row.url + 'stock/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
url: "{% url 'api-part-list' %}"
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -86,6 +86,7 @@ InvenTree
|
|||||||
|
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
|
||||||
|
<script type='text/javascript' src="{% static 'script/inventree/part.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% if build.status == build.PENDING %}
|
{% if build.status == build.PENDING %}
|
||||||
<span class='label label-info'>
|
<span class='label label-info'>
|
||||||
{% elif build.status == build.HOLDING %}
|
{% elif build.status == build.ALLOCATED %}
|
||||||
<span class='label label-warning'>
|
<span class='label label-primary'>
|
||||||
{% elif build.status == build.CANCELLED %}
|
{% elif build.status == build.CANCELLED %}
|
||||||
<span class='label label-danger'>
|
<span class='label label-danger'>
|
||||||
{% elif build.status == build.COMPLETE %}
|
{% elif build.status == build.COMPLETE %}
|
||||||
|
@ -14,6 +14,9 @@
|
|||||||
|
|
||||||
|
|
||||||
{% crispy form %}
|
{% crispy form %}
|
||||||
|
|
||||||
|
{% block form_data %}
|
||||||
|
{% endblock %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% block post_form_content %}
|
{% block post_form_content %}
|
||||||
|
Loading…
Reference in New Issue
Block a user