Merge pull request #836 from SchrodingersGat/serial-number-fixes

Serial number fixes
This commit is contained in:
Oliver 2020-05-26 12:16:02 +10:00 committed by GitHub
commit 3678c940eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 128 additions and 58 deletions

View File

@ -272,8 +272,9 @@ function setupFilterList(tableKey, table, target) {
for (var key in filters) { for (var key in filters) {
var value = getFilterOptionValue(tableKey, key, filters[key]); var value = getFilterOptionValue(tableKey, key, filters[key]);
var title = getFilterTitle(tableKey, key); var title = getFilterTitle(tableKey, key);
var description = getFilterDescription(tableKey, key);
element.append(`<div class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`); element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
} }
// Add a callback for adding a new filter // Add a callback for adding a new filter
@ -362,6 +363,15 @@ function getFilterTitle(tableKey, filterKey) {
} }
/**
* Return the pretty description for the given table and filter selection
*/
function getFilterDescription(tableKey, filterKey) {
var settings = getFilterSettings(tableKey, filterKey);
return settings.title;
}
/* /*
* Return a description for the given table and filter selection. * Return a description for the given table and filter selection.
*/ */

View File

@ -33,6 +33,13 @@
<td>{{ part.revision }}</td> <td>{{ part.revision }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if part.trackable %}
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td><b>{% trans "Next Serial Number" %}</b></td>
<td>{{ part.getNextSerialNumber }}</td>
</tr>
{% endif %}
<tr> <tr>
<td><span class='fas fa-info-circle'></span></td> <td><span class='fas fa-info-circle'></span></td>
<td><b>{% trans "Description" %}</b></td> <td><b>{% trans "Description" %}</b></td>

View File

@ -6,11 +6,14 @@
{% block content %} {% block content %}
{% if part.virtual %}
<div class='alert alert-info alert-block'>
{% trans "This part is a virtual part" %}
</div>
{% endif %}
{% if part.is_template %} {% if part.is_template %}
<div class='alert alert-info alert-block'> <div class='alert alert-info alert-block'>
{% trans "This part is a template part." %} {% trans "This part is a template part." %}
<br>
{% trans "It is not a real part, but real parts can be based on this template." %}
</div> </div>
{% endif %} {% endif %}
{% if part.variant_of %} {% if part.variant_of %}

View File

@ -13,9 +13,11 @@
<a href="{% url 'part-variants' part.id %}">{% trans "Variants" %} <span class='badge'>{{ part.variants.count }}</span></span></a> <a href="{% url 'part-variants' part.id %}">{% trans "Variants" %} <span class='badge'>{{ part.variants.count }}</span></span></a>
</li> </li>
{% endif %} {% endif %}
{% if not part.virtual %}
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}> <li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
<a href="{% url 'part-stock' part.id %}">{% trans "Stock" %} <span class="badge">{% decimal part.total_stock %}</span></a> <a href="{% url 'part-stock' part.id %}">{% trans "Stock" %} <span class="badge">{% decimal part.total_stock %}</span></a>
</li> </li>
{% endif %}
{% if part.component or part.used_in_count > 0 %} {% if part.component or part.used_in_count > 0 %}
<li{% ifequal tab 'allocation' %} class="active"{% endifequal %}> <li{% ifequal tab 'allocation' %} class="active"{% endifequal %}>
<a href="{% url 'part-allocation' part.id %}">{% trans "Allocated" %} <span class="badge">{% decimal part.allocation_count %}</span></a> <a href="{% url 'part-allocation' part.id %}">{% trans "Allocated" %} <span class="badge">{% decimal part.allocation_count %}</span></a>

View File

@ -488,6 +488,19 @@ class StockList(generics.ListCreateAPIView):
if serial_number is not None: if serial_number is not None:
queryset = queryset.filter(serial=serial_number) queryset = queryset.filter(serial=serial_number)
# Filter by range of serial numbers?
serial_number_gte = params.get('serial_gte', None)
serial_number_lte = params.get('serial_lte', None)
if serial_number_gte is not None or serial_number_lte is not None:
queryset = queryset.exclude(serial=None)
if serial_number_gte is not None:
queryset = queryset.filter(serial__gte=serial_number_gte)
if serial_number_lte is not None:
queryset = queryset.filter(serial__lte=serial_number_lte)
in_stock = params.get('in_stock', None) in_stock = params.get('in_stock', None)
if in_stock is not None: if in_stock is not None:

View File

@ -32,7 +32,7 @@
<input class='numberinput' <input class='numberinput'
min='0' min='0'
{% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %} {% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %}
value='{{ item.new_quantity }}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/> value='{% decimal item.new_quantity %}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
{% if item.error %} {% if item.error %}
<br><span class='help-inline'>{{ item.error }}</span> <br><span class='help-inline'>{{ item.error }}</span>
{% endif %} {% endif %}

View File

@ -1036,6 +1036,36 @@ class StockItemCreate(AjaxCreateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Create new Stock Item') ajax_form_title = _('Create new Stock Item')
def get_part(self, form=None):
"""
Attempt to get the "part" associted with this new stockitem.
- May be passed to the form as a query parameter (e.g. ?part=<id>)
- May be passed via the form field itself.
"""
# Try to extract from the URL query
part_id = self.request.GET.get('part', None)
if part_id:
try:
part = Part.objects.get(pk=part_id)
return part
except (Part.DoesNotExist, ValueError):
pass
# Try to get from the form
if form:
try:
part_id = form['part'].value()
part = Part.objects.get(pk=part_id)
return part
except (Part.DoesNotExist, ValueError):
pass
# Could not extract a part object
return None
def get_form(self): def get_form(self):
""" Get form for StockItem creation. """ Get form for StockItem creation.
Overrides the default get_form() method to intelligently limit Overrides the default get_form() method to intelligently limit
@ -1044,53 +1074,44 @@ class StockItemCreate(AjaxCreateView):
form = super().get_form() form = super().get_form()
part = None part = self.get_part(form=form)
# If the user has selected a Part, limit choices for SupplierPart if part is not None:
if form['part'].value(): sn = part.getNextSerialNumber()
part_id = form['part'].value() form.field_placeholder['serial_numbers'] = _('Next available serial number is') + ' ' + str(sn)
try: form.rebuild_layout()
part = Part.objects.get(id=part_id)
sn = part.getNextSerialNumber() # Hide the 'part' field (as a valid part is selected)
form.field_placeholder['serial_numbers'] = _('Next available serial number is') + ' ' + str(sn) form.fields['part'].widget = HiddenInput()
form.rebuild_layout() # trackable parts get special consideration
if part.trackable:
form.fields['delete_on_deplete'].widget = HiddenInput()
form.fields['delete_on_deplete'].initial = False
else:
form.fields.pop('serial_numbers')
# Hide the 'part' field (as a valid part is selected) # If the part is NOT purchaseable, hide the supplier_part field
form.fields['part'].widget = HiddenInput() if not part.purchaseable:
form.fields['supplier_part'].widget = HiddenInput()
else:
# Pre-select the allowable SupplierPart options
parts = form.fields['supplier_part'].queryset
parts = parts.filter(part=part.id)
# trackable parts get special consideration form.fields['supplier_part'].queryset = parts
if part.trackable:
form.fields['delete_on_deplete'].widget = HiddenInput()
form.fields['delete_on_deplete'].initial = False
else:
form.fields.pop('serial_numbers')
# If the part is NOT purchaseable, hide the supplier_part field # If there is one (and only one) supplier part available, pre-select it
if not part.purchaseable: all_parts = parts.all()
form.fields['supplier_part'].widget = HiddenInput()
else:
# Pre-select the allowable SupplierPart options
parts = form.fields['supplier_part'].queryset
parts = parts.filter(part=part.id)
form.fields['supplier_part'].queryset = parts if len(all_parts) == 1:
# If there is one (and only one) supplier part available, pre-select it # TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
all_parts = parts.all() form.fields['supplier_part'].initial = all_parts[0].id
if len(all_parts) == 1:
# TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
form.fields['supplier_part'].initial = all_parts[0].id
except Part.DoesNotExist:
pass
# Otherwise if the user has selected a SupplierPart, we know what Part they meant! # Otherwise if the user has selected a SupplierPart, we know what Part they meant!
elif form['supplier_part'].value() is not None: if form['supplier_part'].value() is not None:
pass pass
return form return form
@ -1113,27 +1134,20 @@ class StockItemCreate(AjaxCreateView):
else: else:
initials = super(StockItemCreate, self).get_initial().copy() initials = super(StockItemCreate, self).get_initial().copy()
part_id = self.request.GET.get('part', None) part = self.get_part()
loc_id = self.request.GET.get('location', None) loc_id = self.request.GET.get('location', None)
sup_part_id = self.request.GET.get('supplier_part', None) sup_part_id = self.request.GET.get('supplier_part', None)
part = None
location = None location = None
supplier_part = None supplier_part = None
# Part field has been specified if part is not None:
if part_id: # Check that the supplied part is 'valid'
try: if not part.is_template and part.active and not part.virtual:
part = Part.objects.get(pk=part_id) initials['part'] = part
initials['location'] = part.get_default_location()
# Check that the supplied part is 'valid' initials['supplier_part'] = part.default_supplier
if not part.is_template and part.active and not part.virtual:
initials['part'] = part
initials['location'] = part.get_default_location()
initials['supplier_part'] = part.default_supplier
except (ValueError, Part.DoesNotExist):
pass
# SupplierPart field has been specified # SupplierPart field has been specified
# It must match the Part, if that has been supplied # It must match the Part, if that has been supplied

View File

@ -337,12 +337,21 @@ function loadStockTable(table, options) {
} else { } else {
return '-'; return '-';
} }
} else if (field == 'location__path') { } else if (field == 'location_detail.pathstring') {
/* Determine how many locations */ /* Determine how many locations */
var locations = []; var locations = [];
data.forEach(function(item) { data.forEach(function(item) {
var loc = item.location;
var loc = null;
if (item.location_detail) {
loc = item.location_detail.pathstring;
} else {
loc = "{% trans "Undefined location" %}";
}
console.log("Location: " + loc);
if (!locations.includes(loc)) { if (!locations.includes(loc)) {
locations.push(loc); locations.push(loc);
@ -353,7 +362,11 @@ function loadStockTable(table, options) {
return "In " + locations.length + " locations"; return "In " + locations.length + " locations";
} else { } else {
// A single location! // A single location!
return renderLink(row.location__path, '/stock/location/' + row.location + '/') if (row.location_detail) {
return renderLink(row.location_detail.pathstring, `/stock/location/${row.location}/`);
} else {
return "<i>{% trans "Undefined location" %}</i>";
}
} }
} else if (field == 'notes') { } else if (field == 'notes') {
var notes = []; var notes = [];

View File

@ -34,6 +34,14 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Is allocated" %}', title: '{% trans "Is allocated" %}',
description: '{% trans "Item has been alloacted" %}', description: '{% trans "Item has been alloacted" %}',
}, },
serial_gte: {
title: "{% trans "Serial number GTE" %}",
description: "{% trans "Serial number greater than or equal to" %}"
},
serial_lte: {
title: "{% trans "Serial number LTE" %}",
description: "{% trans "Serial number less than or equal to" %}",
},
}; };
} }