From b351976ae9d1269e89b864f136129bf521d0d86e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 Apr 2020 08:46:28 +1000 Subject: [PATCH] Mark a SalesOrder as "shipped" - Option to hide non-stock items from stock list - Update models with new feature --- InvenTree/InvenTree/static/css/inventree.css | 4 +++ .../static/script/inventree/stock.js | 1 + InvenTree/order/models.py | 36 +++++++++++++++++-- .../templates/order/sales_order_ship.html | 2 ++ InvenTree/order/views.py | 16 +++++---- InvenTree/stock/api.py | 15 ++++++-- InvenTree/stock/models.py | 5 +++ .../stock/templates/stock/item_base.html | 6 ++++ 8 files changed, 73 insertions(+), 12 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index bdfeb42f4c..e57188aa8c 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -336,6 +336,10 @@ padding-bottom: 2px; }; +.panel-heading .badge { + float: right; +} + .badge { float: right; background-color: #777; diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index e21971bb0f..3f1331dd2a 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -48,6 +48,7 @@ function loadStockTable(table, options) { options.params['part_detail'] = true; options.params['location_detail'] = true; + options.params['in_stock'] = true; var params = options.params || {}; diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 2eadd59138..647c5c49ec 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -315,10 +315,15 @@ class SalesOrder(Order): def ship_order(self, user): """ Mark this order as 'shipped' """ - return False + # The order can only be 'shipped' if the current status is PENDING if not self.status == SalesOrderStatus.PENDING: - return False + raise ValidationError({'status': _("SalesOrder cannot be shipped as it is not currently pending")}) + + # Complete the allocation for each allocated StockItem + for line in self.lines.all(): + for allocation in line.allocations.all(): + allocation.complete_allocation(user) # Ensure the order status is marked as "Shipped" self.status = SalesOrderStatus.SHIPPED @@ -552,3 +557,30 @@ class SalesOrderAllocation(models.Model): return self.item.location.pathstring else: return "" + + def complete_allocation(self, user): + """ + Complete this allocation (called when the parent SalesOrder is marked as "shipped"): + + - Determine if the referenced StockItem needs to be "split" (if allocated quantity != stock quantity) + - Mark the StockItem as belonging to the Customer (this will remove it from stock) + """ + + item = self.item + + # 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) + + # Assign the StockItem to the SalesOrder customer + item.customer = self.line.order.customer + + # Clear the location + item.location = None + + item.save() + + print("Finalizing allocation for: " + str(self.item)) diff --git a/InvenTree/order/templates/order/sales_order_ship.html b/InvenTree/order/templates/order/sales_order_ship.html index cb4a01f2f0..0060561e71 100644 --- a/InvenTree/order/templates/order/sales_order_ship.html +++ b/InvenTree/order/templates/order/sales_order_ship.html @@ -22,6 +22,8 @@ {% endif %}
+ {% trans "Sales Order" %} {{ order.reference }} - {{ order.customer.name }} +
{% trans "Shipping this order means that the order will no longer be editable." %}
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index eb0895f244..476a61b5e3 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -504,16 +504,14 @@ class SalesOrderShip(AjaxUpdateView): context_object_name = 'order' ajax_template_name = 'order/sales_order_ship.html' ajax_form_title = _('Ship Order') - - def context_data(self): - ctx = super().get_context_data() - ctx['order'] = self.get_object() - - return ctx def post(self, request, *args, **kwargs): + self.request = request + order = self.get_object() + self.object = order + form = self.get_form() confirm = str2bool(request.POST.get('confirm', False)) @@ -534,7 +532,11 @@ class SalesOrderShip(AjaxUpdateView): 'form_valid': valid, } - return self.renderJsonResponse(request, form, data) + context = self.get_context_data() + + context['order'] = order + + return self.renderJsonResponse(request, form, data, context) class PurchaseOrderExport(AjaxView): diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 2c22665da0..9b7518580e 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -363,8 +363,17 @@ class StockList(generics.ListCreateAPIView): # Start with all objects stock_list = super().filter_queryset(queryset) - # Filter out parts which are not actually "in stock" - stock_list = stock_list.filter(customer=None, belongs_to=None) + in_stock = self.request.query_params.get('in_stock', None) + + if in_stock is not None: + in_stock = str2bool(in_stock) + + if in_stock: + # Filter out parts which are not actually "in stock" + stock_list = stock_list.filter(customer=None, belongs_to=None) + else: + # Only show parts which are not in stock + stock_list = stock_list.exclude(customer=None, belongs_to=None) # Filter by 'allocated' patrs? allocated = self.request.query_params.get('allocated', None) @@ -418,7 +427,7 @@ class StockList(generics.ListCreateAPIView): # Does the client wish to filter by stock location? loc_id = self.request.query_params.get('location', None) - cascade = str2bool(self.request.query_params.get('cascade', False)) + cascade = str2bool(self.request.query_params.get('cascade', True)) if loc_id is not None: diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4ea271c6ab..119b69de7c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -634,6 +634,8 @@ class StockItem(MPTTModel): # Remove the specified quantity from THIS stock item self.take_stock(quantity, user, 'Split {n} items into new stock item'.format(n=quantity)) + # Return a copy of the "new" stock item + @transaction.atomic def move(self, location, notes, user, **kwargs): """ Move part to a new location. @@ -656,6 +658,9 @@ class StockItem(MPTTModel): except InvalidOperation: return False + if not self.in_stock: + raise ValidationError(_("StockItem cannot be moved as it is not in stock")) + if quantity <= 0: return False diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index b6114927f0..77023eba35 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -15,6 +15,12 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block pre_content %} {% include 'stock/loc_link.html' with location=item.location %} +{% if item.customer %} +
+ {% trans "This stock item has been sent to" %} {{ item.customer.name }} +
+{% endif %} + {% for allocation in item.sales_order_allocations.all %}
{% trans "This stock item is allocated to Sales Order" %} #{{ allocation.line.order.reference }} ({% trans "Quantity" %}: {% decimal allocation.quantity %})