From 8316086c61cb06247645c04457b38e3cc797cbd2 Mon Sep 17 00:00:00 2001 From: bloemp Date: Tue, 24 Jan 2023 23:34:15 +0100 Subject: [PATCH] Fix for returning stock from the customer to a specified location (#4208) * Fix for returning stock from the customer to a specified location When returning stock from the customer, the allocations of that stock are freed. If the new stock location is the same as the parent stock, merge the stock back into the parent. * Added tests for allocateToCustomer and return_from_customer * Added additional test that checks total stock * Fixed typos --- InvenTree/stock/models.py | 20 +++++++++-- InvenTree/stock/tests.py | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 69b960c6b0..b651580a21 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -974,7 +974,11 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel): @transaction.atomic def return_from_customer(self, location, user=None, **kwargs): - """Return stock item from customer, back into the specified location.""" + """Return stock item from customer, back into the specified location. + + If the selected location is the same as the parent, merge stock back into the parent. + Otherwise create the stock in the new location + """ notes = kwargs.get('notes', '') tracking_info = {} @@ -991,7 +995,10 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel): location=location ) + self.clearAllocations() self.customer = None + self.belongs_to = None + self.sales_order = None self.location = location trigger_event( @@ -999,7 +1006,16 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel): id=self.id, ) - self.save() + """If new location is the same as the parent location, merge this stock back in the parent""" + if self.parent and self.location == self.parent.location: + self.parent.merge_stock_items( + {self}, + user=user, + location=location, + notes=notes + ) + else: + self.save() def is_allocated(self): """Return True if this StockItem is allocated to a SalesOrder or a Build.""" diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index ddfc625ca9..d30c121ccf 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -8,8 +8,10 @@ from django.test import override_settings from build.models import Build from common.models import InvenTreeSetting +from company.models import Company from InvenTree.helpers import InvenTreeTestCase from InvenTree.status_codes import StockHistoryCode +from order.models import SalesOrder from part.models import Part from .models import (StockItem, StockItemTestResult, StockItemTracking, @@ -465,6 +467,75 @@ class StockTest(StockTestBase): self.assertFalse(it.add_stock(-10, None)) + def test_allocate_to_customer(self): + """Test allocating stock to a customer""" + it = StockItem.objects.get(pk=2) + n = it.quantity + an = n - 10 + customer = Company.objects.create(name="MyTestCompany") + order = SalesOrder.objects.create(description="Test order") + ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock') + + # Check if new stockitem is created + self.assertTrue(ait) + # Check correct quantity of new allocated stock + self.assertEqual(ait.quantity, an) + # Check if new stock is assigned to correct customer + self.assertEqual(ait.customer, customer) + # Check if new stock is assigned to correct sales order + self.assertEqual(ait.sales_order, order) + # Check location is None because this stock is now allocated to a user + self.assertFalse(ait.location) + + # Check that a tracking item was added + track = StockItemTracking.objects.filter(item=ait).latest('id') + + self.assertEqual(track.tracking_type, StockHistoryCode.SENT_TO_CUSTOMER) + self.assertIn('Allocated some stock', track.notes) + + def test_return_from_customer(self): + """Test removing previous allocated stock from customer""" + + it = StockItem.objects.get(pk=2) + + # First establish total stock for this part + allstock_before = StockItem.objects.filter(part=it.part).aggregate(Sum("quantity"))["quantity__sum"] + + n = it.quantity + an = n - 10 + customer = Company.objects.create(name="MyTestCompany") + order = SalesOrder.objects.create(description="Test order") + + ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock') + ait.return_from_customer(it.location, None, notes="Stock removed from customer") + + # When returned stock is returned to its original (parent) location, check that the parent has correct quantity + self.assertEqual(it.quantity, n) + + ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock') + ait.return_from_customer(self.drawer3, None, notes="Stock removed from customer") + + # Check correct assignment of the new location + self.assertEqual(ait.location, self.drawer3) + # We should be un allocated + self.assertFalse(ait.is_allocated()) + # No customer should be assigned + self.assertFalse(ait.customer) + # We dont belong to anyone + self.assertFalse(ait.belongs_to) + # Assigned sales order should be None + self.assertFalse(ait.sales_order) + + # Check that a tracking item was added + track = StockItemTracking.objects.filter(item=ait).latest('id') + + self.assertEqual(track.tracking_type, StockHistoryCode.RETURNED_FROM_CUSTOMER) + self.assertIn('Stock removed from customer', track.notes) + + # Establish total stock for the part after remove from customer to check that we still have the correct quantity in stock + allstock_after = StockItem.objects.filter(part=it.part).aggregate(Sum("quantity"))["quantity__sum"] + self.assertEqual(allstock_before, allstock_after) + def test_take_stock(self): """Test stock removal.""" it = StockItem.objects.get(pk=2)