From 50bab299c53aa2ea37aee3bbffc6c122835a59bf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Jun 2019 21:52:43 +1000 Subject: [PATCH 01/33] Add hover image to 'parts to order' table --- InvenTree/templates/required_part_table.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/required_part_table.html b/InvenTree/templates/required_part_table.html index 24eae0012f..6f21f5f7df 100644 --- a/InvenTree/templates/required_part_table.html +++ b/InvenTree/templates/required_part_table.html @@ -9,7 +9,10 @@ {% for part in parts %} - {{ part.full_name }} + + {% include "hover_image.html" with image=part.image hover=True %} + {{ part.full_name }} + {{ part.description }} {{ part.allocation_count }} {{ part.total_stock }} From 8e82488f70cfcf8005e5b32045ea4e12be3fc122 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Jun 2019 21:58:20 +1000 Subject: [PATCH 02/33] Display 'net stock' value on front page --- InvenTree/part/models.py | 13 +++++++++++++ InvenTree/static/css/inventree.css | 4 ++++ InvenTree/templates/required_part_table.html | 6 +++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 67a82a9504..4fd7360810 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -410,6 +410,19 @@ class Part(models.Model): return max(total, 0) + @property + def net_stock(self): + """ Return the 'net' stock. It takes into account: + + - Stock on hand (total_stock) + - Stock on order (on_order) + - Stock allocated (allocation_count) + + This number (unlike 'available_stock') can be negative. + """ + + return self.total_stock - self.allocation_count + self.on_order + def isStarredBy(self, user): """ Return True if this part has been starred by a particular user """ diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 3bef992d34..2a98814dda 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -14,6 +14,10 @@ color: #ffbb00; } +.red-cell { + background-color: #ec7f7f; +} + .part-price { color: rgb(13, 245, 25); } diff --git a/InvenTree/templates/required_part_table.html b/InvenTree/templates/required_part_table.html index 6f21f5f7df..74feb07336 100644 --- a/InvenTree/templates/required_part_table.html +++ b/InvenTree/templates/required_part_table.html @@ -2,9 +2,9 @@ Part Description - Required In Stock On Order + Allocted Net Stock {% for part in parts %} @@ -14,10 +14,10 @@ {{ part.full_name }} {{ part.description }} - {{ part.allocation_count }} {{ part.total_stock }} {{ part.on_order }} - {{ part.available_stock }} + {{ part.allocation_count }} + {{ part.net_stock }} {% endfor %} \ No newline at end of file From bc05146e72d68dcdbe1f66f64dec1266b7efba42 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Jun 2019 23:37:32 +1000 Subject: [PATCH 03/33] First pass at 'order parts' fom - Select parts in modal form window --- InvenTree/InvenTree/views.py | 2 +- .../templates/order/order_select_parts.html | 44 +++++++++ InvenTree/order/urls.py | 2 + InvenTree/order/views.py | 95 ++++++++++++++++++- InvenTree/part/models.py | 9 +- InvenTree/static/script/inventree/stock.js | 16 ++++ InvenTree/templates/modal_form.html | 4 + InvenTree/templates/stock_table.html | 3 +- 8 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 InvenTree/order/templates/order/order_select_parts.html diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 282541d19f..15f93c9133 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -170,7 +170,7 @@ class AjaxView(AjaxMixin, View): """ def post(self, request, *args, **kwargs): - return JsonResponse('', safe=False) + return self.renderJsonResponse(request) def get(self, request, *args, **kwargs): diff --git a/InvenTree/order/templates/order/order_select_parts.html b/InvenTree/order/templates/order/order_select_parts.html new file mode 100644 index 0000000000..eede585b82 --- /dev/null +++ b/InvenTree/order/templates/order/order_select_parts.html @@ -0,0 +1,44 @@ +{% extends "modal_form.html" %} + +{% block form %} + +
+ {% csrf_token %} + {% load crispy_forms_tags %} + + + + + + + + + + + {% for part in parts %} + + + + + + + {% endfor %} +
PartSupplierQuantity
+ {% include "hover_image.html" with image=part.image hover=False %} + {{ part.full_name }} {{ part.description }} + + + + + + +
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 20379edb76..410efb8129 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -26,6 +26,8 @@ purchase_order_urls = [ url(r'^new/', views.PurchaseOrderCreate.as_view(), name='purchase-order-create'), + url(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'), + # Display detail view for a single purchase order url(r'^(?P\d+)/', include(purchase_order_detail_urls)), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 718b26c98c..6382ab1d99 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -7,14 +7,17 @@ from __future__ import unicode_literals from django.utils.translation import ugettext as _ from django.views.generic import DetailView, ListView +from django.views.generic.edit import FormMixin from django.forms import HiddenInput from .models import PurchaseOrder, PurchaseOrderLineItem from company.models import Company, SupplierPart +from stock.models import StockItem +from part.models import Part from . import forms as order_forms -from InvenTree.views import AjaxCreateView, AjaxUpdateView +from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView from InvenTree.helpers import str2bool from InvenTree.status_codes import OrderStatus @@ -135,6 +138,96 @@ class PurchaseOrderIssue(AjaxUpdateView): return self.renderJsonResponse(request, form, data) +class OrderParts(AjaxView): + """ View for adding various SupplierPart items to a Purchase Order. + + SupplierParts can be selected from a variety of 'sources': + + - ?supplier_parts[]= -> Direct list of SupplierPart objects + - ?parts[]= -> List of base Part objects (user must then select supplier parts) + - ?stock[]= -> List of StockItem objects (user must select supplier parts) + - ?build= -> A Build object (user must select parts, then supplier parts) + + """ + + ajax_form_title = "Order Parts" + ajax_template_name = 'order/order_select_parts.html' + + # List of Parts we wish to order + parts = [] + + def get_context_data(self): + + ctx = {} + + print("Getting context data") + + ctx['parts'] = self.get_parts() + + return ctx + + def get_parts(self): + """ Determine which parts the user wishes to order. + This is performed on the initial GET request. + """ + + self.parts = [] + + part_ids = set() + + # User has passed a list of stock items + if 'stock[]' in self.request.GET: + + stock_id_list = self.request.GET.getlist('stock[]') + + print("Looking up parts from stock items:") + print(stock_id_list) + + """ Get a list of all the parts associated with the stock items. + - Base part must be purchaseable. + - Return a set of corresponding Part IDs + """ + stock_items = StockItem.objects.filter( + part__purchaseable=True, + id__in=stock_id_list) + + for item in stock_items: + part_ids.add(item.part.id) + + print("Parts:", part_ids) + + + # Create the list of parts + for id in part_ids: + try: + part = Part.objects.get(id=id) + except Part.DoesNotExist: + continue + + self.parts.append(part) + + return self.parts + + def get(self, request, *args, **kwargs): + + self.request = request + + print("GET HERE") + + print(request.GET) + + self.get_parts() + + return self.renderJsonResponse(request) + + def post(self, request, *args, **kwargs): + + self.request = request + print("POST here") + + return self.renderJsonResponse(request) + + class POLineItemCreate(AjaxCreateView): """ AJAX view for creating a new PurchaseOrderLineItem object """ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4fd7360810..38acae3360 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -339,7 +339,7 @@ class Part(models.Model): """ if self.default_supplier: - return self.default_suppliers + return self.default_supplier if self.supplier_count == 1: return self.supplier_parts.first() @@ -410,6 +410,13 @@ class Part(models.Model): return max(total, 0) + @property + def quantity_to_order(self): + """ Return the quantity needing to be ordered for this part. """ + + required = -1 * self.net_stock + return max(required, 0) + @property def net_stock(self): """ Return the 'net' stock. It takes into account: diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index ae3ab55a7e..baa9c2ee87 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -237,6 +237,22 @@ function loadStockTable(table, options) { $("#multi-item-move").click(function() { stockAdjustment('move'); }); + + $("#multi-item-order").click(function() { + var selections = $("#stock-table").bootstrapTable("getSelections"); + + var stock = []; + + selections.forEach(function(item) { + stock.push(item.pk); + }); + + launchModalForm("/order/purchase-order/order-parts/", { + data: { + stock: stock, + }, + }); + }); } diff --git a/InvenTree/templates/modal_form.html b/InvenTree/templates/modal_form.html index 1f5482f8d6..eb5334b445 100644 --- a/InvenTree/templates/modal_form.html +++ b/InvenTree/templates/modal_form.html @@ -21,6 +21,8 @@ {% block pre_form_content %} {% endblock %} + +{% block form %} {% csrf_token %} {% load crispy_forms_tags %} @@ -32,5 +34,7 @@ {% endblock %} +{% endblock %} + {% block post_form_content %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index acdd371d08..aa07f99959 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -9,7 +9,8 @@
  • Add stock
  • Remove stock
  • Count stock
  • -
  • Move stock
  • +
  • Move stock
  • +
  • Order stock
  • From 0bc8190e8c7b2ff6640aa3da822a0988b4142925 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Jun 2019 23:41:15 +1000 Subject: [PATCH 04/33] Style supplier-part selection as 'select2' --- .../templates/order/order_select_parts.html | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/templates/order/order_select_parts.html b/InvenTree/order/templates/order/order_select_parts.html index eede585b82..dbedcc4a91 100644 --- a/InvenTree/order/templates/order/order_select_parts.html +++ b/InvenTree/order/templates/order/order_select_parts.html @@ -22,12 +22,16 @@ {{ part.full_name }} {{ part.description }} - +
    +
    + +
    +
    From 0ca01fb0e7523d106e5d3c4cb2fdf0ce763c286d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Jun 2019 23:47:09 +1000 Subject: [PATCH 05/33] Template improvements --- .../order/templates/order/order_select_parts.html | 10 +++++++--- InvenTree/order/views.py | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/InvenTree/order/templates/order/order_select_parts.html b/InvenTree/order/templates/order/order_select_parts.html index dbedcc4a91..4dc233c72d 100644 --- a/InvenTree/order/templates/order/order_select_parts.html +++ b/InvenTree/order/templates/order/order_select_parts.html @@ -16,7 +16,7 @@ {% for part in parts %} - + {% include "hover_image.html" with image=part.image hover=False %} {{ part.full_name }} {{ part.description }} @@ -34,11 +34,15 @@ - +
    +
    + +
    +
    diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 6382ab1d99..26e48e59fe 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -225,7 +225,11 @@ class OrderParts(AjaxView): self.request = request print("POST here") - return self.renderJsonResponse(request) + data = { + 'form_valid': False, + } + + return self.renderJsonResponse(request, data=data) class POLineItemCreate(AjaxCreateView): From 1cb6c670861ca3075ec75e4ee96c150eca7557cf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Jun 2019 23:55:20 +1000 Subject: [PATCH 06/33] Launch order dialog from the 'Parts' table --- InvenTree/order/views.py | 16 +++++++++++++++- InvenTree/part/templates/part/category.html | 1 + InvenTree/static/script/inventree/part.js | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 26e48e59fe..d200392084 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -194,8 +194,22 @@ class OrderParts(AjaxView): for item in stock_items: part_ids.add(item.part.id) - print("Parts:", part_ids) + # User has passed a list of part ID values + if 'parts[]' in self.request.GET: + part_id_list = self.request.GET.getlist('parts[]') + + print("Provided list of part:") + print(part_id_list) + + parts = Part.objects.filter( + purchaseable=True, + id__in=part_id_list) + + for part in parts: + part_ids.add(part.id) + + print("Parts:", part_ids) # Create the list of parts for id in part_ids: diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 2d76b08ac9..bbe449d217 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -49,6 +49,7 @@ diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index 7c933ef303..d56a8d4769 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -192,4 +192,22 @@ function loadPartTable(table, url, options={}) { if (options.buttons) { linkButtonsToSelection($(table), options.buttons); } + + /* Button callbacks for part table buttons */ + + $("#multi-part-order").click(function() { + var selections = $(table).bootstrapTable("getSelections"); + + var parts = []; + + selections.forEach(function(item) { + parts.push(item.pk); + }); + + launchModalForm("/order/purchase-order/order-parts/", { + data: { + parts: parts, + }, + }); + }); } \ No newline at end of file From 3460a48b634ce8a01b35dd8d58a1067fe92aa793 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 11 Jun 2019 23:58:00 +1000 Subject: [PATCH 07/33] Extra form info --- .../order/templates/order/order_select_parts.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/InvenTree/order/templates/order/order_select_parts.html b/InvenTree/order/templates/order/order_select_parts.html index 4dc233c72d..4191010df5 100644 --- a/InvenTree/order/templates/order/order_select_parts.html +++ b/InvenTree/order/templates/order/order_select_parts.html @@ -2,6 +2,16 @@ {% block form %} +{% if parts|length > 0 %} + +{% else %} + +{% endif %} +
    {% csrf_token %} {% load crispy_forms_tags %} From e4f5cc8ccdd86df7440f5f9e9f7e6a9ce5b39d45 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 12 Jun 2019 00:04:20 +1000 Subject: [PATCH 08/33] Order from a part page --- InvenTree/order/views.py | 14 +++++++++++++- InvenTree/part/templates/part/detail.html | 11 +++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d200392084..6f466d687c 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -194,9 +194,21 @@ class OrderParts(AjaxView): for item in stock_items: part_ids.add(item.part.id) + # User has passed a single Part ID + elif 'part' in self.request.GET: + try: + part_id = self.request.GET.get('part') + part = Part.objects.get(id=part_id) + + part_ids.add(part.id) + + except Part.DoesNotExist: + pass + + # User has passed a list of part ID values - if 'parts[]' in self.request.GET: + elif 'parts[]' in self.request.GET: part_id_list = self.request.GET.getlist('parts[]') print("Provided list of part:") diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index b536a5542e..fe357a6bdc 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -15,6 +15,9 @@