diff --git a/InvenTree/company/fixtures/supplier_part.yaml b/InvenTree/company/fixtures/supplier_part.yaml index b9649704e1..446339d58b 100644 --- a/InvenTree/company/fixtures/supplier_part.yaml +++ b/InvenTree/company/fixtures/supplier_part.yaml @@ -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 diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index b4baec2976..7037170862 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -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) diff --git a/InvenTree/order/fixtures/order.yaml b/InvenTree/order/fixtures/order.yaml index 6fab8a705d..0ba4bdbeb5 100644 --- a/InvenTree/order/fixtures/order.yaml +++ b/InvenTree/order/fixtures/order.yaml @@ -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 diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 306b748a79..ef79a4d39b 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -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): diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index a197455a8a..9ef3c80276 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -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 diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 3a773ad0e9..c271e0be43 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -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): diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 25d0dddb52..fafd313022 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -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