mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #522 from SchrodingersGat/order-improvements
Order improvements
This commit is contained in:
commit
9e1f56cdb8
@ -15,6 +15,13 @@
|
||||
supplier: 1
|
||||
SKU: 'ACME0002'
|
||||
|
||||
- model: company.supplierpart
|
||||
pk: 3
|
||||
fields:
|
||||
part: 1
|
||||
supplier: 1
|
||||
SKU: 'ACME0003'
|
||||
|
||||
# Widget purchaseable from ACME
|
||||
- model: company.supplierpart
|
||||
pk: 100
|
||||
@ -33,7 +40,7 @@
|
||||
|
||||
# M2x4 LPHS from Zerg Corp
|
||||
- model: company.supplierpart
|
||||
pk: 3
|
||||
pk: 7
|
||||
fields:
|
||||
part: 1
|
||||
supplier: 3
|
||||
|
@ -56,7 +56,7 @@ class CompanySimpleTest(TestCase):
|
||||
zerg = Company.objects.get(pk=3)
|
||||
|
||||
self.assertTrue(acme.has_parts)
|
||||
self.assertEqual(acme.part_count, 3)
|
||||
self.assertEqual(acme.part_count, 4)
|
||||
|
||||
self.assertTrue(appel.has_parts)
|
||||
self.assertEqual(appel.part_count, 2)
|
||||
|
@ -35,8 +35,16 @@
|
||||
quantity: 250
|
||||
received: 50
|
||||
|
||||
# 1000 x ACME0003
|
||||
- model: order.purchaseorderlineitem
|
||||
fields:
|
||||
order: 1
|
||||
part: 3
|
||||
quantity: 1000
|
||||
|
||||
# 100 x ZERGLPHS (M2x4 LPHS)
|
||||
- model: order.purchaseorderlineitem
|
||||
pk: 22
|
||||
fields:
|
||||
order: 2
|
||||
part: 3
|
||||
|
@ -5,6 +5,7 @@ Order model definitions
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db.models import F
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
@ -226,7 +227,7 @@ class PurchaseOrder(Order):
|
||||
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
|
||||
def receive_line_item(self, line, location, quantity, user):
|
||||
|
@ -4,6 +4,12 @@ from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from InvenTree.status_codes import OrderStatus
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class OrderViewTestCase(TestCase):
|
||||
|
||||
@ -75,3 +81,106 @@ class POTests(OrderViewTestCase):
|
||||
|
||||
# Response should be streaming-content (file download)
|
||||
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
|
||||
|
@ -47,10 +47,10 @@ class OrderTest(TestCase):
|
||||
for supplier in part.supplier_parts.all():
|
||||
open_orders += supplier.open_orders()
|
||||
|
||||
self.assertEqual(len(open_orders), 3)
|
||||
self.assertEqual(len(open_orders), 4)
|
||||
|
||||
# Test the total on-order quantity
|
||||
self.assertEqual(part.on_order, 400)
|
||||
self.assertEqual(part.on_order, 1400)
|
||||
|
||||
def test_add_items(self):
|
||||
""" Test functions for adding line items to an order """
|
||||
@ -58,7 +58,7 @@ class OrderTest(TestCase):
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
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')
|
||||
part = sku.part
|
||||
@ -76,11 +76,11 @@ class OrderTest(TestCase):
|
||||
order.add_line_item(sku, 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.add_line_item(sku, 50)
|
||||
self.assertEqual(order.lines.count(), 3)
|
||||
self.assertEqual(order.lines.count(), 4)
|
||||
self.assertEqual(part.on_order, 150)
|
||||
|
||||
# Try to order a supplier part from the wrong supplier
|
||||
@ -101,7 +101,7 @@ class OrderTest(TestCase):
|
||||
loc = StockLocation.objects.get(id=1)
|
||||
|
||||
# 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"
|
||||
self.assertEqual(order.status, OrderStatus.PENDING)
|
||||
@ -117,7 +117,7 @@ class OrderTest(TestCase):
|
||||
|
||||
self.assertEqual(line.remaining(), 50)
|
||||
|
||||
self.assertEqual(part.on_order, 350)
|
||||
self.assertEqual(part.on_order, 1350)
|
||||
|
||||
# Try to order some invalid things
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
@ -132,7 +132,12 @@ class OrderTest(TestCase):
|
||||
line = PurchaseOrderLineItem.objects.get(id=2)
|
||||
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)
|
||||
|
||||
def test_export(self):
|
||||
|
@ -243,6 +243,11 @@ class PurchaseOrderReceive(AjaxView):
|
||||
except (PurchaseOrderLineItem.DoesNotExist, ValueError):
|
||||
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
|
||||
try:
|
||||
if line.part is None:
|
||||
@ -277,6 +282,7 @@ class PurchaseOrderReceive(AjaxView):
|
||||
|
||||
return self.renderJsonResponse(request, data=data)
|
||||
|
||||
@transaction.atomic
|
||||
def receive_parts(self):
|
||||
""" Called once the form has been validated.
|
||||
Create new stockitems against received parts.
|
||||
@ -611,13 +617,30 @@ class POLineItemCreate(AjaxCreateView):
|
||||
|
||||
valid = form.is_valid()
|
||||
|
||||
# Extract the SupplierPart ID from the form
|
||||
part_id = form['part'].value()
|
||||
|
||||
# Extract the Order ID from the form
|
||||
order_id = form['order'].value()
|
||||
|
||||
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):
|
||||
valid = False
|
||||
form.errors['part'] = [_('This field is required')]
|
||||
form.errors['part'] = [_('Invalid SupplierPart selection')]
|
||||
|
||||
data = {
|
||||
'form_valid': valid,
|
||||
@ -639,6 +662,11 @@ class POLineItemCreate(AjaxCreateView):
|
||||
|
||||
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()
|
||||
|
||||
try:
|
||||
@ -660,7 +688,7 @@ class POLineItemCreate(AjaxCreateView):
|
||||
|
||||
form.fields['part'].queryset = query
|
||||
form.fields['order'].widget = HiddenInput()
|
||||
except PurchaseOrder.DoesNotExist:
|
||||
except (ValueError, PurchaseOrder.DoesNotExist):
|
||||
pass
|
||||
|
||||
return form
|
||||
|
Loading…
Reference in New Issue
Block a user