Merge remote-tracking branch 'inventree/master' into build-fixes

# Conflicts:
#	InvenTree/InvenTree/views.py
#	InvenTree/build/views.py
#	InvenTree/locale/de/LC_MESSAGES/django.po
#	InvenTree/locale/en/LC_MESSAGES/django.po
#	InvenTree/locale/es/LC_MESSAGES/django.po
#	InvenTree/order/views.py
#	InvenTree/part/api.py
#	InvenTree/part/views.py
#	InvenTree/templates/js/bom.js
This commit is contained in:
Oliver Walters 2020-10-30 22:44:25 +11:00
commit 3a702266e6
20 changed files with 1467 additions and 1132 deletions

View File

@ -213,6 +213,39 @@ class AjaxMixin(InvenTreeRoleMixin):
"""
return {}
def pre_save(self, obj, form, **kwargs):
"""
Hook for doing something *before* an object is saved.
obj: The object to be saved
form: The cleaned form
"""
# Do nothing by default
pass
def post_save(self, obj, form, **kwargs):
"""
Hook for doing something *after* an object is saved.
"""
# Do nothing by default
pass
def validate(self, obj, form, **kwargs):
"""
Hook for performing custom form validation steps.
If a form error is detected, add it to the form,
with 'form.add_error()'
Ref: https://docs.djangoproject.com/en/dev/topics/forms/
"""
# Do nothing by default
pass
def renderJsonResponse(self, request, form=None, data={}, context=None):
""" Render a JSON response based on specific class context.
@ -320,32 +353,6 @@ class AjaxCreateView(AjaxMixin, CreateView):
- Handles form validation via AJAX POST requests
"""
def validate(self, request, form, cleaned_data, **kwargs):
"""
Hook for performing any extra validation, over and above the regular form.is_valid
If any errors exist, add them to the form, using form.add_error
"""
pass
def pre_save(self, form, request, **kwargs):
"""
Hook for doing something before the form is validated
"""
pass
def post_save(self, **kwargs):
"""
Hook for doing something with the created object after it is saved
kwargs:
request - The request object
new_object - The newly created object
"""
pass
def get(self, request, *args, **kwargs):
""" Creates form with initial data, and renders JSON response """
@ -355,6 +362,17 @@ class AjaxCreateView(AjaxMixin, CreateView):
form = self.get_form()
return self.renderJsonResponse(request, form)
def do_save(self, form):
"""
Method for actually saving the form to the database.
Default implementation is very simple,
but can be overridden if required.
"""
self.object = form.save()
return self.object
def post(self, request, *args, **kwargs):
""" Responds to form POST. Validates POST data and returns status info.
@ -365,13 +383,12 @@ class AjaxCreateView(AjaxMixin, CreateView):
self.request = request
self.form = self.get_form()
# Perform regular form validation
valid = self.form.is_valid()
# Perform initial form validation
self.form.is_valid()
# Perform custom validation (no object can be provided yet)
self.validate(None, self.form)
# Perform any extra validation steps
self.validate(request, self.form, self.form.cleaned_data)
# Check if form is valid again (after performing any custom validation)
valid = self.form.is_valid()
# Extra JSON data sent alongside form
@ -379,11 +396,20 @@ class AjaxCreateView(AjaxMixin, CreateView):
'form_valid': valid
}
# Add in any extra class data
for value, key in enumerate(self.get_data()):
data[key] = value
if valid:
self.pre_save(self.form, request)
self.object = self.form.save()
self.post_save(new_object=self.object, request=request, form=self.form, data=self.form.cleaned_data)
# Perform (optional) pre-save step
self.pre_save(None, self.form)
# Save the object to the database
self.do_save(self.form)
# Perform (optional) post-save step
self.post_save(self.object, self.form)
# Return the PK of the newly-created object
data['pk'] = self.object.pk
@ -414,6 +440,17 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data())
def do_save(self, form):
"""
Method for updating the object in the database.
Default implementation is very simple,
but can be overridden if required.
"""
self.object = form.save()
return self.object
def post(self, request, *args, **kwargs):
""" Respond to POST request.
@ -423,22 +460,44 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
- Otherwise, return sucess status
"""
self.request = request
# Make sure we have an object to point to
self.object = self.get_object()
form = self.get_form()
# Perform initial form validation
form.is_valid()
# Perform custom validation
self.validate(self.object, form)
valid = form.is_valid()
data = {
'form_valid': form.is_valid()
'form_valid': valid
}
if form.is_valid():
obj = form.save()
# Add in any extra class data
for value, key in enumerate(self.get_data()):
data[key] = value
if valid:
# Perform (optional) pre-save step
self.pre_save(self.object, form)
# Save the updated objec to the database
obj = self.do_save(form)
# Perform (optional) post-save step
self.post_save(obj, form)
# Include context data about the updated object
data['pk'] = obj.id
data['pk'] = obj.pk
self.post_save(new_object=obj, request=request)
self.post_save(obj, form)
try:
data['url'] = obj.get_absolute_url()
@ -447,13 +506,6 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
return self.renderJsonResponse(request, form, data)
def post_save(self, **kwargs):
"""
Hook called after the form data is saved.
(Optional)
"""
pass
class AjaxDeleteView(AjaxMixin, UpdateView):

View File

@ -60,30 +60,25 @@ class BuildCancel(AjaxUpdateView):
form_class = forms.CancelBuildForm
role_required = 'build.change'
def post(self, request, *args, **kwargs):
""" Handle POST request. Mark the build status as CANCELLED """
def validate(self, build, form, **kwargs):
build = self.get_object()
confirm = str2bool(form.cleaned_data.get('confirm_cancel', False))
form = self.get_form()
valid = form.is_valid()
confirm = str2bool(request.POST.get('confirm_cancel', False))
if confirm:
build.cancelBuild(request.user)
else:
if not confirm:
form.add_error('confirm_cancel', _('Confirm build cancellation'))
valid = False
data = {
'form_valid': valid,
def post_save(self, build, form, **kwargs):
"""
Cancel the build.
"""
build.cancelBuild(self.request.user)
def get_data(self):
return {
'danger': _('Build was cancelled')
}
return self.renderJsonResponse(request, form, data=data)
class BuildAutoAllocate(AjaxUpdateView):
""" View to auto-allocate parts for a build.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ from .models import SalesOrderAllocation
class IssuePurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_('Place order'))
confirm = forms.BooleanField(required=True, initial=False, help_text=_('Place order'))
class Meta:
model = PurchaseOrder
@ -32,7 +32,7 @@ class IssuePurchaseOrderForm(HelperForm):
class CompletePurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_("Mark order as complete"))
confirm = forms.BooleanField(required=True, help_text=_("Mark order as complete"))
class Meta:
model = PurchaseOrder
@ -43,7 +43,7 @@ class CompletePurchaseOrderForm(HelperForm):
class CancelPurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_('Cancel order'))
confirm = forms.BooleanField(required=True, help_text=_('Cancel order'))
class Meta:
model = PurchaseOrder
@ -54,7 +54,7 @@ class CancelPurchaseOrderForm(HelperForm):
class CancelSalesOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_('Cancel order'))
confirm = forms.BooleanField(required=True, help_text=_('Cancel order'))
class Meta:
model = SalesOrder
@ -65,7 +65,7 @@ class CancelSalesOrderForm(HelperForm):
class ShipSalesOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_('Ship order'))
confirm = forms.BooleanField(required=True, help_text=_('Ship order'))
class Meta:
model = SalesOrder

View File

@ -209,6 +209,7 @@ class PurchaseOrder(Order):
line.save()
@transaction.atomic
def place_order(self):
""" Marks the PurchaseOrder as PLACED. Order must be currently PENDING. """
@ -217,6 +218,7 @@ class PurchaseOrder(Order):
self.issue_date = datetime.now().date()
self.save()
@transaction.atomic
def complete_order(self):
""" Marks the PurchaseOrder as COMPLETE. Order must be currently PLACED. """
@ -225,10 +227,16 @@ class PurchaseOrder(Order):
self.complete_date = datetime.now().date()
self.save()
def can_cancel(self):
return self.status not in [
PurchaseOrderStatus.PLACED,
PurchaseOrderStatus.PENDING
]
def cancel_order(self):
""" Marks the PurchaseOrder as CANCELLED. """
if self.status in [PurchaseOrderStatus.PLACED, PurchaseOrderStatus.PENDING]:
if self.can_cancel():
self.status = PurchaseOrderStatus.CANCELLED
self.save()
@ -377,6 +385,16 @@ class SalesOrder(Order):
return True
def can_cancel(self):
"""
Return True if this order can be cancelled
"""
if not self.status == SalesOrderStatus.PENDING:
return False
return True
@transaction.atomic
def cancel_order(self):
"""
@ -386,7 +404,7 @@ class SalesOrder(Order):
- Delete any StockItems which have been allocated
"""
if not self.status == SalesOrderStatus.PENDING:
if not self.can_cancel():
return False
self.status = SalesOrderStatus.CANCELLED

View File

@ -113,6 +113,7 @@ class POTests(OrderViewTestCase):
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
# Test WITH confirmation
@ -151,7 +152,6 @@ class POTests(OrderViewTestCase):
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
self.assertIn('Invalid Purchase Order', str(data['html_form']))
# POST with a part that does not match the purchase order
post_data['order'] = 1
@ -159,14 +159,12 @@ class POTests(OrderViewTestCase):
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
self.assertIn('must match for Part and Order', str(data['html_form']))
# POST with an invalid part
post_data['part'] = 12345
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
self.assertIn('Invalid SupplierPart selection', str(data['html_form']))
# POST the form with valid data
post_data['part'] = 100

View File

@ -100,9 +100,9 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView):
ajax_template_name = "modal_form.html"
role_required = 'purchase_order.add'
def post_save(self, **kwargs):
self.object.user = self.request.user
self.object.save()
def post_save(self, attachment, form, **kwargs):
attachment.user = self.request.user
attachment.save()
def get_data(self):
return {
@ -148,7 +148,7 @@ class SalesOrderAttachmentCreate(AjaxCreateView):
ajax_form_title = _('Add Sales Order Attachment')
role_required = 'sales_order.add'
def post_save(self, **kwargs):
def post_save(self, attachment, form, **kwargs):
self.object.user = self.request.user
self.object.save()
@ -319,11 +319,11 @@ class PurchaseOrderCreate(AjaxCreateView):
return initials
def post_save(self, **kwargs):
def post_save(self, order, form, **kwargs):
# Record the user who created this purchase order
self.object.created_by = self.request.user
self.object.save()
order.created_by = self.request.user
order.save()
class SalesOrderCreate(AjaxCreateView):
@ -351,10 +351,10 @@ class SalesOrderCreate(AjaxCreateView):
return initials
def post_save(self, **kwargs):
def post_save(self, order, form, **kwargs):
# Record the user who created this sales order
self.object.created_by = self.request.user
self.object.save()
order.created_by = self.request.user
order.save()
class PurchaseOrderEdit(AjaxUpdateView):
@ -404,29 +404,19 @@ class PurchaseOrderCancel(AjaxUpdateView):
form_class = order_forms.CancelPurchaseOrderForm
role_required = 'purchase_order.change'
def post(self, request, *args, **kwargs):
""" Mark the PO as 'CANCELLED' """
order = self.get_object()
form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False))
valid = False
def validate(self, order, form, **kwargs):
confirm = str2bool(form.cleaned_data.get('confirm', False))
if not confirm:
form.add_error('confirm', _('Confirm order cancellation'))
else:
valid = True
data = {
'form_valid': valid
}
if not order.can_cancel():
form.add_error(None, _('Order cannot be cancelled'))
if valid:
order.cancel_order()
def post_save(self, order, form, **kwargs):
return self.renderJsonResponse(request, form, data)
order.cancel_order()
class SalesOrderCancel(AjaxUpdateView):
@ -438,30 +428,19 @@ class SalesOrderCancel(AjaxUpdateView):
form_class = order_forms.CancelSalesOrderForm
role_required = 'sales_order.change'
def post(self, request, *args, **kwargs):
def validate(self, order, form, **kwargs):
order = self.get_object()
form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False))
valid = False
confirm = str2bool(form.cleaned_data.get('confirm', False))
if not confirm:
form.add_error('confirm', _('Confirm order cancellation'))
else:
valid = True
if valid:
if not order.cancel_order():
form.add_error(None, _('Could not cancel order'))
valid = False
if not order.can_cancel():
form.add_error(None, _('Order cannot be cancelled'))
data = {
'form_valid': valid,
}
def post_save(self, order, form, **kwargs):
return self.renderJsonResponse(request, form, data)
order.cancel_order()
class PurchaseOrderIssue(AjaxUpdateView):
@ -473,30 +452,22 @@ class PurchaseOrderIssue(AjaxUpdateView):
form_class = order_forms.IssuePurchaseOrderForm
role_required = 'purchase_order.change'
def post(self, request, *args, **kwargs):
""" Mark the purchase order as 'PLACED' """
def validate(self, order, form, **kwargs):
order = self.get_object()
form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False))
valid = False
confirm = str2bool(self.request.POST.get('confirm', False))
if not confirm:
form.add_error('confirm', _('Confirm order placement'))
else:
valid = True
data = {
'form_valid': valid,
def post_save(self, order, form, **kwargs):
order.place_order()
def get_data(self):
return {
'success': _('Purchase order issued')
}
if valid:
order.place_order()
return self.renderJsonResponse(request, form, data)
class PurchaseOrderComplete(AjaxUpdateView):
""" View for marking a PurchaseOrder as complete.
@ -517,23 +488,22 @@ class PurchaseOrderComplete(AjaxUpdateView):
return ctx
def post(self, request, *args, **kwargs):
def validate(self, order, form, **kwargs):
confirm = str2bool(request.POST.get('confirm', False))
confirm = str2bool(form.cleaned_data.get('confirm', False))
if confirm:
po = self.get_object()
po.status = PurchaseOrderStatus.COMPLETE
po.save()
if not confirm:
form.add_error('confirm', _('Confirm order completion'))
data = {
'form_valid': confirm
def post_save(self, order, form, **kwargs):
order.complete_order()
def get_data(self):
return {
'success': _('Purchase order completed')
}
form = self.get_form()
return self.renderJsonResponse(request, form, data)
class SalesOrderShip(AjaxUpdateView):
""" View for 'shipping' a SalesOrder """
@ -1117,52 +1087,21 @@ class POLineItemCreate(AjaxCreateView):
ajax_form_title = _('Add Line Item')
role_required = 'purchase_order.add'
def post(self, request, *arg, **kwargs):
def validate(self, item, form, **kwargs):
self.request = request
order = form.cleaned_data.get('order', None)
form = self.get_form()
part = form.cleaned_data.get('part', None)
valid = form.is_valid()
if not part:
form.add_error('part', _('Supplier part must be specified'))
# Extract the SupplierPart ID from the form
part_id = form['part'].value()
# Extract the Order ID from the form
order_id = form['order'].value()
try:
order = PurchaseOrder.objects.get(id=order_id)
except (ValueError, PurchaseOrder.DoesNotExist):
order = None
form.add_error('order', _('Invalid Purchase Order'))
valid = False
try:
sp = SupplierPart.objects.get(id=part_id)
if order is not None:
if not sp.supplier == order.supplier:
form.add_error('part', _('Supplier must match for Part and Order'))
valid = False
except (SupplierPart.DoesNotExist, ValueError):
valid = False
form.add_error('part', _('Invalid SupplierPart selection'))
data = {
'form_valid': valid,
}
if valid:
self.object = form.save()
data['pk'] = self.object.pk
data['text'] = str(self.object)
else:
self.object = None
return self.renderJsonResponse(request, form, data,)
if part and order:
if not part.supplier == order.supplier:
form.add_error(
'part',
_('Supplier must match for Part and Order')
)
def get_form(self):
""" Limit choice options based on the selected order, etc

View File

@ -786,12 +786,11 @@ class BomList(generics.ListCreateAPIView):
validated = params.get('validated', None)
if validated is not None:
validated = str2bool(validated)
# Work out which lines have actually been validated
pks = []
for bom_item in queryset.all():
if bom_item.is_line_valid:
pks.append(bom_item.pk)

View File

@ -19,10 +19,15 @@ from .models import PartParameterTemplate, PartParameter
from .models import PartTestTemplate
from .models import PartSellPriceBreak
from common.models import Currency
class PartModelChoiceField(forms.ModelChoiceField):
""" Extending string representation of Part instance with available stock """
def label_from_instance(self, part):
return f'{part} - {part.available_stock}'
class PartImageForm(HelperForm):
""" Form for uploading a Part image """
@ -77,6 +82,38 @@ class BomExportForm(forms.Form):
self.fields['file_format'].choices = self.get_choices()
class BomDuplicateForm(HelperForm):
"""
Simple confirmation form for BOM duplication.
Select which parent to select from.
"""
parent = PartModelChoiceField(
label=_('Parent Part'),
help_text=_('Select parent part to copy BOM from'),
queryset=Part.objects.filter(is_template=True),
)
clear = forms.BooleanField(
required=False, initial=True,
help_text=_('Clear existing BOM items')
)
confirm = forms.BooleanField(
required=False, initial=False,
help_text=_('Confirm BOM duplication')
)
class Meta:
model = Part
fields = [
'parent',
'clear',
'confirm',
]
class BomValidateForm(HelperForm):
""" Simple confirmation form for BOM validation.
User is presented with a single checkbox input,
@ -210,12 +247,6 @@ class EditCategoryForm(HelperForm):
]
class PartModelChoiceField(forms.ModelChoiceField):
""" Extending string representation of Part instance with available stock """
def label_from_instance(self, part):
return f'{part} - {part.available_stock}'
class EditBomItemForm(HelperForm):
""" Form for editing a BomItem object """

View File

@ -1134,6 +1134,60 @@ class Part(MPTTModel):
max(buy_price_range[1], bom_price_range[1])
)
@transaction.atomic
def copy_bom_from(self, other, clear=True, **kwargs):
"""
Copy the BOM from another part.
args:
other - The part to copy the BOM from
clear - Remove existing BOM items first (default=True)
"""
if clear:
# Remove existing BOM items
self.bom_items.all().delete()
for bom_item in other.bom_items.all():
# If this part already has a BomItem pointing to the same sub-part,
# delete that BomItem from this part first!
try:
existing = BomItem.objects.get(part=self, sub_part=bom_item.sub_part)
existing.delete()
except (BomItem.DoesNotExist):
pass
bom_item.part = self
bom_item.pk = None
bom_item.save()
@transaction.atomic
def copy_parameters_from(self, other, **kwargs):
clear = kwargs.get('clear', True)
if clear:
self.get_parameters().delete()
for parameter in other.get_parameters():
# If this part already has a parameter pointing to the same template,
# delete that parameter from this part first!
try:
existing = PartParameter.objects.get(part=self, template=parameter.template)
existing.delete()
except (PartParameter.DoesNotExist):
pass
parameter.part = self
parameter.pk = None
parameter.save()
@transaction.atomic
def deepCopy(self, other, **kwargs):
""" Duplicates non-field data from another part.
Does not alter the normal fields of this part,
@ -1153,24 +1207,12 @@ class Part(MPTTModel):
# Copy the BOM data
if kwargs.get('bom', False):
for item in other.bom_items.all():
# Point the item to THIS part.
# Set the pk to None so a new entry is created.
item.part = self
item.pk = None
item.save()
self.copy_bom_from(other)
# Copy the parameters data
if kwargs.get('parameters', True):
# Get template part parameters
parameters = other.get_parameters()
# Copy template part parameters to new variant part
for parameter in parameters:
PartParameter.create(part=self,
template=parameter.template,
data=parameter.data,
save=True)
self.copy_parameters_from(other)
# Copy the fields that aren't available in the duplicate form
self.salable = other.salable
self.assembly = other.assembly

View File

@ -39,8 +39,13 @@
<span class='fas fa-trash-alt'></span>
</button>
<button class='btn btn-primary' type='button' title='{% trans "Import BOM data" %}' id='bom-upload'>
<span class='fas fa-file-upload'></span> {% trans "Upload" %}
<span class='fas fa-file-upload'></span> {% trans "Import from File" %}
</button>
{% if part.variant_of %}
<button class='btn btn-default' type='button' title='{% trans "Copy BOM from parent part" %}' id='bom-duplicate'>
<span class='fas fa-clone'></span> {% trans "Copy from Parent" %}
</button>
{% endif %}
<button class='btn btn-default' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'>
<span class='fas fa-plus-circle'></span> {% trans "Add Item" %}
</button>
@ -157,6 +162,17 @@
location.href = "{% url 'upload-bom' part.id %}";
});
$('#bom-duplicate').click(function() {
launchModalForm(
"{% url 'duplicate-bom' part.id %}",
{
success: function() {
$('#bom-table').bootstrapTable('refresh');
}
}
);
});
$("#bom-item-new").click(function () {
launchModalForm(
"{% url 'bom-item-create' %}?parent={{ part.id }}",

View File

@ -0,0 +1,17 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
<p>
{% trans "Select parent part to copy BOM from" %}
</p>
{% if part.has_bom %}
<div class='alert alert-block alert-danger'>
<b>{% trans "Warning" %}</b><br>
{% trans "This part already has a Bill of Materials" %}<br>
</div>
{% endif %}
{% endblock %}

View File

@ -46,6 +46,7 @@ part_detail_urls = [
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'),
url(r'^params/', views.PartDetail.as_view(template_name='part/params.html'), name='part-params'),
url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'),

View File

@ -371,6 +371,8 @@ class MakePartVariant(AjaxCreateView):
initials = model_to_dict(part_template)
initials['is_template'] = False
initials['variant_of'] = part_template
initials['bom_copy'] = InvenTreeSetting.get_setting('PART_COPY_BOM')
initials['parameters_copy'] = InvenTreeSetting.get_setting('PART_COPY_PARAMETERS')
return initials
@ -832,8 +834,60 @@ class PartEdit(AjaxUpdateView):
return form
class BomDuplicate(AjaxUpdateView):
"""
View for duplicating BOM from a parent item.
"""
model = Part
context_object_name = 'part'
ajax_form_title = _('Duplicate BOM')
ajax_template_name = 'part/bom_duplicate.html'
form_class = part_forms.BomDuplicateForm
role_required = 'part.change'
def get_form(self):
form = super().get_form()
# Limit choices to parents of the current part
parents = self.get_object().get_ancestors()
form.fields['parent'].queryset = parents
return form
def get_initial(self):
initials = super().get_initial()
parents = self.get_object().get_ancestors()
if parents.count() == 1:
initials['parent'] = parents[0]
return initials
def validate(self, part, form):
confirm = str2bool(form.cleaned_data.get('confirm', False))
if not confirm:
form.add_error('confirm', _('Confirm duplication of BOM from parent'))
def post_save(self, part, form):
parent = form.cleaned_data.get('parent', None)
clear = str2bool(form.cleaned_data.get('clear', True))
if parent:
part.copy_bom_from(parent, clear=clear)
class BomValidate(AjaxUpdateView):
""" Modal form view for validating a part BOM """
"""
Modal form view for validating a part BOM
"""
model = Part
ajax_form_title = _("Validate BOM")
@ -854,23 +908,21 @@ class BomValidate(AjaxUpdateView):
return self.renderJsonResponse(request, form, context=self.get_context())
def post(self, request, *args, **kwargs):
def validate(self, part, form, **kwargs):
form = self.get_form()
part = self.get_object()
confirm = str2bool(form.cleaned_data.get('validate', False))
confirmed = str2bool(request.POST.get('validate', False))
if confirmed:
part.validate_bom(request.user)
else:
if not confirm:
form.add_error('validate', _('Confirm that the BOM is valid'))
data = {
'form_valid': confirmed
}
def post_save(self, part, form, **kwargs):
return self.renderJsonResponse(request, form, data, context=self.get_context())
part.validate_bom(self.request.user)
def get_data(self):
return {
'success': _('Validated Bill of Materials')
}
class BomUpload(InvenTreeRoleMixin, FormView):

View File

@ -1143,6 +1143,22 @@ class StockItem(MPTTModel):
return s
@transaction.atomic
def clear_test_results(self, **kwargs):
"""
Remove all test results
kwargs:
TODO
"""
# All test results
results = self.test_results.all()
# TODO - Perhaps some filtering options supplied by kwargs?
results.delete()
def getTestResults(self, test=None, result=None, user=None):
"""
Return all test results associated with this StockItem.

View File

@ -164,11 +164,11 @@ class StockItemAttachmentCreate(AjaxCreateView):
ajax_template_name = "modal_form.html"
role_required = 'stock.add'
def post_save(self, **kwargs):
def post_save(self, attachment, form, **kwargs):
""" Record the user that uploaded the attachment """
self.object.user = self.request.user
self.object.save()
attachment.user = self.request.user
attachment.save()
def get_data(self):
return {
@ -245,32 +245,28 @@ class StockItemAssignToCustomer(AjaxUpdateView):
form_class = StockForms.AssignStockItemToCustomerForm
role_required = 'stock.change'
def post(self, request, *args, **kwargs):
def validate(self, item, form, **kwargs):
customer = request.POST.get('customer', None)
customer = form.cleaned_data.get('customer', None)
if not customer:
form.add_error('customer', _('Customer must be specified'))
def post_save(self, item, form, **kwargs):
"""
Assign the stock item to the customer.
"""
customer = form.cleaned_data.get('customer', None)
if customer:
try:
customer = Company.objects.get(pk=customer)
except (ValueError, Company.DoesNotExist):
customer = None
if customer is not None:
stock_item = self.get_object()
item = stock_item.allocateToCustomer(
item = item.allocateToCustomer(
customer,
user=request.user
user=self.request.user
)
item.clearAllocations()
data = {
'form_valid': True,
}
return self.renderJsonResponse(request, self.get_form(), data)
class StockItemReturnToStock(AjaxUpdateView):
"""
@ -283,30 +279,25 @@ class StockItemReturnToStock(AjaxUpdateView):
form_class = StockForms.ReturnStockItemForm
role_required = 'stock.change'
def post(self, request, *args, **kwargs):
def validate(self, item, form, **kwargs):
location = request.POST.get('location', None)
location = form.cleaned_data.get('location', None)
if not location:
form.add_error('location', _('Specify a valid location'))
def post_save(self, item, form, **kwargs):
location = form.cleaned_data.get('location', None)
if location:
try:
location = StockLocation.objects.get(pk=location)
except (ValueError, StockLocation.DoesNotExist):
location = None
item.returnFromCustomer(location, self.request.user)
if location:
stock_item = self.get_object()
stock_item.returnFromCustomer(location, request.user)
else:
raise ValidationError({'location': _("Specify a valid location")})
data = {
'form_valid': True,
'success': _("Stock item returned from customer")
def get_data(self):
return {
'success': _('Stock item returned from customer')
}
return self.renderJsonResponse(request, self.get_form(), data)
class StockItemSelectLabels(AjaxView):
"""
@ -440,11 +431,11 @@ class StockItemTestResultCreate(AjaxCreateView):
ajax_form_title = _("Add Test Result")
role_required = 'stock.add'
def post_save(self, **kwargs):
def post_save(self, result, form, **kwargs):
""" Record the user that uploaded the test result """
self.object.user = self.request.user
self.object.save()
result.user = self.request.user
result.save()
def get_initial(self):

View File

@ -102,7 +102,7 @@ function loadBomTable(table, options) {
*
* BOM data are retrieved from the server via AJAX query
*/
var params = {
part: options.parent_id,
ordering: 'name',
@ -111,22 +111,22 @@ function loadBomTable(table, options) {
if (options.part_detail) {
params.part_detail = true;
}
if (options.sub_part_detail) {
params.sub_part_detail = true;
}
params.sub_part_detail = true;
var filters = {};
if (!options.disableFilters) {
filters = loadTableFilters("bom");
filters = loadTableFilters('bom');
}
for (var key in params) {
filters[key] = params[key];
}
setupFilterList("bom", $(table));
setupFilterList('bom', $(table));
// Construct the table columns
var cols = [];
@ -161,7 +161,7 @@ function loadBomTable(table, options) {
}
if (sub_part.is_template) {
html += makeIconBadge('fa-clone', '{% trans "Templat part" %}');
html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
}
// Display an extra icon if this part is an assembly
@ -357,7 +357,7 @@ function loadBomTable(table, options) {
return {classes: 'rowinvalid'};
}
},
formatNoMatches: function() { return "{% trans "No BOM items found" %}"; },
formatNoMatches: function() { return '{% trans "No BOM items found" %}'; },
clickToSelect: true,
queryParams: filters,
original: params,