mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-05 13:10:57 +00:00
[Feature] Stocktake reports (#4345)
* Add settings to control upcoming stocktake features * Adds migration for "cost range" when performing stocktake * Add cost data to PartStocktakeSerializer Implement a new custom serializer for currency data type * Refactor existing currency serializers * Update stocktake table and forms * Prevent trailing zeroes in forms * Calculate cost range when adding manual stocktake entry * Display interactive chart for part stocktake history * Ensure chart data are converted to common currency * Adds new model for building stocktake reports * Add admin integration for new model * Adds API endpoint to expose list of stocktake reports available for download - No ability to edit or delete via API * Add setting to control automated deletion of old stocktake reports * Updates for settings page - Load part stocktake report table - Refactor function to render a downloadable media file - Fix bug with forcing files to be downloaded - Split js code into separate templates - Make use of onPanelLoad functionalitty * Fix conflicting migration files * Adds API endpoint for manual generation of stocktake report * Offload task to generate new stocktake report * Adds python function to perform stocktake on a single part instance * Small bug fixes * Various tweaks - Prevent new stocktake models from triggering plugin events when created - Construct a simple csv dataset * Generate new report * Updates for report generation - Prefetch related data - Add extra columns - Keep track of stocktake instances (for saving to database later on) * Updates: - Add confirmation message - Serializer validation checks * Ensure that background worker is running before manually scheduling a new stocktake report * Add extra fields to stocktake models Also move code from part/models.py to part/tasks.py * Add 'part_count' to PartStocktakeReport table * Updates for stocktake generation - remove old performStocktake javascript code - Now handled by automated server-side calculation - Generate report for a single part * Add a new "role" for stocktake - Allows fine-grained control on viewing / creating / deleting stocktake data - More in-line with existing permission controls - Remove STOCKTAKE_OWNER setting * Add serializer field to limit stocktake report to particular locations * Use location restriction when generating a stocktake report * Add UI buttons to perform stocktake for a whole category tree * Add button to perform stocktake report for a location tree * Adds a background tasks to handle periodic generation of stocktake reports - Reports are generated at fixed intervals - Deletes old reports after certain number of days * Implement notifications for new stocktake reports - If manually requested by a user, notify that user - Cleanup notification table - Amend PartStocktakeModel for better notification rendering * Hide buttons on location and category page if stocktake is not enabled * Cleanup log messages during server start * Extend functionality of RoleRequired permission mixin - Allow 'role_required' attribute to be added to an API view - Useful when using a serializer class that does not have a model defined * Add boolean option to toggle whether a report will be generated * Update generateStocktake function * Improve location filtering - Don't limit the actual stock items - Instead, select only parts which exist within a given location tree * Update API version * String tweaks * Fix permissions for PartStocktake API * More unit testing for stocktake functionality * QoL fix * Fix for assigning inherited permissions
This commit is contained in:
@ -2335,7 +2335,7 @@ class PartPricing(common.models.MetaMixin):
|
||||
force_async=True
|
||||
)
|
||||
|
||||
def update_pricing(self, counter: int = 0):
|
||||
def update_pricing(self, counter: int = 0, cascade: bool = True):
|
||||
"""Recalculate all cost data for the referenced Part instance"""
|
||||
|
||||
if self.pk is not None:
|
||||
@ -2362,8 +2362,9 @@ class PartPricing(common.models.MetaMixin):
|
||||
pass
|
||||
|
||||
# Update parent assemblies and templates
|
||||
self.update_assemblies(counter)
|
||||
self.update_templates(counter)
|
||||
if cascade:
|
||||
self.update_assemblies(counter)
|
||||
self.update_templates(counter)
|
||||
|
||||
def update_assemblies(self, counter: int = 0):
|
||||
"""Schedule updates for any assemblies which use this part"""
|
||||
@ -2890,6 +2891,7 @@ class PartStocktake(models.Model):
|
||||
A 'stocktake' is a representative count of available stock:
|
||||
- Performed on a given date
|
||||
- Records quantity of part in stock (across multiple stock items)
|
||||
- Records estimated value of "stock on hand"
|
||||
- Records user information
|
||||
"""
|
||||
|
||||
@ -2901,6 +2903,12 @@ class PartStocktake(models.Model):
|
||||
help_text=_('Part for stocktake'),
|
||||
)
|
||||
|
||||
item_count = models.IntegerField(
|
||||
default=1,
|
||||
verbose_name=_('Item Count'),
|
||||
help_text=_('Number of individual stock entries at time of stocktake'),
|
||||
)
|
||||
|
||||
quantity = models.DecimalField(
|
||||
max_digits=19, decimal_places=5,
|
||||
validators=[MinValueValidator(0)],
|
||||
@ -2929,6 +2937,18 @@ class PartStocktake(models.Model):
|
||||
help_text=_('User who performed this stocktake'),
|
||||
)
|
||||
|
||||
cost_min = InvenTree.fields.InvenTreeModelMoneyField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_('Minimum Stock Cost'),
|
||||
help_text=_('Estimated minimum cost of stock on hand'),
|
||||
)
|
||||
|
||||
cost_max = InvenTree.fields.InvenTreeModelMoneyField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_('Maximum Stock Cost'),
|
||||
help_text=_('Estimated maximum cost of stock on hand'),
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=PartStocktake, dispatch_uid='post_save_stocktake')
|
||||
def update_last_stocktake(sender, instance, created, **kwargs):
|
||||
@ -2944,6 +2964,68 @@ def update_last_stocktake(sender, instance, created, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def save_stocktake_report(instance, filename):
|
||||
"""Save stocktake reports to the correct subdirectory"""
|
||||
|
||||
filename = os.path.basename(filename)
|
||||
return os.path.join('stocktake', 'report', filename)
|
||||
|
||||
|
||||
class PartStocktakeReport(models.Model):
|
||||
"""A PartStocktakeReport is a generated report which provides a summary of current stock on hand.
|
||||
|
||||
Reports are generated by the background worker process, and saved as .csv files for download.
|
||||
Background processing is preferred as (for very large datasets), report generation may take a while.
|
||||
|
||||
A report can be manually requested by a user, or automatically generated periodically.
|
||||
|
||||
When generating a report, the "parts" to be reported can be filtered, e.g. by "category".
|
||||
|
||||
A stocktake report contains the following information, with each row relating to a single Part instance:
|
||||
|
||||
- Number of individual stock items on hand
|
||||
- Total quantity of stock on hand
|
||||
- Estimated total cost of stock on hand (min:max range)
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
"""Construct a simple string representation for the report"""
|
||||
return os.path.basename(self.report.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return the URL for the associaed report file for download"""
|
||||
if self.report:
|
||||
return self.report.url
|
||||
else:
|
||||
return None
|
||||
|
||||
date = models.DateField(
|
||||
verbose_name=_('Date'),
|
||||
auto_now_add=True
|
||||
)
|
||||
|
||||
report = models.FileField(
|
||||
upload_to=save_stocktake_report,
|
||||
unique=False, blank=False,
|
||||
verbose_name=_('Report'),
|
||||
help_text=_('Stocktake report file (generated internally)'),
|
||||
)
|
||||
|
||||
part_count = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name=_('Part Count'),
|
||||
help_text=_('Number of parts covered by stocktake'),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User, blank=True, null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='stocktake_reports',
|
||||
verbose_name=_('User'),
|
||||
help_text=_('User who requested this stocktake report'),
|
||||
)
|
||||
|
||||
|
||||
class PartAttachment(InvenTreeAttachment):
|
||||
"""Model for storing file attachments against a Part object."""
|
||||
|
||||
|
Reference in New Issue
Block a user