mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
dd4f5d4630
commit
35defe78c0
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user