mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 03:26:45 +00:00
* Updated all dependencies * fixed some spelling errors * More spelling corrections * Fixes for changes in react-query
2289 lines
70 KiB
Python
2289 lines
70 KiB
Python
"""Tests for the Order API."""
|
|
|
|
import base64
|
|
import io
|
|
from datetime import datetime, timedelta
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import connection
|
|
from django.test.utils import CaptureQueriesContext
|
|
from django.urls import reverse
|
|
|
|
from djmoney.money import Money
|
|
from icalendar import Calendar
|
|
from rest_framework import status
|
|
|
|
from common.settings import currency_codes
|
|
from company.models import Company
|
|
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
|
ReturnOrderStatus, SalesOrderStatus,
|
|
SalesOrderStatusGroups, StockStatus)
|
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
|
from order import models
|
|
from part.models import Part
|
|
from stock.models import StockItem
|
|
|
|
|
|
class OrderTest(InvenTreeAPITestCase):
|
|
"""Base class for order API unit testing"""
|
|
fixtures = [
|
|
'category',
|
|
'part',
|
|
'company',
|
|
'location',
|
|
'supplier_part',
|
|
'stock',
|
|
'order',
|
|
'sales_order',
|
|
]
|
|
|
|
roles = [
|
|
'purchase_order.change',
|
|
'sales_order.change',
|
|
]
|
|
|
|
def filter(self, filters, count):
|
|
"""Test API filters."""
|
|
response = self.get(
|
|
self.LIST_URL,
|
|
filters
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(len(response.data), count)
|
|
|
|
return response
|
|
|
|
|
|
class PurchaseOrderTest(OrderTest):
|
|
"""Tests for the PurchaseOrder API."""
|
|
|
|
LIST_URL = reverse('api-po-list')
|
|
|
|
def test_options(self):
|
|
"""Test the PurchaseOrder OPTIONS endpoint."""
|
|
|
|
self.assignRole('purchase_order.add')
|
|
|
|
response = self.options(self.LIST_URL, expected_code=200)
|
|
|
|
data = response.data
|
|
self.assertEqual(data['name'], 'Purchase Order List')
|
|
|
|
post = data['actions']['POST']
|
|
|
|
def check_options(data, field_name, spec):
|
|
"""Helper function to check that the options are configured correctly."""
|
|
field_data = data[field_name]
|
|
|
|
for k, v in spec.items():
|
|
self.assertIn(k, field_data)
|
|
self.assertEqual(field_data[k], v)
|
|
|
|
# Checks for the 'order_currency' field
|
|
check_options(post, 'order_currency', {
|
|
'type': 'choice',
|
|
'required': False,
|
|
'read_only': False,
|
|
'label': 'Order Currency',
|
|
'help_text': 'Currency for this order (leave blank to use company default)',
|
|
})
|
|
|
|
# Checks for the 'reference' field
|
|
check_options(post, 'reference', {
|
|
'type': 'string',
|
|
'required': True,
|
|
'read_only': False,
|
|
'label': 'Reference',
|
|
})
|
|
|
|
# Checks for the 'supplier' field
|
|
check_options(post, 'supplier', {
|
|
'type': 'related field',
|
|
'required': True,
|
|
'api_url': '/api/company/',
|
|
})
|
|
|
|
def test_po_list(self):
|
|
"""Test the PurchaseOrder list API endpoint"""
|
|
# List *ALL* PurchaseOrder items
|
|
self.filter({}, 7)
|
|
|
|
# Filter by assigned-to-me
|
|
self.filter({'assigned_to_me': 1}, 0)
|
|
self.filter({'assigned_to_me': 0}, 7)
|
|
|
|
# Filter by supplier
|
|
self.filter({'supplier': 1}, 1)
|
|
self.filter({'supplier': 3}, 5)
|
|
|
|
# Filter by "outstanding"
|
|
self.filter({'outstanding': True}, 5)
|
|
self.filter({'outstanding': False}, 2)
|
|
|
|
# Filter by "status"
|
|
self.filter({'status': 10}, 3)
|
|
self.filter({'status': 40}, 1)
|
|
|
|
# Filter by "reference"
|
|
self.filter({'reference': 'PO-0001'}, 1)
|
|
self.filter({'reference': 'PO-9999'}, 0)
|
|
|
|
# Filter by "assigned_to_me"
|
|
self.filter({'assigned_to_me': 1}, 0)
|
|
self.filter({'assigned_to_me': 0}, 7)
|
|
|
|
# Filter by "part"
|
|
self.filter({'part': 1}, 2)
|
|
self.filter({'part': 2}, 0) # Part not assigned to any PO
|
|
|
|
# Filter by "supplier_part"
|
|
self.filter({'supplier_part': 1}, 1)
|
|
self.filter({'supplier_part': 3}, 2)
|
|
self.filter({'supplier_part': 4}, 0)
|
|
|
|
def test_total_price(self):
|
|
"""Unit tests for the 'total_price' field"""
|
|
|
|
# Ensure we have exchange rate data
|
|
self.generate_exchange_rates()
|
|
|
|
currencies = currency_codes()
|
|
n = len(currencies)
|
|
|
|
idx = 0
|
|
|
|
new_orders = []
|
|
|
|
# Let's generate some more orders
|
|
for supplier in Company.objects.filter(is_supplier=True):
|
|
for _idx in range(10):
|
|
new_orders.append(
|
|
models.PurchaseOrder(
|
|
supplier=supplier,
|
|
reference=f'PO-{idx + 100}'
|
|
)
|
|
)
|
|
|
|
idx += 1
|
|
|
|
models.PurchaseOrder.objects.bulk_create(new_orders)
|
|
|
|
idx = 0
|
|
|
|
# Create some purchase order line items
|
|
lines = []
|
|
|
|
for po in models.PurchaseOrder.objects.all():
|
|
for sp in po.supplier.supplied_parts.all():
|
|
lines.append(
|
|
models.PurchaseOrderLineItem(
|
|
order=po,
|
|
part=sp,
|
|
quantity=idx + 1,
|
|
purchase_price=Money((idx + 1) / 10, currencies[idx % n]),
|
|
)
|
|
)
|
|
|
|
idx += 1
|
|
|
|
models.PurchaseOrderLineItem.objects.bulk_create(lines)
|
|
|
|
# List all purchase orders
|
|
for limit in [1, 5, 10, 100]:
|
|
with CaptureQueriesContext(connection) as ctx:
|
|
response = self.get(self.LIST_URL, data={'limit': limit}, expected_code=200)
|
|
|
|
# Total database queries must be below 15, independent of the number of results
|
|
self.assertLess(len(ctx), 15)
|
|
|
|
for result in response.data['results']:
|
|
self.assertIn('total_price', result)
|
|
self.assertIn('order_currency', result)
|
|
|
|
def test_overdue(self):
|
|
"""Test "overdue" status."""
|
|
self.filter({'overdue': True}, 0)
|
|
self.filter({'overdue': False}, 7)
|
|
|
|
order = models.PurchaseOrder.objects.get(pk=1)
|
|
order.target_date = datetime.now().date() - timedelta(days=10)
|
|
order.save()
|
|
|
|
self.filter({'overdue': True}, 1)
|
|
self.filter({'overdue': False}, 6)
|
|
|
|
def test_po_detail(self):
|
|
"""Test the PurchaseOrder detail API endpoint"""
|
|
url = '/api/order/po/1/'
|
|
|
|
response = self.get(url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
data = response.data
|
|
|
|
self.assertEqual(data['pk'], 1)
|
|
self.assertEqual(data['description'], 'Ordering some screws')
|
|
|
|
def test_po_reference(self):
|
|
"""Test that a reference with a too big / small reference is handled correctly."""
|
|
# get permissions
|
|
self.assignRole('purchase_order.add')
|
|
|
|
url = reverse('api-po-list')
|
|
huge_number = "PO-92233720368547758089999999999999999"
|
|
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'supplier': 1,
|
|
'reference': huge_number,
|
|
'description': 'PO created via the API',
|
|
},
|
|
expected_code=201,
|
|
)
|
|
|
|
order = models.PurchaseOrder.objects.get(pk=response.data['pk'])
|
|
|
|
self.assertEqual(order.reference, 'PO-92233720368547758089999999999999999')
|
|
self.assertEqual(order.reference_int, 0x7fffffff)
|
|
|
|
def test_po_attachments(self):
|
|
"""Test the list endpoint for the PurchaseOrderAttachment model"""
|
|
url = reverse('api-po-attachment-list')
|
|
|
|
response = self.get(url)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
def test_po_operations(self):
|
|
"""Test that we can create / edit and delete a PurchaseOrder via the API."""
|
|
n = models.PurchaseOrder.objects.count()
|
|
|
|
url = reverse('api-po-list')
|
|
|
|
# Initially we do not have "add" permission for the PurchaseOrder model,
|
|
# so this POST request should return 403
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'supplier': 1,
|
|
'reference': '123456789-xyz',
|
|
'description': 'PO created via the API',
|
|
},
|
|
expected_code=403
|
|
)
|
|
|
|
# And no new PurchaseOrder objects should have been created
|
|
self.assertEqual(models.PurchaseOrder.objects.count(), n)
|
|
|
|
# Ok, now let's give this user the correct permission
|
|
self.assignRole('purchase_order.add')
|
|
|
|
# Initially we do not have "add" permission for the PurchaseOrder model,
|
|
# so this POST request should return 403
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'supplier': 1,
|
|
'reference': 'PO-123456789',
|
|
'description': 'PO created via the API',
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
self.assertEqual(models.PurchaseOrder.objects.count(), n + 1)
|
|
|
|
pk = response.data['pk']
|
|
|
|
# Try to create a PurchaseOrder with identical reference (should fail!)
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'supplier': 1,
|
|
'reference': '123456789-xyz',
|
|
'description': 'A different description',
|
|
},
|
|
expected_code=400
|
|
)
|
|
|
|
self.assertEqual(models.PurchaseOrder.objects.count(), n + 1)
|
|
|
|
url = reverse('api-po-detail', kwargs={'pk': pk})
|
|
|
|
# Get detail info!
|
|
response = self.get(url)
|
|
self.assertEqual(response.data['pk'], pk)
|
|
self.assertEqual(response.data['reference'], 'PO-123456789')
|
|
|
|
# Try to alter (edit) the PurchaseOrder
|
|
response = self.patch(
|
|
url,
|
|
{
|
|
'reference': 'PO-12345',
|
|
},
|
|
expected_code=200
|
|
)
|
|
|
|
# Reference should have changed
|
|
self.assertEqual(response.data['reference'], 'PO-12345')
|
|
|
|
# Now, let's try to delete it!
|
|
# Initially, we do *not* have the required permission!
|
|
response = self.delete(url, expected_code=403)
|
|
|
|
# Now, add the "delete" permission!
|
|
self.assignRole("purchase_order.delete")
|
|
|
|
response = self.delete(url, expected_code=204)
|
|
|
|
# Number of PurchaseOrder objects should have decreased
|
|
self.assertEqual(models.PurchaseOrder.objects.count(), n)
|
|
|
|
# And if we try to access the detail view again, it has gone
|
|
response = self.get(url, expected_code=404)
|
|
|
|
def test_po_create(self):
|
|
"""Test that we can create a new PurchaseOrder via the API."""
|
|
self.assignRole('purchase_order.add')
|
|
|
|
self.post(
|
|
reverse('api-po-list'),
|
|
{
|
|
'reference': 'PO-12345678',
|
|
'supplier': 1,
|
|
'description': 'A test purchase order',
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
def test_po_duplicate(self):
|
|
"""Test that we can duplicate a PurchaseOrder via the API"""
|
|
|
|
self.assignRole('purchase_order.add')
|
|
|
|
po = models.PurchaseOrder.objects.get(pk=1)
|
|
|
|
self.assertTrue(po.lines.count() > 0)
|
|
|
|
lines = []
|
|
|
|
# Add some extra line items to this order
|
|
for idx in range(5):
|
|
lines.append(models.PurchaseOrderExtraLine(
|
|
order=po,
|
|
quantity=idx + 10,
|
|
reference='some reference',
|
|
))
|
|
|
|
# bulk create orders
|
|
models.PurchaseOrderExtraLine.objects.bulk_create(lines)
|
|
|
|
data = self.get(reverse('api-po-detail', kwargs={'pk': 1})).data
|
|
|
|
del data['pk']
|
|
del data['reference']
|
|
|
|
# Duplicate with non-existent PK to provoke error
|
|
data['duplicate_order'] = 10000001
|
|
data['duplicate_line_items'] = True
|
|
data['duplicate_extra_lines'] = False
|
|
|
|
data['reference'] = 'PO-9999'
|
|
|
|
# Duplicate via the API
|
|
response = self.post(
|
|
reverse('api-po-list'),
|
|
data,
|
|
expected_code=400
|
|
)
|
|
|
|
data['duplicate_order'] = 1
|
|
data['duplicate_line_items'] = True
|
|
data['duplicate_extra_lines'] = False
|
|
|
|
data['reference'] = 'PO-9999'
|
|
|
|
# Duplicate via the API
|
|
response = self.post(
|
|
reverse('api-po-list'),
|
|
data,
|
|
expected_code=201
|
|
)
|
|
|
|
# Order is for the same supplier
|
|
self.assertEqual(response.data['supplier'], po.supplier.pk)
|
|
|
|
po_dup = models.PurchaseOrder.objects.get(pk=response.data['pk'])
|
|
|
|
self.assertEqual(po_dup.extra_lines.count(), 0)
|
|
self.assertEqual(po_dup.lines.count(), po.lines.count())
|
|
|
|
data['reference'] = 'PO-9998'
|
|
data['duplicate_line_items'] = False
|
|
data['duplicate_extra_lines'] = True
|
|
|
|
response = self.post(
|
|
reverse('api-po-list'),
|
|
data,
|
|
expected_code=201,
|
|
)
|
|
|
|
po_dup = models.PurchaseOrder.objects.get(pk=response.data['pk'])
|
|
|
|
self.assertEqual(po_dup.extra_lines.count(), po.extra_lines.count())
|
|
self.assertEqual(po_dup.lines.count(), 0)
|
|
|
|
def test_po_cancel(self):
|
|
"""Test the PurchaseOrderCancel API endpoint."""
|
|
po = models.PurchaseOrder.objects.get(pk=1)
|
|
|
|
self.assertEqual(po.status, PurchaseOrderStatus.PENDING)
|
|
|
|
url = reverse('api-po-cancel', kwargs={'pk': po.pk})
|
|
|
|
# Try to cancel the PO, but without required permissions
|
|
self.post(url, {}, expected_code=403)
|
|
|
|
self.assignRole('purchase_order.add')
|
|
|
|
self.post(
|
|
url,
|
|
{},
|
|
expected_code=201,
|
|
)
|
|
|
|
po.refresh_from_db()
|
|
|
|
self.assertEqual(po.status, PurchaseOrderStatus.CANCELLED)
|
|
|
|
# Try to cancel again (should fail)
|
|
self.post(url, {}, expected_code=400)
|
|
|
|
def test_po_complete(self):
|
|
"""Test the PurchaseOrderComplete API endpoint."""
|
|
po = models.PurchaseOrder.objects.get(pk=3)
|
|
|
|
url = reverse('api-po-complete', kwargs={'pk': po.pk})
|
|
|
|
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
|
|
|
|
# Try to complete the PO, without required permissions
|
|
self.post(url, {}, expected_code=403)
|
|
|
|
self.assignRole('purchase_order.add')
|
|
|
|
# Should fail due to incomplete lines
|
|
response = self.post(url, {}, expected_code=400)
|
|
|
|
self.assertIn('Order has incomplete line items', str(response.data['accept_incomplete']))
|
|
|
|
# Post again, accepting incomplete line items
|
|
self.post(
|
|
url,
|
|
{
|
|
'accept_incomplete': True,
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
po.refresh_from_db()
|
|
|
|
self.assertEqual(po.status, PurchaseOrderStatus.COMPLETE)
|
|
|
|
def test_po_issue(self):
|
|
"""Test the PurchaseOrderIssue API endpoint."""
|
|
po = models.PurchaseOrder.objects.get(pk=2)
|
|
|
|
url = reverse('api-po-issue', kwargs={'pk': po.pk})
|
|
|
|
# Try to issue the PO, without required permissions
|
|
self.post(url, {}, expected_code=403)
|
|
|
|
self.assignRole('purchase_order.add')
|
|
|
|
self.post(url, {}, expected_code=201)
|
|
|
|
po.refresh_from_db()
|
|
|
|
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
|
|
|
|
def test_po_calendar(self):
|
|
"""Test the calendar export endpoint"""
|
|
|
|
# Create required purchase orders
|
|
self.assignRole('purchase_order.add')
|
|
|
|
for i in range(1, 9):
|
|
self.post(
|
|
reverse('api-po-list'),
|
|
{
|
|
'reference': f'PO-1100000{i}',
|
|
'supplier': 1,
|
|
'description': f'Calendar PO {i}',
|
|
'target_date': f'2024-12-{i:02d}',
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
# Get some of these orders with target date, complete or cancel them
|
|
for po in models.PurchaseOrder.objects.filter(target_date__isnull=False):
|
|
if po.reference in ['PO-11000001', 'PO-11000002', 'PO-11000003', 'PO-11000004']:
|
|
# Set issued status for these POs
|
|
self.post(
|
|
reverse('api-po-issue', kwargs={'pk': po.pk}),
|
|
{},
|
|
expected_code=201
|
|
)
|
|
|
|
if po.reference in ['PO-11000001', 'PO-11000002']:
|
|
# Set complete status for these POs
|
|
self.post(
|
|
reverse('api-po-complete', kwargs={'pk': po.pk}),
|
|
{
|
|
'accept_incomplete': True,
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
elif po.reference in ['PO-11000005', 'PO-11000006']:
|
|
# Set cancel status for these POs
|
|
self.post(
|
|
reverse('api-po-cancel', kwargs={'pk': po.pk}),
|
|
{
|
|
'accept_incomplete': True,
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
url = reverse('api-po-so-calendar', kwargs={'ordertype': 'purchase-order'})
|
|
|
|
# Test without completed orders
|
|
response = self.get(url, expected_code=200, format=None)
|
|
|
|
number_orders = len(models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE.value))
|
|
|
|
# Transform content to a Calendar object
|
|
calendar = Calendar.from_ical(response.content)
|
|
n_events = 0
|
|
# Count number of events in calendar
|
|
for component in calendar.walk():
|
|
if component.name == 'VEVENT':
|
|
n_events += 1
|
|
|
|
self.assertGreaterEqual(n_events, 1)
|
|
self.assertEqual(number_orders, n_events)
|
|
|
|
# Test with completed orders
|
|
response = self.get(url, data={'include_completed': 'True'}, expected_code=200, format=None)
|
|
|
|
number_orders_incl_completed = len(models.PurchaseOrder.objects.filter(target_date__isnull=False))
|
|
|
|
self.assertGreater(number_orders_incl_completed, number_orders)
|
|
|
|
# Transform content to a Calendar object
|
|
calendar = Calendar.from_ical(response.content)
|
|
n_events = 0
|
|
# Count number of events in calendar
|
|
for component in calendar.walk():
|
|
if component.name == 'VEVENT':
|
|
n_events += 1
|
|
|
|
self.assertGreaterEqual(n_events, 1)
|
|
self.assertEqual(number_orders_incl_completed, n_events)
|
|
|
|
def test_po_calendar_noauth(self):
|
|
"""Test accessing calendar without authorization"""
|
|
self.client.logout()
|
|
response = self.client.get(reverse('api-po-so-calendar', kwargs={'ordertype': 'purchase-order'}), format='json')
|
|
|
|
self.assertEqual(response.status_code, 401)
|
|
|
|
resp_dict = response.json()
|
|
self.assertEqual(resp_dict['detail'], "Authentication credentials were not provided.")
|
|
|
|
def test_po_calendar_auth(self):
|
|
"""Test accessing calendar with header authorization"""
|
|
self.client.logout()
|
|
base64_token = base64.b64encode(f'{self.username}:{self.password}'.encode('ascii')).decode('ascii')
|
|
response = self.client.get(
|
|
reverse('api-po-so-calendar', kwargs={'ordertype': 'purchase-order'}),
|
|
format='json',
|
|
HTTP_AUTHORIZATION=f'basic {base64_token}'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
|
|
class PurchaseOrderLineItemTest(OrderTest):
|
|
"""Unit tests for PurchaseOrderLineItems."""
|
|
|
|
LIST_URL = reverse('api-po-line-list')
|
|
|
|
def test_po_line_list(self):
|
|
"""Test the PurchaseOrderLine list API endpoint"""
|
|
# List *ALL* PurchaseOrderLine items
|
|
self.filter({}, 5)
|
|
|
|
# Filter by pending status
|
|
self.filter({'pending': 1}, 5)
|
|
self.filter({'pending': 0}, 0)
|
|
|
|
# Filter by received status
|
|
self.filter({'received': 1}, 0)
|
|
self.filter({'received': 0}, 5)
|
|
|
|
# Filter by has_pricing status
|
|
self.filter({'has_pricing': 1}, 0)
|
|
self.filter({'has_pricing': 0}, 5)
|
|
|
|
def test_po_line_bulk_delete(self):
|
|
"""Test that we can bulk delete multiple PurchaseOrderLineItems via the API."""
|
|
n = models.PurchaseOrderLineItem.objects.count()
|
|
|
|
self.assignRole('purchase_order.delete')
|
|
|
|
url = reverse('api-po-line-list')
|
|
|
|
# Try to delete a set of line items via their IDs
|
|
self.delete(
|
|
url,
|
|
{
|
|
'items': [1, 2],
|
|
},
|
|
expected_code=204,
|
|
)
|
|
|
|
# We should have 2 less PurchaseOrderLineItems after deletign them
|
|
self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n - 2)
|
|
|
|
|
|
class PurchaseOrderDownloadTest(OrderTest):
|
|
"""Unit tests for downloading PurchaseOrder data via the API endpoint."""
|
|
|
|
required_cols = [
|
|
'id',
|
|
'line_items',
|
|
'description',
|
|
'issue_date',
|
|
'notes',
|
|
'reference',
|
|
'status',
|
|
'supplier_reference',
|
|
]
|
|
|
|
excluded_cols = [
|
|
'metadata',
|
|
]
|
|
|
|
def test_download_wrong_format(self):
|
|
"""Incorrect format should default raise an error."""
|
|
url = reverse('api-po-list')
|
|
|
|
with self.assertRaises(ValueError):
|
|
self.download_file(
|
|
url,
|
|
{
|
|
'export': 'xyz',
|
|
}
|
|
)
|
|
|
|
def test_download_csv(self):
|
|
"""Download PurchaseOrder data as .csv."""
|
|
with self.download_file(
|
|
reverse('api-po-list'),
|
|
{
|
|
'export': 'csv',
|
|
},
|
|
expected_code=200,
|
|
expected_fn='InvenTree_PurchaseOrders.csv',
|
|
) as file:
|
|
|
|
data = self.process_csv(
|
|
file,
|
|
required_cols=self.required_cols,
|
|
excluded_cols=self.excluded_cols,
|
|
required_rows=models.PurchaseOrder.objects.count()
|
|
)
|
|
|
|
for row in data:
|
|
order = models.PurchaseOrder.objects.get(pk=row['id'])
|
|
|
|
self.assertEqual(order.description, row['description'])
|
|
self.assertEqual(order.reference, row['reference'])
|
|
|
|
def test_download_line_items(self):
|
|
"""Test that the PurchaseOrderLineItems can be downloaded to a file"""
|
|
with self.download_file(
|
|
reverse('api-po-line-list'),
|
|
{
|
|
'export': 'xlsx',
|
|
},
|
|
decode=False,
|
|
expected_code=200,
|
|
expected_fn='InvenTree_PurchaseOrderItems.xlsx',
|
|
) as file:
|
|
|
|
self.assertTrue(isinstance(file, io.BytesIO))
|
|
|
|
|
|
class PurchaseOrderReceiveTest(OrderTest):
|
|
"""Unit tests for receiving items against a PurchaseOrder."""
|
|
|
|
def setUp(self):
|
|
"""Init routines for this unit test class"""
|
|
super().setUp()
|
|
|
|
self.assignRole('purchase_order.add')
|
|
|
|
self.url = reverse('api-po-receive', kwargs={'pk': 1})
|
|
|
|
# Number of stock items which exist at the start of each test
|
|
self.n = StockItem.objects.count()
|
|
|
|
# Mark the order as "placed" so we can receive line items
|
|
order = models.PurchaseOrder.objects.get(pk=1)
|
|
order.status = PurchaseOrderStatus.PLACED.value
|
|
order.save()
|
|
|
|
def test_empty(self):
|
|
"""Test without any POST data."""
|
|
data = self.post(self.url, {}, expected_code=400).data
|
|
|
|
self.assertIn('This field is required', str(data['items']))
|
|
self.assertIn('This field is required', str(data['location']))
|
|
|
|
# No new stock items have been created
|
|
self.assertEqual(self.n, StockItem.objects.count())
|
|
|
|
def test_no_items(self):
|
|
"""Test with an empty list of items."""
|
|
data = self.post(
|
|
self.url,
|
|
{
|
|
"items": [],
|
|
"location": None,
|
|
},
|
|
expected_code=400
|
|
).data
|
|
|
|
self.assertIn('Line items must be provided', str(data))
|
|
|
|
# No new stock items have been created
|
|
self.assertEqual(self.n, StockItem.objects.count())
|
|
|
|
def test_invalid_items(self):
|
|
"""Test than errors are returned as expected for invalid data."""
|
|
data = self.post(
|
|
self.url,
|
|
{
|
|
"items": [
|
|
{
|
|
"line_item": 12345,
|
|
"location": 12345
|
|
}
|
|
]
|
|
},
|
|
expected_code=400
|
|
).data
|
|
|
|
items = data['items'][0]
|
|
|
|
self.assertIn('Invalid pk "12345"', str(items['line_item']))
|
|
self.assertIn("object does not exist", str(items['location']))
|
|
|
|
# No new stock items have been created
|
|
self.assertEqual(self.n, StockItem.objects.count())
|
|
|
|
def test_invalid_status(self):
|
|
"""Test with an invalid StockStatus value."""
|
|
data = self.post(
|
|
self.url,
|
|
{
|
|
"items": [
|
|
{
|
|
"line_item": 22,
|
|
"location": 1,
|
|
"status": 99999,
|
|
"quantity": 5,
|
|
}
|
|
]
|
|
},
|
|
expected_code=400
|
|
).data
|
|
|
|
self.assertIn('"99999" is not a valid choice.', str(data))
|
|
|
|
# No new stock items have been created
|
|
self.assertEqual(self.n, StockItem.objects.count())
|
|
|
|
def test_mismatched_items(self):
|
|
"""Test for supplier parts which *do* exist but do not match the order supplier."""
|
|
data = self.post(
|
|
self.url,
|
|
{
|
|
'items': [
|
|
{
|
|
'line_item': 22,
|
|
'quantity': 123,
|
|
'location': 1,
|
|
}
|
|
],
|
|
'location': None,
|
|
},
|
|
expected_code=400
|
|
).data
|
|
|
|
self.assertIn('Line item does not match purchase order', str(data))
|
|
|
|
# No new stock items have been created
|
|
self.assertEqual(self.n, StockItem.objects.count())
|
|
|
|
def test_null_barcode(self):
|
|
"""Test than a "null" barcode field can be provided."""
|
|
# Set stock item barcode
|
|
item = StockItem.objects.get(pk=1)
|
|
item.save()
|
|
|
|
# Test with "null" value
|
|
self.post(
|
|
self.url,
|
|
{
|
|
'items': [
|
|
{
|
|
'line_item': 1,
|
|
'quantity': 50,
|
|
'barcode': None,
|
|
}
|
|
],
|
|
'location': 1,
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
def test_invalid_barcodes(self):
|
|
"""Tests for checking in items with invalid barcodes:
|
|
|
|
- Cannot check in "duplicate" barcodes
|
|
- Barcodes cannot match 'barcode_hash' field for existing StockItem
|
|
"""
|
|
# Set stock item barcode
|
|
item = StockItem.objects.get(pk=1)
|
|
item.assign_barcode(barcode_data='MY-BARCODE-HASH')
|
|
|
|
response = self.post(
|
|
self.url,
|
|
{
|
|
'items': [
|
|
{
|
|
'line_item': 1,
|
|
'quantity': 50,
|
|
'barcode': 'MY-BARCODE-HASH',
|
|
}
|
|
],
|
|
'location': 1,
|
|
},
|
|
expected_code=400
|
|
)
|
|
|
|
self.assertIn('Barcode is already in use', str(response.data))
|
|
|
|
response = self.post(
|
|
self.url,
|
|
{
|
|
'items': [
|
|
{
|
|
'line_item': 1,
|
|
'quantity': 5,
|
|
'barcode': 'MY-BARCODE-HASH-1',
|
|
},
|
|
{
|
|
'line_item': 1,
|
|
'quantity': 5,
|
|
'barcode': 'MY-BARCODE-HASH-1'
|
|
},
|
|
],
|
|
'location': 1,
|
|
},
|
|
expected_code=400
|
|
)
|
|
|
|
self.assertIn('barcode values must be unique', str(response.data))
|
|
|
|
# No new stock items have been created
|
|
self.assertEqual(self.n, StockItem.objects.count())
|
|
|
|
def test_valid(self):
|
|
"""Test receipt of valid data."""
|
|
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
|
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
|
|
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0)
|
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0)
|
|
|
|
self.assertEqual(line_1.received, 0)
|
|
self.assertEqual(line_2.received, 50)
|
|
|
|
valid_data = {
|
|
'items': [
|
|
{
|
|
'line_item': 1,
|
|
'quantity': 50,
|
|
'barcode': 'MY-UNIQUE-BARCODE-123',
|
|
},
|
|
{
|
|
'line_item': 2,
|
|
'quantity': 200,
|
|
'location': 2, # Explicit location
|
|
'barcode': 'MY-UNIQUE-BARCODE-456',
|
|
}
|
|
],
|
|
'location': 1, # Default location
|
|
}
|
|
|
|
# Before posting "valid" data, we will mark the purchase order as "pending"
|
|
# In this case we do expect an error!
|
|
order = models.PurchaseOrder.objects.get(pk=1)
|
|
order.status = PurchaseOrderStatus.PENDING.value
|
|
order.save()
|
|
|
|
response = self.post(
|
|
self.url,
|
|
valid_data,
|
|
expected_code=400
|
|
)
|
|
|
|
self.assertIn('can only be received against', str(response.data))
|
|
|
|
# Now, set the PurchaseOrder back to "PLACED" so the items can be received
|
|
order.status = PurchaseOrderStatus.PLACED.value
|
|
order.save()
|
|
|
|
# Receive two separate line items against this order
|
|
self.post(
|
|
self.url,
|
|
valid_data,
|
|
expected_code=201,
|
|
)
|
|
|
|
# There should be two newly created stock items
|
|
self.assertEqual(self.n + 2, StockItem.objects.count())
|
|
|
|
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
|
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
|
|
|
self.assertEqual(line_1.received, 50)
|
|
self.assertEqual(line_2.received, 250)
|
|
|
|
stock_1 = StockItem.objects.filter(supplier_part=line_1.part)
|
|
stock_2 = StockItem.objects.filter(supplier_part=line_2.part)
|
|
|
|
# 1 new stock item created for each supplier part
|
|
self.assertEqual(stock_1.count(), 1)
|
|
self.assertEqual(stock_2.count(), 1)
|
|
|
|
# Same location for each received item, as overall 'location' field is provided
|
|
self.assertEqual(stock_1.last().location.pk, 1)
|
|
self.assertEqual(stock_2.last().location.pk, 1)
|
|
|
|
# Barcodes should have been assigned to the stock items
|
|
self.assertTrue(StockItem.objects.filter(barcode_data='MY-UNIQUE-BARCODE-123').exists())
|
|
self.assertTrue(StockItem.objects.filter(barcode_data='MY-UNIQUE-BARCODE-456').exists())
|
|
|
|
def test_batch_code(self):
|
|
"""Test that we can supply a 'batch code' when receiving items."""
|
|
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
|
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
|
|
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0)
|
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0)
|
|
|
|
data = {
|
|
'items': [
|
|
{
|
|
'line_item': 1,
|
|
'quantity': 10,
|
|
'batch_code': 'B-abc-123',
|
|
},
|
|
{
|
|
'line_item': 2,
|
|
'quantity': 10,
|
|
'batch_code': 'B-xyz-789',
|
|
}
|
|
],
|
|
'location': 1,
|
|
}
|
|
|
|
n = StockItem.objects.count()
|
|
|
|
self.post(
|
|
self.url,
|
|
data,
|
|
expected_code=201,
|
|
)
|
|
|
|
# Check that two new stock items have been created!
|
|
self.assertEqual(n + 2, StockItem.objects.count())
|
|
|
|
item_1 = StockItem.objects.filter(supplier_part=line_1.part).first()
|
|
item_2 = StockItem.objects.filter(supplier_part=line_2.part).first()
|
|
|
|
self.assertEqual(item_1.batch, 'B-abc-123')
|
|
self.assertEqual(item_2.batch, 'B-xyz-789')
|
|
|
|
def test_serial_numbers(self):
|
|
"""Test that we can supply a 'serial number' when receiving items."""
|
|
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
|
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
|
|
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0)
|
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0)
|
|
|
|
data = {
|
|
'items': [
|
|
{
|
|
'line_item': 1,
|
|
'quantity': 10,
|
|
'batch_code': 'B-abc-123',
|
|
'serial_numbers': '100+',
|
|
},
|
|
{
|
|
'line_item': 2,
|
|
'quantity': 10,
|
|
'batch_code': 'B-xyz-789',
|
|
}
|
|
],
|
|
'location': 1,
|
|
}
|
|
|
|
n = StockItem.objects.count()
|
|
|
|
self.post(
|
|
self.url,
|
|
data,
|
|
expected_code=201,
|
|
)
|
|
|
|
# Check that the expected number of stock items has been created
|
|
self.assertEqual(n + 11, StockItem.objects.count())
|
|
|
|
# 10 serialized stock items created for the first line item
|
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 10)
|
|
|
|
# Check that the correct serial numbers have been allocated
|
|
for i in range(100, 110):
|
|
item = StockItem.objects.get(serial_int=i)
|
|
self.assertEqual(item.serial, str(i))
|
|
self.assertEqual(item.quantity, 1)
|
|
self.assertEqual(item.batch, 'B-abc-123')
|
|
|
|
# A single stock item (quantity 10) created for the second line item
|
|
items = StockItem.objects.filter(supplier_part=line_2.part)
|
|
self.assertEqual(items.count(), 1)
|
|
|
|
item = items.first()
|
|
|
|
self.assertEqual(item.quantity, 10)
|
|
self.assertEqual(item.batch, 'B-xyz-789')
|
|
|
|
|
|
class SalesOrderTest(OrderTest):
|
|
"""Tests for the SalesOrder API."""
|
|
|
|
LIST_URL = reverse('api-so-list')
|
|
|
|
def test_so_list(self):
|
|
"""Test the SalesOrder list API endpoint"""
|
|
# All orders
|
|
self.filter({}, 5)
|
|
|
|
# Filter by customer
|
|
self.filter({'customer': 4}, 3)
|
|
self.filter({'customer': 5}, 2)
|
|
|
|
# Filter by outstanding
|
|
self.filter({'outstanding': True}, 3)
|
|
self.filter({'outstanding': False}, 2)
|
|
|
|
# Filter by status
|
|
self.filter({'status': 10}, 3) # PENDING
|
|
self.filter({'status': 20}, 1) # SHIPPED
|
|
self.filter({'status': 99}, 0) # Invalid
|
|
|
|
# Filter by "reference"
|
|
self.filter({'reference': 'ABC123'}, 1)
|
|
self.filter({'reference': 'XXX999'}, 0)
|
|
|
|
# Filter by "assigned_to_me"
|
|
self.filter({'assigned_to_me': 1}, 0)
|
|
self.filter({'assigned_to_me': 0}, 5)
|
|
|
|
def test_total_price(self):
|
|
"""Unit tests for the 'total_price' field"""
|
|
|
|
# Ensure we have exchange rate data
|
|
self.generate_exchange_rates()
|
|
|
|
currencies = currency_codes()
|
|
n = len(currencies)
|
|
|
|
idx = 0
|
|
new_orders = []
|
|
|
|
# Generate some new SalesOrders
|
|
for customer in Company.objects.filter(is_customer=True):
|
|
for _idx in range(10):
|
|
new_orders.append(
|
|
models.SalesOrder(
|
|
customer=customer,
|
|
reference=f'SO-{idx + 100}',
|
|
)
|
|
)
|
|
|
|
idx += 1
|
|
|
|
models.SalesOrder.objects.bulk_create(new_orders)
|
|
|
|
idx = 0
|
|
|
|
# Create some new SalesOrderLineItem objects
|
|
|
|
lines = []
|
|
extra_lines = []
|
|
|
|
for so in models.SalesOrder.objects.all():
|
|
for p in Part.objects.filter(salable=True):
|
|
lines.append(
|
|
models.SalesOrderLineItem(
|
|
order=so,
|
|
part=p,
|
|
quantity=idx + 1,
|
|
sale_price=Money((idx + 1) / 5, currencies[idx % n])
|
|
)
|
|
)
|
|
|
|
idx += 1
|
|
|
|
# Create some extra lines against this order
|
|
for _ in range(3):
|
|
extra_lines.append(
|
|
models.SalesOrderExtraLine(
|
|
order=so,
|
|
quantity=(idx + 2) % 10,
|
|
price=Money(10, 'CAD'),
|
|
)
|
|
)
|
|
|
|
models.SalesOrderLineItem.objects.bulk_create(lines)
|
|
models.SalesOrderExtraLine.objects.bulk_create(extra_lines)
|
|
|
|
# List all SalesOrder objects and count queries
|
|
for limit in [1, 5, 10, 100]:
|
|
with CaptureQueriesContext(connection) as ctx:
|
|
response = self.get(self.LIST_URL, data={'limit': limit}, expected_code=200)
|
|
|
|
# Total database queries must be less than 15
|
|
self.assertLess(len(ctx), 15)
|
|
|
|
n = len(response.data['results'])
|
|
|
|
for result in response.data['results']:
|
|
self.assertIn('total_price', result)
|
|
self.assertIn('order_currency', result)
|
|
|
|
def test_overdue(self):
|
|
"""Test "overdue" status."""
|
|
self.filter({'overdue': True}, 0)
|
|
self.filter({'overdue': False}, 5)
|
|
|
|
for pk in [1, 2]:
|
|
order = models.SalesOrder.objects.get(pk=pk)
|
|
order.target_date = datetime.now().date() - timedelta(days=10)
|
|
order.save()
|
|
|
|
self.filter({'overdue': True}, 2)
|
|
self.filter({'overdue': False}, 3)
|
|
|
|
def test_so_detail(self):
|
|
"""Test the SalesOrder detail endpoint"""
|
|
url = '/api/order/so/1/'
|
|
|
|
response = self.get(url)
|
|
|
|
data = response.data
|
|
|
|
self.assertEqual(data['pk'], 1)
|
|
|
|
def test_so_attachments(self):
|
|
"""Test the list endpoint for the SalesOrderAttachment model"""
|
|
url = reverse('api-so-attachment-list')
|
|
|
|
self.get(url)
|
|
|
|
def test_so_operations(self):
|
|
"""Test that we can create / edit and delete a SalesOrder via the API."""
|
|
n = models.SalesOrder.objects.count()
|
|
|
|
url = reverse('api-so-list')
|
|
|
|
# Initially we do not have "add" permission for the SalesOrder model,
|
|
# so this POST request should return 403 (denied)
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'customer': 4,
|
|
'reference': '12345',
|
|
'description': 'Sales order',
|
|
},
|
|
expected_code=403,
|
|
)
|
|
|
|
self.assignRole('sales_order.add')
|
|
|
|
# Now we should be able to create a SalesOrder via the API
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'customer': 4,
|
|
'reference': 'SO-12345',
|
|
'description': 'Sales order',
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
# Check that the new order has been created
|
|
self.assertEqual(models.SalesOrder.objects.count(), n + 1)
|
|
|
|
# Grab the PK for the newly created SalesOrder
|
|
pk = response.data['pk']
|
|
|
|
# Try to create a SO with identical reference (should fail)
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'customer': 4,
|
|
'reference': 'SO-12345',
|
|
'description': 'Another sales order',
|
|
},
|
|
expected_code=400
|
|
)
|
|
|
|
url = reverse('api-so-detail', kwargs={'pk': pk})
|
|
|
|
# Extract detail info for the SalesOrder
|
|
response = self.get(url)
|
|
self.assertEqual(response.data['reference'], 'SO-12345')
|
|
|
|
# Try to alter (edit) the SalesOrder
|
|
# Initially try with an invalid reference field value
|
|
response = self.patch(
|
|
url,
|
|
{
|
|
'reference': 'SO-12345-a',
|
|
},
|
|
expected_code=400
|
|
)
|
|
|
|
response = self.patch(
|
|
url,
|
|
{
|
|
'reference': 'SO-12346',
|
|
},
|
|
expected_code=200
|
|
)
|
|
|
|
# Reference should have changed
|
|
self.assertEqual(response.data['reference'], 'SO-12346')
|
|
|
|
# Now, let's try to delete this SalesOrder
|
|
# Initially, we do not have the required permission
|
|
response = self.delete(url, expected_code=403)
|
|
|
|
self.assignRole('sales_order.delete')
|
|
|
|
response = self.delete(url, expected_code=204)
|
|
|
|
# Check that the number of sales orders has decreased
|
|
self.assertEqual(models.SalesOrder.objects.count(), n)
|
|
|
|
# And the resource should no longer be available
|
|
response = self.get(url, expected_code=404)
|
|
|
|
def test_so_create(self):
|
|
"""Test that we can create a new SalesOrder via the API."""
|
|
self.assignRole('sales_order.add')
|
|
|
|
url = reverse('api-so-list')
|
|
|
|
# Will fail due to invalid reference field
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'reference': '1234566778',
|
|
'customer': 4,
|
|
'description': 'A test sales order',
|
|
},
|
|
expected_code=400,
|
|
)
|
|
|
|
self.assertIn('Reference must match required pattern', str(response.data['reference']))
|
|
|
|
self.post(
|
|
url,
|
|
{
|
|
'reference': 'SO-12345',
|
|
'customer': 4,
|
|
'description': 'A better test sales order',
|
|
},
|
|
expected_code=201,
|
|
)
|
|
|
|
def test_so_cancel(self):
|
|
"""Test API endpoint for cancelling a SalesOrder."""
|
|
so = models.SalesOrder.objects.get(pk=1)
|
|
|
|
self.assertEqual(so.status, SalesOrderStatus.PENDING)
|
|
|
|
url = reverse('api-so-cancel', kwargs={'pk': so.pk})
|
|
|
|
# Try to cancel, without permission
|
|
self.post(url, {}, expected_code=403)
|
|
|
|
self.assignRole('sales_order.add')
|
|
|
|
self.post(url, {}, expected_code=201)
|
|
|
|
so.refresh_from_db()
|
|
|
|
self.assertEqual(so.status, SalesOrderStatus.CANCELLED)
|
|
|
|
def test_so_calendar(self):
|
|
"""Test the calendar export endpoint"""
|
|
|
|
# Create required sales orders
|
|
self.assignRole('sales_order.add')
|
|
|
|
for i in range(1, 9):
|
|
self.post(
|
|
reverse('api-so-list'),
|
|
{
|
|
'reference': f'SO-1100000{i}',
|
|
'customer': 4,
|
|
'description': f'Calendar SO {i}',
|
|
'target_date': f'2024-12-{i:02d}',
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
# Cancel a few orders - these will not show in incomplete view below
|
|
for so in models.SalesOrder.objects.filter(target_date__isnull=False):
|
|
if so.reference in ['SO-11000006', 'SO-11000007', 'SO-11000008', 'SO-11000009']:
|
|
self.post(
|
|
reverse('api-so-cancel', kwargs={'pk': so.pk}),
|
|
expected_code=201
|
|
)
|
|
|
|
url = reverse('api-po-so-calendar', kwargs={'ordertype': 'sales-order'})
|
|
|
|
# Test without completed orders
|
|
response = self.get(url, expected_code=200, format=None)
|
|
|
|
number_orders = len(models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED.value))
|
|
|
|
# Transform content to a Calendar object
|
|
calendar = Calendar.from_ical(response.content)
|
|
n_events = 0
|
|
# Count number of events in calendar
|
|
for component in calendar.walk():
|
|
if component.name == 'VEVENT':
|
|
n_events += 1
|
|
|
|
self.assertGreaterEqual(n_events, 1)
|
|
self.assertEqual(number_orders, n_events)
|
|
|
|
# Test with completed orders
|
|
response = self.get(url, data={'include_completed': 'True'}, expected_code=200, format=None)
|
|
|
|
number_orders_incl_complete = len(models.SalesOrder.objects.filter(target_date__isnull=False))
|
|
self.assertGreater(number_orders_incl_complete, number_orders)
|
|
|
|
# Transform content to a Calendar object
|
|
calendar = Calendar.from_ical(response.content)
|
|
n_events = 0
|
|
# Count number of events in calendar
|
|
for component in calendar.walk():
|
|
if component.name == 'VEVENT':
|
|
n_events += 1
|
|
|
|
self.assertGreaterEqual(n_events, 1)
|
|
self.assertEqual(number_orders_incl_complete, n_events)
|
|
|
|
def test_export(self):
|
|
"""Test we can export the SalesOrder list"""
|
|
|
|
n = models.SalesOrder.objects.count()
|
|
|
|
# Check there are some sales orders
|
|
self.assertGreater(n, 0)
|
|
|
|
for order in models.SalesOrder.objects.all():
|
|
# Reconstruct the total price
|
|
order.save()
|
|
|
|
# Download file, check we get a 200 response
|
|
for fmt in ['csv', 'xls', 'xlsx']:
|
|
self.download_file(
|
|
reverse('api-so-list'),
|
|
{'export': fmt},
|
|
decode=True if fmt == 'csv' else False,
|
|
expected_code=200,
|
|
expected_fn=f"InvenTree_SalesOrders.{fmt}"
|
|
)
|
|
|
|
|
|
class SalesOrderLineItemTest(OrderTest):
|
|
"""Tests for the SalesOrderLineItem API."""
|
|
|
|
LIST_URL = reverse('api-so-line-list')
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
"""Init routine for this unit test class"""
|
|
super().setUpTestData()
|
|
|
|
# List of salable parts
|
|
parts = Part.objects.filter(salable=True)
|
|
|
|
lines = []
|
|
|
|
# Create a bunch of SalesOrderLineItems for each order
|
|
for idx, so in enumerate(models.SalesOrder.objects.all()):
|
|
|
|
for part in parts:
|
|
lines.append(
|
|
models.SalesOrderLineItem(
|
|
order=so,
|
|
part=part,
|
|
quantity=(idx + 1) * 5,
|
|
reference=f"Order {so.reference} - line {idx}",
|
|
)
|
|
)
|
|
|
|
# Bulk create
|
|
models.SalesOrderLineItem.objects.bulk_create(lines)
|
|
|
|
cls.url = reverse('api-so-line-list')
|
|
|
|
def test_so_line_list(self):
|
|
"""Test list endpoint"""
|
|
response = self.get(
|
|
self.url,
|
|
{},
|
|
expected_code=200,
|
|
)
|
|
|
|
n = models.SalesOrderLineItem.objects.count()
|
|
|
|
# We should have received *all* lines
|
|
self.assertEqual(len(response.data), n)
|
|
|
|
# List *all* lines, but paginate
|
|
response = self.get(
|
|
self.url,
|
|
{
|
|
"limit": 5,
|
|
},
|
|
expected_code=200,
|
|
)
|
|
|
|
self.assertEqual(response.data['count'], n)
|
|
self.assertEqual(len(response.data['results']), 5)
|
|
|
|
n_orders = models.SalesOrder.objects.count()
|
|
n_parts = Part.objects.filter(salable=True).count()
|
|
|
|
# List by part
|
|
for part in Part.objects.filter(salable=True)[:3]:
|
|
response = self.get(
|
|
self.url,
|
|
{
|
|
'part': part.pk,
|
|
'limit': 10,
|
|
}
|
|
)
|
|
|
|
self.assertEqual(response.data['count'], n_orders)
|
|
|
|
# List by order
|
|
for order in models.SalesOrder.objects.all()[:3]:
|
|
response = self.get(
|
|
self.url,
|
|
{
|
|
'order': order.pk,
|
|
'limit': 10,
|
|
}
|
|
)
|
|
|
|
self.assertEqual(response.data['count'], n_parts)
|
|
|
|
# Filter by has_pricing status
|
|
self.filter({'has_pricing': 1}, 0)
|
|
self.filter({'has_pricing': 0}, n)
|
|
|
|
# Filter by has_pricing status
|
|
self.filter({'completed': 1}, 0)
|
|
self.filter({'completed': 0}, n)
|
|
|
|
|
|
class SalesOrderDownloadTest(OrderTest):
|
|
"""Unit tests for downloading SalesOrder data via the API endpoint."""
|
|
|
|
def test_download_fail(self):
|
|
"""Test that downloading without the 'export' option fails."""
|
|
url = reverse('api-so-list')
|
|
|
|
with self.assertRaises(ValueError):
|
|
self.download_file(url, {}, expected_code=200)
|
|
|
|
def test_download_xls(self):
|
|
"""Test xls file download"""
|
|
url = reverse('api-so-list')
|
|
|
|
# Download .xls file
|
|
with self.download_file(
|
|
url,
|
|
{
|
|
'export': 'xls',
|
|
},
|
|
expected_code=200,
|
|
expected_fn='InvenTree_SalesOrders.xls',
|
|
decode=False,
|
|
) as file:
|
|
self.assertTrue(isinstance(file, io.BytesIO))
|
|
|
|
def test_download_csv(self):
|
|
"""Test that the list of sales orders can be downloaded as a .csv file"""
|
|
url = reverse('api-so-list')
|
|
|
|
required_cols = [
|
|
'line_items',
|
|
'id',
|
|
'reference',
|
|
'customer',
|
|
'status',
|
|
'shipment_date',
|
|
'notes',
|
|
'description',
|
|
]
|
|
|
|
excluded_cols = [
|
|
'metadata'
|
|
]
|
|
|
|
# Download .xls file
|
|
with self.download_file(
|
|
url,
|
|
{
|
|
'export': 'csv',
|
|
},
|
|
expected_code=200,
|
|
expected_fn='InvenTree_SalesOrders.csv',
|
|
decode=True
|
|
) as file:
|
|
|
|
data = self.process_csv(
|
|
file,
|
|
required_cols=required_cols,
|
|
excluded_cols=excluded_cols,
|
|
required_rows=models.SalesOrder.objects.count()
|
|
)
|
|
|
|
for line in data:
|
|
|
|
order = models.SalesOrder.objects.get(pk=line['id'])
|
|
|
|
self.assertEqual(line['description'], order.description)
|
|
self.assertEqual(line['status'], str(order.status))
|
|
|
|
# Download only outstanding sales orders
|
|
with self.download_file(
|
|
url,
|
|
{
|
|
'export': 'tsv',
|
|
'outstanding': True,
|
|
},
|
|
expected_code=200,
|
|
expected_fn='InvenTree_SalesOrders.tsv',
|
|
decode=True,
|
|
) as file:
|
|
|
|
self.process_csv(
|
|
file,
|
|
required_cols=required_cols,
|
|
excluded_cols=excluded_cols,
|
|
required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatusGroups.OPEN).count(),
|
|
delimiter='\t',
|
|
)
|
|
|
|
|
|
class SalesOrderAllocateTest(OrderTest):
|
|
"""Unit tests for allocating stock items against a SalesOrder."""
|
|
|
|
def setUp(self):
|
|
"""Init routines for this unit testing class"""
|
|
super().setUp()
|
|
|
|
self.assignRole('sales_order.add')
|
|
|
|
self.url = reverse('api-so-allocate', kwargs={'pk': 1})
|
|
|
|
self.order = models.SalesOrder.objects.get(pk=1)
|
|
|
|
# Create some line items for this purchase order
|
|
parts = Part.objects.filter(salable=True)
|
|
|
|
for part in parts:
|
|
|
|
# Create a new line item
|
|
models.SalesOrderLineItem.objects.create(
|
|
order=self.order,
|
|
part=part,
|
|
quantity=5,
|
|
)
|
|
|
|
# Ensure we have stock!
|
|
StockItem.objects.create(
|
|
part=part,
|
|
quantity=100,
|
|
)
|
|
|
|
# Create a new shipment against this SalesOrder
|
|
self.shipment = models.SalesOrderShipment.objects.create(
|
|
order=self.order,
|
|
)
|
|
|
|
def test_invalid(self):
|
|
"""Test POST with invalid data."""
|
|
# No data
|
|
response = self.post(self.url, {}, expected_code=400)
|
|
|
|
self.assertIn('This field is required', str(response.data['items']))
|
|
self.assertIn('This field is required', str(response.data['shipment']))
|
|
|
|
# Test with a single line items
|
|
line = self.order.lines.first()
|
|
part = line.part
|
|
|
|
# Valid stock_item, but quantity is invalid
|
|
data = {
|
|
'items': [{
|
|
"line_item": line.pk,
|
|
"stock_item": part.stock_items.last().pk,
|
|
"quantity": 0,
|
|
}],
|
|
}
|
|
|
|
response = self.post(self.url, data, expected_code=400)
|
|
|
|
self.assertIn('Quantity must be positive', str(response.data['items']))
|
|
|
|
# Valid stock item, too much quantity
|
|
data['items'][0]['quantity'] = 250
|
|
|
|
response = self.post(self.url, data, expected_code=400)
|
|
|
|
self.assertIn('Available quantity (100) exceeded', str(response.data['items']))
|
|
|
|
# Valid stock item, valid quantity
|
|
data['items'][0]['quantity'] = 50
|
|
|
|
# Invalid shipment!
|
|
data['shipment'] = 9999
|
|
|
|
response = self.post(self.url, data, expected_code=400)
|
|
|
|
self.assertIn('does not exist', str(response.data['shipment']))
|
|
|
|
# Valid shipment, but points to the wrong order
|
|
shipment = models.SalesOrderShipment.objects.create(
|
|
order=models.SalesOrder.objects.get(pk=2),
|
|
)
|
|
|
|
data['shipment'] = shipment.pk
|
|
|
|
response = self.post(self.url, data, expected_code=400)
|
|
|
|
self.assertIn('Shipment is not associated with this order', str(response.data['shipment']))
|
|
|
|
def test_allocate(self):
|
|
"""Test that the allocation endpoint acts as expected, when provided with valid data!"""
|
|
# First, check that there are no line items allocated against this SalesOrder
|
|
self.assertEqual(self.order.stock_allocations.count(), 0)
|
|
|
|
data = {
|
|
"items": [],
|
|
"shipment": self.shipment.pk,
|
|
}
|
|
|
|
for line in self.order.lines.all():
|
|
stock_item = line.part.stock_items.last()
|
|
|
|
# Fully-allocate each line
|
|
data['items'].append({
|
|
"line_item": line.pk,
|
|
"stock_item": stock_item.pk,
|
|
"quantity": 5
|
|
})
|
|
|
|
self.post(self.url, data, expected_code=201)
|
|
|
|
# There should have been 1 stock item allocated against each line item
|
|
n_lines = self.order.lines.count()
|
|
|
|
self.assertEqual(self.order.stock_allocations.count(), n_lines)
|
|
|
|
for line in self.order.lines.all():
|
|
self.assertEqual(line.allocations.count(), 1)
|
|
|
|
def test_allocate_variant(self):
|
|
"""Test that the allocation endpoint acts as expected, when provided with variant"""
|
|
# First, check that there are no line items allocated against this SalesOrder
|
|
self.assertEqual(self.order.stock_allocations.count(), 0)
|
|
|
|
data = {
|
|
"items": [],
|
|
"shipment": self.shipment.pk,
|
|
}
|
|
|
|
def check_template(line_item):
|
|
return line_item.part.is_template
|
|
|
|
for line in filter(check_template, self.order.lines.all()):
|
|
|
|
stock_item = None
|
|
|
|
# Allocate a matching variant
|
|
parts = Part.objects.filter(salable=True).filter(variant_of=line.part.pk)
|
|
for part in parts:
|
|
stock_item = part.stock_items.last()
|
|
break
|
|
|
|
# Fully-allocate each line
|
|
data['items'].append({
|
|
"line_item": line.pk,
|
|
"stock_item": stock_item.pk,
|
|
"quantity": 5
|
|
})
|
|
|
|
self.post(self.url, data, expected_code=201)
|
|
|
|
# At least one item should be allocated, and all should be variants
|
|
self.assertGreater(self.order.stock_allocations.count(), 0)
|
|
for allocation in self.order.stock_allocations.all():
|
|
self.assertNotEquals(allocation.item.part.pk, allocation.line.part.pk)
|
|
|
|
def test_shipment_complete(self):
|
|
"""Test that we can complete a shipment via the API."""
|
|
url = reverse('api-so-shipment-ship', kwargs={'pk': self.shipment.pk})
|
|
|
|
self.assertFalse(self.shipment.is_complete())
|
|
self.assertFalse(self.shipment.check_can_complete(raise_error=False))
|
|
|
|
with self.assertRaises(ValidationError):
|
|
self.shipment.check_can_complete()
|
|
|
|
# Attempting to complete this shipment via the API should fail
|
|
response = self.post(
|
|
url, {},
|
|
expected_code=400
|
|
)
|
|
|
|
self.assertIn('Shipment has no allocated stock items', str(response.data))
|
|
|
|
# Allocate stock against this shipment
|
|
line = self.order.lines.first()
|
|
part = line.part
|
|
|
|
models.SalesOrderAllocation.objects.create(
|
|
shipment=self.shipment,
|
|
line=line,
|
|
item=part.stock_items.last(),
|
|
quantity=5
|
|
)
|
|
|
|
# Shipment should now be able to be completed
|
|
self.assertTrue(self.shipment.check_can_complete())
|
|
|
|
# Attempt with an invalid date
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'shipment_date': 'asfasd',
|
|
},
|
|
expected_code=400,
|
|
)
|
|
|
|
self.assertIn('Date has wrong format', str(response.data))
|
|
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'invoice_number': 'INV01234',
|
|
'link': 'http://test.com/link.html',
|
|
'tracking_number': 'TRK12345',
|
|
'shipment_date': '2020-12-05',
|
|
'delivery_date': '2023-12-05',
|
|
},
|
|
expected_code=201,
|
|
)
|
|
|
|
self.shipment.refresh_from_db()
|
|
|
|
self.assertTrue(self.shipment.is_complete())
|
|
self.assertEqual(self.shipment.tracking_number, 'TRK12345')
|
|
self.assertEqual(self.shipment.invoice_number, 'INV01234')
|
|
self.assertEqual(self.shipment.link, 'http://test.com/link.html')
|
|
self.assertEqual(self.shipment.delivery_date, datetime(2023, 12, 5).date())
|
|
self.assertTrue(self.shipment.is_delivered())
|
|
|
|
def test_shipment_delivery_date(self):
|
|
"""Test delivery date functions via API."""
|
|
url = reverse('api-so-shipment-detail', kwargs={'pk': self.shipment.pk})
|
|
|
|
# Attempt remove delivery_date from shipment
|
|
response = self.patch(
|
|
url,
|
|
{
|
|
'delivery_date': None,
|
|
},
|
|
expected_code=200,
|
|
)
|
|
|
|
# Shipment should not be marked as delivered
|
|
self.assertFalse(self.shipment.is_delivered())
|
|
|
|
# Attempt to set delivery date
|
|
response = self.patch(
|
|
url,
|
|
{
|
|
'delivery_date': 'asfasd',
|
|
},
|
|
expected_code=400,
|
|
)
|
|
|
|
self.assertIn('Date has wrong format', str(response.data))
|
|
|
|
response = self.patch(
|
|
url,
|
|
{
|
|
'delivery_date': '2023-05-15',
|
|
},
|
|
expected_code=200,
|
|
)
|
|
self.shipment.refresh_from_db()
|
|
|
|
# Shipment should now be marked as delivered
|
|
self.assertTrue(self.shipment.is_delivered())
|
|
self.assertEqual(self.shipment.delivery_date, datetime(2023, 5, 15).date())
|
|
|
|
def test_sales_order_shipment_list(self):
|
|
"""Test the SalesOrderShipment list API endpoint"""
|
|
url = reverse('api-so-shipment-list')
|
|
|
|
# Count before creation
|
|
count_before = models.SalesOrderShipment.objects.count()
|
|
|
|
# Create some new shipments via the API
|
|
for order in models.SalesOrder.objects.all():
|
|
|
|
for idx in range(3):
|
|
self.post(
|
|
url,
|
|
{
|
|
'order': order.pk,
|
|
'reference': f"SH{idx + 1}",
|
|
'tracking_number': f"TRK_{order.pk}_{idx}"
|
|
},
|
|
expected_code=201
|
|
)
|
|
|
|
# Filter API by order
|
|
response = self.get(
|
|
url,
|
|
{
|
|
'order': order.pk,
|
|
},
|
|
expected_code=200,
|
|
)
|
|
|
|
# 3 shipments returned for each SalesOrder instance
|
|
self.assertGreaterEqual(len(response.data), 3)
|
|
|
|
# List *all* shipments
|
|
response = self.get(url, expected_code=200)
|
|
|
|
self.assertEqual(len(response.data), count_before + 3 * models.SalesOrder.objects.count())
|
|
|
|
|
|
class ReturnOrderTests(InvenTreeAPITestCase):
|
|
"""Unit tests for ReturnOrder API endpoints"""
|
|
|
|
fixtures = [
|
|
'category',
|
|
'company',
|
|
'return_order',
|
|
'part',
|
|
'location',
|
|
'supplier_part',
|
|
'stock',
|
|
]
|
|
|
|
def test_options(self):
|
|
"""Test the OPTIONS endpoint"""
|
|
|
|
self.assignRole('return_order.add')
|
|
data = self.options(reverse('api-return-order-list'), expected_code=200).data
|
|
|
|
self.assertEqual(data['name'], 'Return Order List')
|
|
|
|
# Some checks on the 'reference' field
|
|
post = data['actions']['POST']
|
|
reference = post['reference']
|
|
|
|
self.assertEqual(reference['default'], 'RMA-0007')
|
|
self.assertEqual(reference['label'], 'Reference')
|
|
self.assertEqual(reference['help_text'], 'Return Order reference')
|
|
self.assertEqual(reference['required'], True)
|
|
self.assertEqual(reference['type'], 'string')
|
|
|
|
def test_list(self):
|
|
"""Tests for the list endpoint"""
|
|
|
|
url = reverse('api-return-order-list')
|
|
|
|
response = self.get(url, expected_code=200)
|
|
|
|
self.assertEqual(len(response.data), 6)
|
|
|
|
# Paginated query
|
|
data = self.get(
|
|
url,
|
|
{
|
|
'limit': 1,
|
|
'ordering': 'reference',
|
|
'customer_detail': True,
|
|
},
|
|
expected_code=200
|
|
).data
|
|
|
|
self.assertEqual(data['count'], 6)
|
|
self.assertEqual(len(data['results']), 1)
|
|
result = data['results'][0]
|
|
self.assertEqual(result['reference'], 'RMA-001')
|
|
self.assertEqual(result['customer_detail']['name'], 'A customer')
|
|
|
|
# Reverse ordering
|
|
data = self.get(
|
|
url,
|
|
{
|
|
'ordering': '-reference',
|
|
},
|
|
expected_code=200
|
|
).data
|
|
|
|
self.assertEqual(data[0]['reference'], 'RMA-006')
|
|
|
|
# Filter by customer
|
|
for cmp_id in [4, 5]:
|
|
data = self.get(
|
|
url,
|
|
{
|
|
'customer': cmp_id,
|
|
},
|
|
expected_code=200
|
|
).data
|
|
|
|
self.assertEqual(len(data), 3)
|
|
|
|
for result in data:
|
|
self.assertEqual(result['customer'], cmp_id)
|
|
|
|
# Filter by status
|
|
data = self.get(
|
|
url,
|
|
{
|
|
'status': 20,
|
|
},
|
|
expected_code=200
|
|
).data
|
|
|
|
self.assertEqual(len(data), 2)
|
|
|
|
for result in data:
|
|
self.assertEqual(result['status'], 20)
|
|
|
|
def test_create(self):
|
|
"""Test creation of ReturnOrder via the API"""
|
|
|
|
url = reverse('api-return-order-list')
|
|
|
|
# Do not have required permissions yet
|
|
self.post(
|
|
url,
|
|
{
|
|
'customer': 1,
|
|
'description': 'a return order',
|
|
},
|
|
expected_code=403
|
|
)
|
|
|
|
self.assignRole('return_order.add')
|
|
|
|
data = self.post(
|
|
url,
|
|
{
|
|
'customer': 4,
|
|
'customer_reference': 'cr',
|
|
'description': 'a return order',
|
|
},
|
|
expected_code=201
|
|
).data
|
|
|
|
# Reference automatically generated
|
|
self.assertEqual(data['reference'], 'RMA-0007')
|
|
self.assertEqual(data['customer_reference'], 'cr')
|
|
|
|
def test_update(self):
|
|
"""Test that we can update a ReturnOrder via the API"""
|
|
|
|
url = reverse('api-return-order-detail', kwargs={'pk': 1})
|
|
|
|
# Test detail endpoint
|
|
data = self.get(url, expected_code=200).data
|
|
|
|
self.assertEqual(data['reference'], 'RMA-001')
|
|
|
|
# Attempt to update, incorrect permissions
|
|
self.patch(
|
|
url,
|
|
{
|
|
'customer_reference': 'My customer reference',
|
|
},
|
|
expected_code=403
|
|
)
|
|
|
|
self.assignRole('return_order.change')
|
|
|
|
self.patch(
|
|
url,
|
|
{
|
|
'customer_reference': 'customer ref',
|
|
},
|
|
expected_code=200
|
|
)
|
|
|
|
rma = models.ReturnOrder.objects.get(pk=1)
|
|
self.assertEqual(rma.customer_reference, 'customer ref')
|
|
|
|
def test_ro_issue(self):
|
|
"""Test the 'issue' order for a ReturnOrder"""
|
|
|
|
order = models.ReturnOrder.objects.get(pk=1)
|
|
self.assertEqual(order.status, ReturnOrderStatus.PENDING)
|
|
self.assertIsNone(order.issue_date)
|
|
|
|
url = reverse('api-return-order-issue', kwargs={'pk': 1})
|
|
|
|
# POST without required permissions
|
|
self.post(url, expected_code=403)
|
|
|
|
self.assignRole('return_order.add')
|
|
|
|
self.post(url, expected_code=201)
|
|
order.refresh_from_db()
|
|
self.assertEqual(order.status, ReturnOrderStatus.IN_PROGRESS)
|
|
self.assertIsNotNone(order.issue_date)
|
|
|
|
def test_receive(self):
|
|
"""Test that we can receive items against a ReturnOrder"""
|
|
|
|
customer = Company.objects.get(pk=4)
|
|
|
|
# Create an order
|
|
rma = models.ReturnOrder.objects.create(
|
|
customer=customer,
|
|
description='A return order',
|
|
)
|
|
|
|
self.assertEqual(rma.reference, 'RMA-0007')
|
|
|
|
# Create some line items
|
|
part = Part.objects.get(pk=25)
|
|
for idx in range(3):
|
|
stock_item = StockItem.objects.create(
|
|
part=part, customer=customer,
|
|
quantity=1, serial=idx
|
|
)
|
|
|
|
line_item = models.ReturnOrderLineItem.objects.create(
|
|
order=rma,
|
|
item=stock_item,
|
|
)
|
|
|
|
self.assertEqual(line_item.outcome, ReturnOrderLineStatus.PENDING)
|
|
self.assertIsNone(line_item.received_date)
|
|
self.assertFalse(line_item.received)
|
|
|
|
self.assertEqual(rma.lines.count(), 3)
|
|
|
|
def receive(items, location=None, expected_code=400):
|
|
"""Helper function to receive items against this ReturnOrder"""
|
|
url = reverse('api-return-order-receive', kwargs={'pk': rma.pk})
|
|
|
|
response = self.post(
|
|
url,
|
|
{
|
|
'items': items,
|
|
'location': location,
|
|
},
|
|
expected_code=expected_code
|
|
)
|
|
|
|
return response.data
|
|
|
|
# Receive without required permissions
|
|
receive([], expected_code=403)
|
|
|
|
self.assignRole('return_order.add')
|
|
|
|
# Receive, without any location
|
|
data = receive([], expected_code=400)
|
|
self.assertIn('This field may not be null', str(data['location']))
|
|
|
|
# Receive, with incorrect order code
|
|
data = receive([], 1, expected_code=400)
|
|
self.assertIn('Items can only be received against orders which are in progress', str(data))
|
|
|
|
# Issue the order (via the API)
|
|
self.assertIsNone(rma.issue_date)
|
|
self.post(
|
|
reverse("api-return-order-issue", kwargs={"pk": rma.pk}),
|
|
expected_code=201,
|
|
)
|
|
|
|
rma.refresh_from_db()
|
|
self.assertIsNotNone(rma.issue_date)
|
|
self.assertEqual(rma.status, ReturnOrderStatus.IN_PROGRESS)
|
|
|
|
# Receive, without any items
|
|
data = receive([], 1, expected_code=400)
|
|
self.assertIn('Line items must be provided', str(data))
|
|
|
|
# Get a reference to one of the stock items
|
|
stock_item = rma.lines.first().item
|
|
|
|
n_tracking = stock_item.tracking_info.count()
|
|
|
|
# Receive items successfully
|
|
data = receive(
|
|
[{'item': line.pk} for line in rma.lines.all()],
|
|
1,
|
|
expected_code=201
|
|
)
|
|
|
|
# Check that all line items have been received
|
|
for line in rma.lines.all():
|
|
self.assertTrue(line.received)
|
|
self.assertIsNotNone(line.received_date)
|
|
|
|
# A single tracking entry should have been added to the item
|
|
self.assertEqual(stock_item.tracking_info.count(), n_tracking + 1)
|
|
|
|
tracking_entry = stock_item.tracking_info.last()
|
|
deltas = tracking_entry.deltas
|
|
|
|
self.assertEqual(deltas['status'], StockStatus.QUARANTINED)
|
|
self.assertEqual(deltas['customer'], customer.pk)
|
|
self.assertEqual(deltas['location'], 1)
|
|
self.assertEqual(deltas['returnorder'], rma.pk)
|
|
|
|
def test_ro_calendar(self):
|
|
"""Test the calendar export endpoint"""
|
|
|
|
# Full test is in test_po_calendar. Since these use the same backend, test only
|
|
# that the endpoint is available
|
|
url = reverse('api-po-so-calendar', kwargs={'ordertype': 'return-order'})
|
|
|
|
# Test without completed orders
|
|
response = self.get(url, expected_code=200, format=None)
|
|
calendar = Calendar.from_ical(response.content)
|
|
self.assertIsInstance(calendar, Calendar)
|
|
|
|
|
|
class OrderMetadataAPITest(InvenTreeAPITestCase):
|
|
"""Unit tests for the various metadata endpoints of API."""
|
|
|
|
fixtures = [
|
|
'category',
|
|
'part',
|
|
'company',
|
|
'location',
|
|
'supplier_part',
|
|
'stock',
|
|
'order',
|
|
'sales_order',
|
|
'return_order',
|
|
]
|
|
|
|
roles = [
|
|
'purchase_order.change',
|
|
'sales_order.change',
|
|
'return_order.change',
|
|
]
|
|
|
|
def metatester(self, apikey, model):
|
|
"""Generic tester"""
|
|
|
|
modeldata = model.objects.first()
|
|
|
|
# Useless test unless a model object is found
|
|
self.assertIsNotNone(modeldata)
|
|
|
|
url = reverse(apikey, kwargs={'pk': modeldata.pk})
|
|
|
|
# Metadata is initially null
|
|
self.assertIsNone(modeldata.metadata)
|
|
|
|
numstr = f'12{len(apikey)}'
|
|
|
|
self.patch(
|
|
url,
|
|
{
|
|
'metadata': {
|
|
f'abc-{numstr}': f'xyz-{apikey}-{numstr}',
|
|
}
|
|
},
|
|
expected_code=200
|
|
)
|
|
|
|
# Refresh
|
|
modeldata.refresh_from_db()
|
|
self.assertEqual(modeldata.get_metadata(f'abc-{numstr}'), f'xyz-{apikey}-{numstr}')
|
|
|
|
def test_metadata(self):
|
|
"""Test all endpoints"""
|
|
|
|
for apikey, model in {
|
|
'api-po-metadata': models.PurchaseOrder,
|
|
'api-po-line-metadata': models.PurchaseOrderLineItem,
|
|
'api-po-extra-line-metadata': models.PurchaseOrderExtraLine,
|
|
'api-so-shipment-metadata': models.SalesOrderShipment,
|
|
'api-so-metadata': models.SalesOrder,
|
|
'api-so-line-metadata': models.SalesOrderLineItem,
|
|
'api-so-extra-line-metadata': models.SalesOrderExtraLine,
|
|
'api-return-order-metadata': models.ReturnOrder,
|
|
'api-return-order-line-metadata': models.ReturnOrderLineItem,
|
|
'api-return-order-extra-line-metadata': models.ReturnOrderExtraLine,
|
|
}.items():
|
|
self.metatester(apikey, model)
|