Merge pull request #522 from SchrodingersGat/order-improvements

Order improvements
This commit is contained in:
Oliver 2019-09-13 21:16:47 +10:00 committed by GitHub
commit 9e1f56cdb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 14 deletions

View File

@ -15,6 +15,13 @@
supplier: 1 supplier: 1
SKU: 'ACME0002' SKU: 'ACME0002'
- model: company.supplierpart
pk: 3
fields:
part: 1
supplier: 1
SKU: 'ACME0003'
# Widget purchaseable from ACME # Widget purchaseable from ACME
- model: company.supplierpart - model: company.supplierpart
pk: 100 pk: 100
@ -33,7 +40,7 @@
# M2x4 LPHS from Zerg Corp # M2x4 LPHS from Zerg Corp
- model: company.supplierpart - model: company.supplierpart
pk: 3 pk: 7
fields: fields:
part: 1 part: 1
supplier: 3 supplier: 3

View File

@ -56,7 +56,7 @@ class CompanySimpleTest(TestCase):
zerg = Company.objects.get(pk=3) zerg = Company.objects.get(pk=3)
self.assertTrue(acme.has_parts) self.assertTrue(acme.has_parts)
self.assertEqual(acme.part_count, 3) self.assertEqual(acme.part_count, 4)
self.assertTrue(appel.has_parts) self.assertTrue(appel.has_parts)
self.assertEqual(appel.part_count, 2) self.assertEqual(appel.part_count, 2)

View File

@ -35,8 +35,16 @@
quantity: 250 quantity: 250
received: 50 received: 50
# 1000 x ACME0003
- model: order.purchaseorderlineitem
fields:
order: 1
part: 3
quantity: 1000
# 100 x ZERGLPHS (M2x4 LPHS) # 100 x ZERGLPHS (M2x4 LPHS)
- model: order.purchaseorderlineitem - model: order.purchaseorderlineitem
pk: 22
fields: fields:
order: 2 order: 2
part: 3 part: 3

View File

@ -5,6 +5,7 @@ Order model definitions
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F
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
@ -226,7 +227,7 @@ class PurchaseOrder(Order):
Any line item where 'received' < 'quantity' will be returned. Any line item where 'received' < 'quantity' will be returned.
""" """
return [line for line in self.lines.all() if line.quantity > line.received] return self.lines.filter(quantity__gt=F('received'))
@transaction.atomic @transaction.atomic
def receive_line_item(self, line, location, quantity, user): def receive_line_item(self, line, location, quantity, user):

View File

@ -4,6 +4,12 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from InvenTree.status_codes import OrderStatus
from .models import PurchaseOrder, PurchaseOrderLineItem
import json
class OrderViewTestCase(TestCase): class OrderViewTestCase(TestCase):
@ -75,3 +81,106 @@ class POTests(OrderViewTestCase):
# Response should be streaming-content (file download) # Response should be streaming-content (file download)
self.assertIn('streaming_content', dir(response)) self.assertIn('streaming_content', dir(response))
def test_po_issue(self):
""" Test PurchaseOrderIssue view """
url = reverse('purchase-order-issue', args=(1,))
order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.status, OrderStatus.PENDING)
# Test without confirmation
response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
# Test WITH confirmation
response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertTrue(data['form_valid'])
# Test that the order was actually placed
order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.status, OrderStatus.PLACED)
def test_line_item_create(self):
""" Test the form for adding a new LineItem to a PurchaseOrder """
# Record the number of line items in the PurchaseOrder
po = PurchaseOrder.objects.get(pk=1)
n = po.lines.count()
self.assertEqual(po.status, OrderStatus.PENDING)
url = reverse('po-line-item-create')
# GET the form (pass the correct info)
response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
post_data = {
'part': 100,
'quantity': 45,
'reference': 'Test reference field',
'notes': 'Test notes field'
}
# POST with an invalid purchase order
post_data['order'] = 99
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
self.assertIn('Invalid Purchase Order', str(data['html_form']))
# POST with a part that does not match the purchase order
post_data['order'] = 1
post_data['part'] = 7
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
self.assertIn('must match for Part and Order', str(data['html_form']))
# POST with an invalid part
post_data['part'] = 12345
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
data = json.loads(response.content)
self.assertFalse(data['form_valid'])
self.assertIn('Invalid SupplierPart selection', str(data['html_form']))
# POST the form with valid data
post_data['part'] = 100
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertTrue(data['form_valid'])
self.assertEqual(n + 1, PurchaseOrder.objects.get(pk=1).lines.count())
line = PurchaseOrderLineItem.objects.get(order=1, part=100)
self.assertEqual(line.quantity, 45)
def test_line_item_edit(self):
""" Test editing form for PO line item """
url = reverse('po-line-item-edit', args=(22,))
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
class TestPOReceive(OrderViewTestCase):
""" Tests for receiving a purchase order """
def setUp(self):
super().setUp()
self.po = PurchaseOrder.objects.get(pk=1)
self.url = reverse('purchase-order-receive', args=(1,))
def test_receive_lines(self):
# TODO
pass

View File

@ -47,10 +47,10 @@ class OrderTest(TestCase):
for supplier in part.supplier_parts.all(): for supplier in part.supplier_parts.all():
open_orders += supplier.open_orders() open_orders += supplier.open_orders()
self.assertEqual(len(open_orders), 3) self.assertEqual(len(open_orders), 4)
# Test the total on-order quantity # Test the total on-order quantity
self.assertEqual(part.on_order, 400) self.assertEqual(part.on_order, 1400)
def test_add_items(self): def test_add_items(self):
""" Test functions for adding line items to an order """ """ Test functions for adding line items to an order """
@ -58,7 +58,7 @@ class OrderTest(TestCase):
order = PurchaseOrder.objects.get(pk=1) order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.status, OrderStatus.PENDING) self.assertEqual(order.status, OrderStatus.PENDING)
self.assertEqual(order.lines.count(), 2) self.assertEqual(order.lines.count(), 3)
sku = SupplierPart.objects.get(SKU='ACME-WIDGET') sku = SupplierPart.objects.get(SKU='ACME-WIDGET')
part = sku.part part = sku.part
@ -76,11 +76,11 @@ class OrderTest(TestCase):
order.add_line_item(sku, 100) order.add_line_item(sku, 100)
self.assertEqual(part.on_order, 100) self.assertEqual(part.on_order, 100)
self.assertEqual(order.lines.count(), 3) self.assertEqual(order.lines.count(), 4)
# Order the same part again (it should be merged) # Order the same part again (it should be merged)
order.add_line_item(sku, 50) order.add_line_item(sku, 50)
self.assertEqual(order.lines.count(), 3) self.assertEqual(order.lines.count(), 4)
self.assertEqual(part.on_order, 150) self.assertEqual(part.on_order, 150)
# Try to order a supplier part from the wrong supplier # Try to order a supplier part from the wrong supplier
@ -101,7 +101,7 @@ class OrderTest(TestCase):
loc = StockLocation.objects.get(id=1) loc = StockLocation.objects.get(id=1)
# There should be two lines against this order # There should be two lines against this order
self.assertEqual(len(order.pending_line_items()), 2) self.assertEqual(len(order.pending_line_items()), 3)
# Should fail, as order is 'PENDING' not 'PLACED" # Should fail, as order is 'PENDING' not 'PLACED"
self.assertEqual(order.status, OrderStatus.PENDING) self.assertEqual(order.status, OrderStatus.PENDING)
@ -117,7 +117,7 @@ class OrderTest(TestCase):
self.assertEqual(line.remaining(), 50) self.assertEqual(line.remaining(), 50)
self.assertEqual(part.on_order, 350) self.assertEqual(part.on_order, 1350)
# Try to order some invalid things # Try to order some invalid things
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
@ -132,7 +132,12 @@ class OrderTest(TestCase):
line = PurchaseOrderLineItem.objects.get(id=2) line = PurchaseOrderLineItem.objects.get(id=2)
order.receive_line_item(line, loc, 2 * line.quantity, user=None) order.receive_line_item(line, loc, 2 * line.quantity, user=None)
self.assertEqual(part.on_order, 100) self.assertEqual(part.on_order, 1100)
self.assertEqual(order.status, OrderStatus.PLACED)
for line in order.pending_line_items():
order.receive_line_item(line, loc, line.quantity, user=None)
self.assertEqual(order.status, OrderStatus.COMPLETE) self.assertEqual(order.status, OrderStatus.COMPLETE)
def test_export(self): def test_export(self):

View File

@ -243,6 +243,11 @@ class PurchaseOrderReceive(AjaxView):
except (PurchaseOrderLineItem.DoesNotExist, ValueError): except (PurchaseOrderLineItem.DoesNotExist, ValueError):
continue continue
# Check that line matches the order
if not line.order == self.order:
# TODO - Display a non-field error?
continue
# Ignore a part that doesn't map to a SupplierPart # Ignore a part that doesn't map to a SupplierPart
try: try:
if line.part is None: if line.part is None:
@ -277,6 +282,7 @@ class PurchaseOrderReceive(AjaxView):
return self.renderJsonResponse(request, data=data) return self.renderJsonResponse(request, data=data)
@transaction.atomic
def receive_parts(self): def receive_parts(self):
""" Called once the form has been validated. """ Called once the form has been validated.
Create new stockitems against received parts. Create new stockitems against received parts.
@ -611,13 +617,30 @@ class POLineItemCreate(AjaxCreateView):
valid = form.is_valid() valid = form.is_valid()
# Extract the SupplierPart ID from the form
part_id = form['part'].value() part_id = form['part'].value()
# Extract the Order ID from the form
order_id = form['order'].value()
try: try:
SupplierPart.objects.get(id=part_id) order = PurchaseOrder.objects.get(id=order_id)
except (ValueError, PurchaseOrder.DoesNotExist):
order = None
form.errors['order'] = [_('Invalid Purchase Order')]
valid = False
try:
sp = SupplierPart.objects.get(id=part_id)
if order is not None:
if not sp.supplier == order.supplier:
form.errors['part'] = [_('Supplier must match for Part and Order')]
valid = False
except (SupplierPart.DoesNotExist, ValueError): except (SupplierPart.DoesNotExist, ValueError):
valid = False valid = False
form.errors['part'] = [_('This field is required')] form.errors['part'] = [_('Invalid SupplierPart selection')]
data = { data = {
'form_valid': valid, 'form_valid': valid,
@ -639,6 +662,11 @@ class POLineItemCreate(AjaxCreateView):
form = super().get_form() form = super().get_form()
# Limit the available to orders to ones that are PENDING
query = form.fields['order'].queryset
query = query.filter(status=OrderStatus.PENDING)
form.fields['order'].queryset = query
order_id = form['order'].value() order_id = form['order'].value()
try: try:
@ -660,7 +688,7 @@ class POLineItemCreate(AjaxCreateView):
form.fields['part'].queryset = query form.fields['part'].queryset = query
form.fields['order'].widget = HiddenInput() form.fields['order'].widget = HiddenInput()
except PurchaseOrder.DoesNotExist: except (ValueError, PurchaseOrder.DoesNotExist):
pass pass
return form return form