From 1290e7f289cee9b697147e2e5fab1bbe3b5c5091 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 15 Jun 2019 19:39:57 +1000 Subject: [PATCH] Implement POST for receiving items - Create new StockItem in the correct location --- .../migrations/0011_auto_20190615_1928.py | 26 ++++++ InvenTree/order/models.py | 55 ++++++++++- .../order/purchase_order_detail.html | 12 ++- .../order/templates/order/receive_parts.html | 24 ++++- InvenTree/order/views.py | 92 ++++++++++++++++++- .../0006_stockitem_purchase_order.py | 20 ++++ InvenTree/stock/models.py | 9 ++ InvenTree/stock/templates/stock/item.html | 6 ++ 8 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 InvenTree/order/migrations/0011_auto_20190615_1928.py create mode 100644 InvenTree/stock/migrations/0006_stockitem_purchase_order.py diff --git a/InvenTree/order/migrations/0011_auto_20190615_1928.py b/InvenTree/order/migrations/0011_auto_20190615_1928.py new file mode 100644 index 0000000000..cf6f1f61dc --- /dev/null +++ b/InvenTree/order/migrations/0011_auto_20190615_1928.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.2 on 2019-06-15 09:28 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('order', '0010_purchaseorderlineitem_notes'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorder', + name='complete_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='purchaseorder', + name='received_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 4cb58f401b..a8d8b12542 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -4,7 +4,7 @@ Order model definitions # -*- coding: utf-8 -*- -from django.db import models +from django.db import models, transaction from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError from django.contrib.auth.models import User @@ -14,6 +14,7 @@ from django.utils.translation import ugettext as _ import tablib from datetime import datetime +from stock.models import StockItem from company.models import Company, SupplierPart from InvenTree.status_codes import OrderStatus @@ -33,6 +34,7 @@ class Order(models.Model): creation_date: Automatic date of order creation created_by: User who created this order (automatically captured) issue_date: Date the order was issued + complete_date: Date the order was completed """ @@ -70,6 +72,8 @@ class Order(models.Model): issue_date = models.DateField(blank=True, null=True) + complete_date = models.DateField(blank=True, null=True) + notes = models.TextField(blank=True, help_text=_('Order notes')) def place_order(self): @@ -80,13 +84,21 @@ class Order(models.Model): self.issue_date = datetime.now().date() self.save() + def complete_order(self): + """ Marks the order as COMPLETE. Order must be currently PLACED. """ + + if self.status == OrderStatus.PLACED: + self.status = OrderStatus.COMPLETE + self.complete_date = datetime.now().date() + self.save() + class PurchaseOrder(Order): """ A PurchaseOrder represents goods shipped inwards from an external supplier. Attributes: supplier: Reference to the company supplying the goods in the order - + received_by: User that received the goods """ ORDER_PREFIX = "PO" @@ -100,6 +112,12 @@ class PurchaseOrder(Order): help_text=_('Company') ) + received_by = models.ForeignKey(User, + on_delete=models.SET_NULL, + blank=True, null=True, + related_name='+' + ) + def export_to_file(self, **kwargs): """ Export order information to external file """ @@ -151,6 +169,7 @@ class PurchaseOrder(Order): def get_absolute_url(self): return reverse('purchase-order-detail', kwargs={'pk': self.id}) + @transaction.atomic def add_line_item(self, supplier_part, quantity, group=True, reference=''): """ Add a new line item to this purchase order. This function will check that: @@ -202,6 +221,38 @@ class PurchaseOrder(Order): return [line for line in self.lines.all() if line.quantity > line.received] + @transaction.atomic + def receive_line_item(self, line, location, quantity, user): + """ Receive a line item (or partial line item) against this PO + """ + + # Create a new stock item + if line.part: + stock = StockItem( + part=line.part.part, + location=location, + quantity=quantity, + purchase_order=self) + + stock.save() + + # Add a new transaction note to the newly created stock item + stock.addTransactionNote("Received items", user, "Received {q} items against order '{po}'".format( + q=line.receive_quantity, + po=str(self)) + ) + + + # Update the number of parts received against the particular line item + line.received += quantity + line.save() + + # Has this order been completed? + if len(self.pending_line_items()) == 0: + + self.received_by = user + self.complete_order() # This will save the model + class OrderLineItem(models.Model): """ Abstract model for an order line item diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index f663df629e..68370c9c22 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -41,11 +41,7 @@ InvenTree | {{ order }} Created - {{ order.creation_date }} - - - Created By - {{ order.created_by }} + {{ order.creation_date }}{{ order.created_by }} {% if order.issue_date %} @@ -53,6 +49,12 @@ InvenTree | {{ order }} {{ order.issue_date }} {% endif %} + {% if order.status == OrderStatus.COMPLETE %} + + Received + {{ order.complete_date }}{{ order.received_by }} + + {% endif %} diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html index 42c104e274..e2d57e9c2a 100644 --- a/InvenTree/order/templates/order/receive_parts.html +++ b/InvenTree/order/templates/order/receive_parts.html @@ -2,12 +2,32 @@ {% block form %} -

Receive parts for {{ order }}

+Receive outstanding parts for {{ order }} - {{ order.description }}
{% csrf_token %} {% load crispy_forms_tags %} +
+ +
+ + {% if not destination %} + Select location to receive parts + {% else %} +

Location of received parts

+ {% endif %} +
+
+ + +

Select parts to receive against this order.

+ @@ -32,7 +52,7 @@ diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 48fb466367..6e2cdf4a98 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -5,6 +5,7 @@ Django views for interacting with Order app # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.db import transaction from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ from django.views.generic import DetailView, ListView @@ -15,7 +16,7 @@ import logging from .models import PurchaseOrder, PurchaseOrderLineItem from build.models import Build from company.models import Company, SupplierPart -from stock.models import StockItem +from stock.models import StockItem, StockLocation from part.models import Part from . import forms as order_forms @@ -176,24 +177,113 @@ class PurchaseOrderReceive(AjaxView): ajax_form_title = "Receive Parts" ajax_template_name = "order/receive_parts.html" + # Where the parts will be going (selected in POST request) + destination = None + def get_context_data(self): ctx = { 'order': self.order, 'lines': self.lines, + 'locations': StockLocation.objects.all(), + 'destination': self.destination, } return ctx 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. + """ self.request = request self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) self.lines = self.order.pending_line_items() + for line in self.lines: + # Pre-fill the remaining quantity + line.receive_quantity = line.remaining() + return self.renderJsonResponse(request) + def post(self, request, *args, **kwargs): + """ Respond to a POST request. Data checking and error handling. + If the request is valid, new StockItem objects will be made + for each received item. + """ + + self.request = request + self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) + + self.lines = [] + self.destination = None + + # Extract the destination for received parts + if 'receive_location' in request.POST: + pk = request.POST['receive_location'] + try: + self.destination = StockLocation.objects.get(id=pk) + except (StockLocation.DoesNotExist, ValueError): + pass + + errors = self.destination is None + + # Extract information on all submitted line items + for item in request.POST: + if item.startswith('line-'): + pk = item.replace('line-', '') + + try: + line = PurchaseOrderLineItem.objects.get(id=pk) + except (PurchaseOrderLineItem.DoesNotExist, ValueError): + continue + + # Ignore a part that doesn't map to a SupplierPart + try: + if line.part is None: + continue + except SupplierPart.DoesNotExist: + continue + + receive = self.request.POST[item] + + try: + receive = int(receive) + except ValueError: + # In the case on an invalid input, reset to default + receive = line.remaining() + errors = True + + if receive < 0: + receive = 0 + errors = True + + line.receive_quantity = receive + self.lines.append(line) + + # No errors? Receive the submitted parts! + if errors is False: + self.receive_parts() + + data = { + 'form_valid': errors is False, + 'success': 'Items marked as received', + } + + return self.renderJsonResponse(request, data=data) + + def receive_parts(self): + """ Called once the form has been validated. + Create new stockitems against received parts. + """ + + for line in self.lines: + + if not line.part: + continue + + self.order.receive_line_item(line, self.destination, line.receive_quantity, self.request.user) class OrderParts(AjaxView): diff --git a/InvenTree/stock/migrations/0006_stockitem_purchase_order.py b/InvenTree/stock/migrations/0006_stockitem_purchase_order.py new file mode 100644 index 0000000000..08d7f4de01 --- /dev/null +++ b/InvenTree/stock/migrations/0006_stockitem_purchase_order.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.2 on 2019-06-15 09:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0010_purchaseorderlineitem_notes'), + ('stock', '0005_auto_20190602_1944'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='purchase_order', + field=models.ForeignKey(blank=True, help_text='Purchase order for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.PurchaseOrder'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 50a0f39e72..a3a0c1d6ef 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -96,6 +96,7 @@ class StockItem(models.Model): delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus) notes: Extra notes field + purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted """ @@ -249,6 +250,14 @@ class StockItem(models.Model): updated = models.DateField(auto_now=True, null=True) + purchase_order = models.ForeignKey( + 'order.PurchaseOrder', + on_delete=models.SET_NULL, + related_name='stock_items', + blank=True, null=True, + help_text='Purchase order for this stock item' + ) + # last time the stock was checked / counted stocktake_date = models.DateField(blank=True, null=True) diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 40d78eb7e1..7300855389 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -71,6 +71,12 @@ {% endif %} + {% if item.purchase_order %} + + + + + {% endif %} {% if item.customer %}
Part
- +
{{ item.batch }}
Purchase Order{{ item.purchase_order }}
Customer