mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
* Add new BuildLine model - Represents an instance of a BOM item against a BuildOrder * Create BuildLine instances automatically When a new Build is created, automatically generate new BuildLine items * Improve logic for handling exchange rate backends * logic fixes * Adds API endpoints Add list and detail API endpoints for new BuildLine model * update users/models.py - Add new model to roles definition * bulk-create on auto_allocate Save database hits by performing a bulk-create * Add skeleton data migration * Create BuildLines for existing orders * Working on building out BuildLine table * Adds link for "BuildLine" to "BuildItem" - A "BuildItem" will now be tracked against a BuildLine - Not tracked directly against a build - Not tracked directly against a BomItem - Add schema migration - Add data migration to update links * Adjust migration 0045 - bom_item and build fields are about to be removed - Set them to "nullable" so the data doesn't get removed * Remove old fields from BuildItem model - build fk - bom_item fk - A lot of other required changes too * Update BuildLine.bom_item field - Delete the BuildLine if the BomItem is removed - This is closer to current behaviour * Cleanup for Build model - tracked_bom_items -> tracked_line_items - untracked_bom_items -> tracked_bom_items - remove build.can_complete - move bom_item specific methods to the BuildLine model - Cleanup / consolidation * front-end work - Update javascript - Cleanup HTML templates * Add serializer annotation and filtering - Annotate 'allocated' quantity - Filter by allocated / trackable / optional / consumable * Make table sortable * Add buttons * Add callback for building new stock * Fix Part annotation * Adds callback to order parts * Allocation works again * template cleanup * Fix allocate / unallocate actions - Also turns out "unallocate" is not a word.. * auto-allocate works again * Fix call to build.is_over_allocated * Refactoring updates * Bump API version * Cleaner implementation of allocation sub-table * Fix rendering in build output table * Improvements to StockItem list API - Refactor very old code - Add option to include test results to queryset * Add TODO for later me * Fix for serializers.py * Working on cleaner implementation of build output table * Add function to determine if a single output is fully allocated * Updates to build.js - Button callbacks - Table rendering * Revert previous changes to build.serializers.py * Fix for forms.js * Rearrange code in build.js * Rebuild "allocated lines" for output table * Fix allocation calculation * Show or hide column for tracked parts * Improve debug messages * Refactor "loadBuildLineTable" - Allow it to also be used as output sub-table * Refactor "completed tests" column * Remove old javascript - Cleans up a *lot* of crusty old code * Annotate the available stock quantity to BuildLine serializer - Similar pattern to BomItem serializer - Needs refactoring in the future * Update available column * Fix build allocation table - Bug fix - Make pretty * linting fixes * Allow sorting by available stock * Tweak for "required tests" column * Bug fix for completing a build output * Fix for consumable stock * Fix for trim_allocated_stock * Fix for creating new build * Migration fix - Ensure initial django_q migrations are applied - Why on earth is this failing now? * Catch exception * Update for exception handling * Update migrations - Ensure inventreesetting is added * Catch all exceptions when getting default currency code * Bug fix for currency exchange rates update * Working on unit tests * Unit test fixes * More work on unit tests * Use bulk_create in unit test * Update required quantity when a BuildOrder is saved * Tweak overage display in BOM table * Fix icon in BOM table * Fix spelling error * More unit test fixes * Build reports - Add line_items - Update docs - Cleanup * Reimplement is_partially_allocated method * Update docs about overage * Unit testing for data migration * Add "required_for_build_orders" annotation - Makes API query *much* faster now - remove old "required_parts_to_complete_build" method - Cleanup part API filter code * Adjust order of fixture loading * Fix unit test * Prevent "schedule_pricing_update" in unit tests - Should cut down on DB hits significantly * Unit test updates * Improvements for unit test - Don't hard-code pk values - postgresql no likey * Better unit test
324 lines
11 KiB
Python
324 lines
11 KiB
Python
"""Unit tests for the SalesOrder models"""
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import Group
|
|
from django.core.exceptions import ValidationError
|
|
from django.test import TestCase
|
|
|
|
import order.tasks
|
|
from common.models import InvenTreeSetting, NotificationMessage
|
|
from company.models import Company
|
|
from InvenTree import status_codes as status
|
|
from order.models import (SalesOrder, SalesOrderAllocation,
|
|
SalesOrderExtraLine, SalesOrderLineItem,
|
|
SalesOrderShipment)
|
|
from part.models import Part
|
|
from stock.models import StockItem
|
|
from users.models import Owner
|
|
|
|
|
|
class SalesOrderTest(TestCase):
|
|
"""Run tests to ensure that the SalesOrder model is working correctly."""
|
|
|
|
fixtures = [
|
|
'users',
|
|
]
|
|
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
"""Initial setup for this set of unit tests"""
|
|
# Create a Company to ship the goods to
|
|
cls.customer = Company.objects.create(name="ABC Co", description="My customer", is_customer=True)
|
|
|
|
# Create a Part to ship
|
|
cls.part = Part.objects.create(name='Spanner', salable=True, description='A spanner that I sell')
|
|
|
|
# Create some stock!
|
|
cls.Sa = StockItem.objects.create(part=cls.part, quantity=100)
|
|
cls.Sb = StockItem.objects.create(part=cls.part, quantity=200)
|
|
|
|
# Create a SalesOrder to ship against
|
|
cls.order = SalesOrder.objects.create(
|
|
customer=cls.customer,
|
|
reference='SO-1234',
|
|
customer_reference='ABC 55555'
|
|
)
|
|
|
|
# Create a Shipment against this SalesOrder
|
|
cls.shipment = SalesOrderShipment.objects.create(
|
|
order=cls.order,
|
|
reference='SO-001',
|
|
)
|
|
|
|
# Create a line item
|
|
cls.line = SalesOrderLineItem.objects.create(quantity=50, order=cls.order, part=cls.part)
|
|
|
|
# Create an extra line
|
|
cls.extraline = SalesOrderExtraLine.objects.create(quantity=1, order=cls.order, reference="Extra line")
|
|
|
|
def test_so_reference(self):
|
|
"""Unit tests for sales order generation"""
|
|
|
|
# Test that a good reference is created when we have no existing orders
|
|
SalesOrder.objects.all().delete()
|
|
|
|
self.assertEqual(SalesOrder.generate_reference(), 'SO-0001')
|
|
|
|
def test_rebuild_reference(self):
|
|
"""Test that the 'reference_int' field gets rebuilt when the model is saved"""
|
|
|
|
self.assertEqual(self.order.reference_int, 1234)
|
|
|
|
self.order.reference = '999'
|
|
self.order.save()
|
|
self.assertEqual(self.order.reference_int, 999)
|
|
|
|
self.order.reference = '1000K'
|
|
self.order.save()
|
|
self.assertEqual(self.order.reference_int, 1000)
|
|
|
|
def test_overdue(self):
|
|
"""Tests for overdue functionality."""
|
|
today = datetime.now().date()
|
|
|
|
# By default, order is *not* overdue as the target date is not set
|
|
self.assertFalse(self.order.is_overdue)
|
|
|
|
# Set target date in the past
|
|
self.order.target_date = today - timedelta(days=5)
|
|
self.order.save()
|
|
self.assertTrue(self.order.is_overdue)
|
|
|
|
# Set target date in the future
|
|
self.order.target_date = today + timedelta(days=5)
|
|
self.order.save()
|
|
self.assertFalse(self.order.is_overdue)
|
|
|
|
def test_empty_order(self):
|
|
"""Test for an empty order"""
|
|
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_overallocated())
|
|
|
|
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 is accepted"""
|
|
|
|
for ii in range(1, 5):
|
|
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii)
|
|
|
|
def allocate_stock(self, full=True):
|
|
"""Allocate stock to the order"""
|
|
SalesOrderAllocation.objects.create(
|
|
line=self.line,
|
|
shipment=self.shipment,
|
|
item=StockItem.objects.get(pk=self.Sa.pk),
|
|
quantity=25)
|
|
|
|
SalesOrderAllocation.objects.create(
|
|
line=self.line,
|
|
shipment=self.shipment,
|
|
item=StockItem.objects.get(pk=self.Sb.pk),
|
|
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)
|
|
|
|
with self.assertRaises(ValidationError):
|
|
self.order.can_complete(raise_error=True)
|
|
|
|
# Now try to ship it - should fail
|
|
result = self.order.complete_order(None)
|
|
self.assertFalse(result)
|
|
|
|
def test_complete_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)
|
|
|
|
# Attempt to complete the order (but shipments are not completed!)
|
|
result = self.order.complete_order(None)
|
|
|
|
self.assertFalse(result)
|
|
|
|
self.assertIsNone(self.shipment.shipment_date)
|
|
self.assertFalse(self.shipment.is_complete())
|
|
|
|
# Mark the shipments as complete
|
|
self.shipment.complete_shipment(None)
|
|
self.assertTrue(self.shipment.is_complete())
|
|
|
|
# Now, should be OK to ship
|
|
result = self.order.complete_order(None)
|
|
|
|
self.assertTrue(result)
|
|
|
|
self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED)
|
|
self.assertIsNotNone(self.order.shipment_date)
|
|
|
|
# There should now be 4 stock items
|
|
self.assertEqual(StockItem.objects.count(), 4)
|
|
|
|
sa = StockItem.objects.get(pk=self.Sa.pk)
|
|
sb = StockItem.objects.get(pk=self.Sb.pk)
|
|
|
|
# 25 units subtracted from each of the original items
|
|
self.assertEqual(sa.quantity, 75)
|
|
self.assertEqual(sb.quantity, 175)
|
|
|
|
# And 2 items created which are associated with the order
|
|
outputs = StockItem.objects.filter(sales_order=self.order)
|
|
self.assertEqual(outputs.count(), 2)
|
|
|
|
for item in outputs.all():
|
|
self.assertEqual(item.quantity, 25)
|
|
|
|
self.assertEqual(sa.sales_order, None)
|
|
self.assertEqual(sb.sales_order, None)
|
|
|
|
# And the allocations still exist
|
|
self.assertEqual(SalesOrderAllocation.objects.count(), 2)
|
|
|
|
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(), 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)
|
|
|
|
def test_shipment_delivery(self):
|
|
"""Test the shipment delivery settings"""
|
|
|
|
# Shipment delivery date should be empty before setting date
|
|
self.assertIsNone(self.shipment.delivery_date)
|
|
self.assertFalse(self.shipment.is_delivered())
|
|
|
|
def test_overdue_notification(self):
|
|
"""Test overdue sales order notification"""
|
|
|
|
self.order.created_by = get_user_model().objects.get(pk=3)
|
|
self.order.responsible = Owner.create(obj=Group.objects.get(pk=2))
|
|
self.order.target_date = datetime.now().date() - timedelta(days=1)
|
|
self.order.save()
|
|
|
|
# Check for overdue sales orders
|
|
order.tasks.check_overdue_sales_orders()
|
|
|
|
messages = NotificationMessage.objects.filter(
|
|
category='order.overdue_sales_order',
|
|
)
|
|
|
|
self.assertEqual(len(messages), 1)
|
|
|
|
def test_new_so_notification(self):
|
|
"""Test that a notification is sent when a new SalesOrder is created.
|
|
|
|
- The responsible user should receive a notification
|
|
- The creating user should *not* receive a notification
|
|
"""
|
|
|
|
SalesOrder.objects.create(
|
|
customer=self.customer,
|
|
reference='1234567',
|
|
created_by=get_user_model().objects.get(pk=3),
|
|
responsible=Owner.create(obj=Group.objects.get(pk=3))
|
|
)
|
|
|
|
messages = NotificationMessage.objects.filter(
|
|
category='order.new_salesorder',
|
|
)
|
|
|
|
# A notification should have been generated for user 4 (who is a member of group 3)
|
|
self.assertTrue(messages.filter(user__pk=4).exists())
|
|
|
|
# However *no* notification should have been generated for the creating user
|
|
self.assertFalse(messages.filter(user__pk=3).exists())
|
|
|
|
def test_metadata(self):
|
|
"""Unit tests for the metadata field."""
|
|
for model in [SalesOrder, SalesOrderLineItem, SalesOrderExtraLine, SalesOrderShipment]:
|
|
p = model.objects.first()
|
|
|
|
self.assertIsNone(p.get_metadata('test'))
|
|
self.assertEqual(p.get_metadata('test', backup_value=123), 123)
|
|
|
|
# Test update via the set_metadata() method
|
|
p.set_metadata('test', 3)
|
|
self.assertEqual(p.get_metadata('test'), 3)
|
|
|
|
for k in ['apple', 'banana', 'carrot', 'carrot', 'banana']:
|
|
p.set_metadata(k, k)
|
|
|
|
self.assertEqual(len(p.metadata.keys()), 4)
|