mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'case-insensitive-names'
This commit is contained in:
commit
6fbe588aee
@ -104,9 +104,9 @@ def str2bool(text, test=True):
|
||||
True if the text looks like the selected boolean value
|
||||
"""
|
||||
if test:
|
||||
return str(text).lower() in ['1', 'y', 'yes', 't', 'true', 'ok', ]
|
||||
return str(text).lower() in ['1', 'y', 'yes', 't', 'true', 'ok', 'on', ]
|
||||
else:
|
||||
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', ]
|
||||
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ]
|
||||
|
||||
|
||||
def WrapWithQuotes(text, quote='"'):
|
||||
|
@ -9,7 +9,7 @@ from .models import BomItem
|
||||
|
||||
class PartAdmin(ImportExportModelAdmin):
|
||||
|
||||
list_display = ('name', 'IPN', 'description', 'total_stock', 'category')
|
||||
list_display = ('long_name', 'IPN', 'description', 'total_stock', 'category')
|
||||
|
||||
|
||||
class PartCategoryAdmin(ImportExportModelAdmin):
|
||||
|
@ -60,9 +60,12 @@ class EditPartAttachmentForm(HelperForm):
|
||||
class EditPartForm(HelperForm):
|
||||
""" Form for editing a Part object """
|
||||
|
||||
confirm_creation = forms.BooleanField(required=False, initial=False, help_text='Confirm part creation', widget=forms.HiddenInput())
|
||||
|
||||
class Meta:
|
||||
model = Part
|
||||
fields = [
|
||||
'confirm_creation',
|
||||
'category',
|
||||
'name',
|
||||
'variant',
|
||||
|
@ -24,6 +24,8 @@ from django.contrib.auth.models import User
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from fuzzywuzzy import fuzz
|
||||
|
||||
from InvenTree import helpers
|
||||
from InvenTree import validators
|
||||
from InvenTree.models import InvenTreeTree
|
||||
@ -88,8 +90,6 @@ def before_delete_part_category(sender, instance, using, **kwargs):
|
||||
child.save()
|
||||
|
||||
|
||||
# Function to automatically rename a part image on upload
|
||||
# Format: part_pk.<img>
|
||||
def rename_part_image(instance, filename):
|
||||
""" Function for renaming a part image file
|
||||
|
||||
@ -116,6 +116,56 @@ def rename_part_image(instance, filename):
|
||||
return os.path.join(base, fn)
|
||||
|
||||
|
||||
def match_part_names(match, threshold=80, reverse=True, compare_length=False):
|
||||
""" Return a list of parts whose name matches the search term using fuzzy search.
|
||||
|
||||
Args:
|
||||
match: Term to match against
|
||||
threshold: Match percentage that must be exceeded (default = 65)
|
||||
reverse: Ordering for search results (default = True - highest match is first)
|
||||
compare_length: Include string length checks
|
||||
|
||||
Returns:
|
||||
A sorted dict where each element contains the following key:value pairs:
|
||||
- 'part' : The matched part
|
||||
- 'ratio' : The matched ratio
|
||||
"""
|
||||
|
||||
match = str(match).strip().lower()
|
||||
|
||||
if len(match) == 0:
|
||||
return []
|
||||
|
||||
parts = Part.objects.all()
|
||||
|
||||
matches = []
|
||||
|
||||
for part in parts:
|
||||
compare = str(part.name).strip().lower()
|
||||
|
||||
if len(compare) == 0:
|
||||
continue
|
||||
|
||||
ratio = fuzz.partial_token_sort_ratio(compare, match)
|
||||
|
||||
if compare_length:
|
||||
# Also employ primitive length comparison
|
||||
l_min = min(len(match), len(compare))
|
||||
l_max = max(len(match), len(compare))
|
||||
|
||||
ratio *= (l_min / l_max)
|
||||
|
||||
if ratio >= threshold:
|
||||
matches.append({
|
||||
'part': part,
|
||||
'ratio': ratio
|
||||
})
|
||||
|
||||
matches = sorted(matches, key=lambda item: item['ratio'], reverse=reverse)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class Part(models.Model):
|
||||
""" The Part object represents an abstract part, the 'concept' of an actual entity.
|
||||
|
||||
|
19
InvenTree/part/templates/part/create_part.html
Normal file
19
InvenTree/part/templates/part/create_part.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "modal_form.html" %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
{% if matches %}
|
||||
<b>Possible Matching Parts</b>
|
||||
<p>The new part may be a duplicate of these existing parts:</p>
|
||||
<ul class='list-group'>
|
||||
{% for match in matches %}
|
||||
<li class='list-group-item list-group-item-condensed'>
|
||||
{{ match.part.name }} - <i>{{ match.part.description }}</i> ({{ match.ratio }}%)
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -10,21 +10,15 @@ from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.forms.models import model_to_dict
|
||||
from django.forms import HiddenInput
|
||||
from django.forms import HiddenInput, CheckboxInput
|
||||
|
||||
from company.models import Company
|
||||
from .models import PartCategory, Part, PartAttachment
|
||||
from .models import BomItem
|
||||
from .models import SupplierPart
|
||||
from .models import match_part_names
|
||||
|
||||
from .forms import PartImageForm
|
||||
from .forms import EditPartForm
|
||||
from .forms import EditPartAttachmentForm
|
||||
from .forms import EditCategoryForm
|
||||
from .forms import EditBomItemForm
|
||||
from .forms import BomExportForm
|
||||
|
||||
from .forms import EditSupplierPartForm
|
||||
from . import forms as part_forms
|
||||
|
||||
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||
from InvenTree.views import QRCodeView
|
||||
@ -60,7 +54,7 @@ class PartAttachmentCreate(AjaxCreateView):
|
||||
- The view only makes sense if a Part object is passed to it
|
||||
"""
|
||||
model = PartAttachment
|
||||
form_class = EditPartAttachmentForm
|
||||
form_class = part_forms.EditPartAttachmentForm
|
||||
ajax_form_title = "Add part attachment"
|
||||
ajax_template_name = "modal_form.html"
|
||||
|
||||
@ -99,7 +93,7 @@ class PartAttachmentCreate(AjaxCreateView):
|
||||
class PartAttachmentEdit(AjaxUpdateView):
|
||||
""" View for editing a PartAttachment object """
|
||||
model = PartAttachment
|
||||
form_class = EditPartAttachmentForm
|
||||
form_class = part_forms.EditPartAttachmentForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Edit attachment'
|
||||
|
||||
@ -139,10 +133,10 @@ class PartCreate(AjaxCreateView):
|
||||
- Copy an existing Part
|
||||
"""
|
||||
model = Part
|
||||
form_class = EditPartForm
|
||||
form_class = part_forms.EditPartForm
|
||||
|
||||
ajax_form_title = 'Create new part'
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_template_name = 'part/create_part.html'
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -181,6 +175,51 @@ class PartCreate(AjaxCreateView):
|
||||
|
||||
return form
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
context = {}
|
||||
|
||||
valid = form.is_valid()
|
||||
|
||||
name = request.POST.get('name', None)
|
||||
|
||||
if name:
|
||||
matches = match_part_names(name)
|
||||
|
||||
if len(matches) > 0:
|
||||
context['matches'] = matches
|
||||
|
||||
# Enforce display of the checkbox
|
||||
form.fields['confirm_creation'].widget = CheckboxInput()
|
||||
|
||||
# Check if the user has checked the 'confirm_creation' input
|
||||
confirmed = str2bool(request.POST.get('confirm_creation', False))
|
||||
|
||||
if not confirmed:
|
||||
form.errors['confirm_creation'] = ['Possible matches exist - confirm creation of new part']
|
||||
|
||||
form.pre_form_warning = 'Possible matches exist - confirm creation of new part'
|
||||
valid = False
|
||||
|
||||
data = {
|
||||
'form_valid': valid
|
||||
}
|
||||
|
||||
if valid:
|
||||
# Create the new Part
|
||||
part = form.save()
|
||||
|
||||
data['pk'] = part.pk
|
||||
|
||||
try:
|
||||
data['url'] = part.get_absolute_url()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return self.renderJsonResponse(request, form, data, context=context)
|
||||
|
||||
def get_initial(self):
|
||||
""" Get initial data for the new Part object:
|
||||
|
||||
@ -260,7 +299,7 @@ class PartImage(AjaxUpdateView):
|
||||
model = Part
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Upload Part Image'
|
||||
form_class = PartImageForm
|
||||
form_class = part_forms.PartImageForm
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -272,7 +311,7 @@ class PartEdit(AjaxUpdateView):
|
||||
""" View for editing Part object """
|
||||
|
||||
model = Part
|
||||
form_class = EditPartForm
|
||||
form_class = part_forms.EditPartForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Edit Part Properties'
|
||||
context_object_name = 'part'
|
||||
@ -298,7 +337,7 @@ class BomExport(AjaxView):
|
||||
ajax_form_title = 'Export BOM'
|
||||
ajax_template_name = 'part/bom_export.html'
|
||||
context_object_name = 'part'
|
||||
form_class = BomExportForm
|
||||
form_class = part_forms.BomExportForm
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(Part, pk=self.kwargs['pk'])
|
||||
@ -396,7 +435,7 @@ class CategoryDetail(DetailView):
|
||||
class CategoryEdit(AjaxUpdateView):
|
||||
""" Update view to edit a PartCategory """
|
||||
model = PartCategory
|
||||
form_class = EditCategoryForm
|
||||
form_class = part_forms.EditCategoryForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Edit Part Category'
|
||||
|
||||
@ -449,7 +488,7 @@ class CategoryCreate(AjaxCreateView):
|
||||
ajax_form_action = reverse_lazy('category-create')
|
||||
ajax_form_title = 'Create new part category'
|
||||
ajax_template_name = 'modal_form.html'
|
||||
form_class = EditCategoryForm
|
||||
form_class = part_forms.EditCategoryForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Add extra context data to template.
|
||||
@ -496,7 +535,7 @@ class BomItemDetail(DetailView):
|
||||
class BomItemCreate(AjaxCreateView):
|
||||
""" Create view for making a new BomItem object """
|
||||
model = BomItem
|
||||
form_class = EditBomItemForm
|
||||
form_class = part_forms.EditBomItemForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Create BOM item'
|
||||
|
||||
@ -554,7 +593,7 @@ class BomItemEdit(AjaxUpdateView):
|
||||
""" Update view for editing BomItem """
|
||||
|
||||
model = BomItem
|
||||
form_class = EditBomItemForm
|
||||
form_class = part_forms.EditBomItemForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Edit BOM item'
|
||||
|
||||
@ -580,7 +619,7 @@ class SupplierPartEdit(AjaxUpdateView):
|
||||
|
||||
model = SupplierPart
|
||||
context_object_name = 'part'
|
||||
form_class = EditSupplierPartForm
|
||||
form_class = part_forms.EditSupplierPartForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Edit Supplier Part'
|
||||
|
||||
@ -589,7 +628,7 @@ class SupplierPartCreate(AjaxCreateView):
|
||||
""" Create view for making new SupplierPart """
|
||||
|
||||
model = SupplierPart
|
||||
form_class = EditSupplierPartForm
|
||||
form_class = part_forms.EditSupplierPartForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Create new Supplier Part'
|
||||
context_object_name = 'part'
|
||||
|
@ -27,6 +27,10 @@
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.list-group-item-condensed {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
/* Force select2 elements in modal forms to be full width */
|
||||
.select-full-width {
|
||||
width: 100%;
|
||||
|
@ -178,7 +178,11 @@ class StockMove(APIView):
|
||||
for item in stock_list:
|
||||
try:
|
||||
stock_id = int(item['pk'])
|
||||
if 'quantity' in item:
|
||||
quantity = int(item['quantity'])
|
||||
else:
|
||||
# If quantity not supplied, we'll move the entire stock
|
||||
quantity = None
|
||||
except ValueError:
|
||||
# Ignore this one
|
||||
continue
|
||||
@ -192,6 +196,9 @@ class StockMove(APIView):
|
||||
except StockItem.DoesNotExist:
|
||||
continue
|
||||
|
||||
if quantity is None:
|
||||
quantity = stock.quantity
|
||||
|
||||
stock.move(location, data.get('notes'), request.user, quantity=quantity)
|
||||
|
||||
return Response({'success': 'Moved parts to {loc}'.format(
|
||||
|
@ -1,13 +1,26 @@
|
||||
{% block pre_form_content %}
|
||||
{% endblock %}
|
||||
|
||||
<div>
|
||||
{% if form.pre_form_info %}
|
||||
<div class='alert alert-info' role='alert' style='display: block;'>
|
||||
{{ form.pre_form_info }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.pre_form_warning %}
|
||||
<div class='alert alert-warning' role='alert' style='display: block;'>
|
||||
{{ form.pre_form_warning }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block non_field_error %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class='alert alert-danger' role='alert' style='display: block;'>
|
||||
<b>Error Submitting Form:</b>
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block pre_form_content %}
|
||||
{% endblock %}
|
||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
@ -15,3 +15,4 @@ django-qr-code==1.0.0 # Generate QR codes
|
||||
flake8==3.3.0 # PEP checking
|
||||
coverage>=4.5.3 # Unit test coverage
|
||||
python-coveralls==2.9.1 # Coveralls linking (for Travis)
|
||||
fuzzywuzzy>=0.17.0 # Fuzzy string matching
|
Loading…
Reference in New Issue
Block a user