InvenTree/InvenTree/part/views.py

1207 lines
34 KiB
Python
Raw Normal View History

2019-04-27 12:18:07 +00:00
"""
Django views for interacting with Part app
"""
2018-04-16 14:32:02 +00:00
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.core.exceptions import ValidationError
2018-04-15 15:02:17 +00:00
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy
from django.views.generic import DetailView, ListView, FormView
2019-06-27 12:15:58 +00:00
from django.views.generic.edit import FormMixin
from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput
from .models import PartCategory, Part, PartAttachment
from .models import BomItem
from .models import match_part_names
2019-05-18 10:24:09 +00:00
from company.models import SupplierPart
2019-05-10 23:51:45 +00:00
from . import forms as part_forms
2019-06-27 14:15:23 +00:00
from .bom import MakeBomTemplate, BomUploadManager
2018-04-15 15:02:17 +00:00
2019-04-16 12:32:43 +00:00
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView
from InvenTree.helpers import DownloadFile, str2bool
2019-06-05 11:47:22 +00:00
from InvenTree.status_codes import OrderStatus
2018-04-27 15:16:47 +00:00
2019-04-16 12:32:43 +00:00
class PartIndex(ListView):
2019-04-27 12:18:07 +00:00
""" View for displaying list of Part objects
"""
model = Part
template_name = 'part/category.html'
context_object_name = 'parts'
def get_queryset(self):
2019-05-20 14:31:34 +00:00
return Part.objects.all().select_related('category')
def get_context_data(self, **kwargs):
context = super(PartIndex, self).get_context_data(**kwargs).copy()
# View top-level categories
children = PartCategory.objects.filter(parent=None)
context['children'] = children
return context
class PartAttachmentCreate(AjaxCreateView):
""" View for creating a new PartAttachment object
- The view only makes sense if a Part object is passed to it
"""
model = PartAttachment
2019-05-10 23:51:45 +00:00
form_class = part_forms.EditPartAttachmentForm
ajax_form_title = "Add part attachment"
ajax_template_name = "modal_form.html"
def get_data(self):
return {
'success': 'Added attachment'
}
def get_initial(self):
""" Get initial data for new PartAttachment object.
- Client should have requested this form with a parent part in mind
- e.g. ?part=<pk>
"""
initials = super(AjaxCreateView, self).get_initial()
# TODO - If the proper part was not sent, return an error message
initials['part'] = Part.objects.get(id=self.request.GET.get('part'))
return initials
def get_form(self):
""" Create a form to upload a new PartAttachment
- Hide the 'part' field
"""
form = super(AjaxCreateView, self).get_form()
form.fields['part'].widget = HiddenInput()
return form
class PartAttachmentEdit(AjaxUpdateView):
""" View for editing a PartAttachment object """
model = PartAttachment
2019-05-10 23:51:45 +00:00
form_class = part_forms.EditPartAttachmentForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit attachment'
def get_data(self):
return {
'success': 'Part attachment updated'
}
def get_form(self):
form = super(AjaxUpdateView, self).get_form()
form.fields['part'].widget = HiddenInput()
return form
class PartAttachmentDelete(AjaxDeleteView):
""" View for deleting a PartAttachment """
model = PartAttachment
2019-05-03 15:03:43 +00:00
ajax_form_title = "Delete Part Attachment"
ajax_template_name = "part/attachment_delete.html"
context_object_name = "attachment"
def get_data(self):
return {
'danger': 'Deleted part attachment'
}
class PartSetCategory(AjaxView):
""" View for settings the part category for multiple parts at once """
ajax_template_name = 'part/set_category.html'
ajax_form_title = 'Set Part Category'
category = None
parts = []
def get(self, request, *args, **kwargs):
""" Respond to a GET request to this view """
self.request = request
if 'parts[]' in request.GET:
self.parts = Part.objects.filter(id__in=request.GET.getlist('parts[]'))
else:
self.parts = []
return self.renderJsonResponse(request, context=self.get_context_data())
def post(self, request, *args, **kwargs):
""" Respond to a POST request to this view """
self.parts = []
for item in request.POST:
if item.startswith('part_id_'):
pk = item.replace('part_id_', '')
try:
part = Part.objects.get(pk=pk)
except (Part.DoesNotExist, ValueError):
continue
self.parts.append(part)
self.category = None
if 'part_category' in request.POST:
pk = request.POST['part_category']
try:
self.category = PartCategory.objects.get(pk=pk)
except (PartCategory.DoesNotExist, ValueError):
self.category = None
valid = self.category is not None
data = {
'form_valid': valid,
'success': _('Set category for {n} parts'.format(n=len(self.parts)))
}
if valid:
for part in self.parts:
part.set_category(self.category)
return self.renderJsonResponse(request, data=data, context=self.get_context_data())
def get_context_data(self):
""" Return context data for rendering in the form """
ctx = {}
ctx['parts'] = self.parts
ctx['categories'] = PartCategory.objects.all()
ctx['category'] = self.category
return ctx
2019-06-25 09:16:24 +00:00
class MakePartVariant(AjaxCreateView):
2019-05-26 06:05:54 +00:00
""" View for creating a new variant based on an existing template Part
- Part <pk> is provided in the URL '/part/<pk>/make_variant/'
- Automatically copy relevent data (BOM, etc, etc)
"""
model = Part
form_class = part_forms.EditPartForm
ajax_form_title = 'Create Variant'
ajax_template_name = 'part/variant_part.html'
def get_part_template(self):
return get_object_or_404(Part, id=self.kwargs['pk'])
def get_context_data(self):
return {
'part': self.get_part_template(),
}
def get_form(self):
form = super(AjaxCreateView, self).get_form()
# Hide some variant-related fields
form.fields['is_template'].widget = HiddenInput()
form.fields['variant_of'].widget = HiddenInput()
return form
def post(self, request, *args, **kwargs):
form = self.get_form()
context = self.get_context_data()
part_template = self.get_part_template()
valid = form.is_valid()
data = {
'form_valid': valid,
}
if valid:
# Create the new part variant
part = form.save(commit=False)
part.variant_of = part_template
part.is_template = False
part.save()
data['pk'] = part.pk
data['text'] = str(part)
data['url'] = part.get_absolute_url()
# Copy relevent information from the template part
part.deepCopy(part_template, bom=True)
return self.renderJsonResponse(request, form, data, context=context)
def get_initial(self):
part_template = self.get_part_template()
initials = model_to_dict(part_template)
initials['is_template'] = False
initials['variant_of'] = part_template
return initials
class PartDuplicate(AjaxCreateView):
""" View for duplicating an existing Part object.
- Part <pk> is provided in the URL '/part/<pk>/copy/'
- Option for 'deep-copy' which will duplicate all BOM items (default = True)
"""
model = Part
form_class = part_forms.EditPartForm
ajax_form_title = "Duplicate Part"
ajax_template_name = "part/copy_part.html"
def get_data(self):
return {
'success': 'Copied part'
}
def get_part_to_copy(self):
try:
return Part.objects.get(id=self.kwargs['pk'])
except Part.DoesNotExist:
return None
def get_context_data(self):
return {
'part': self.get_part_to_copy()
}
def get_form(self):
form = super(AjaxCreateView, self).get_form()
# Force display of the 'deep_copy' widget
form.fields['deep_copy'].widget = CheckboxInput()
return form
def post(self, request, *args, **kwargs):
""" Capture the POST request for part duplication
- If the deep_copy object is set, copy all the BOM items too!
"""
form = self.get_form()
context = self.get_context_data()
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
data['text'] = str(part)
deep_copy = str2bool(request.POST.get('deep_copy', False))
original = self.get_part_to_copy()
if original:
part.deepCopy(original, bom=deep_copy)
try:
data['url'] = part.get_absolute_url()
except AttributeError:
pass
if valid:
pass
return self.renderJsonResponse(request, form, data, context=context)
def get_initial(self):
""" Get initial data based on the Part to be copied from.
"""
part = self.get_part_to_copy()
if part:
initials = model_to_dict(part)
else:
initials = super(AjaxCreateView, self).get_initial()
return initials
2018-04-26 08:22:41 +00:00
class PartCreate(AjaxCreateView):
2019-04-27 12:18:07 +00:00
""" View for creating a new Part object.
Options for providing initial conditions:
- Provide a category object as initial data
"""
model = Part
2019-05-10 23:51:45 +00:00
form_class = part_forms.EditPartForm
2019-04-18 13:47:04 +00:00
2018-04-26 08:22:41 +00:00
ajax_form_title = 'Create new part'
ajax_template_name = 'part/create_part.html'
2018-04-26 08:22:41 +00:00
def get_data(self):
return {
'success': "Created new part",
}
def get_category_id(self):
return self.request.GET.get('category', None)
def get_context_data(self, **kwargs):
2019-04-27 12:18:07 +00:00
""" Provide extra context information for the form to display:
- Add category information (if provided)
"""
context = super(PartCreate, self).get_context_data(**kwargs)
# Add category information to the page
cat_id = self.get_category_id()
if cat_id:
try:
context['category'] = PartCategory.objects.get(pk=cat_id)
except PartCategory.DoesNotExist:
pass
return context
def get_form(self):
2019-04-28 13:57:29 +00:00
""" Create Form for making new Part object.
Remove the 'default_supplier' field as there are not yet any matching SupplierPart objects
"""
form = super(AjaxCreateView, self).get_form()
# Hide the default_supplier field (there are no matching supplier parts yet!)
form.fields['default_supplier'].widget = HiddenInput()
2019-04-28 13:57:29 +00:00
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
data['text'] = str(part)
try:
data['url'] = part.get_absolute_url()
except AttributeError:
pass
return self.renderJsonResponse(request, form, data, context=context)
def get_initial(self):
2019-04-27 12:18:07 +00:00
""" Get initial data for the new Part object:
- If a category is provided, pre-fill the Category field
"""
initials = super(PartCreate, self).get_initial()
if self.get_category_id():
try:
category = PartCategory.objects.get(pk=self.get_category_id())
initials['category'] = category
initials['keywords'] = category.default_keywords
except PartCategory.DoesNotExist:
pass
return initials
class PartDetail(DetailView):
2019-04-27 12:18:07 +00:00
""" Detail view for Part object
"""
context_object_name = 'part'
2019-05-20 14:31:34 +00:00
queryset = Part.objects.all().select_related('category')
template_name = 'part/detail.html'
# Add in some extra context information based on query params
def get_context_data(self, **kwargs):
2019-04-27 12:18:07 +00:00
""" Provide extra context data to template
- If '?editing=True', set 'editing_enabled' context variable
"""
context = super(PartDetail, self).get_context_data(**kwargs)
part = self.get_object()
2019-04-27 12:18:07 +00:00
if str2bool(self.request.GET.get('edit', '')):
# Allow BOM editing if the part is active
context['editing_enabled'] = 1 if part.active else 0
else:
context['editing_enabled'] = 0
context['starred'] = part.isStarredBy(self.request.user)
2019-06-18 12:54:32 +00:00
context['disabled'] = not part.active
2019-06-05 11:47:22 +00:00
context['OrderStatus'] = OrderStatus
return context
class PartQRCode(QRCodeView):
""" View for displaying a QR code for a Part object """
ajax_form_title = "Part QR Code"
def get_qr_data(self):
""" Generate QR code data for the Part """
try:
part = Part.objects.get(id=self.pk)
return part.format_barcode()
except Part.DoesNotExist:
return None
2018-04-29 02:25:07 +00:00
class PartImage(AjaxUpdateView):
2019-04-27 12:18:07 +00:00
""" View for uploading Part image """
2018-04-29 02:25:07 +00:00
model = Part
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Upload Part Image'
2019-05-10 23:51:45 +00:00
form_class = part_forms.PartImageForm
2018-04-29 02:25:07 +00:00
def get_data(self):
return {
'success': 'Updated part image',
}
2018-04-29 02:25:07 +00:00
2018-04-26 13:28:27 +00:00
class PartEdit(AjaxUpdateView):
2019-04-27 12:18:07 +00:00
""" View for editing Part object """
model = Part
2019-05-10 23:51:45 +00:00
form_class = part_forms.EditPartForm
2018-04-26 13:28:27 +00:00
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Part Properties'
context_object_name = 'part'
def get_form(self):
""" Create form for Part editing.
Overrides default get_form() method to limit the choices
for the 'default_supplier' field to SupplierParts that reference this part
"""
form = super(AjaxUpdateView, self).get_form()
part = self.get_object()
form.fields['default_supplier'].queryset = SupplierPart.objects.filter(part=part)
return form
2019-05-12 06:27:50 +00:00
class BomValidate(AjaxUpdateView):
""" Modal form view for validating a part BOM """
model = Part
ajax_form_title = "Validate BOM"
ajax_template_name = 'part/bom_validate.html'
context_object_name = 'part'
form_class = part_forms.BomValidateForm
def get_context(self):
return {
'part': self.get_object(),
}
def get(self, request, *args, **kwargs):
form = self.get_form()
return self.renderJsonResponse(request, form, context=self.get_context())
def post(self, request, *args, **kwargs):
form = self.get_form()
part = self.get_object()
confirmed = str2bool(request.POST.get('validate', False))
if confirmed:
part.validate_bom(request.user)
else:
form.errors['validate'] = ['Confirm that the BOM is valid']
data = {
'form_valid': confirmed
}
return self.renderJsonResponse(request, form, data, context=self.get_context())
class BomUpload(FormView):
""" View for uploading a BOM file, and handling BOM data importing.
The BOM upload process is as follows:
1. (Client) Select and upload BOM file
2. (Server) Verify that supplied file is a file compatible with tablib library
3. (Server) Introspect data file, try to find sensible columns / values / etc
4. (Server) Send suggestions back to the client
5. (Client) Makes choices based on suggestions:
- Accept automatic matching to parts found in database
- Accept suggestions for 'partial' or 'fuzzy' matches
- Create new parts in case of parts not being available
6. (Client) Sends updated dataset back to server
7. (Server) Check POST data for validity, sanity checking, etc.
8. (Server) Respond to POST request
- If data are valid, proceed to 9.
- If data not valid, return to 4.
9. (Server) Send confirmation form to user
- Display the actions which will occur
- Provide final "CONFIRM" button
10. (Client) Confirm final changes
11. (Server) Apply changes to database, update BOM items.
During these steps, data are passed between the server/client as JSON objects.
"""
2019-07-02 09:07:59 +00:00
template_name='part/bom_upload/upload_file.html'
# Context data passed to the forms (initially empty, extracted from uploaded file)
bom_headers = []
bom_columns = []
bom_rows = []
missing_columns = []
2019-07-02 09:07:59 +00:00
def get_success_url(self):
part = self.get_object()
return reverse('upload-bom', kwargs={'pk': part.id})
def get_form_class(self):
form_step = self.request.POST.get('form_step', None)
if form_step == 'select_fields':
return part_forms.BomUploadSelectFields
else:
# Default form is the starting point
return part_forms.BomUploadSelectFile
2019-06-27 11:17:33 +00:00
def get_context_data(self, *args, **kwargs):
ctx = super().get_context_data(*args, **kwargs)
ctx['part'] = self.part
ctx['bom_headers'] = BomUploadManager.HEADERS
ctx['bom_columns'] = self.bom_columns
ctx['bom_rows'] = self.bom_rows
ctx['missing_columns'] = self.missing_columns
return ctx
def get(self, request, *args, **kwargs):
2019-06-27 14:15:23 +00:00
""" Perform the initial 'GET' request.
Initially returns a form for file upload """
self.request = request
# A valid Part object must be supplied. This is the 'parent' part for the BOM
self.part = get_object_or_404(Part, pk=self.kwargs['pk'])
self.form = self.get_form()
form_class = self.get_form_class()
form = self.get_form(form_class)
return self.render_to_response(self.get_context_data(form=form))
def handleBomFileUpload(self):
""" Process a BOM file upload form.
This function validates that the uploaded file was valid,
and contains tabulated data that can be extracted.
If the file does not satisfy these requirements,
the "upload file" form is again shown to the user.
"""
bom_file = self.request.FILES.get('bom_file', None)
manager = None
bom_file_valid = False
if bom_file is None:
self.form.errors['bom_file'] = [_('No BOM file provided')]
else:
# Create a BomUploadManager object - will perform initial data validation
# (and raise a ValidationError if there is something wrong with the file)
try:
manager = BomUploadManager(bom_file)
bom_file_valid = True
except ValidationError as e:
errors = e.error_dict
2019-06-27 14:15:23 +00:00
for k, v in errors.items():
self.form.errors[k] = v
if bom_file_valid:
# BOM file is valid? Proceed to the next step!
form = part_forms.BomUploadSelectFields
self.template_name = 'part/bom_upload/select_fields.html'
self.extractDataFromFile(manager)
else:
form = self.form
2019-07-02 09:07:59 +00:00
form.errors['bom_file'] = [_('no errors')]
2019-07-02 09:07:59 +00:00
return self.render_to_response(self.get_context_data(form=form))
def extractDataFromFile(self, bom):
""" Read data from the BOM file """
self.bom_columns = bom.columns()
self.bom_rows = bom.rows()
def handleFieldSelection(self):
""" Handle the output of the field selection form.
Here the user is presented with the raw data and must select the
column names and which rows to process.
"""
# Map the columns
column_names = {}
column_selections = {}
row_data = {}
for item in self.request.POST:
value = self.request.POST[item]
# Extract the column names
if item.startswith('col_name_'):
col_id = int(item.replace('col_name_', ''))
col_name = value
column_names[col_id] = col_name
# Extract the column selections
if item.startswith('col_select_'):
col_id = int(item.replace('col_select_', ''))
col_name = value
column_selections[col_id] = value
# Extract the row data
if item.startswith('row_'):
# Item should be of the format row_<r>_col_<c>
s = item.split('_')
if len(s) < 4:
continue
row_id = int(s[1])
col_id = int(s[3])
if row_id not in row_data:
row_data[row_id] = {}
row_data[row_id][col_id] = value
col_ids = sorted(column_names.keys())
self.bom_columns = []
2019-07-03 11:19:31 +00:00
# Track any duplicate column selections
duplicates = False
for col in col_ids:
if col not in column_selections:
continue
header = ({
'name': column_names[col],
'guess': column_selections[col]
})
# Duplicate guess?
guess = column_selections[col]
if guess:
n = list(column_selections.values()).count(column_selections[col])
if n > 1:
header['duplicate'] = True
2019-07-03 11:19:31 +00:00
duplicates = True
self.bom_columns.append(header)
# Are there any missing columns?
self.missing_columns = []
for col in BomUploadManager.REQUIRED_HEADERS:
if col not in column_selections.values():
self.missing_columns.append(col)
2019-06-28 10:21:21 +00:00
# Re-construct the data table
self.bom_rows = []
2019-06-28 10:21:21 +00:00
for row_idx in sorted(row_data.keys()):
row = row_data[row_idx]
items = []
for col_idx in sorted(row.keys()):
2019-06-28 10:24:18 +00:00
if col_idx not in column_selections.keys():
2019-06-28 10:24:18 +00:00
continue
2019-06-28 10:21:21 +00:00
value = row[col_idx]
items.append(value)
self.bom_rows.append({'index': row_idx, 'data': items})
2019-06-28 10:21:21 +00:00
2019-07-03 11:19:31 +00:00
valid = len(self.missing_columns) == 0 and not duplicates
form = part_forms.BomUploadSelectFields
2019-07-03 11:19:31 +00:00
if valid:
form = self.template_name = 'part/bom_upload/select_parts.html'
else:
self.template_name = 'part/bom_upload/select_fields.html'
2019-06-28 10:21:21 +00:00
return self.render_to_response(self.get_context_data(form=form))
def post(self, request, *args, **kwargs):
""" Perform the various 'POST' requests required.
"""
self.request = request
self.part = get_object_or_404(Part, pk=self.kwargs['pk'])
2019-07-02 09:07:59 +00:00
self.form = self.get_form(self.get_form_class())
# Did the user POST a file named bom_file?
form_step = request.POST.get('form_step', None)
if form_step == 'select_file':
return self.handleBomFileUpload()
elif form_step == 'select_fields':
return self.handleFieldSelection()
2019-07-02 09:07:59 +00:00
return self.render_to_response(self.get_context_data(form=self.form))
2019-06-27 12:15:58 +00:00
class BomUploadTemplate(AjaxView):
"""
Provide a BOM upload template file for download.
- Generates a template file in the provided format e.g. ?format=csv
"""
def get(self, request, *args, **kwargs):
export_format = request.GET.get('format', 'csv')
return MakeBomTemplate(export_format)
class BomDownload(AjaxView):
"""
Provide raw download of a BOM file.
- File format should be passed as a query param e.g. ?format=csv
"""
model = Part
def get(self, request, *args, **kwargs):
part = get_object_or_404(Part, pk=self.kwargs['pk'])
export_format = request.GET.get('format', 'csv')
# Placeholder to test file export
filename = '"' + part.name + '_BOM.' + export_format + '"'
filedata = part.export_bom(format=export_format)
return DownloadFile(filedata, filename)
def get_data(self):
return {
'info': 'Exported BOM'
}
class PartDelete(AjaxDeleteView):
2019-04-27 12:18:07 +00:00
""" View to delete a Part object """
model = Part
ajax_template_name = 'part/partial_delete.html'
ajax_form_title = 'Confirm Part Deletion'
context_object_name = 'part'
success_url = '/part/'
def get_data(self):
return {
'danger': 'Part was deleted',
}
class PartPricing(AjaxView):
""" View for inspecting part pricing information """
model = Part
ajax_template_name = "part/part_pricing.html"
ajax_form_title = "Part Pricing"
form_class = part_forms.PartPriceForm
def get_part(self):
try:
return Part.objects.get(id=self.kwargs['pk'])
except Part.DoesNotExist:
return None
def get_pricing(self, quantity=1):
2019-05-19 21:53:23 +00:00
try:
quantity = int(quantity)
except ValueError:
quantity = 1
if quantity < 1:
quantity = 1
part = self.get_part()
ctx = {
'part': part,
'quantity': quantity
}
if part is None:
return ctx
2019-05-18 13:44:43 +00:00
# Supplier pricing information
if part.supplier_count > 0:
2019-05-20 13:53:39 +00:00
buy_price = part.get_supplier_price_range(quantity)
2019-05-20 13:53:39 +00:00
if buy_price is not None:
min_buy_price, max_buy_price = buy_price
2019-05-20 13:53:39 +00:00
if min_buy_price:
ctx['min_total_buy_price'] = min_buy_price
ctx['min_unit_buy_price'] = min_buy_price / quantity
if max_buy_price:
ctx['max_total_buy_price'] = max_buy_price
ctx['max_unit_buy_price'] = max_buy_price / quantity
2019-05-18 13:44:43 +00:00
# BOM pricing information
if part.bom_count > 0:
2019-05-20 13:53:39 +00:00
bom_price = part.get_bom_price_range(quantity)
2019-05-18 13:44:43 +00:00
2019-05-20 13:53:39 +00:00
if bom_price is not None:
min_bom_price, max_bom_price = bom_price
if min_bom_price:
ctx['min_total_bom_price'] = min_bom_price
ctx['min_unit_bom_price'] = min_bom_price / quantity
if max_bom_price:
ctx['max_total_bom_price'] = max_bom_price
ctx['max_unit_bom_price'] = max_bom_price / quantity
return ctx
def get(self, request, *args, **kwargs):
return self.renderJsonResponse(request, self.form_class(), context=self.get_pricing())
def post(self, request, *args, **kwargs):
try:
quantity = int(self.request.POST.get('quantity', 1))
except ValueError:
quantity = 1
# Always mark the form as 'invalid' (the user may wish to keep getting pricing data)
data = {
'form_valid': False,
}
return self.renderJsonResponse(request, self.form_class(), data=data, context=self.get_pricing(quantity))
class CategoryDetail(DetailView):
2019-04-27 12:18:07 +00:00
""" Detail view for PartCategory """
model = PartCategory
context_object_name = 'category'
2019-05-20 14:31:34 +00:00
queryset = PartCategory.objects.all().prefetch_related('children')
template_name = 'part/category.html'
class CategoryEdit(AjaxUpdateView):
2019-04-27 12:18:07 +00:00
""" Update view to edit a PartCategory """
model = PartCategory
2019-05-10 23:51:45 +00:00
form_class = part_forms.EditCategoryForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Part Category'
def get_context_data(self, **kwargs):
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
try:
context['category'] = self.get_object()
except:
pass
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):
2019-04-27 12:18:07 +00:00
""" Delete view to delete a PartCategory """
model = PartCategory
ajax_template_name = 'part/category_delete.html'
2019-05-04 07:20:05 +00:00
ajax_form_title = 'Delete Part Category'
context_object_name = 'category'
2018-04-15 15:02:17 +00:00
success_url = '/part/'
def get_data(self):
return {
'danger': 'Part category was deleted',
}
class CategoryCreate(AjaxCreateView):
2019-04-27 12:18:07 +00:00
""" Create view to make a new PartCategory """
model = PartCategory
ajax_form_action = reverse_lazy('category-create')
ajax_form_title = 'Create new part category'
ajax_template_name = 'modal_form.html'
2019-05-10 23:51:45 +00:00
form_class = part_forms.EditCategoryForm
def get_context_data(self, **kwargs):
2019-04-27 12:18:07 +00:00
""" Add extra context data to template.
- If parent category provided, pass the category details to the template
"""
context = super(CategoryCreate, self).get_context_data(**kwargs).copy()
parent_id = self.request.GET.get('category', None)
if parent_id:
try:
context['category'] = PartCategory.objects.get(pk=parent_id)
except PartCategory.DoesNotExist:
pass
return context
def get_initial(self):
2019-04-27 12:18:07 +00:00
""" Get initial data for new PartCategory
- If parent provided, pre-fill the parent category
"""
initials = super(CategoryCreate, self).get_initial().copy()
parent_id = self.request.GET.get('category', None)
if parent_id:
try:
initials['parent'] = PartCategory.objects.get(pk=parent_id)
except PartCategory.DoesNotExist:
pass
return initials
class BomItemDetail(DetailView):
2019-04-27 12:18:07 +00:00
""" Detail view for BomItem """
2018-04-15 15:02:17 +00:00
context_object_name = 'item'
queryset = BomItem.objects.all()
template_name = 'part/bom-detail.html'
class BomItemCreate(AjaxCreateView):
2019-04-27 12:18:07 +00:00
""" Create view for making a new BomItem object """
model = BomItem
2019-05-10 23:51:45 +00:00
form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Create BOM item'
def get_form(self):
""" Override get_form() method to reduce Part selection options.
- Do not allow part to be added to its own BOM
- Remove any Part items that are already in the BOM
"""
2019-04-29 14:18:58 +00:00
form = super(AjaxCreateView, self).get_form()
part_id = form['part'].value()
try:
part = Part.objects.get(id=part_id)
# Only allow active parts to be selected
query = form.fields['part'].queryset.filter(active=True)
form.fields['part'].queryset = query
# Don't allow selection of sub_part objects which are already added to the Bom!
query = form.fields['sub_part'].queryset
# Don't allow a part to be added to its own BOM
query = query.exclude(id=part.id)
query = query.filter(active=True)
# Eliminate any options that are already in the BOM!
2019-04-29 14:18:58 +00:00
query = query.exclude(id__in=[item.id for item in part.required_parts()])
form.fields['sub_part'].queryset = query
form.fields['part'].widget = HiddenInput()
except Part.DoesNotExist:
pass
return form
def get_initial(self):
2019-04-27 12:18:07 +00:00
""" Provide initial data for the BomItem:
- If 'parent' provided, set the parent part field
"""
# Look for initial values
initials = super(BomItemCreate, self).get_initial().copy()
# Parent part for this item?
parent_id = self.request.GET.get('parent', None)
if parent_id:
try:
initials['part'] = Part.objects.get(pk=parent_id)
except Part.DoesNotExist:
pass
return initials
class BomItemEdit(AjaxUpdateView):
2019-04-27 12:18:07 +00:00
""" Update view for editing BomItem """
model = BomItem
2019-05-10 23:51:45 +00:00
form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit BOM item'
2018-04-26 14:54:01 +00:00
class BomItemDelete(AjaxDeleteView):
2019-04-27 12:18:07 +00:00
""" Delete view for removing BomItem """
model = BomItem
ajax_template_name = 'part/bom-delete.html'
context_object_name = 'item'
2018-04-26 14:54:01 +00:00
ajax_form_title = 'Confim BOM item deletion'