mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add some unit testing for the SalesOrder model
This commit is contained in:
parent
5e309a62f7
commit
3685ca4b95
18
InvenTree/order/migrations/0032_auto_20200427_0044.py
Normal file
18
InvenTree/order/migrations/0032_auto_20200427_0044.py
Normal 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')},
|
||||||
|
),
|
||||||
|
]
|
@ -24,7 +24,7 @@ from stock import models as stock_models
|
|||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
|
|
||||||
from InvenTree.fields import RoundingDecimalField
|
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.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
||||||
from InvenTree.models import InvenTreeAttachment
|
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})
|
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):
|
def fulfilled_quantity(self):
|
||||||
"""
|
"""
|
||||||
Return the total stock quantity fulfilled against this line item.
|
Return the total stock quantity fulfilled against this line item.
|
||||||
@ -482,6 +487,10 @@ class SalesOrderLineItem(OrderLineItem):
|
|||||||
|
|
||||||
def is_fully_allocated(self):
|
def is_fully_allocated(self):
|
||||||
""" Return True if this line item is fully allocated """
|
""" 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
|
return self.allocated_quantity() >= self.quantity
|
||||||
|
|
||||||
def is_over_allocated(self):
|
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'))
|
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, help_text=_('Enter stock allocation quantity'))
|
||||||
|
|
||||||
def get_allocated(self):
|
def get_serial(self):
|
||||||
""" String representation of the allocated quantity """
|
return self.item.serial
|
||||||
if self.item.serial and self.quantity == 1:
|
|
||||||
return "# {sn}".format(sn=self.item.serial)
|
|
||||||
else:
|
|
||||||
return normalize(self.quantity)
|
|
||||||
|
|
||||||
def get_location(self):
|
def get_location(self):
|
||||||
return self.item.location.id if self.item.location else None
|
return self.item.location.id if self.item.location else None
|
||||||
|
@ -170,7 +170,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
location_path = serializers.CharField(source='get_location_path')
|
location_path = serializers.CharField(source='get_location_path')
|
||||||
location_id = serializers.IntegerField(source='get_location')
|
location_id = serializers.IntegerField(source='get_location')
|
||||||
allocated = serializers.CharField(source='get_allocated')
|
serial = serializers.CharField(source='get_serial')
|
||||||
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrderAllocation
|
model = SalesOrderAllocation
|
||||||
@ -178,7 +179,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'line',
|
'line',
|
||||||
'allocated',
|
'serial',
|
||||||
|
'quantity',
|
||||||
'location_id',
|
'location_id',
|
||||||
'location_path',
|
'location_path',
|
||||||
'item',
|
'item',
|
||||||
|
@ -62,7 +62,15 @@ function showAllocationSubTable(index, row, element) {
|
|||||||
field: 'allocated',
|
field: 'allocated',
|
||||||
title: 'Quantity',
|
title: 'Quantity',
|
||||||
formatter: function(value, row, index, field) {
|
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',
|
field: 'stock',
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
var text = '';
|
var text = '';
|
||||||
if (row.serial) {
|
if (row.serial && row.quantity == 1) {
|
||||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||||
} else {
|
} else {
|
||||||
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||||
|
138
InvenTree/order/test_sales_order.py
Normal file
138
InvenTree/order/test_sales_order.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user