mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	Stocktake location filter (#5185)
* Pass specified location to "perform_stocktake" * Separately track total stocktake and location stocktake data * Catch any exception
This commit is contained in:
		@@ -10,7 +10,6 @@ from django.core.files.base import ContentFile
 | 
				
			|||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import tablib
 | 
					import tablib
 | 
				
			||||||
from djmoney.contrib.exchange.exceptions import MissingRate
 | 
					 | 
				
			||||||
from djmoney.contrib.exchange.models import convert_money
 | 
					from djmoney.contrib.exchange.models import convert_money
 | 
				
			||||||
from djmoney.money import Money
 | 
					from djmoney.money import Money
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,11 +31,21 @@ def perform_stocktake(target: part.models.Part, user: User, note: str = '', comm
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    kwargs:
 | 
					    kwargs:
 | 
				
			||||||
        exclude_external: If True, exclude stock items in external locations (default = False)
 | 
					        exclude_external: If True, exclude stock items in external locations (default = False)
 | 
				
			||||||
 | 
					        location: Optional StockLocation to filter results for generated report
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Returns:
 | 
					    Returns:
 | 
				
			||||||
        PartStocktake: A new PartStocktake model instance (for the specified Part)
 | 
					        PartStocktake: A new PartStocktake model instance (for the specified Part)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note that while we record a *total stocktake* for the Part instance which gets saved to the database,
 | 
				
			||||||
 | 
					    the user may have requested a stocktake limited to a particular location.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    In this case, the stocktake *report* will be limited to the specified location.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Determine which locations are "valid" for the generated report
 | 
				
			||||||
 | 
					    location = kwargs.get('location', None)
 | 
				
			||||||
 | 
					    locations = location.get_descendants(include_self=True) if location else []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Grab all "available" stock items for the Part
 | 
					    # Grab all "available" stock items for the Part
 | 
				
			||||||
    # We do not include variant stock when performing a stocktake,
 | 
					    # We do not include variant stock when performing a stocktake,
 | 
				
			||||||
    # otherwise the stocktake entries will be duplicated
 | 
					    # otherwise the stocktake entries will be duplicated
 | 
				
			||||||
@@ -58,41 +67,59 @@ def perform_stocktake(target: part.models.Part, user: User, note: str = '', comm
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    base_currency = common.settings.currency_code_default()
 | 
					    base_currency = common.settings.currency_code_default()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Keep track of total quantity and cost for this part
 | 
				
			||||||
    total_quantity = 0
 | 
					    total_quantity = 0
 | 
				
			||||||
    total_cost_min = Money(0, base_currency)
 | 
					    total_cost_min = Money(0, base_currency)
 | 
				
			||||||
    total_cost_max = Money(0, base_currency)
 | 
					    total_cost_max = Money(0, base_currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Separately, keep track of stock quantity and value within the specified location
 | 
				
			||||||
 | 
					    location_item_count = 0
 | 
				
			||||||
 | 
					    location_quantity = 0
 | 
				
			||||||
 | 
					    location_cost_min = Money(0, base_currency)
 | 
				
			||||||
 | 
					    location_cost_max = Money(0, base_currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for entry in stock_entries:
 | 
					    for entry in stock_entries:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Update total quantity value
 | 
					        entry_cost_min = None
 | 
				
			||||||
        total_quantity += entry.quantity
 | 
					        entry_cost_max = None
 | 
				
			||||||
 | 
					 | 
				
			||||||
        has_pricing = False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Update price range values
 | 
					        # Update price range values
 | 
				
			||||||
        if entry.purchase_price:
 | 
					        if entry.purchase_price:
 | 
				
			||||||
            # If purchase price is available, use that
 | 
					            entry_cost_min = entry.purchase_price
 | 
				
			||||||
            try:
 | 
					            entry_cost_max = entry.purchase_price
 | 
				
			||||||
                pp = convert_money(entry.purchase_price, base_currency) * entry.quantity
 | 
					 | 
				
			||||||
                total_cost_min += pp
 | 
					 | 
				
			||||||
                total_cost_max += pp
 | 
					 | 
				
			||||||
                has_pricing = True
 | 
					 | 
				
			||||||
            except MissingRate:
 | 
					 | 
				
			||||||
                logger.warning(f"MissingRate exception occurred converting {entry.purchase_price} to {base_currency}")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not has_pricing:
 | 
					        else:
 | 
				
			||||||
            # Fall back to the part pricing data
 | 
					            # If no purchase price is available, fall back to the part pricing data
 | 
				
			||||||
            p_min = pricing.overall_min or pricing.overall_max
 | 
					            entry_cost_min = pricing.overall_min or pricing.overall_max
 | 
				
			||||||
            p_max = pricing.overall_max or pricing.overall_min
 | 
					            entry_cost_max = pricing.overall_max or pricing.overall_min
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if p_min or p_max:
 | 
					        # Convert to base currency
 | 
				
			||||||
                try:
 | 
					        try:
 | 
				
			||||||
                    total_cost_min += convert_money(p_min, base_currency) * entry.quantity
 | 
					            entry_cost_min = convert_money(entry_cost_min, base_currency) * entry.quantity
 | 
				
			||||||
                    total_cost_max += convert_money(p_max, base_currency) * entry.quantity
 | 
					            entry_cost_max = convert_money(entry_cost_max, base_currency) * entry.quantity
 | 
				
			||||||
                except MissingRate:
 | 
					        except Exception:
 | 
				
			||||||
                    logger.warning(f"MissingRate exception occurred converting {p_min}:{p_max} to {base_currency}")
 | 
					            logger.warning(f"Could not convert {entry.purchase_price} to {base_currency}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            entry_cost_min = Money(0, base_currency)
 | 
				
			||||||
 | 
					            entry_cost_max = Money(0, base_currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Update total cost values
 | 
				
			||||||
 | 
					        total_quantity += entry.quantity
 | 
				
			||||||
 | 
					        total_cost_min += entry_cost_min
 | 
				
			||||||
 | 
					        total_cost_max += entry_cost_max
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test if this stock item is within the specified location
 | 
				
			||||||
 | 
					        if location and entry.location not in locations:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Update location cost values
 | 
				
			||||||
 | 
					        location_item_count += 1
 | 
				
			||||||
 | 
					        location_quantity += entry.quantity
 | 
				
			||||||
 | 
					        location_cost_min += entry_cost_min
 | 
				
			||||||
 | 
					        location_cost_max += entry_cost_max
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Construct PartStocktake instance
 | 
					    # Construct PartStocktake instance
 | 
				
			||||||
 | 
					    # Note that we use the *total* values for the PartStocktake instance
 | 
				
			||||||
    instance = part.models.PartStocktake(
 | 
					    instance = part.models.PartStocktake(
 | 
				
			||||||
        part=target,
 | 
					        part=target,
 | 
				
			||||||
        item_count=stock_entries.count(),
 | 
					        item_count=stock_entries.count(),
 | 
				
			||||||
@@ -106,6 +133,12 @@ def perform_stocktake(target: part.models.Part, user: User, note: str = '', comm
 | 
				
			|||||||
    if commit:
 | 
					    if commit:
 | 
				
			||||||
        instance.save()
 | 
					        instance.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add location-specific data to the instance
 | 
				
			||||||
 | 
					    instance.location_item_count = location_item_count
 | 
				
			||||||
 | 
					    instance.location_quantity = location_quantity
 | 
				
			||||||
 | 
					    instance.location_cost_min = location_cost_min
 | 
				
			||||||
 | 
					    instance.location_cost_max = location_cost_max
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return instance
 | 
					    return instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -211,7 +244,11 @@ def generate_stocktake_report(**kwargs):
 | 
				
			|||||||
    for p in parts:
 | 
					    for p in parts:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create a new stocktake for this part (do not commit, this will take place later on)
 | 
					        # Create a new stocktake for this part (do not commit, this will take place later on)
 | 
				
			||||||
        stocktake = perform_stocktake(p, user, commit=False, exclude_external=exclude_external)
 | 
					        stocktake = perform_stocktake(
 | 
				
			||||||
 | 
					            p, user, commit=False,
 | 
				
			||||||
 | 
					            exclude_external=exclude_external,
 | 
				
			||||||
 | 
					            location=location,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if stocktake.quantity == 0:
 | 
					        if stocktake.quantity == 0:
 | 
				
			||||||
            # Skip rows with zero total quantity
 | 
					            # Skip rows with zero total quantity
 | 
				
			||||||
@@ -228,10 +265,10 @@ def generate_stocktake_report(**kwargs):
 | 
				
			|||||||
            p.description,
 | 
					            p.description,
 | 
				
			||||||
            p.category.pk if p.category else '',
 | 
					            p.category.pk if p.category else '',
 | 
				
			||||||
            p.category.name if p.category else '',
 | 
					            p.category.name if p.category else '',
 | 
				
			||||||
            stocktake.item_count,
 | 
					            stocktake.location_item_count,
 | 
				
			||||||
            stocktake.quantity,
 | 
					            stocktake.location_quantity,
 | 
				
			||||||
            InvenTree.helpers.normalize(stocktake.cost_min.amount),
 | 
					            InvenTree.helpers.normalize(stocktake.location_cost_min.amount),
 | 
				
			||||||
            InvenTree.helpers.normalize(stocktake.cost_max.amount),
 | 
					            InvenTree.helpers.normalize(stocktake.location_cost_max.amount),
 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Save a new PartStocktakeReport instance
 | 
					    # Save a new PartStocktakeReport instance
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user