diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 34cc5a1d7b..333f11898b 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -346,6 +346,8 @@ class AjaxUpdateView(AjaxMixin, UpdateView): # Include context data about the updated object data['pk'] = obj.id + self.post_save(obj) + try: data['url'] = obj.get_absolute_url() except AttributeError: @@ -353,6 +355,13 @@ class AjaxUpdateView(AjaxMixin, UpdateView): return self.renderJsonResponse(request, form, data) + def post_save(self, obj, *args, **kwargs): + """ + Hook called after the form data is saved. + (Optional) + """ + pass + class AjaxDeleteView(AjaxMixin, UpdateView): diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 2ebc6a6793..b607d314a0 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -632,24 +632,14 @@ class SalesOrderAllocation(models.Model): order = self.line.order - item = self.item + item = self.item.allocateToCustomer( + order.customer, + quantity=self.quantity, + order=order, + user=user + ) - # If the allocated quantity is less than the amount available, - # then split the stock item into two lots - if item.quantity > self.quantity: - - # Grab a copy of the new stock item (which will keep track of its "parent") - item = item.splitStock(self.quantity, None, user) - - # Update our own reference to the new item - self.item = item - self.save() - - # Assign the StockItem to the SalesOrder customer - item.sales_order = order - - # Clear the location - item.location = None - item.status = StockStatus.SHIPPED - - item.save() + # Update our own reference to the StockItem + # (It may have changed if the stock was split) + self.item = item + self.save() diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index ddd7390246..bb403d837d 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -34,6 +34,18 @@ class EditStockItemAttachmentForm(HelperForm): ] +class AssignStockItemToCustomerForm(HelperForm): + """ + Form for manually assigning a StockItem to a Customer + """ + + class Meta: + model = StockItem + fields = [ + 'customer', + ] + + class EditStockItemTestResultForm(HelperForm): """ Form for creating / editing a StockItemTestResult object. diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4e18842304..eafbe05baf 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -218,12 +218,6 @@ class StockItem(MPTTModel): super().clean() - if self.status == StockStatus.SHIPPED and self.sales_order is None: - raise ValidationError({ - 'sales_order': "SalesOrder must be specified as status is marked as SHIPPED", - 'status': "Status cannot be marked as SHIPPED if the Customer is not set", - }) - if self.status == StockStatus.ASSIGNED_TO_OTHER_ITEM and self.belongs_to is None: raise ValidationError({ 'belongs_to': "Belongs_to field must be specified as statis is marked as ASSIGNED_TO_OTHER_ITEM", @@ -442,6 +436,65 @@ class StockItem(MPTTModel): help_text=_('Stock Item Notes') ) + def clearAllocations(self): + """ + Clear all order allocations for this StockItem: + + - SalesOrder allocations + - Build allocations + """ + + # Delete outstanding SalesOrder allocations + self.sales_order_allocations.all().delete() + + # Delete outstanding BuildOrder allocations + self.allocations.all().delete() + + def allocateToCustomer(self, customer, quantity=None, order=None, user=None, notes=None): + """ + Allocate a StockItem to a customer. + + This action can be called by the following processes: + - Completion of a SalesOrder + - User manually assigns a StockItem to the customer + + Args: + customer: The customer (Company) to assign the stock to + quantity: Quantity to assign (if not supplied, total quantity is used) + order: SalesOrder reference + user: User that performed the action + notes: Notes field + """ + + if quantity is None: + quantity = self.quantity + + if quantity >= self.quantity: + item = self + else: + item = self.splitStock(quantity, None, user) + + # Update StockItem fields with new information + item.sales_order = order + item.status = StockStatus.SHIPPED + item.customer = customer + item.location = None + + item.save() + + # TODO - Remove any stock item allocations from this stock item + + item.addTransactionNote( + _("Assigned to Customer"), + user, + notes=_("Manually assigned to customer") + " " + customer.name, + system=True + ) + + # Return the reference to the stock item + return item + + # If stock item is incoming, an (optional) ETA field # expected_arrival = models.DateField(null=True, blank=True) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index f34ac51f39..657154c446 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -332,6 +332,16 @@ $("#show-qr-code").click(function() { {% if item.in_stock %} +{% if item.part.salable %} +$("#stock-assign-to-customer").click(function() { + launchModalForm("{% url 'stock-item-assign' item.id %}", + { + reload: true, + } + ); +}); +{% endif %} + function itemAdjust(action) { launchModalForm("/stock/adjust/", { diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 7742f96fe2..65e9c6742b 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -23,6 +23,7 @@ stock_item_detail_urls = [ url(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'), url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'), url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'), + url(r'^assign/', views.StockItemAssignToCustomer.as_view(), name='stock-item-assign'), url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 8d3e5ebbf1..2b73229e7f 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -223,6 +223,44 @@ class StockItemAttachmentDelete(AjaxDeleteView): } +class StockItemAssignToCustomer(AjaxUpdateView): + """ + View for manually assigning a StockItem to a Customer + """ + + model = StockItem + ajax_form_title = _("Assign to Customer") + context_object_name = "item" + form_class = StockForms.AssignStockItemToCustomerForm + + def post(self, request, *args, **kwargs): + + customer = request.POST.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( + customer, + user=request.user + ) + + item.clearAllocations() + + data = { + 'form_valid': True, + } + + return self.renderJsonResponse(request, self.get_form(), data) + + + class StockItemDeleteTestData(AjaxUpdateView): """ View for deleting all test data