From 21e369e6cc399efaacc110567becbe8e0d0b089a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Sep 2019 19:02:36 +1000 Subject: [PATCH 1/7] Update ReceivePurchaseOrder form - Location field is now a proper MPTT field - Ability to create a new location --- InvenTree/order/forms.py | 14 ++++++++++++++ .../order/purchase_order_detail.html | 8 ++++++++ .../order/templates/order/receive_parts.html | 19 ++----------------- InvenTree/order/views.py | 13 ++++++------- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 4844ef4344..8432d9a6ef 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -8,8 +8,11 @@ from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext as _ +from mptt.fields import TreeNodeChoiceField + from InvenTree.forms import HelperForm +from stock.models import StockLocation from .models import PurchaseOrder, PurchaseOrderLineItem @@ -35,6 +38,17 @@ class CancelPurchaseOrderForm(HelperForm): ] +class ReceivePurchaseOrderForm(HelperForm): + + location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), required=True, help_text=_('Receive parts to this location')) + + class Meta: + model = PurchaseOrder + fields = [ + 'location', + ] + + class EditPurchaseOrderForm(HelperForm): """ Form for editing a PurchaseOrder object """ diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 83bd526a96..a3a92e8612 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -190,6 +190,14 @@ $("#cancel-order").click(function() { $("#receive-order").click(function() { launchModalForm("{% url 'purchase-order-receive' order.id %}", { reload: true, + secondary: [ + { + field: 'location', + label: 'New Location', + title: 'Create new stock location', + url: "{% url 'stock-location-create' %}", + }, + ] }); }); diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html index 9f773cd411..3ebaa66889 100644 --- a/InvenTree/order/templates/order/receive_parts.html +++ b/InvenTree/order/templates/order/receive_parts.html @@ -8,23 +8,6 @@ Receive outstanding parts for {{ order }} - {{ order.description }} - -
- - {% if not destination %} - Select location to receive parts - {% else %} -

Location of received parts

- {% endif %} -
- -

Select parts to receive against this order.

@@ -64,6 +47,8 @@ Receive outstanding parts for {{ order }} - {{ order.description }} {% endfor %} + + {% crispy form %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 49d2ed01ed..d2300f2527 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -206,7 +206,7 @@ class PurchaseOrderExport(AjaxView): return DownloadFile(filedata, filename) -class PurchaseOrderReceive(AjaxView): +class PurchaseOrderReceive(AjaxUpdateView): """ View for receiving parts which are outstanding against a PurchaseOrder. Any parts which are outstanding are listed. @@ -214,6 +214,7 @@ class PurchaseOrderReceive(AjaxView): """ + form_class = order_forms.ReceivePurchaseOrderForm ajax_form_title = "Receive Parts" ajax_template_name = "order/receive_parts.html" @@ -225,8 +226,6 @@ class PurchaseOrderReceive(AjaxView): ctx = { 'order': self.order, 'lines': self.lines, - 'locations': StockLocation.objects.all(), - 'destination': self.destination, } return ctx @@ -245,7 +244,7 @@ class PurchaseOrderReceive(AjaxView): # Pre-fill the remaining quantity line.receive_quantity = line.remaining() - return self.renderJsonResponse(request) + return self.renderJsonResponse(request, form=self.get_form()) def post(self, request, *args, **kwargs): """ Respond to a POST request. Data checking and error handling. @@ -260,8 +259,8 @@ class PurchaseOrderReceive(AjaxView): self.destination = None # Extract the destination for received parts - if 'receive_location' in request.POST: - pk = request.POST['receive_location'] + if 'location' in request.POST: + pk = request.POST['location'] try: self.destination = StockLocation.objects.get(id=pk) except (StockLocation.DoesNotExist, ValueError): @@ -316,7 +315,7 @@ class PurchaseOrderReceive(AjaxView): 'success': 'Items marked as received', } - return self.renderJsonResponse(request, data=data) + return self.renderJsonResponse(request, data=data, form=self.get_form()) @transaction.atomic def receive_parts(self): From 0d68dbcfa76ee05f602c0d9a3a1485cd2f53176d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Sep 2019 19:05:22 +1000 Subject: [PATCH 2/7] Display which lines have been received against a PO --- InvenTree/order/templates/order/purchase_order_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index a3a92e8612..5debf92c5b 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -115,7 +115,7 @@ InvenTree | {{ order }} {% for line in order.lines.all %} - + {{ forloop.counter }} From 8d92960f1031c5994c75261cf785143cc0c57716 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Sep 2019 19:31:18 +1000 Subject: [PATCH 3/7] Ability to receive PO lines items individually --- .../order/purchase_order_detail.html | 25 ++++++++++++++--- InvenTree/order/views.py | 27 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 5debf92c5b..d988f20739 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -108,9 +108,7 @@ InvenTree | {{ order }} Received {% endif %} Note - {% if order.status == OrderStatus.PENDING %} - {% endif %} @@ -137,18 +135,23 @@ InvenTree | {{ order }} {{ line.notes }} - {% if order.status == OrderStatus.PENDING %}
+ {% if order.status == OrderStatus.PENDING %} + {% endif %} + {% if order.status == OrderStatus.PLACED and line.received < line.quantity %} + + {% endif %}
- {% endif %} {% endfor %} @@ -187,6 +190,20 @@ $("#cancel-order").click(function() { }); }); +$("#po-lines-table").on('click', ".line-receive", function() { + + var button = $(this); + + console.log('clicked! ' + button.attr('pk')); + + launchModalForm("{% url 'purchase-order-receive' order.id %}", { + reload: true, + data: { + line: button.attr('pk') + } + }); +}); + $("#receive-order").click(function() { launchModalForm("{% url 'purchase-order-receive' order.id %}", { reload: true, diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d2300f2527..5ae955b3c8 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -230,6 +230,31 @@ class PurchaseOrderReceive(AjaxUpdateView): return ctx + def get_lines(self): + """ + Extract particular line items from the request, + or default to *all* pending line items if none are provided + """ + + lines = None + + if 'line' in self.request.GET: + line_id = self.request.GET.get('line') + + try: + lines = PurchaseOrderLineItem.objects.filter(pk=line_id) + except (PurchaseOrderLineItem.DoesNotExist, ValueError): + pass + + # TODO - Option to pass multiple lines? + + # No lines specified - default selection + if lines is None: + lines = self.order.pending_line_items() + + return lines + + def get(self, request, *args, **kwargs): """ Respond to a GET request. Determines which parts are outstanding, and presents a list of these parts to the user. @@ -238,7 +263,7 @@ class PurchaseOrderReceive(AjaxUpdateView): self.request = request self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) - self.lines = self.order.pending_line_items() + self.lines = self.get_lines() for line in self.lines: # Pre-fill the remaining quantity From b1380687e6be844a5291515a3e975b8717752777 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Sep 2019 19:31:50 +1000 Subject: [PATCH 4/7] PEP --- InvenTree/order/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 5ae955b3c8..01ad682e6a 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -254,7 +254,6 @@ class PurchaseOrderReceive(AjaxUpdateView): return lines - def get(self, request, *args, **kwargs): """ Respond to a GET request. Determines which parts are outstanding, and presents a list of these parts to the user. From 52ec213a28a869b86624c1913caa3fd1bbeaecdf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 24 Sep 2019 07:43:14 +1000 Subject: [PATCH 5/7] Save user information when creating a new purchase order --- InvenTree/InvenTree/views.py | 31 +++++++++++++++++++++++-------- InvenTree/order/views.py | 6 ++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index ad0d70bfe8..8a19059aa3 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -238,6 +238,18 @@ class AjaxCreateView(AjaxMixin, CreateView): - Handles form validation via AJAX POST requests """ + def pre_save(self, **kwargs): + """ + Hook for doing something before the form is validated + """ + pass + + def post_save(self, **kwargs): + """ + Hook for doing something with the created object after it is saved + """ + pass + def get(self, request, *args, **kwargs): """ Creates form with initial data, and renders JSON response """ @@ -255,26 +267,29 @@ class AjaxCreateView(AjaxMixin, CreateView): - Return status info (success / failure) """ self.request = request - form = self.get_form() + self.form = self.get_form() # Extra JSON data sent alongside form data = { - 'form_valid': form.is_valid(), + 'form_valid': self.form.is_valid(), } - if form.is_valid(): - obj = form.save() + if self.form.is_valid(): + + self.pre_save() + self.object = self.form.save() + self.post_save() # Return the PK of the newly-created object - data['pk'] = obj.pk - data['text'] = str(obj) + data['pk'] = self.object.pk + data['text'] = str(object) try: - data['url'] = obj.get_absolute_url() + data['url'] = self.object.get_absolute_url() except AttributeError: pass - return self.renderJsonResponse(request, form, data) + return self.renderJsonResponse(request, self.form, data) class AjaxUpdateView(AjaxMixin, UpdateView): diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 01ad682e6a..d78c9c059e 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -91,6 +91,12 @@ class PurchaseOrderCreate(AjaxCreateView): return initials + def post_save(self, **kwargs): + # Record the user who created this purchase order + + self.object.created_by = self.request.user + self.object.save() + class PurchaseOrderEdit(AjaxUpdateView): """ View for editing a PurchaseOrder using a modal form """ From 41c07fc42308f4ada24662ae6eb25b563aaffc07 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 24 Sep 2019 07:54:18 +1000 Subject: [PATCH 6/7] Save user who created a stock item - Handled differently for batch or serialized parts --- InvenTree/InvenTree/views.py | 2 +- InvenTree/stock/models.py | 4 +++- InvenTree/stock/views.py | 16 +++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 8a19059aa3..91261827e7 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -245,7 +245,7 @@ class AjaxCreateView(AjaxMixin, CreateView): pass def post_save(self, **kwargs): - """ + """ Hook for doing something with the created object after it is saved """ pass diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7b5d01c58b..f2c31083c0 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -129,13 +129,15 @@ class StockItem(models.Model): else: add_note = False + user = kwargs.pop('user', None) + super(StockItem, self).save(*args, **kwargs) if add_note: # This StockItem is being saved for the first time self.addTransactionNote( 'Created stock item', - None, + user, notes="Created new stock item for part '{p}'".format(p=str(self.part)), system=True ) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 1bb9a27a83..d8833d59d0 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -792,6 +792,8 @@ class StockItemCreate(AjaxCreateView): form = self.get_form() + data = {} + valid = form.is_valid() if valid: @@ -850,7 +852,7 @@ class StockItemCreate(AjaxCreateView): URL=data.get('URL'), ) - item.save() + item.save(user=request.user) except ValidationError as e: form.errors['serial_numbers'] = e.messages @@ -861,11 +863,15 @@ class StockItemCreate(AjaxCreateView): # We need to call _post_clean() here because it is prevented in the form implementation form.clean() form._post_clean() - form.save() + + item = form.save(commit=False) + item.save(user=request.user) - data = { - 'form_valid': valid, - } + data['pk'] = item.pk + data['url'] = item.get_absolute_url() + data['success'] = _("Created new stock item") + + data['form_valid'] = valid return self.renderJsonResponse(request, form, data=data) From 7c1615a2b69c6239c9f3e1bbfd65047ee96b0af4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 24 Sep 2019 07:59:59 +1000 Subject: [PATCH 7/7] Fix user recording when serializing stock --- InvenTree/stock/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index f2c31083c0..1cf1a870b8 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -130,6 +130,8 @@ class StockItem(models.Model): add_note = False user = kwargs.pop('user', None) + + add_note = add_note and kwargs.pop('note', True) super(StockItem, self).save(*args, **kwargs) @@ -468,7 +470,8 @@ class StockItem(models.Model): if location: new_item.location = location - new_item.save() + # The item already has a transaction history, don't create a new note + new_item.save(user=user, note=False) # Copy entire transaction history new_item.copyHistoryFrom(self)