diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 06764f2bc9..0a015e2b66 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -14,6 +14,8 @@ from InvenTree.forms import HelperForm from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import DatePickerFormField +import part.models + from stock.models import StockLocation from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment @@ -211,22 +213,43 @@ class EditSalesOrderLineItemForm(HelperForm): ] -class CreateSalesOrderAllocationForm(HelperForm): +class AllocateSerialsToSalesOrderForm(HelperForm): """ - Form for creating a SalesOrderAllocation item. - - This can be allocated by selecting a specific stock item, - or by providing a sequence of serial numbers + Form for assigning stock to a sales order, + by serial number lookup """ - quantity = RoundingDecimalFormField(max_digits = 10, decimal_places=5) + line = forms.ModelChoiceField( + queryset = SalesOrderLineItem.objects.all(), + ) + + part = forms.ModelChoiceField( + queryset = part.models.Part.objects.all(), + ) serials = forms.CharField( label=_("Serial Numbers"), required=False, - help_text=_('Enter stock serial numbers'), + help_text=_('Enter stock item serial numbers'), ) + class Meta: + model = SalesOrderAllocation + + fields = [ + 'line', + 'part', + 'serials', + ] + + +class CreateSalesOrderAllocationForm(HelperForm): + """ + Form for creating a SalesOrderAllocation item. + """ + + quantity = RoundingDecimalFormField(max_digits = 10, decimal_places=5) + class Meta: model = SalesOrderAllocation diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index c3b33eaace..b621cda156 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -732,6 +732,12 @@ class SalesOrderAllocation(models.Model): errors = {} + try: + if not self.item: + raise ValidationError({'item': _('Stock item has not been assigned')}) + except stock_models.StockItem.DoesNotExist: + raise ValidationError({'item': _('Stock item has not been assigned')}) + try: if not self.line.part == self.item.part: errors['item'] = _('Cannot allocate stock item to a line with a different part') diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 27bbd542dd..7b4a4ed4f7 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -275,15 +275,20 @@ $("#so-lines-table").inventreeTable({ if (row.part) { var part = row.part_detail; + if (part.trackable) { + html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}'); + } + + html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}'); + if (part.purchaseable) { - html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Buy parts" %}'); + html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}'); } if (part.assembly) { - html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build parts" %}'); + html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}'); } - html += makeIconButton('fa-plus icon-green', 'button-add', pk, '{% trans "Allocate parts" %}'); } html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}'); @@ -316,10 +321,28 @@ function setupCallbacks() { var pk = $(this).attr('pk'); launchModalForm(`/order/sales-order/line/${pk}/delete/`, { - reload: true, + success: reloadTable, }); }); + table.find(".button-add-by-sn").click(function() { + var pk = $(this).attr('pk'); + + inventreeGet(`/api/order/so-line/${pk}/`, {}, + { + success: function(response) { + launchModalForm('{% url "so-assign-serials" %}', { + success: reloadTable, + data: { + line: pk, + part: response.part, + } + }); + } + } + ); + }); + table.find(".button-add").click(function() { var pk = $(this).attr('pk'); diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 7707b73f37..97903d81c1 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -81,6 +81,7 @@ sales_order_urls = [ # URLs for sales order allocations url(r'^allocation/', include([ url(r'^new/', views.SalesOrderAllocationCreate.as_view(), name='so-allocation-create'), + url(r'^assign-serials/', views.SalesOrderAssignSerials.as_view(), name='so-assign-serials'), url(r'(?P\d+)/', include([ url(r'^edit/', views.SalesOrderAllocationEdit.as_view(), name='so-allocation-edit'), url(r'^delete/', views.SalesOrderAllocationDelete.as_view(), name='so-allocation-delete'), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 8b40d6d724..eed4868557 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1291,6 +1291,75 @@ class SOLineItemDelete(AjaxDeleteView): } +class SalesOrderAssignSerials(AjaxCreateView): + """ + View for assigning stock items to a sales order, + by serial number lookup. + """ + + model = SalesOrderAllocation + role_required = 'sales_order.change' + ajax_form_title = _('Allocate Serial Numbers') + form_class = order_forms.AllocateSerialsToSalesOrderForm + + # Keep track of SalesOrderLineItem and Part references + line = None + part = None + + def get_initial(self): + """ + Initial values are passed as query params + """ + + initials = super().get_initial() + + try: + self.line = SalesOrderLineItem.objects.get(pk=self.request.GET.get('line', None)) + initials['line'] = self.line + except (ValueError, SalesOrderLineItem.DoesNotExist): + pass + + try: + self.part = Part.objects.get(pk=self.request.GET.get('part', None)) + initials['part'] = self.part + except (ValueError, Part.DoesNotExist): + pass + + return initials + + def get_form(self): + + form = super().get_form() + + if self.line is not None: + form.fields['line'].widget = HiddenInput() + + # Hide the 'part' field if value provided + try: + print(form['part']) + # self.part = Part.objects.get(form['part'].value()) + except (ValueError, Part.DoesNotExist): + self.part = None + + if self.part is not None: + form.fields['part'].widget = HiddenInput() + + return form + + def get_context_data(self): + return { + 'line': self.line, + 'part': self.part, + } + + def get(self, request, *args, **kwargs): + return self.renderJsonResponse( + request, + self.get_form(), + context=self.get_context_data(), + ) + + class SalesOrderAllocationCreate(AjaxCreateView): """ View for creating a new SalesOrderAllocation """