mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
08d743a735
@ -104,9 +104,9 @@ def str2bool(text, test=True):
|
|||||||
True if the text looks like the selected boolean value
|
True if the text looks like the selected boolean value
|
||||||
"""
|
"""
|
||||||
if test:
|
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:
|
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='"'):
|
def WrapWithQuotes(text, quote='"'):
|
||||||
|
@ -9,7 +9,7 @@ from .models import BomItem
|
|||||||
|
|
||||||
class PartAdmin(ImportExportModelAdmin):
|
class PartAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
list_display = ('name', 'IPN', 'description', 'total_stock', 'category')
|
list_display = ('long_name', 'IPN', 'description', 'total_stock', 'category')
|
||||||
|
|
||||||
|
|
||||||
class PartCategoryAdmin(ImportExportModelAdmin):
|
class PartCategoryAdmin(ImportExportModelAdmin):
|
||||||
|
@ -60,9 +60,12 @@ class EditPartAttachmentForm(HelperForm):
|
|||||||
class EditPartForm(HelperForm):
|
class EditPartForm(HelperForm):
|
||||||
""" Form for editing a Part object """
|
""" Form for editing a Part object """
|
||||||
|
|
||||||
|
confirm_creation = forms.BooleanField(required=False, initial=False, help_text='Confirm part creation', widget=forms.HiddenInput())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Part
|
model = Part
|
||||||
fields = [
|
fields = [
|
||||||
|
'confirm_creation',
|
||||||
'category',
|
'category',
|
||||||
'name',
|
'name',
|
||||||
'variant',
|
'variant',
|
||||||
|
@ -24,6 +24,8 @@ from django.contrib.auth.models import User
|
|||||||
from django.db.models.signals import pre_delete
|
from django.db.models.signals import pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from fuzzywuzzy import fuzz
|
||||||
|
|
||||||
from InvenTree import helpers
|
from InvenTree import helpers
|
||||||
from InvenTree import validators
|
from InvenTree import validators
|
||||||
from InvenTree.models import InvenTreeTree
|
from InvenTree.models import InvenTreeTree
|
||||||
@ -88,8 +90,6 @@ def before_delete_part_category(sender, instance, using, **kwargs):
|
|||||||
child.save()
|
child.save()
|
||||||
|
|
||||||
|
|
||||||
# Function to automatically rename a part image on upload
|
|
||||||
# Format: part_pk.<img>
|
|
||||||
def rename_part_image(instance, filename):
|
def rename_part_image(instance, filename):
|
||||||
""" Function for renaming a part image file
|
""" Function for renaming a part image file
|
||||||
|
|
||||||
@ -116,6 +116,56 @@ def rename_part_image(instance, filename):
|
|||||||
return os.path.join(base, fn)
|
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):
|
class Part(models.Model):
|
||||||
""" The Part object represents an abstract part, the 'concept' of an actual entity.
|
""" 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.urls import reverse_lazy
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.forms.models import model_to_dict
|
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 company.models import Company
|
||||||
from .models import PartCategory, Part, PartAttachment
|
from .models import PartCategory, Part, PartAttachment
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import SupplierPart
|
from .models import SupplierPart
|
||||||
|
from .models import match_part_names
|
||||||
|
|
||||||
from .forms import PartImageForm
|
from . import forms as part_forms
|
||||||
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 InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
from InvenTree.views import QRCodeView
|
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
|
- The view only makes sense if a Part object is passed to it
|
||||||
"""
|
"""
|
||||||
model = PartAttachment
|
model = PartAttachment
|
||||||
form_class = EditPartAttachmentForm
|
form_class = part_forms.EditPartAttachmentForm
|
||||||
ajax_form_title = "Add part attachment"
|
ajax_form_title = "Add part attachment"
|
||||||
ajax_template_name = "modal_form.html"
|
ajax_template_name = "modal_form.html"
|
||||||
|
|
||||||
@ -99,7 +93,7 @@ class PartAttachmentCreate(AjaxCreateView):
|
|||||||
class PartAttachmentEdit(AjaxUpdateView):
|
class PartAttachmentEdit(AjaxUpdateView):
|
||||||
""" View for editing a PartAttachment object """
|
""" View for editing a PartAttachment object """
|
||||||
model = PartAttachment
|
model = PartAttachment
|
||||||
form_class = EditPartAttachmentForm
|
form_class = part_forms.EditPartAttachmentForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit attachment'
|
ajax_form_title = 'Edit attachment'
|
||||||
|
|
||||||
@ -139,10 +133,10 @@ class PartCreate(AjaxCreateView):
|
|||||||
- Copy an existing Part
|
- Copy an existing Part
|
||||||
"""
|
"""
|
||||||
model = Part
|
model = Part
|
||||||
form_class = EditPartForm
|
form_class = part_forms.EditPartForm
|
||||||
|
|
||||||
ajax_form_title = 'Create new part'
|
ajax_form_title = 'Create new part'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'part/create_part.html'
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
@ -181,6 +175,51 @@ class PartCreate(AjaxCreateView):
|
|||||||
|
|
||||||
return form
|
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):
|
def get_initial(self):
|
||||||
""" Get initial data for the new Part object:
|
""" Get initial data for the new Part object:
|
||||||
|
|
||||||
@ -260,7 +299,7 @@ class PartImage(AjaxUpdateView):
|
|||||||
model = Part
|
model = Part
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Upload Part Image'
|
ajax_form_title = 'Upload Part Image'
|
||||||
form_class = PartImageForm
|
form_class = part_forms.PartImageForm
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
@ -272,7 +311,7 @@ class PartEdit(AjaxUpdateView):
|
|||||||
""" View for editing Part object """
|
""" View for editing Part object """
|
||||||
|
|
||||||
model = Part
|
model = Part
|
||||||
form_class = EditPartForm
|
form_class = part_forms.EditPartForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Part Properties'
|
ajax_form_title = 'Edit Part Properties'
|
||||||
context_object_name = 'part'
|
context_object_name = 'part'
|
||||||
@ -298,7 +337,7 @@ class BomExport(AjaxView):
|
|||||||
ajax_form_title = 'Export BOM'
|
ajax_form_title = 'Export BOM'
|
||||||
ajax_template_name = 'part/bom_export.html'
|
ajax_template_name = 'part/bom_export.html'
|
||||||
context_object_name = 'part'
|
context_object_name = 'part'
|
||||||
form_class = BomExportForm
|
form_class = part_forms.BomExportForm
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
return get_object_or_404(Part, pk=self.kwargs['pk'])
|
return get_object_or_404(Part, pk=self.kwargs['pk'])
|
||||||
@ -396,7 +435,7 @@ class CategoryDetail(DetailView):
|
|||||||
class CategoryEdit(AjaxUpdateView):
|
class CategoryEdit(AjaxUpdateView):
|
||||||
""" Update view to edit a PartCategory """
|
""" Update view to edit a PartCategory """
|
||||||
model = PartCategory
|
model = PartCategory
|
||||||
form_class = EditCategoryForm
|
form_class = part_forms.EditCategoryForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Part Category'
|
ajax_form_title = 'Edit Part Category'
|
||||||
|
|
||||||
@ -449,7 +488,7 @@ class CategoryCreate(AjaxCreateView):
|
|||||||
ajax_form_action = reverse_lazy('category-create')
|
ajax_form_action = reverse_lazy('category-create')
|
||||||
ajax_form_title = 'Create new part category'
|
ajax_form_title = 'Create new part category'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
form_class = EditCategoryForm
|
form_class = part_forms.EditCategoryForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add extra context data to template.
|
""" Add extra context data to template.
|
||||||
@ -496,7 +535,7 @@ class BomItemDetail(DetailView):
|
|||||||
class BomItemCreate(AjaxCreateView):
|
class BomItemCreate(AjaxCreateView):
|
||||||
""" Create view for making a new BomItem object """
|
""" Create view for making a new BomItem object """
|
||||||
model = BomItem
|
model = BomItem
|
||||||
form_class = EditBomItemForm
|
form_class = part_forms.EditBomItemForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Create BOM item'
|
ajax_form_title = 'Create BOM item'
|
||||||
|
|
||||||
@ -554,7 +593,7 @@ class BomItemEdit(AjaxUpdateView):
|
|||||||
""" Update view for editing BomItem """
|
""" Update view for editing BomItem """
|
||||||
|
|
||||||
model = BomItem
|
model = BomItem
|
||||||
form_class = EditBomItemForm
|
form_class = part_forms.EditBomItemForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit BOM item'
|
ajax_form_title = 'Edit BOM item'
|
||||||
|
|
||||||
@ -580,7 +619,7 @@ class SupplierPartEdit(AjaxUpdateView):
|
|||||||
|
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
context_object_name = 'part'
|
context_object_name = 'part'
|
||||||
form_class = EditSupplierPartForm
|
form_class = part_forms.EditSupplierPartForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Supplier Part'
|
ajax_form_title = 'Edit Supplier Part'
|
||||||
|
|
||||||
@ -589,7 +628,7 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
""" Create view for making new SupplierPart """
|
""" Create view for making new SupplierPart """
|
||||||
|
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
form_class = EditSupplierPartForm
|
form_class = part_forms.EditSupplierPartForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Create new Supplier Part'
|
ajax_form_title = 'Create new Supplier Part'
|
||||||
context_object_name = 'part'
|
context_object_name = 'part'
|
||||||
|
@ -27,6 +27,10 @@
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-group-item-condensed {
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Force select2 elements in modal forms to be full width */
|
/* Force select2 elements in modal forms to be full width */
|
||||||
.select-full-width {
|
.select-full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1,13 +1,26 @@
|
|||||||
{% block pre_form_content %}
|
<div>
|
||||||
{% endblock %}
|
{% 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 %}
|
{% if form.non_field_errors %}
|
||||||
<div class='alert alert-danger' role='alert' style='display: block;'>
|
<div class='alert alert-danger' role='alert' style='display: block;'>
|
||||||
<b>Error Submitting Form:</b>
|
<b>Error Submitting Form:</b>
|
||||||
{{ form.non_field_errors }}
|
{{ form.non_field_errors }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
{% endblock %}
|
||||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
@ -15,3 +15,4 @@ django-qr-code==1.0.0 # Generate QR codes
|
|||||||
flake8==3.3.0 # PEP checking
|
flake8==3.3.0 # PEP checking
|
||||||
coverage>=4.5.3 # Unit test coverage
|
coverage>=4.5.3 # Unit test coverage
|
||||||
python-coveralls==2.9.1 # Coveralls linking (for Travis)
|
python-coveralls==2.9.1 # Coveralls linking (for Travis)
|
||||||
|
fuzzywuzzy>=0.17.0 # Fuzzy string matching
|
Loading…
Reference in New Issue
Block a user