mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
commit
3a702266e6
@ -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):
|
||||
|
||||
|
@ -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.
|
||||
|
Binary file not shown.
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
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 }}",
|
||||
|
17
InvenTree/part/templates/part/bom_duplicate.html
Normal file
17
InvenTree/part/templates/part/bom_duplicate.html
Normal 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 %}
|
@ -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'),
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user