Add some unit testing for the SalesOrder model

This commit is contained in:
Oliver Walters 2020-04-27 11:32:20 +10:00
parent 5e309a62f7
commit 3685ca4b95
5 changed files with 182 additions and 11 deletions

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-04-27 00:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('part', '0035_auto_20200406_0045'),
('order', '0031_auto_20200426_0612'),
]
operations = [
migrations.AlterUniqueTogether(
name='salesorderlineitem',
unique_together={('order', 'part')},
),
]

View File

@ -24,7 +24,7 @@ from stock import models as stock_models
from company.models import Company, SupplierPart
from InvenTree.fields import RoundingDecimalField
from InvenTree.helpers import decimal2string, normalize
from InvenTree.helpers import decimal2string
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
from InvenTree.models import InvenTreeAttachment
@ -461,6 +461,11 @@ class SalesOrderLineItem(OrderLineItem):
part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, help_text=_('Part'), limit_choices_to={'salable': True})
class Meta:
unique_together = [
('order', 'part'),
]
def fulfilled_quantity(self):
"""
Return the total stock quantity fulfilled against this line item.
@ -482,6 +487,10 @@ class SalesOrderLineItem(OrderLineItem):
def is_fully_allocated(self):
""" Return True if this line item is fully allocated """
if self.order.status == SalesOrderStatus.SHIPPED:
return self.fulfilled_quantity() >= self.quantity
return self.allocated_quantity() >= self.quantity
def is_over_allocated(self):
@ -561,12 +570,8 @@ class SalesOrderAllocation(models.Model):
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, help_text=_('Enter stock allocation quantity'))
def get_allocated(self):
""" String representation of the allocated quantity """
if self.item.serial and self.quantity == 1:
return "# {sn}".format(sn=self.item.serial)
else:
return normalize(self.quantity)
def get_serial(self):
return self.item.serial
def get_location(self):
return self.item.location.id if self.item.location else None

View File

@ -170,7 +170,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
location_path = serializers.CharField(source='get_location_path')
location_id = serializers.IntegerField(source='get_location')
allocated = serializers.CharField(source='get_allocated')
serial = serializers.CharField(source='get_serial')
quantity = serializers.FloatField()
class Meta:
model = SalesOrderAllocation
@ -178,7 +179,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
fields = [
'pk',
'line',
'allocated',
'serial',
'quantity',
'location_id',
'location_path',
'item',

View File

@ -62,7 +62,15 @@ function showAllocationSubTable(index, row, element) {
field: 'allocated',
title: 'Quantity',
formatter: function(value, row, index, field) {
return renderLink(value, `/stock/item/${row.item}/`);
var text = '';
if (row.serial != null && row.quantity == 1) {
text = `{% trans "Serial Number" %}: ${row.serial}`;
} else {
text = `{% trans "Quantity" %}: ${row.quantity}`;
}
return renderLink(text, `/stock/item/${row.item}/`);
},
},
{
@ -138,7 +146,7 @@ function showFulfilledSubTable(index, row, element) {
field: 'stock',
formatter: function(value, row) {
var text = '';
if (row.serial) {
if (row.serial && row.quantity == 1) {
text = `{% trans "Serial Number" %}: ${row.serial}`;
} else {
text = `{% trans "Quantity" %}: ${row.quantity}`;

View File

@ -0,0 +1,138 @@
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from company.models import Company
from stock.models import StockItem
from order.models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation
from part.models import Part
from InvenTree import status_codes as status
class SalesOrderTest(TestCase):
"""
Run tests to ensure that the SalesOrder model is working correctly.
"""
def setUp(self):
# Create a Company to ship the goods to
self.customer = Company.objects.create(name="ABC Co", description="My customer", is_customer=True)
# Create a Part to ship
self.part = Part.objects.create(name='Spanner', salable=True, description='A spanner that I sell')
# Create some stock!
StockItem.objects.create(part=self.part, quantity=100)
StockItem.objects.create(part=self.part, quantity=200)
# Create a SalesOrder to ship against
self.order = SalesOrder.objects.create(
customer=self.customer,
reference='1234',
customer_reference='ABC 55555'
)
# Create a line item
self.line = SalesOrderLineItem.objects.create(quantity=50, order=self.order, part=self.part)
def test_empty_order(self):
self.assertEqual(self.line.quantity, 50)
self.assertEqual(self.line.allocated_quantity(), 0)
self.assertEqual(self.line.fulfilled_quantity(), 0)
self.assertFalse(self.line.is_fully_allocated())
self.assertFalse(self.line.is_over_allocated())
self.assertTrue(self.order.is_pending)
self.assertFalse(self.order.is_fully_allocated())
def test_add_duplicate_line_item(self):
# Adding a duplicate line item to a SalesOrder must throw an error
with self.assertRaises(IntegrityError):
SalesOrderLineItem.objects.create(order=self.order, part=self.part)
def allocate_stock(self, full=True):
# Allocate stock to the order
SalesOrderAllocation.objects.create(
line=self.line,
item=StockItem.objects.get(pk=1),
quantity=25)
SalesOrderAllocation.objects.create(
line=self.line,
item=StockItem.objects.get(pk=2),
quantity=25 if full else 20
)
def test_allocate_partial(self):
# Partially allocate stock
self.allocate_stock(False)
self.assertFalse(self.order.is_fully_allocated())
self.assertFalse(self.line.is_fully_allocated())
self.assertEqual(self.line.allocated_quantity(), 45)
self.assertEqual(self.line.fulfilled_quantity(), 0)
def test_allocate_full(self):
# Fully allocate stock
self.allocate_stock(True)
self.assertTrue(self.order.is_fully_allocated())
self.assertTrue(self.line.is_fully_allocated())
self.assertEqual(self.line.allocated_quantity(), 50)
def test_order_cancel(self):
# Allocate line items then cancel the order
self.allocate_stock(True)
self.assertEqual(SalesOrderAllocation.objects.count(), 2)
self.assertEqual(self.order.status, status.SalesOrderStatus.PENDING)
self.order.cancel_order()
self.assertEqual(SalesOrderAllocation.objects.count(), 0)
self.assertEqual(self.order.status, status.SalesOrderStatus.CANCELLED)
# Now try to ship it - should fail
with self.assertRaises(ValidationError):
self.order.ship_order(None)
def test_ship_order(self):
# Allocate line items, then ship the order
# Assert some stuff before we run the test
# Initially there are two stock items
self.assertEqual(StockItem.objects.count(), 2)
# Take 25 units from each StockItem
self.allocate_stock(True)
self.assertEqual(SalesOrderAllocation.objects.count(), 2)
self.order.ship_order(None)
# There should now be 4 stock items
self.assertEqual(StockItem.objects.count(), 4)
self.assertEqual(StockItem.objects.get(pk=1).quantity, 75)
self.assertEqual(StockItem.objects.get(pk=2).quantity, 175)
self.assertEqual(StockItem.objects.get(pk=3).quantity, 25)
self.assertEqual(StockItem.objects.get(pk=3).quantity, 25)
self.assertEqual(StockItem.objects.get(pk=1).sales_order, None)
self.assertEqual(StockItem.objects.get(pk=2).sales_order, None)
self.assertEqual(StockItem.objects.get(pk=3).sales_order, self.order)
self.assertEqual(StockItem.objects.get(pk=4).sales_order, self.order)
# And no allocations
self.assertEqual(SalesOrderAllocation.objects.count(), 0)
self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED)
self.assertTrue(self.order.is_fully_allocated())
self.assertTrue(self.line.is_fully_allocated())
self.assertEqual(self.line.fulfilled_quantity(), 50)
self.assertEqual(self.line.allocated_quantity(), 0)