Merge pull request #1095 from SchrodingersGat/forms

Refactor how form errors are handled
This commit is contained in:
Oliver 2020-10-30 22:12:45 +11:00 committed by GitHub
commit ad90adbc04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 269 additions and 249 deletions

View File

@ -362,6 +362,17 @@ class AjaxCreateView(AjaxMixin, CreateView):
form = self.get_form() form = self.get_form()
return self.renderJsonResponse(request, 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): def post(self, request, *args, **kwargs):
""" Responds to form POST. Validates POST data and returns status info. """ Responds to form POST. Validates POST data and returns status info.
@ -385,13 +396,17 @@ class AjaxCreateView(AjaxMixin, CreateView):
'form_valid': valid 'form_valid': valid
} }
# Add in any extra class data
for value, key in enumerate(self.get_data()):
data[key] = value
if valid: if valid:
# Perform (optional) pre-save step # Perform (optional) pre-save step
self.pre_save(None, self.form) self.pre_save(None, self.form)
# Save the object to the database # Save the object to the database
self.object = self.form.save() self.do_save(self.form)
# Perform (optional) post-save step # Perform (optional) post-save step
self.post_save(self.object, self.form) self.post_save(self.object, self.form)
@ -425,6 +440,17 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data()) 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): def post(self, request, *args, **kwargs):
""" Respond to POST request. """ Respond to POST request.
@ -453,13 +479,17 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
'form_valid': valid 'form_valid': valid
} }
# Add in any extra class data
for value, key in enumerate(self.get_data()):
data[key] = value
if valid: if valid:
# Perform (optional) pre-save step # Perform (optional) pre-save step
self.pre_save(self.object, form) self.pre_save(self.object, form)
# Save the updated objec to the database # Save the updated objec to the database
obj = form.save() obj = self.do_save(form)
# Perform (optional) post-save step # Perform (optional) post-save step
self.post_save(obj, form) self.post_save(obj, form)
@ -485,7 +515,7 @@ class AjaxDeleteView(AjaxMixin, UpdateView):
""" """
form_class = DeleteForm form_class = DeleteForm
ajax_form_title = "Delete Item" ajax_form_title = _("Delete Item")
ajax_template_name = "modal_delete_form.html" ajax_template_name = "modal_delete_form.html"
context_object_name = 'item' context_object_name = 'item'
@ -534,7 +564,7 @@ class AjaxDeleteView(AjaxMixin, UpdateView):
if confirmed: if confirmed:
obj.delete() obj.delete()
else: else:
form.errors['confirm_delete'] = ['Check box to confirm item deletion'] form.add_error('confirm_delete', _('Check box to confirm item deletion'))
context[self.context_object_name] = self.get_object() context[self.context_object_name] = self.get_object()
data = { data = {
@ -549,7 +579,7 @@ class EditUserView(AjaxUpdateView):
""" View for editing user information """ """ View for editing user information """
ajax_template_name = "modal_form.html" ajax_template_name = "modal_form.html"
ajax_form_title = "Edit User Information" ajax_form_title = _("Edit User Information")
form_class = EditUserForm form_class = EditUserForm
def get_object(self): def get_object(self):
@ -560,7 +590,7 @@ class SetPasswordView(AjaxUpdateView):
""" View for setting user password """ """ View for setting user password """
ajax_template_name = "InvenTree/password.html" ajax_template_name = "InvenTree/password.html"
ajax_form_title = "Set Password" ajax_form_title = _("Set Password")
form_class = SetPasswordForm form_class = SetPasswordForm
def get_object(self): def get_object(self):
@ -579,9 +609,9 @@ class SetPasswordView(AjaxUpdateView):
# Passwords must match # Passwords must match
if not p1 == p2: if not p1 == p2:
error = 'Password fields must match' error = _('Password fields must match')
form.errors['enter_password'] = [error] form.add_error('enter_password', error)
form.errors['confirm_password'] = [error] form.add_error('confirm_password', error)
valid = False valid = False

View File

@ -60,30 +60,25 @@ class BuildCancel(AjaxUpdateView):
form_class = forms.CancelBuildForm form_class = forms.CancelBuildForm
role_required = 'build.change' role_required = 'build.change'
def post(self, request, *args, **kwargs): def validate(self, build, form, **kwargs):
""" Handle POST request. Mark the build status as CANCELLED """
build = self.get_object() confirm = str2bool(form.cleaned_data.get('confirm_cancel', False))
form = self.get_form() if not confirm:
form.add_error('confirm_cancel', _('Confirm build cancellation'))
valid = form.is_valid() def post_save(self, build, form, **kwargs):
"""
Cancel the build.
"""
confirm = str2bool(request.POST.get('confirm_cancel', False)) build.cancelBuild(self.request.user)
if confirm: def get_data(self):
build.cancelBuild(request.user) return {
else:
form.errors['confirm_cancel'] = [_('Confirm build cancellation')]
valid = False
data = {
'form_valid': valid,
'danger': _('Build was cancelled') 'danger': _('Build was cancelled')
} }
return self.renderJsonResponse(request, form, data=data)
class BuildAutoAllocate(AjaxUpdateView): class BuildAutoAllocate(AjaxUpdateView):
""" View to auto-allocate parts for a build. """ View to auto-allocate parts for a build.
@ -128,8 +123,8 @@ class BuildAutoAllocate(AjaxUpdateView):
valid = False valid = False
if confirm is False: if confirm is False:
form.errors['confirm'] = [_('Confirm stock allocation')] form.add_error('confirm', _('Confirm stock allocation'))
form.non_field_errors = [_('Check the confirmation box at the bottom of the list')] form.add_error(None, _('Check the confirmation box at the bottom of the list'))
else: else:
build.autoAllocate() build.autoAllocate()
valid = True valid = True
@ -163,8 +158,8 @@ class BuildUnallocate(AjaxUpdateView):
valid = False valid = False
if confirm is False: if confirm is False:
form.errors['confirm'] = [_('Confirm unallocation of build stock')] form.add_error('confirm', _('Confirm unallocation of build stock'))
form.non_field_errors = [_('Check the confirmation box')] form.add_error(None, _('Check the confirmation box'))
else: else:
build.unallocateStock() build.unallocateStock()
valid = True valid = True
@ -266,15 +261,13 @@ class BuildComplete(AjaxUpdateView):
valid = False valid = False
if confirm is False: if confirm is False:
form.errors['confirm'] = [ form.add_error('confirm', _('Confirm completion of build'))
_('Confirm completion of build'),
]
else: else:
try: try:
location = StockLocation.objects.get(id=loc_id) location = StockLocation.objects.get(id=loc_id)
valid = True valid = True
except (ValueError, StockLocation.DoesNotExist): except (ValueError, StockLocation.DoesNotExist):
form.errors['location'] = [_('Invalid location selected')] form.add_error('location', _('Invalid location selected'))
serials = [] serials = []
@ -291,24 +284,20 @@ class BuildComplete(AjaxUpdateView):
# Exctract a list of provided serial numbers # Exctract a list of provided serial numbers
serials = ExtractSerialNumbers(sn, build.quantity) serials = ExtractSerialNumbers(sn, build.quantity)
existing = [] existing = build.part.find_conflicting_serial_numbers(serials)
for serial in serials:
if build.part.checkIfSerialNumberExists(serial):
existing.append(serial)
if len(existing) > 0: if len(existing) > 0:
exists = ",".join([str(x) for x in existing]) exists = ",".join([str(x) for x in existing])
form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))] form.add_error('serial_numbers', _('The following serial numbers already exist: ({sn})'.format(sn=exists)))
valid = False valid = False
except ValidationError as e: except ValidationError as e:
form.errors['serial_numbers'] = e.messages form.add_error('serial_numbers', e.messages)
valid = False valid = False
if valid: if valid:
if not build.completeBuild(location, serials, request.user): if not build.completeBuild(location, serials, request.user):
form.non_field_errors = [('Build could not be completed')] form.add_error(None, _('Build could not be completed'))
valid = False valid = False
data = { data = {

View File

@ -21,7 +21,7 @@ from .models import SalesOrderAllocation
class IssuePurchaseOrderForm(HelperForm): 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: class Meta:
model = PurchaseOrder model = PurchaseOrder
@ -32,7 +32,7 @@ class IssuePurchaseOrderForm(HelperForm):
class CompletePurchaseOrderForm(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: class Meta:
model = PurchaseOrder model = PurchaseOrder
@ -43,7 +43,7 @@ class CompletePurchaseOrderForm(HelperForm):
class CancelPurchaseOrderForm(HelperForm): class CancelPurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_('Cancel order')) confirm = forms.BooleanField(required=True, help_text=_('Cancel order'))
class Meta: class Meta:
model = PurchaseOrder model = PurchaseOrder
@ -54,7 +54,7 @@ class CancelPurchaseOrderForm(HelperForm):
class CancelSalesOrderForm(HelperForm): class CancelSalesOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_('Cancel order')) confirm = forms.BooleanField(required=True, help_text=_('Cancel order'))
class Meta: class Meta:
model = SalesOrder model = SalesOrder
@ -65,7 +65,7 @@ class CancelSalesOrderForm(HelperForm):
class ShipSalesOrderForm(HelperForm): class ShipSalesOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_('Ship order')) confirm = forms.BooleanField(required=True, help_text=_('Ship order'))
class Meta: class Meta:
model = SalesOrder model = SalesOrder

View File

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

View File

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

View File

@ -404,30 +404,20 @@ class PurchaseOrderCancel(AjaxUpdateView):
form_class = order_forms.CancelPurchaseOrderForm form_class = order_forms.CancelPurchaseOrderForm
role_required = 'purchase_order.change' role_required = 'purchase_order.change'
def post(self, request, *args, **kwargs): def validate(self, order, form, **kwargs):
""" Mark the PO as 'CANCELLED' """
order = self.get_object() confirm = str2bool(form.cleaned_data.get('confirm', False))
form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False))
valid = False
if not confirm: if not confirm:
form.errors['confirm'] = [_('Confirm order cancellation')] form.add_error('confirm', _('Confirm order cancellation'))
else:
valid = True
data = { if not order.can_cancel():
'form_valid': valid form.add_error(None, _('Order cannot be cancelled'))
}
def post_save(self, order, form, **kwargs):
if valid:
order.cancel_order() order.cancel_order()
return self.renderJsonResponse(request, form, data)
class SalesOrderCancel(AjaxUpdateView): class SalesOrderCancel(AjaxUpdateView):
""" View for cancelling a sales order """ """ View for cancelling a sales order """
@ -438,30 +428,19 @@ class SalesOrderCancel(AjaxUpdateView):
form_class = order_forms.CancelSalesOrderForm form_class = order_forms.CancelSalesOrderForm
role_required = 'sales_order.change' role_required = 'sales_order.change'
def post(self, request, *args, **kwargs): def validate(self, order, form, **kwargs):
order = self.get_object() confirm = str2bool(form.cleaned_data.get('confirm', False))
form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False))
valid = False
if not confirm: if not confirm:
form.errors['confirm'] = [_('Confirm order cancellation')] form.add_error('confirm', _('Confirm order cancellation'))
else:
valid = True
if valid: if not order.can_cancel():
if not order.cancel_order(): form.add_error(None, _('Order cannot be cancelled'))
form.non_field_errors = [_('Could not cancel order')]
valid = False
data = { def post_save(self, order, form, **kwargs):
'form_valid': valid,
}
return self.renderJsonResponse(request, form, data) order.cancel_order()
class PurchaseOrderIssue(AjaxUpdateView): class PurchaseOrderIssue(AjaxUpdateView):
@ -473,29 +452,21 @@ class PurchaseOrderIssue(AjaxUpdateView):
form_class = order_forms.IssuePurchaseOrderForm form_class = order_forms.IssuePurchaseOrderForm
role_required = 'purchase_order.change' role_required = 'purchase_order.change'
def post(self, request, *args, **kwargs): def validate(self, order, form, **kwargs):
""" Mark the purchase order as 'PLACED' """
order = self.get_object() confirm = str2bool(self.request.POST.get('confirm', False))
form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False))
valid = False
if not confirm: if not confirm:
form.errors['confirm'] = [_('Confirm order placement')] form.add_error('confirm', _('Confirm order placement'))
else:
valid = True
data = { def post_save(self, order, form, **kwargs):
'form_valid': valid,
}
if valid:
order.place_order() order.place_order()
return self.renderJsonResponse(request, form, data) def get_data(self):
return {
'success': _('Purchase order issued')
}
class PurchaseOrderComplete(AjaxUpdateView): class PurchaseOrderComplete(AjaxUpdateView):
@ -517,23 +488,22 @@ class PurchaseOrderComplete(AjaxUpdateView):
return ctx 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: if not confirm:
po = self.get_object() form.add_error('confirm', _('Confirm order completion'))
po.status = PurchaseOrderStatus.COMPLETE
po.save()
data = { def post_save(self, order, form, **kwargs):
'form_valid': confirm
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): class SalesOrderShip(AjaxUpdateView):
""" View for 'shipping' a SalesOrder """ """ View for 'shipping' a SalesOrder """
@ -558,13 +528,13 @@ class SalesOrderShip(AjaxUpdateView):
valid = False valid = False
if not confirm: if not confirm:
form.errors['confirm'] = [_('Confirm order shipment')] form.add_error('confirm', _('Confirm order shipment'))
else: else:
valid = True valid = True
if valid: if valid:
if not order.ship_order(request.user): if not order.ship_order(request.user):
form.non_field_errors = [_('Could not ship order')] form.add_error(None, _('Could not ship order'))
valid = False valid = False
data = { data = {
@ -1117,52 +1087,21 @@ class POLineItemCreate(AjaxCreateView):
ajax_form_title = _('Add Line Item') ajax_form_title = _('Add Line Item')
role_required = 'purchase_order.add' 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 if part and order:
part_id = form['part'].value() if not part.supplier == order.supplier:
form.add_error(
# Extract the Order ID from the form 'part',
order_id = form['order'].value() _('Supplier must match for Part and Order')
)
try:
order = PurchaseOrder.objects.get(id=order_id)
except (ValueError, PurchaseOrder.DoesNotExist):
order = None
form.errors['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.errors['part'] = [_('Supplier must match for Part and Order')]
valid = False
except (SupplierPart.DoesNotExist, ValueError):
valid = False
form.errors['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,)
def get_form(self): def get_form(self):
""" Limit choice options based on the selected order, etc """ Limit choice options based on the selected order, etc

View File

@ -360,7 +360,7 @@ class Part(MPTTModel):
# And recursively check too # And recursively check too
item.sub_part.checkAddToBOM(parent) item.sub_part.checkAddToBOM(parent)
def checkIfSerialNumberExists(self, sn): def checkIfSerialNumberExists(self, sn, exclude_self=False):
""" """
Check if a serial number exists for this Part. Check if a serial number exists for this Part.
@ -369,10 +369,27 @@ class Part(MPTTModel):
""" """
parts = Part.objects.filter(tree_id=self.tree_id) parts = Part.objects.filter(tree_id=self.tree_id)
stock = StockModels.StockItem.objects.filter(part__in=parts, serial=sn) stock = StockModels.StockItem.objects.filter(part__in=parts, serial=sn)
if exclude_self:
stock = stock.exclude(pk=self.pk)
return stock.exists() return stock.exists()
def find_conflicting_serial_numbers(self, serials):
"""
For a provided list of serials, return a list of those which are conflicting.
"""
conflicts = []
for serial in serials:
if self.checkIfSerialNumberExists(serial, exclude_self=True):
conflicts.append(serial)
return conflicts
def getLatestSerialNumber(self): def getLatestSerialNumber(self):
""" """
Return the "latest" serial number for this Part. Return the "latest" serial number for this Part.
@ -1116,6 +1133,31 @@ class Part(MPTTModel):
bom_item.save() 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): def deepCopy(self, other, **kwargs):
""" Duplicates non-field data from another part. """ Duplicates non-field data from another part.
Does not alter the normal fields of this part, Does not alter the normal fields of this part,
@ -1139,14 +1181,7 @@ class Part(MPTTModel):
# Copy the parameters data # Copy the parameters data
if kwargs.get('parameters', True): if kwargs.get('parameters', True):
# Get template part parameters self.copy_parameters_from(other)
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)
# Copy the fields that aren't available in the duplicate form # Copy the fields that aren't available in the duplicate form
self.salable = other.salable self.salable = other.salable

View File

@ -371,6 +371,8 @@ class MakePartVariant(AjaxCreateView):
initials = model_to_dict(part_template) initials = model_to_dict(part_template)
initials['is_template'] = False initials['is_template'] = False
initials['variant_of'] = part_template 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 return initials
@ -446,9 +448,9 @@ class PartDuplicate(AjaxCreateView):
confirmed = str2bool(request.POST.get('confirm_creation', False)) confirmed = str2bool(request.POST.get('confirm_creation', False))
if not confirmed: if not confirmed:
form.errors['confirm_creation'] = ['Possible matches exist - confirm creation of new part'] msg = _('Possible matches exist - confirm creation of new part')
form.add_error('confirm_creation', msg)
form.pre_form_warning = 'Possible matches exist - confirm creation of new part' form.pre_form_warning = msg
valid = False valid = False
data = { data = {
@ -576,9 +578,10 @@ class PartCreate(AjaxCreateView):
confirmed = str2bool(request.POST.get('confirm_creation', False)) confirmed = str2bool(request.POST.get('confirm_creation', False))
if not confirmed: if not confirmed:
form.errors['confirm_creation'] = ['Possible matches exist - confirm creation of new part'] msg = _('Possible matches exist - confirm creation of new part')
form.add_error('confirm_creation', msg)
form.pre_form_warning = 'Possible matches exist - confirm creation of new part' form.pre_form_warning = msg
valid = False valid = False
data = { data = {
@ -905,24 +908,22 @@ class BomValidate(AjaxUpdateView):
return self.renderJsonResponse(request, form, context=self.get_context()) 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() confirm = str2bool(form.cleaned_data.get('validate', False))
part = self.get_object()
confirmed = str2bool(request.POST.get('validate', False)) if not confirm:
form.add_error('validate', _('Confirm that the BOM is valid'))
if confirmed: def post_save(self, part, form, **kwargs):
part.validate_bom(request.user)
else:
form.errors['validate'] = ['Confirm that the BOM is valid']
data = { part.validate_bom(self.request.user)
'form_valid': confirmed
def get_data(self):
return {
'success': _('Validated Bill of Materials')
} }
return self.renderJsonResponse(request, form, data, context=self.get_context())
class BomUpload(InvenTreeRoleMixin, FormView): class BomUpload(InvenTreeRoleMixin, FormView):
""" View for uploading a BOM file, and handling BOM data importing. """ View for uploading a BOM file, and handling BOM data importing.
@ -1054,7 +1055,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
bom_file_valid = False bom_file_valid = False
if bom_file is None: if bom_file is None:
self.form.errors['bom_file'] = [_('No BOM file provided')] self.form.add_error('bom_file', _('No BOM file provided'))
else: else:
# Create a BomUploadManager object - will perform initial data validation # Create a BomUploadManager object - will perform initial data validation
# (and raise a ValidationError if there is something wrong with the file) # (and raise a ValidationError if there is something wrong with the file)
@ -1065,7 +1066,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
errors = e.error_dict errors = e.error_dict
for k, v in errors.items(): for k, v in errors.items():
self.form.errors[k] = v self.form.add_error(k, v)
if bom_file_valid: if bom_file_valid:
# BOM file is valid? Proceed to the next step! # BOM file is valid? Proceed to the next step!

View File

@ -814,14 +814,11 @@ class StockItem(MPTTModel):
raise ValidationError({"quantity": _("Quantity does not match serial numbers")}) raise ValidationError({"quantity": _("Quantity does not match serial numbers")})
# Test if each of the serial numbers are valid # Test if each of the serial numbers are valid
existing = [] existing = self.part.find_conflicting_serial_numbers(serials)
for serial in serials:
if self.part.checkIfSerialNumberExists(serial):
existing.append(serial)
if len(existing) > 0: if len(existing) > 0:
raise ValidationError({"serial_numbers": _("Serial numbers already exist: ") + str(existing)}) exists = ','.join([str(x) for x in existing])
raise ValidationError({"serial_numbers": _("Serial numbers already exist") + ': ' + exists})
# Create a new stock item for each unique serial number # Create a new stock item for each unique serial number
for serial in serials: for serial in serials:
@ -1129,6 +1126,22 @@ class StockItem(MPTTModel):
return s 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): def getTestResults(self, test=None, result=None, user=None):
""" """
Return all test results associated with this StockItem. Return all test results associated with this StockItem.

View File

@ -407,6 +407,13 @@ class VariantTest(StockTest):
self.assertEqual(chair.getLatestSerialNumber(), '22') self.assertEqual(chair.getLatestSerialNumber(), '22')
# Check for conflicting serial numbers
to_check = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
conflicts = chair.find_conflicting_serial_numbers(to_check)
self.assertEqual(len(conflicts), 6)
# Same operations on a sub-item # Same operations on a sub-item
variant = Part.objects.get(pk=10003) variant = Part.objects.get(pk=10003)
self.assertEqual(variant.getLatestSerialNumber(), '22') self.assertEqual(variant.getLatestSerialNumber(), '22')

View File

@ -245,32 +245,28 @@ class StockItemAssignToCustomer(AjaxUpdateView):
form_class = StockForms.AssignStockItemToCustomerForm form_class = StockForms.AssignStockItemToCustomerForm
role_required = 'stock.change' 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: if customer:
try: item = item.allocateToCustomer(
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(
customer, customer,
user=request.user user=self.request.user
) )
item.clearAllocations() item.clearAllocations()
data = {
'form_valid': True,
}
return self.renderJsonResponse(request, self.get_form(), data)
class StockItemReturnToStock(AjaxUpdateView): class StockItemReturnToStock(AjaxUpdateView):
""" """
@ -283,30 +279,25 @@ class StockItemReturnToStock(AjaxUpdateView):
form_class = StockForms.ReturnStockItemForm form_class = StockForms.ReturnStockItemForm
role_required = 'stock.change' 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: if location:
try: item.returnFromCustomer(location, self.request.user)
location = StockLocation.objects.get(pk=location)
except (ValueError, StockLocation.DoesNotExist):
location = None
if location: def get_data(self):
stock_item = self.get_object() return {
'success': _('Stock item returned from customer')
stock_item.returnFromCustomer(location, request.user)
else:
raise ValidationError({'location': _("Specify a valid location")})
data = {
'form_valid': True,
'success': _("Stock item returned from customer")
} }
return self.renderJsonResponse(request, self.get_form(), data)
class StockItemSelectLabels(AjaxView): class StockItemSelectLabels(AjaxView):
""" """
@ -417,8 +408,8 @@ class StockItemDeleteTestData(AjaxUpdateView):
confirm = str2bool(request.POST.get('confirm', False)) confirm = str2bool(request.POST.get('confirm', False))
if confirm is not True: if confirm is not True:
form.errors['confirm'] = [_('Confirm test data deletion')] form.add_error('confirm', _('Confirm test data deletion'))
form.non_field_errors = [_('Check the confirmation box')] form.add_error(None, _('Check the confirmation box'))
else: else:
stock_item.test_results.all().delete() stock_item.test_results.all().delete()
valid = True valid = True
@ -918,7 +909,7 @@ class StockItemUninstall(AjaxView, FormMixin):
if not confirmed: if not confirmed:
valid = False valid = False
form.errors['confirm'] = [_('Confirm stock adjustment')] form.add_error('confirm', _('Confirm stock adjustment'))
data = { data = {
'form_valid': valid, 'form_valid': valid,
@ -1116,7 +1107,7 @@ class StockAdjust(AjaxView, FormMixin):
if not confirmed: if not confirmed:
valid = False valid = False
form.errors['confirm'] = [_('Confirm stock adjustment')] form.add_error('confirm', _('Confirm stock adjustment'))
data = { data = {
'form_valid': valid, 'form_valid': valid,
@ -1416,7 +1407,7 @@ class StockItemSerialize(AjaxUpdateView):
try: try:
numbers = ExtractSerialNumbers(serials, quantity) numbers = ExtractSerialNumbers(serials, quantity)
except ValidationError as e: except ValidationError as e:
form.errors['serial_numbers'] = e.messages form.add_error('serial_numbers', e.messages)
valid = False valid = False
numbers = [] numbers = []
@ -1428,9 +1419,9 @@ class StockItemSerialize(AjaxUpdateView):
for k in messages.keys(): for k in messages.keys():
if k in ['quantity', 'destination', 'serial_numbers']: if k in ['quantity', 'destination', 'serial_numbers']:
form.errors[k] = messages[k] form.add_error(k, messages[k])
else: else:
form.non_field_errors = [messages[k]] form.add_error(None, messages[k])
valid = False valid = False
@ -1622,14 +1613,14 @@ class StockItemCreate(AjaxCreateView):
part = None part = None
quantity = 1 quantity = 1
valid = False valid = False
form.errors['quantity'] = [_('Invalid quantity')] form.add_error('quantity', _('Invalid quantity'))
if quantity < 0: if quantity < 0:
form.errors['quantity'] = [_('Quantity cannot be less than zero')] form.add_error('quantity', _('Quantity cannot be less than zero'))
valid = False valid = False
if part is None: if part is None:
form.errors['part'] = [_('Invalid part selection')] form.add_error('part', _('Invalid part selection'))
else: else:
# A trackable part must provide serial numbesr # A trackable part must provide serial numbesr
if part.trackable: if part.trackable:
@ -1642,15 +1633,14 @@ class StockItemCreate(AjaxCreateView):
try: try:
serials = ExtractSerialNumbers(sn, quantity) serials = ExtractSerialNumbers(sn, quantity)
existing = [] existing = part.find_conflicting_serial_numbers(serials)
for serial in serials:
if part.checkIfSerialNumberExists(serial):
existing.append(serial)
if len(existing) > 0: if len(existing) > 0:
exists = ",".join([str(x) for x in existing]) exists = ",".join([str(x) for x in existing])
form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))] form.add_error(
'serial_numbers',
_('Serial numbers already exist') + ': ' + exists
)
valid = False valid = False
else: else:
@ -1682,7 +1672,7 @@ class StockItemCreate(AjaxCreateView):
valid = True valid = True
except ValidationError as e: except ValidationError as e:
form.errors['serial_numbers'] = e.messages form.add_error('serial_numbers', e.messages)
valid = False valid = False
else: else: