From 4ddabb82ef47111b1a9a55d9b54dd64525207c5f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 4 Jun 2020 11:50:55 +1000 Subject: [PATCH 1/5] Add a button to assign stock item to customer --- .../stock/templates/stock/item_base.html | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 56ef8a3370..b675cb3709 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -71,43 +71,48 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% include "qr_button.html" %} {% if item.in_stock %} {% if not item.serialized %} - - - {% if item.part.trackable %} - {% endif %} + {% if item.part.salable %} + {% endif %} - - {% endif %} {% if item.part.has_variants %} - {% endif %} {% if item.part.has_test_report_templates %} - {% endif %} - {% if item.can_delete %} - {% endif %} From fbd21827fbb232863d372e8ffea921581f8fd227 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 4 Jun 2020 19:08:33 +1000 Subject: [PATCH 2/5] Add 'customer' field to StockItem --- .../migrations/0045_stockitem_customer.py | 20 +++++++++++++++++++ InvenTree/stock/models.py | 11 ++++++++++ 2 files changed, 31 insertions(+) create mode 100644 InvenTree/stock/migrations/0045_stockitem_customer.py diff --git a/InvenTree/stock/migrations/0045_stockitem_customer.py b/InvenTree/stock/migrations/0045_stockitem_customer.py new file mode 100644 index 0000000000..497d6b7353 --- /dev/null +++ b/InvenTree/stock/migrations/0045_stockitem_customer.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-06-04 03:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0021_remove_supplierpart_manufacturer_name'), + ('stock', '0044_auto_20200528_1036'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='customer', + field=models.ForeignKey(help_text='Customer', limit_choices_to={'is_customer': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_stock', to='company.Company', verbose_name='Customer'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7bebdefaba..4e18842304 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -32,6 +32,7 @@ from InvenTree.status_codes import StockStatus from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeURLField +from company import models as CompanyModels from part import models as PartModels @@ -352,6 +353,16 @@ class StockItem(MPTTModel): help_text=_('Is this item installed in another item?') ) + customer = models.ForeignKey( + CompanyModels.Company, + on_delete=models.SET_NULL, + null=True, + limit_choices_to={'is_customer': True}, + related_name='assigned_stock', + help_text=_("Customer"), + verbose_name=_("Customer"), + ) + serial = models.PositiveIntegerField( verbose_name=_('Serial Number'), blank=True, null=True, From d9071362646045e5c3e42a1144e0bee5a892ed4c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 4 Jun 2020 19:13:37 +1000 Subject: [PATCH 3/5] Display customer in stock item --- InvenTree/stock/templates/stock/item_base.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index b675cb3709..f34ac51f39 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -131,6 +131,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {{ item.part.full_name }} + {% if item.customer %} + + + {% trans "Customer" %} + {{ item.customer.name }} + + {% endif %} {% if item.belongs_to %} From 80019a3ed8841a4d82c1f4ef9f83d5733c32d7e5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 4 Jun 2020 19:45:41 +1000 Subject: [PATCH 4/5] Add forms/views for manually assigning a stock item to a customer --- InvenTree/InvenTree/views.py | 9 +++ InvenTree/order/models.py | 30 +++------ InvenTree/stock/forms.py | 12 ++++ InvenTree/stock/models.py | 65 +++++++++++++++++-- .../stock/templates/stock/item_base.html | 10 +++ InvenTree/stock/urls.py | 1 + InvenTree/stock/views.py | 38 +++++++++++ 7 files changed, 139 insertions(+), 26 deletions(-) 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 From ca1526405b930fa43f7332f1d1e648c809feec1f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 4 Jun 2020 19:51:43 +1000 Subject: [PATCH 5/5] PEP fixes --- InvenTree/stock/models.py | 1 - InvenTree/stock/views.py | 1 - 2 files changed, 2 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index eafbe05baf..e59af0e2e9 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -494,7 +494,6 @@ class StockItem(MPTTModel): # 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/views.py b/InvenTree/stock/views.py index 2b73229e7f..6f9eeeec2e 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -260,7 +260,6 @@ class StockItemAssignToCustomer(AjaxUpdateView): return self.renderJsonResponse(request, self.get_form(), data) - class StockItemDeleteTestData(AjaxUpdateView): """ View for deleting all test data