mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 04:25:42 +00:00
Merge branch 'inventree:master' into fr-2986-shipment-page-action
This commit is contained in:
@ -105,6 +105,9 @@ class PurchaseOrderResource(ModelResource):
|
||||
model = PurchaseOrder
|
||||
skip_unchanged = True
|
||||
clean_model_instances = True
|
||||
exclude = [
|
||||
'metadata',
|
||||
]
|
||||
|
||||
|
||||
class PurchaseOrderLineItemResource(ModelResource):
|
||||
@ -147,6 +150,9 @@ class SalesOrderResource(ModelResource):
|
||||
model = SalesOrder
|
||||
skip_unchanged = True
|
||||
clean_model_instances = True
|
||||
exclude = [
|
||||
'metadata',
|
||||
]
|
||||
|
||||
|
||||
class SalesOrderLineItemResource(ModelResource):
|
||||
|
@ -667,9 +667,9 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
outstanding = str2bool(outstanding)
|
||||
|
||||
if outstanding:
|
||||
queryset = queryset.filter(status__in=models.SalesOrderStatus.OPEN)
|
||||
queryset = queryset.filter(status__in=SalesOrderStatus.OPEN)
|
||||
else:
|
||||
queryset = queryset.exclude(status__in=models.SalesOrderStatus.OPEN)
|
||||
queryset = queryset.exclude(status__in=SalesOrderStatus.OPEN)
|
||||
|
||||
# Filter by 'overdue' status
|
||||
overdue = params.get('overdue', None)
|
||||
|
@ -12,6 +12,8 @@ from decimal import Decimal
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q, F, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -809,6 +811,21 @@ class SalesOrder(Order):
|
||||
return self.pending_shipments().count()
|
||||
|
||||
|
||||
@receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log')
|
||||
def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs):
|
||||
"""
|
||||
Callback function to be executed after a SalesOrder instance is saved
|
||||
"""
|
||||
if created and getSetting('SALESORDER_DEFAULT_SHIPMENT'):
|
||||
# A new SalesOrder has just been created
|
||||
|
||||
# Create default shipment
|
||||
SalesOrderShipment.objects.create(
|
||||
order=instance,
|
||||
reference='1',
|
||||
)
|
||||
|
||||
|
||||
class PurchaseOrderAttachment(InvenTreeAttachment):
|
||||
"""
|
||||
Model for storing file attachments against a PurchaseOrder object
|
||||
|
@ -2,6 +2,8 @@
|
||||
Tests for the Order API
|
||||
"""
|
||||
|
||||
import io
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from rest_framework import status
|
||||
@ -323,6 +325,77 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(order.get_metadata('yam'), 'yum')
|
||||
|
||||
|
||||
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 fo:
|
||||
|
||||
data = self.process_csv(
|
||||
fo,
|
||||
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):
|
||||
|
||||
with self.download_file(
|
||||
reverse('api-po-line-list'),
|
||||
{
|
||||
'export': 'xlsx',
|
||||
},
|
||||
decode=False,
|
||||
expected_code=200,
|
||||
expected_fn='InvenTree_PurchaseOrderItems.xlsx',
|
||||
) as fo:
|
||||
|
||||
self.assertTrue(isinstance(fo, io.BytesIO))
|
||||
|
||||
|
||||
class PurchaseOrderReceiveTest(OrderTest):
|
||||
"""
|
||||
Unit tests for receiving items against a PurchaseOrder
|
||||
@ -908,6 +981,177 @@ class SalesOrderTest(OrderTest):
|
||||
self.assertEqual(order.get_metadata('xyz'), 'abc')
|
||||
|
||||
|
||||
class SalesOrderLineItemTest(OrderTest):
|
||||
"""
|
||||
Tests for the SalesOrderLineItem API
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
# List of salable parts
|
||||
parts = Part.objects.filter(salable=True)
|
||||
|
||||
# Create a bunch of SalesOrderLineItems for each order
|
||||
for idx, so in enumerate(models.SalesOrder.objects.all()):
|
||||
|
||||
for part in parts:
|
||||
models.SalesOrderLineItem.objects.create(
|
||||
order=so,
|
||||
part=part,
|
||||
quantity=(idx + 1) * 5,
|
||||
reference=f"Order {so.reference} - line {idx}",
|
||||
)
|
||||
|
||||
self.url = reverse('api-so-line-list')
|
||||
|
||||
def test_so_line_list(self):
|
||||
|
||||
# List *all* lines
|
||||
|
||||
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):
|
||||
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():
|
||||
response = self.get(
|
||||
self.url,
|
||||
{
|
||||
'order': order.pk,
|
||||
'limit': 10,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(response.data['count'], n_parts)
|
||||
|
||||
|
||||
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):
|
||||
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 fo:
|
||||
self.assertTrue(isinstance(fo, io.BytesIO))
|
||||
|
||||
def test_download_csv(self):
|
||||
|
||||
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 fo:
|
||||
|
||||
data = self.process_csv(
|
||||
fo,
|
||||
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 fo:
|
||||
|
||||
self.process_csv(
|
||||
fo,
|
||||
required_cols=required_cols,
|
||||
excluded_cols=excluded_cols,
|
||||
required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatus.OPEN).count(),
|
||||
delimiter='\t',
|
||||
)
|
||||
|
||||
|
||||
class SalesOrderAllocateTest(OrderTest):
|
||||
"""
|
||||
Unit tests for allocating stock items against a SalesOrder
|
||||
|
@ -10,6 +10,8 @@ from company.models import Company
|
||||
|
||||
from InvenTree import status_codes as status
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
from order.models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation, SalesOrderShipment
|
||||
|
||||
from part.models import Part
|
||||
@ -200,3 +202,37 @@ class SalesOrderTest(TestCase):
|
||||
self.assertTrue(self.line.is_fully_allocated())
|
||||
self.assertEqual(self.line.fulfilled_quantity(), 50)
|
||||
self.assertEqual(self.line.allocated_quantity(), 50)
|
||||
|
||||
def test_default_shipment(self):
|
||||
# Test sales order default shipment creation
|
||||
|
||||
# Default setting value should be False
|
||||
self.assertEqual(False, InvenTreeSetting.get_setting('SALESORDER_DEFAULT_SHIPMENT'))
|
||||
|
||||
# Create an order
|
||||
order_1 = SalesOrder.objects.create(
|
||||
customer=self.customer,
|
||||
reference='1235',
|
||||
customer_reference='ABC 55556'
|
||||
)
|
||||
|
||||
# Order should have no shipments when setting is False
|
||||
self.assertEqual(0, order_1.shipment_count)
|
||||
|
||||
# Update setting to True
|
||||
InvenTreeSetting.set_setting('SALESORDER_DEFAULT_SHIPMENT', True, None)
|
||||
self.assertEqual(True, InvenTreeSetting.get_setting('SALESORDER_DEFAULT_SHIPMENT'))
|
||||
|
||||
# Create a second order
|
||||
order_2 = SalesOrder.objects.create(
|
||||
customer=self.customer,
|
||||
reference='1236',
|
||||
customer_reference='ABC 55557'
|
||||
)
|
||||
|
||||
# Order should have one shipment
|
||||
self.assertEqual(1, order_2.shipment_count)
|
||||
self.assertEqual(1, order_2.pending_shipments().count())
|
||||
|
||||
# Shipment should have default reference of '1'
|
||||
self.assertEqual('1', order_2.pending_shipments()[0].reference)
|
||||
|
Reference in New Issue
Block a user