Merge remote-tracking branch 'inventree/master' into 0.4.x

This commit is contained in:
Oliver 2021-08-04 09:04:24 +10:00
commit c4570a79de
11 changed files with 423 additions and 236 deletions

View File

@ -111,7 +111,7 @@ src="{% static 'img/blank_image.png' %}"
<li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li> <li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
{% endif %} {% endif %}
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %} {% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build"% }</a> <li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build" %}</a>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View File

@ -443,6 +443,8 @@ class PartFilter(rest_filters.FilterSet):
else: else:
queryset = queryset.filter(IPN='') queryset = queryset.filter(IPN='')
return queryset
# Regex filter for name # Regex filter for name
name_regex = rest_filters.CharFilter(label='Filter by name (regex)', field_name='name', lookup_expr='iregex') name_regex = rest_filters.CharFilter(label='Filter by name (regex)', field_name='name', lookup_expr='iregex')

View File

@ -899,7 +899,7 @@
{% for line in price_history %}'{{ line.date }}',{% endfor %} {% for line in price_history %}'{{ line.date }}',{% endfor %}
], ],
datasets: [{ datasets: [{
label: '{% blocktrans %}Single Price - {{currency}}{% endblocktrans %}', label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(255, 99, 132, 0.2)', backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)', borderColor: 'rgb(255, 99, 132)',
yAxisID: 'y', yAxisID: 'y',
@ -911,7 +911,7 @@
}, },
{% if 'price_diff' in price_history.0 %} {% if 'price_diff' in price_history.0 %}
{ {
label: '{% blocktrans %}Single Price Difference - {{currency}}{% endblocktrans %}', label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(68, 157, 68, 0.2)', backgroundColor: 'rgba(68, 157, 68, 0.2)',
borderColor: 'rgb(68, 157, 68)', borderColor: 'rgb(68, 157, 68)',
yAxisID: 'y2', yAxisID: 'y2',
@ -923,7 +923,7 @@
hidden: true, hidden: true,
}, },
{ {
label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}', label: '{% blocktrans %}Supplier Unit Cost - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(70, 127, 155, 0.2)', backgroundColor: 'rgba(70, 127, 155, 0.2)',
borderColor: 'rgb(70, 127, 155)', borderColor: 'rgb(70, 127, 155)',
yAxisID: 'y', yAxisID: 'y',

View File

@ -10,31 +10,22 @@
{% block content %} {% block content %}
<div class='panel panel-default panel-inventree'> <div class="panel panel-default panel-inventree">
<!-- Default panel contents -->
<div class="panel-heading"><h3>{{ part.full_name }}</h3></div>
<div class="panel-body">
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
{% include "part/part_thumb.html" %} {% include "part/part_thumb.html" %}
<div class="media-body"> <div class="media-body">
<h3>
{{ part.full_name }}
{% if user.is_staff and roles.part.change %}
<a href="{% url 'admin:part_part_change' part.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %}
{% if not part.active %}
<div class='label label-large label-large-red'>
{% trans 'Inactive' %}
</div>
{% endif %}
</h3>
{% if part.description %}
<p><em>{{ part.description }}</em></p>
{% endif %}
<p> <p>
<div id='part-properties' class='btn-group' role='group'> <h3>
{% if part.virtual %} <!-- Admin View -->
<span class='fas fa-ghost' title='{% trans "Part is virtual (not a physical part)" %}'></span> {% if user.is_staff and roles.part.change %}
<a href="{% url 'admin:part_part_change' part.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>&ensp;
{% endif %} {% endif %}
<!-- Properties -->
<div id='part-properties' class='btn-group' role='group'>
{% if part.is_template %} {% if part.is_template %}
<span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span> <span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span>
{% endif %} {% endif %}
@ -54,6 +45,23 @@
<span class='fas fa-dollar-sign' title='{% trans "Part can be sold to customers" %}'></span> <span class='fas fa-dollar-sign' title='{% trans "Part can be sold to customers" %}'></span>
{% endif %} {% endif %}
</div> </div>
<!-- Part active -->
{% if not part.active %}
&ensp;
<div class='label label-large label-large-red'>
<span class='fas fa-skull-crossbones' title='{% trans "Part is virtual (not a physical part)" %}'></span>
{% trans 'Inactive' %}
</div>
{% endif %}
<!-- Part virtual -->
{% if part.virtual and part.active %}
&ensp;
<div class='label label-large label-large-yellow'>
<span class='fas fa-ghost' title='{% trans "Part is virtual (not a physical part)" %}'></span>
{% trans 'Virtual' %}
</div>
{% endif %}
</h3>
</p> </p>
<div class='btn-group action-buttons' role='group'> <div class='btn-group action-buttons' role='group'>
@ -122,51 +130,11 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<table class='table table-condensed'>
<col width='25'>
{% if part.keywords %}
<tr>
<td><span class='fas fa-key'></span></td>
<td>{% trans "Keywords" %}</td>
<td>{{ part.keywords }}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a></td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Creation Date" %}</td>
<td>
{{ part.creation_date }}
{% if part.creation_user %}
<span class='badge'>{{ part.creation_user }}</span>
{% endif %}
</td>
</tr>
{% if part.trackable and part.getLatestSerialNumber %}
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Latest Serial Number" %}</td>
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td>
</tr>
{% endif %}
</table>
</div> </div>
</div> </div>
<div class='info-messages'> <div class='info-messages'>
{% if part.virtual %}
<div class='alert alert-warning alert-block'>
{% trans "This is a virtual part" %}
</div>
{% endif %}
{% if part.variant_of %} {% if part.variant_of %}
<div class='alert alert-info alert-block'> <div class='alert alert-info alert-block' style='padding: 10px;'>
{% object_link 'part-detail' part.variant_of.id part.variant_of.full_name as link %} {% object_link 'part-detail' part.variant_of.id part.variant_of.full_name as link %}
{% blocktrans %}This part is a variant of {{link}}{% endblocktrans %} {% blocktrans %}This part is a variant of {{link}}{% endblocktrans %}
</div> </div>
@ -174,13 +142,11 @@
</div> </div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<table class="table table-striped"> <table class='table table-condensed table-striped'>
<col width='25'> <col width='25'>
<tr> <tr>
<td><span class='fas fa-boxes'></span></td> <td><h4><span class='fas fa-boxes'></span></h4></td>
<td> <td><h4>{% trans "Available Stock" %}</h4></td>
<h4>{% trans "Available Stock" %}</h4>
</td>
<td><h4>{% decimal available %}{% if part.units %} {{ part.units }}{% endif %}</h4></td> <td><h4>{% decimal available %}{% if part.units %} {{ part.units }}{% endif %}</h4></td>
</tr> </tr>
<tr> <tr>
@ -220,9 +186,9 @@
{% if not part.is_template %} {% if not part.is_template %}
{% if part.assembly %} {% if part.assembly %}
<tr> <tr>
<td><span class='fas fa-tools'></span></td> <td><h4><span class='fas fa-tools'></span></h4></td>
<td colspan='2'> <td colspan='2'>
<strong>{% trans "Build Status" %}</strong> <h4>{% trans "Build Status" %}</h4>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -242,6 +208,92 @@
</table> </table>
</div> </div>
</div> </div>
</div>
<p>
<!-- Details show/hide button -->
<button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show">
</button>
</p>
<div class="collapse" id="collapsible-part-details">
<div class="card card-body">
<!-- Details Table -->
<table class="table table-striped">
<col width='25'>
{% if part.IPN %}
<tr>
<td><span class='fas fa-tag'></span></td>
<td>{% trans "IPN" %}</td>
<td>{{ part.IPN }}{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Name" %}</td>
<td>{{ part.name }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
{% if part.revision %}
<tr>
<td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Revision" %}</td>
<td>{{ part.revision }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.keywords %}
<tr>
<td><span class='fas fa-key'></span></td>
<td>{% trans "Keywords" %}</td>
<td>{{ part.keywords }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Creation Date" %}</td>
<td>
{{ part.creation_date }}
{% if part.creation_user %}
<span class='badge'>{{ part.creation_user }}</span>
{% endif %}
</td>
</tr>
{% if part.trackable and part.getLatestSerialNumber %}
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Latest Serial Number" %}</td>
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.default_location %}
<tr>
<td><span class='fas fa-search-location'></span></td>
<td>{% trans "Default Location" %}</td>
<td>{{ part.default_location }}</td>
</tr>
{% endif %}
{% if part.default_supplier %}
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Default Supplier" %}</td>
<td>{{ part.default_supplier }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
</div> </div>
@ -450,4 +502,42 @@
}); });
{% endif %} {% endif %}
$("#toggle-part-details").click(function() {
if (this.value == 'show') {
this.innerHTML = '<span class="fas fa-chevron-up"></span> {% trans "Hide Part Details" %}';
this.value = 'hide';
// Store state of part details section
localStorage.setItem("part-details-show", true);
} else {
this.innerHTML = '<span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %}';
this.value = 'show';
// Store state of part details section
localStorage.setItem("part-details-show", false);
}
});
// Load part details section
window.onload = function() {
details_show = localStorage.getItem("part-details-show")
if (details_show === 'true') {
console.log(details_show)
// Get collapsible details section
details = document.getElementById('collapsible-part-details');
// Add "show" class
details.classList.add("in");
// Get toggle
toggle = document.getElementById('toggle-part-details');
// Change state of toggle
toggle.innerHTML = '<span class="fas fa-chevron-up"></span> {% trans "Hide Part Details" %}';
toggle.value = 'hide';
} else {
// Get toggle
toggle = document.getElementById('toggle-part-details');
// Change state of toggle
toggle.innerHTML = '<span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %}';
toggle.value = 'show';
}
}
{% endblock %} {% endblock %}

View File

@ -161,7 +161,7 @@
<div class='panel-content'> <div class='panel-content'>
<h4>{% trans 'Stock Pricing' %} <h4>{% trans 'Stock Pricing' %}
<i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part. <i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part.
The part single price is the current purchase price for that supplier part."></i> The Supplier Unit Cost is the current purchase price for that supplier part."></i>
</h4> </h4>
{% if price_history|length > 0 %} {% if price_history|length > 0 %}
<div style="max-width: 99%; min-height: 300px"> <div style="max-width: 99%; min-height: 300px">

View File

@ -8,7 +8,6 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.forms.utils import ErrorDict from django.forms.utils import ErrorDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from mptt.fields import TreeNodeChoiceField from mptt.fields import TreeNodeChoiceField
@ -241,14 +240,9 @@ class InstallStockForm(HelperForm):
help_text=_('Stock item to install') help_text=_('Stock item to install')
) )
quantity_to_install = RoundingDecimalFormField( to_install = forms.BooleanField(
max_digits=10, decimal_places=5, widget=forms.HiddenInput(),
initial=1, required=False,
label=_('Quantity'),
help_text=_('Stock quantity to assign'),
validators=[
MinValueValidator(0.001)
]
) )
notes = forms.CharField( notes = forms.CharField(
@ -261,7 +255,7 @@ class InstallStockForm(HelperForm):
fields = [ fields = [
'part', 'part',
'stock_item', 'stock_item',
'quantity_to_install', # 'quantity_to_install',
'notes', 'notes',
] ]

View File

@ -119,6 +119,11 @@
<h4>{% trans "Installed Stock Items" %}</h4> <h4>{% trans "Installed Stock Items" %}</h4>
</div> </div>
<div class='panel-content'> <div class='panel-content'>
<div class='btn-group'>
<button type='button' class='btn btn-success' id='stock-item-install'>
<span class='fas fa-plus-circle'></span> {% trans "Install Stock Item" %}
</button>
</div>
<table class='table table-striped table-condensed' id='installed-table'></table> <table class='table table-striped table-condensed' id='installed-table'></table>
</div> </div>
</div> </div>
@ -128,6 +133,20 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
$('#stock-item-install').click(function() {
launchModalForm(
"{% url 'stock-item-install' item.pk %}",
{
data: {
'part': {{ item.part.pk }},
'install_item': true,
},
reload: true,
}
);
});
loadInstalledInTable( loadInstalledInTable(
$('#installed-table'), $('#installed-table'),
{ {

View File

@ -127,9 +127,11 @@
<li><a href='#' id='stock-return-from-customer' title='{% trans "Return to stock" %}'><span class='fas fa-undo'></span> {% trans "Return to stock" %}</a></li> <li><a href='#' id='stock-return-from-customer' title='{% trans "Return to stock" %}'><span class='fas fa-undo'></span> {% trans "Return to stock" %}</a></li>
{% endif %} {% endif %}
{% if item.belongs_to %} {% if item.belongs_to %}
<li> <li><a href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a></li>
<a href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a> {% else %}
</li> {% if item.part.get_used_in %}
<li><a href='#' id='stock-install-in' title='{% trans "Install stock item" %}'><span class='fas fa-link'></span> {% trans "Install" %}</a></li>
{% endif %}
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@ -461,13 +463,27 @@ $("#stock-serialize").click(function() {
); );
}); });
$('#stock-install-in').click(function() {
launchModalForm(
"{% url 'stock-item-install' item.pk %}",
{
data: {
'part': {{ item.part.pk }},
'install_in': true,
},
reload: true,
}
);
});
$('#stock-uninstall').click(function() { $('#stock-uninstall').click(function() {
launchModalForm( launchModalForm(
"{% url 'stock-item-uninstall' %}", "{% url 'stock-item-uninstall' %}",
{ {
data: { data: {
'items[]': [{{ item.pk}}], 'items[]': [{{ item.pk }}],
}, },
reload: true, reload: true,
} }

View File

@ -3,15 +3,31 @@
{% block pre_form_content %} {% block pre_form_content %}
{% if install_item %}
<p> <p>
{% trans "Install another StockItem into this item." %} {% trans "Install another Stock Item into this item." %}
</p> </p>
<p> <p>
{% trans "Stock items can only be installed if they meet the following criteria" %}: {% trans "Stock items can only be installed if they meet the following criteria" %}:
<ul> <ul>
<li>{% trans "The StockItem links to a Part which is in the BOM for this StockItem" %}</li> <li>{% trans "The Stock Item links to a Part which is in the BOM for this Stock Item" %}</li>
<li>{% trans "The StockItem is currently in stock" %}</li> <li>{% trans "The Stock Item is currently in stock" %}</li>
<li>{% trans "The Stock Item is serialized and does not belong to another item" %}</li>
</ul> </ul>
</p> </p>
{% elif install_in %}
<p>
{% trans "Install this Stock Item in another stock item." %}
</p>
<p>
{% trans "Stock items can only be installed if they meet the following criteria" %}:
<ul>
<li>{% trans "The part associated to this Stock Item belongs to another part's BOM" %}</li>
<li>{% trans "This Stock Item is serialized and does not belong to another item" %}</li>
</ul>
</p>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -536,36 +536,73 @@ class StockItemInstall(AjaxUpdateView):
part = None part = None
def get_params(self):
""" Retrieve GET parameters """
# Look at GET params
self.part_id = self.request.GET.get('part', None)
self.install_in = self.request.GET.get('install_in', False)
self.install_item = self.request.GET.get('install_item', False)
if self.part_id is None:
# Look at POST params
self.part_id = self.request.POST.get('part', None)
try:
self.part = Part.objects.get(pk=self.part_id)
except (ValueError, Part.DoesNotExist):
self.part = None
def get_stock_items(self): def get_stock_items(self):
""" """
Return a list of stock items suitable for displaying to the user. Return a list of stock items suitable for displaying to the user.
Requirements: Requirements:
- Items must be in stock - Items must be in stock
- Items must be in BOM of stock item
Filters: - Items must be serialized
- Items can be filtered by Part reference
""" """
# Filter items in stock
items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER) items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
# Filter by Part association # Filter serialized stock items
items = items.exclude(serial__isnull=True).exclude(serial__exact='')
# Look at GET params if self.part:
part_id = self.request.GET.get('part', None) # Filter for parts to install this item in
if self.install_in:
# Get parts using this part
allowed_parts = self.part.get_used_in()
# Filter
items = items.filter(part__in=allowed_parts)
if part_id is None: # Filter for parts to install in this item
# Look at POST params if self.install_item:
part_id = self.request.POST.get('part', None) # Get parts used in this part's BOM
bom_items = self.part.get_bom_items()
try: allowed_parts = [item.sub_part for item in bom_items]
self.part = Part.objects.get(pk=part_id) # Filter
items = items.filter(part=self.part) items = items.filter(part__in=allowed_parts)
except (ValueError, Part.DoesNotExist):
self.part = None
return items return items
def get_context_data(self, **kwargs):
""" Retrieve parameters and update context """
ctx = super().get_context_data(**kwargs)
# Get request parameters
self.get_params()
ctx.update({
'part': self.part,
'install_in': self.install_in,
'install_item': self.install_item,
})
return ctx
def get_initial(self): def get_initial(self):
initials = super().get_initial() initials = super().get_initial()
@ -576,11 +613,16 @@ class StockItemInstall(AjaxUpdateView):
if items.count() == 1: if items.count() == 1:
item = items.first() item = items.first()
initials['stock_item'] = item.pk initials['stock_item'] = item.pk
initials['quantity_to_install'] = item.quantity
if self.part: if self.part:
initials['part'] = self.part initials['part'] = self.part
try:
# Is this stock item being installed in the other stock item?
initials['to_install'] = self.install_in or not self.install_item
except AttributeError:
pass
return initials return initials
def get_form(self): def get_form(self):
@ -593,6 +635,8 @@ class StockItemInstall(AjaxUpdateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.get_params()
form = self.get_form() form = self.get_form()
valid = form.is_valid() valid = form.is_valid()
@ -602,12 +646,18 @@ class StockItemInstall(AjaxUpdateView):
data = form.cleaned_data data = form.cleaned_data
other_stock_item = data['stock_item'] other_stock_item = data['stock_item']
quantity = data['quantity_to_install'] # Quantity will always be 1 for serialized item
quantity = 1
notes = data['notes'] notes = data['notes']
# Install the other stock item into this one # Get stock item
this_stock_item = self.get_object() this_stock_item = self.get_object()
if data['to_install']:
# Install this stock item into the other stock item
other_stock_item.installStockItem(this_stock_item, quantity, request.user, notes)
else:
# Install the other stock item into this one
this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes) this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes)
data = { data = {

View File

@ -262,13 +262,13 @@ function loadBomTable(table, options) {
cols.push( cols.push(
{ {
field: 'price_range', field: 'price_range',
title: '{% trans "Buy Price" %}', title: '{% trans "Supplier Cost" %}',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
if (value) { if (value) {
return value; return value;
} else { } else {
return "<span class='warning-msg'>{% trans 'No pricing available' %}</span>"; return "<span class='warning-msg'>{% trans 'No supplier pricing available' %}</span>";
} }
} }
}); });