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,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # 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):
 | 
			
		||||
        """
 | 
			
		||||
        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 build.models import Build, BuildItem, get_next_build_number
 | 
			
		||||
from stock.models import StockItem
 | 
			
		||||
from part.models import Part, BomItem
 | 
			
		||||
from stock.models import StockItem
 | 
			
		||||
from stock.tasks import delete_old_stock_items
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BuildTest(TestCase):
 | 
			
		||||
@@ -352,6 +353,11 @@ class BuildTest(TestCase):
 | 
			
		||||
        # the original BuildItem objects should have been deleted!
 | 
			
		||||
        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!
 | 
			
		||||
        self.assertEqual(StockItem.objects.count(), 7)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -653,6 +653,9 @@ class StockList(generics.ListCreateAPIView):
 | 
			
		||||
 | 
			
		||||
        queryset = StockItemSerializer.annotate_queryset(queryset)
 | 
			
		||||
 | 
			
		||||
        # Do not expose StockItem objects which are scheduled for deletion
 | 
			
		||||
        queryset = queryset.filter(scheduled_for_deletion=False)
 | 
			
		||||
 | 
			
		||||
        return 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,
 | 
			
		||||
        customer=None,
 | 
			
		||||
        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
 | 
			
		||||
    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):
 | 
			
		||||
        """
 | 
			
		||||
        Save this StockItem to the database. Performs a number of checks:
 | 
			
		||||
@@ -588,6 +594,12 @@ class StockItem(MPTTModel):
 | 
			
		||||
                              help_text=_('Select Owner'),
 | 
			
		||||
                              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):
 | 
			
		||||
        """
 | 
			
		||||
        Returns True if this Stock item is "stale".
 | 
			
		||||
@@ -1294,9 +1306,8 @@ class StockItem(MPTTModel):
 | 
			
		||||
        self.quantity = quantity
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        else:
 | 
			
		||||
            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)
 | 
			
		||||
        w2 = StockItem.objects.get(pk=101)
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(w2.scheduled_for_deletion)
 | 
			
		||||
 | 
			
		||||
        # Take 25 units from w1 (there are only 10 in stock)
 | 
			
		||||
        w1.take_stock(30, None, notes='Took 30')
 | 
			
		||||
 | 
			
		||||
@@ -342,6 +344,16 @@ class StockTest(TestCase):
 | 
			
		||||
        # Take 25 units from w2 (will be deleted)
 | 
			
		||||
        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):
 | 
			
		||||
            w2 = StockItem.objects.get(pk=101)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user