Implement POST for receiving items

- Create new StockItem in the correct location
This commit is contained in:
Oliver Walters 2019-06-15 19:39:57 +10:00
parent c7ca9a3d8f
commit 1290e7f289
8 changed files with 234 additions and 10 deletions

View File

@ -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),
),
]

View File

@ -4,7 +4,7 @@ Order model definitions
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models from django.db import models, transaction
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -14,6 +14,7 @@ from django.utils.translation import ugettext as _
import tablib import tablib
from datetime import datetime from datetime import datetime
from stock.models import StockItem
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from InvenTree.status_codes import OrderStatus from InvenTree.status_codes import OrderStatus
@ -33,6 +34,7 @@ class Order(models.Model):
creation_date: Automatic date of order creation creation_date: Automatic date of order creation
created_by: User who created this order (automatically captured) created_by: User who created this order (automatically captured)
issue_date: Date the order was issued 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) 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')) notes = models.TextField(blank=True, help_text=_('Order notes'))
def place_order(self): def place_order(self):
@ -80,13 +84,21 @@ class Order(models.Model):
self.issue_date = datetime.now().date() self.issue_date = datetime.now().date()
self.save() 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): class PurchaseOrder(Order):
""" A PurchaseOrder represents goods shipped inwards from an external supplier. """ A PurchaseOrder represents goods shipped inwards from an external supplier.
Attributes: Attributes:
supplier: Reference to the company supplying the goods in the order supplier: Reference to the company supplying the goods in the order
received_by: User that received the goods
""" """
ORDER_PREFIX = "PO" ORDER_PREFIX = "PO"
@ -100,6 +112,12 @@ class PurchaseOrder(Order):
help_text=_('Company') 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): def export_to_file(self, **kwargs):
""" Export order information to external file """ """ Export order information to external file """
@ -151,6 +169,7 @@ class PurchaseOrder(Order):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('purchase-order-detail', kwargs={'pk': self.id}) return reverse('purchase-order-detail', kwargs={'pk': self.id})
@transaction.atomic
def add_line_item(self, supplier_part, quantity, group=True, reference=''): def add_line_item(self, supplier_part, quantity, group=True, reference=''):
""" Add a new line item to this purchase order. """ Add a new line item to this purchase order.
This function will check that: 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] 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): class OrderLineItem(models.Model):
""" Abstract model for an order line item """ Abstract model for an order line item

View File

@ -41,11 +41,7 @@ InvenTree | {{ order }}
</tr> </tr>
<tr> <tr>
<td>Created</td> <td>Created</td>
<td>{{ order.creation_date }}</td> <td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
</tr>
<tr>
<td>Created By</td>
<td>{{ order.created_by }}</td>
</tr> </tr>
{% if order.issue_date %} {% if order.issue_date %}
<tr> <tr>
@ -53,6 +49,12 @@ InvenTree | {{ order }}
<td>{{ order.issue_date }}</td> <td>{{ order.issue_date }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if order.status == OrderStatus.COMPLETE %}
<tr>
<td>Received</td>
<td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td>
</tr>
{% endif %}
</table> </table>
</div> </div>
</div> </div>

View File

@ -2,12 +2,32 @@
{% block form %} {% block form %}
<h4>Receive parts for {{ order }}</h4> Receive outstanding parts for <b>{{ order }}</b> - <i>{{ order.description }}</i>
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'> <form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
{% csrf_token %} {% csrf_token %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
<div class='control-group'>
<label class='control-label requiredField'>Location</label>
<div class='controls'>
<select class='select' name='receive_location'>
<option value=''>---------</option>
{% for loc in locations %}
<option value='{{ loc.id }}' {% if destination.id == loc.id %}selected='selected'{% endif %}>{{ loc.pathstring }} - {{ loc.description }}</option>
{% endfor %}
</select>
{% if not destination %}
<span class='help-inline'>Select location to receive parts</span>
{% else %}
<p class='help-block'>Location of received parts</p>
{% endif %}
</div>
</div>
<label class='control-label'>Parts</label>
<p class='help-block'>Select parts to receive against this order.</p>
<table class='table table-striped'> <table class='table table-striped'>
<tr> <tr>
<th>Part</th> <th>Part</th>
@ -32,7 +52,7 @@
<td> <td>
<div class='control-group'> <div class='control-group'>
<div class='controls'> <div class='controls'>
<input class='numberinput' type='number' min='0' value='{{ line.remaining }}' name='receive-quantity-{{ line.id }}'/> <input class='numberinput' type='number' min='0' value='{{ line.receive_quantity }}' name='line-{{ line.id }}'/>
</div> </div>
</div> </div>
</td> </td>

View File

@ -5,6 +5,7 @@ Django views for interacting with Order app
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import transaction
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
@ -15,7 +16,7 @@ import logging
from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrder, PurchaseOrderLineItem
from build.models import Build from build.models import Build
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from stock.models import StockItem from stock.models import StockItem, StockLocation
from part.models import Part from part.models import Part
from . import forms as order_forms from . import forms as order_forms
@ -176,24 +177,113 @@ class PurchaseOrderReceive(AjaxView):
ajax_form_title = "Receive Parts" ajax_form_title = "Receive Parts"
ajax_template_name = "order/receive_parts.html" ajax_template_name = "order/receive_parts.html"
# Where the parts will be going (selected in POST request)
destination = None
def get_context_data(self): def get_context_data(self):
ctx = { ctx = {
'order': self.order, 'order': self.order,
'lines': self.lines, 'lines': self.lines,
'locations': StockLocation.objects.all(),
'destination': self.destination,
} }
return ctx return ctx
def get(self, request, *args, **kwargs): 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.request = request
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
self.lines = self.order.pending_line_items() 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) 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): class OrderParts(AjaxView):

View File

@ -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'),
),
]

View File

@ -96,6 +96,7 @@ class StockItem(models.Model):
delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero 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) status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus)
notes: Extra notes field 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 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) 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 # last time the stock was checked / counted
stocktake_date = models.DateField(blank=True, null=True) stocktake_date = models.DateField(blank=True, null=True)

View File

@ -71,6 +71,12 @@
<td>{{ item.batch }}</td> <td>{{ item.batch }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if item.purchase_order %}
<tr>
<td>Purchase Order</td>
<td><a href="{% url 'purchase-order-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td>
</tr>
{% endif %}
{% if item.customer %} {% if item.customer %}
<tr> <tr>
<td>Customer</td> <td>Customer</td>