2
0
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:
Oliver
2023-02-17 11:42:48 +11:00
committed by GitHub
parent e6c9db2ff3
commit 0f445ea6e4
45 changed files with 1700 additions and 713 deletions

View File

@ -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."""