mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	Merge pull request #2034 from SchrodingersGat/build-complete-scheduling
Build completion scheduling
This commit is contained in:
		@@ -63,6 +63,13 @@ class InvenTreeConfig(AppConfig):
 | 
				
			|||||||
            schedule_type=Schedule.DAILY,
 | 
					            schedule_type=Schedule.DAILY,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Delete "old" stock items
 | 
				
			||||||
 | 
					        InvenTree.tasks.schedule_task(
 | 
				
			||||||
 | 
					            'stock.tasks.delete_old_stock_items',
 | 
				
			||||||
 | 
					            schedule_type=Schedule.MINUTES,
 | 
				
			||||||
 | 
					            minutes=30,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_exchange_rates(self):
 | 
					    def update_exchange_rates(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Update exchange rates each time the server is started, *if*:
 | 
					        Update exchange rates each time the server is started, *if*:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,8 +8,9 @@ from django.db.utils import IntegrityError
 | 
				
			|||||||
from InvenTree import status_codes as status
 | 
					from InvenTree import status_codes as status
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from build.models import Build, BuildItem, get_next_build_number
 | 
					from build.models import Build, BuildItem, get_next_build_number
 | 
				
			||||||
from stock.models import StockItem
 | 
					 | 
				
			||||||
from part.models import Part, BomItem
 | 
					from part.models import Part, BomItem
 | 
				
			||||||
 | 
					from stock.models import StockItem
 | 
				
			||||||
 | 
					from stock.tasks import delete_old_stock_items
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BuildTest(TestCase):
 | 
					class BuildTest(TestCase):
 | 
				
			||||||
@@ -352,6 +353,11 @@ class BuildTest(TestCase):
 | 
				
			|||||||
        # the original BuildItem objects should have been deleted!
 | 
					        # the original BuildItem objects should have been deleted!
 | 
				
			||||||
        self.assertEqual(BuildItem.objects.count(), 0)
 | 
					        self.assertEqual(BuildItem.objects.count(), 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(StockItem.objects.count(), 8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Clean up old stock items
 | 
				
			||||||
 | 
					        delete_old_stock_items()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # New stock items should have been created!
 | 
					        # New stock items should have been created!
 | 
				
			||||||
        self.assertEqual(StockItem.objects.count(), 7)
 | 
					        self.assertEqual(StockItem.objects.count(), 7)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -653,6 +653,9 @@ class StockList(generics.ListCreateAPIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        queryset = StockItemSerializer.annotate_queryset(queryset)
 | 
					        queryset = StockItemSerializer.annotate_queryset(queryset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Do not expose StockItem objects which are scheduled for deletion
 | 
				
			||||||
 | 
					        queryset = queryset.filter(scheduled_for_deletion=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return queryset
 | 
					        return queryset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def filter_queryset(self, queryset):
 | 
					    def filter_queryset(self, queryset):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.4 on 2021-09-07 06:27
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('stock', '0065_auto_20210701_0509'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='stockitem',
 | 
				
			||||||
 | 
					            name='scheduled_for_deletion',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False, help_text='This StockItem will be deleted by the background worker', verbose_name='Scheduled for deletion'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -209,12 +209,18 @@ class StockItem(MPTTModel):
 | 
				
			|||||||
        belongs_to=None,
 | 
					        belongs_to=None,
 | 
				
			||||||
        customer=None,
 | 
					        customer=None,
 | 
				
			||||||
        is_building=False,
 | 
					        is_building=False,
 | 
				
			||||||
        status__in=StockStatus.AVAILABLE_CODES
 | 
					        status__in=StockStatus.AVAILABLE_CODES,
 | 
				
			||||||
 | 
					        scheduled_for_deletion=False,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # A query filter which can be used to filter StockItem objects which have expired
 | 
					    # A query filter which can be used to filter StockItem objects which have expired
 | 
				
			||||||
    EXPIRED_FILTER = IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=datetime.now().date())
 | 
					    EXPIRED_FILTER = IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=datetime.now().date())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def mark_for_deletion(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.scheduled_for_deletion = True
 | 
				
			||||||
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Save this StockItem to the database. Performs a number of checks:
 | 
					        Save this StockItem to the database. Performs a number of checks:
 | 
				
			||||||
@@ -588,6 +594,12 @@ class StockItem(MPTTModel):
 | 
				
			|||||||
                              help_text=_('Select Owner'),
 | 
					                              help_text=_('Select Owner'),
 | 
				
			||||||
                              related_name='stock_items')
 | 
					                              related_name='stock_items')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    scheduled_for_deletion = models.BooleanField(
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					        verbose_name=_('Scheduled for deletion'),
 | 
				
			||||||
 | 
					        help_text=_('This StockItem will be deleted by the background worker'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_stale(self):
 | 
					    def is_stale(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns True if this Stock item is "stale".
 | 
					        Returns True if this Stock item is "stale".
 | 
				
			||||||
@@ -1294,9 +1306,8 @@ class StockItem(MPTTModel):
 | 
				
			|||||||
        self.quantity = quantity
 | 
					        self.quantity = quantity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if quantity == 0 and self.delete_on_deplete and self.can_delete():
 | 
					        if quantity == 0 and self.delete_on_deplete and self.can_delete():
 | 
				
			||||||
 | 
					            self.mark_for_deletion()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # TODO - Do not actually "delete" stock at this point - instead give it a "DELETED" flag
 | 
					 | 
				
			||||||
            self.delete()
 | 
					 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            self.save()
 | 
					            self.save()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								InvenTree/stock/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								InvenTree/stock/tasks.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.exceptions import AppRegistryNotReady
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger = logging.getLogger('inventree')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def delete_old_stock_items():
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    This function removes StockItem objects which have been marked for deletion.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Bulk "delete" operations for database entries with foreign-key relationships
 | 
				
			||||||
 | 
					    can be pretty expensive, and thus can "block" the UI for a period of time.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Thus, instead of immediately deleting multiple StockItems, some UI actions
 | 
				
			||||||
 | 
					    simply mark each StockItem as "scheduled for deletion".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The background worker then manually deletes these at a later stage
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        from stock.models import StockItem
 | 
				
			||||||
 | 
					    except AppRegistryNotReady:
 | 
				
			||||||
 | 
					        logger.info("Could not delete scheduled StockItems - AppRegistryNotReady")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    items = StockItem.objects.filter(scheduled_for_deletion=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if items.count() > 0:
 | 
				
			||||||
 | 
					        logger.info(f"Removing {items.count()} StockItem objects scheduled for deletion")
 | 
				
			||||||
 | 
					        items.delete()
 | 
				
			||||||
@@ -332,6 +332,8 @@ class StockTest(TestCase):
 | 
				
			|||||||
        w1 = StockItem.objects.get(pk=100)
 | 
					        w1 = StockItem.objects.get(pk=100)
 | 
				
			||||||
        w2 = StockItem.objects.get(pk=101)
 | 
					        w2 = StockItem.objects.get(pk=101)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertFalse(w2.scheduled_for_deletion)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Take 25 units from w1 (there are only 10 in stock)
 | 
					        # Take 25 units from w1 (there are only 10 in stock)
 | 
				
			||||||
        w1.take_stock(30, None, notes='Took 30')
 | 
					        w1.take_stock(30, None, notes='Took 30')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -342,6 +344,16 @@ class StockTest(TestCase):
 | 
				
			|||||||
        # Take 25 units from w2 (will be deleted)
 | 
					        # Take 25 units from w2 (will be deleted)
 | 
				
			||||||
        w2.take_stock(30, None, notes='Took 30')
 | 
					        w2.take_stock(30, None, notes='Took 30')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # w2 should now be marked for future deletion
 | 
				
			||||||
 | 
					        w2 = StockItem.objects.get(pk=101)
 | 
				
			||||||
 | 
					        self.assertTrue(w2.scheduled_for_deletion)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        from stock.tasks import delete_old_stock_items
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Now run the "background task" to delete these stock items
 | 
				
			||||||
 | 
					        delete_old_stock_items()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # This StockItem should now have been deleted
 | 
				
			||||||
        with self.assertRaises(StockItem.DoesNotExist):
 | 
					        with self.assertRaises(StockItem.DoesNotExist):
 | 
				
			||||||
            w2 = StockItem.objects.get(pk=101)
 | 
					            w2 = StockItem.objects.get(pk=101)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user