2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +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

@ -15,28 +15,32 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from sql_util.utils import SubqueryCount, SubquerySum
import common.models
import company.models
import InvenTree.helpers
import InvenTree.status
import part.filters
import part.tasks
import stock.models
from common.settings import currency_code_default, currency_code_mappings
from InvenTree.serializers import (DataFileExtractSerializer,
DataFileUploadSerializer,
InvenTreeAttachmentSerializer,
InvenTreeAttachmentSerializerField,
InvenTreeCurrencySerializer,
InvenTreeDecimalField,
InvenTreeImageSerializerField,
InvenTreeModelSerializer,
InvenTreeMoneySerializer, RemoteImageMixin,
UserSerializer)
from InvenTree.status_codes import BuildStatus
from InvenTree.tasks import offload_task
from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
PartCategory, PartCategoryParameterTemplate,
PartInternalPriceBreak, PartParameter,
PartParameterTemplate, PartPricing, PartRelated,
PartSellPriceBreak, PartStar, PartStocktake,
PartTestTemplate)
PartStocktakeReport, PartTestTemplate)
class CategorySerializer(InvenTreeModelSerializer):
@ -137,16 +141,9 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
quantity = InvenTreeDecimalField()
price = InvenTreeMoneySerializer(
allow_null=True
)
price = InvenTreeMoneySerializer(allow_null=True)
price_currency = serializers.ChoiceField(
choices=currency_code_mappings(),
default=currency_code_default,
label=_('Currency'),
help_text=_('Purchase currency of this stock item'),
)
price_currency = InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item'))
class Meta:
"""Metaclass defining serializer fields"""
@ -169,12 +166,7 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
allow_null=True
)
price_currency = serializers.ChoiceField(
choices=currency_code_mappings(),
default=currency_code_default,
label=_('Currency'),
help_text=_('Purchase currency of this stock item'),
)
price_currency = InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item'))
class Meta:
"""Metaclass defining serializer fields"""
@ -720,6 +712,12 @@ class PartStocktakeSerializer(InvenTreeModelSerializer):
user_detail = UserSerializer(source='user', read_only=True, many=False)
cost_min = InvenTreeMoneySerializer(allow_null=True)
cost_min_currency = InvenTreeCurrencySerializer()
cost_max = InvenTreeMoneySerializer(allow_null=True)
cost_max_currency = InvenTreeCurrencySerializer()
class Meta:
"""Metaclass options"""
@ -728,7 +726,12 @@ class PartStocktakeSerializer(InvenTreeModelSerializer):
'pk',
'date',
'part',
'item_count',
'quantity',
'cost_min',
'cost_min_currency',
'cost_max',
'cost_max_currency',
'note',
'user',
'user_detail',
@ -751,6 +754,92 @@ class PartStocktakeSerializer(InvenTreeModelSerializer):
super().save()
class PartStocktakeReportSerializer(InvenTreeModelSerializer):
"""Serializer for stocktake report class"""
user_detail = UserSerializer(source='user', read_only=True, many=False)
report = InvenTreeAttachmentSerializerField(read_only=True)
class Meta:
"""Metaclass defines serializer fields"""
model = PartStocktakeReport
fields = [
'pk',
'date',
'report',
'part_count',
'user',
'user_detail',
]
class PartStocktakeReportGenerateSerializer(serializers.Serializer):
"""Serializer class for manually generating a new PartStocktakeReport via the API"""
part = serializers.PrimaryKeyRelatedField(
queryset=Part.objects.all(),
required=False, allow_null=True,
label=_('Part'), help_text=_('Limit stocktake report to a particular part, and any variant parts')
)
category = serializers.PrimaryKeyRelatedField(
queryset=PartCategory.objects.all(),
required=False, allow_null=True,
label=_('Category'), help_text=_('Limit stocktake report to a particular part category, and any child categories'),
)
location = serializers.PrimaryKeyRelatedField(
queryset=stock.models.StockLocation.objects.all(),
required=False, allow_null=True,
label=_('Location'), help_text=_('Limit stocktake report to a particular stock location, and any child locations')
)
generate_report = serializers.BooleanField(
default=True,
label=_('Generate Report'),
help_text=_('Generate report file containing calculated stocktake data'),
)
update_parts = serializers.BooleanField(
default=True,
label=_('Update Parts'),
help_text=_('Update specified parts with calculated stocktake data')
)
def validate(self, data):
"""Custom validation for this serializer"""
# Stocktake functionality must be enabled
if not common.models.InvenTreeSetting.get_setting('STOCKTAKE_ENABLE', False):
raise serializers.ValidationError(_("Stocktake functionality is not enabled"))
# Check that background worker is running
if not InvenTree.status.is_worker_running():
raise serializers.ValidationError(_("Background worker check failed"))
return data
def save(self):
"""Saving this serializer instance requests generation of a new stocktake report"""
data = self.validated_data
user = self.context['request'].user
# Generate a new report
offload_task(
part.tasks.generate_stocktake_report,
force_async=True,
user=user,
part=data.get('part', None),
category=data.get('category', None),
location=data.get('location', None),
generate_report=data.get('generate_report', True),
update_parts=data.get('update_parts', True),
)
class PartPricingSerializer(InvenTreeModelSerializer):
"""Serializer for Part pricing information"""