"""Admin for stock app.""" from django.contrib import admin from django.db.models import Count from django.utils.translation import gettext_lazy as _ from import_export import widgets from import_export.admin import ImportExportModelAdmin from import_export.fields import Field from build.models import Build from company.models import Company, SupplierPart from InvenTree.admin import InvenTreeResource from order.models import PurchaseOrder, SalesOrder from part.models import Part from .models import (StockItem, StockItemAttachment, StockItemTestResult, StockItemTracking, StockLocation, StockLocationType) class LocationResource(InvenTreeResource): """Class for managing StockLocation data import/export.""" class Meta: """Metaclass options.""" model = StockLocation skip_unchanged = True report_skipped = False clean_model_instances = True exclude = [ # Exclude MPTT internal model fields 'lft', 'rght', 'tree_id', 'level', 'metadata', 'barcode_data', 'barcode_hash', 'owner', 'icon', ] id = Field(attribute='id', column_name=_('Location ID'), widget=widgets.IntegerWidget()) name = Field(attribute='name', column_name=_('Location Name')) description = Field(attribute='description', column_name=_('Description')) parent = Field(attribute='parent', column_name=_('Parent ID'), widget=widgets.ForeignKeyWidget(StockLocation)) parent_name = Field(attribute='parent__name', column_name=_('Parent Name'), readonly=True) pathstring = Field(attribute='pathstring', column_name=_('Location Path')) # Calculated fields items = Field(attribute='item_count', column_name=_('Stock Items'), widget=widgets.IntegerWidget(), readonly=True) def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): """Rebuild after import to keep tree intact.""" super().after_import(dataset, result, using_transactions, dry_run, **kwargs) # Rebuild the StockLocation tree(s) StockLocation.objects.rebuild() class LocationInline(admin.TabularInline): """Inline for sub-locations.""" model = StockLocation class LocationAdmin(ImportExportModelAdmin): """Admin class for Location.""" resource_class = LocationResource list_display = ('name', 'pathstring', 'description') search_fields = ('name', 'description') inlines = [ LocationInline, ] autocomplete_fields = [ 'parent', ] class LocationTypeAdmin(admin.ModelAdmin): """Admin class for StockLocationType.""" list_display = ('name', 'description', 'icon', 'location_count') readonly_fields = ('location_count', ) def get_queryset(self, request): """Annotate queryset to fetch location count.""" return super().get_queryset(request).annotate( location_count=Count("stock_locations"), ) def location_count(self, obj): """Returns the number of locations this location type is assigned to.""" return obj.location_count class StockItemResource(InvenTreeResource): """Class for managing StockItem data import/export.""" class Meta: """Metaclass options.""" model = StockItem skip_unchanged = True report_skipped = False clean_model_instance = True exclude = [ # Exclude MPTT internal model fields 'lft', 'rght', 'tree_id', 'level', # Exclude internal fields 'serial_int', 'metadata', 'barcode_hash', 'barcode_data', 'owner', ] id = Field(attribute='pk', column_name=_('Stock Item ID'), widget=widgets.IntegerWidget()) part = Field(attribute='part', column_name=_('Part ID'), widget=widgets.ForeignKeyWidget(Part)) part_name = Field(attribute='part__full_name', column_name=_('Part Name'), readonly=True) quantity = Field(attribute='quantity', column_name=_('Quantity'), widget=widgets.DecimalWidget()) serial = Field(attribute='serial', column_name=_('Serial')) batch = Field(attribute='batch', column_name=_('Batch')) status_label = Field(attribute='status_label', column_name=_('Status'), readonly=True) status = Field(attribute='status', column_name=_('Status Code'), widget=widgets.IntegerWidget()) location = Field(attribute='location', column_name=_('Location ID'), widget=widgets.ForeignKeyWidget(StockLocation)) location_name = Field(attribute='location__name', column_name=_('Location Name'), readonly=True) supplier_part = Field(attribute='supplier_part', column_name=_('Supplier Part ID'), widget=widgets.ForeignKeyWidget(SupplierPart)) supplier = Field(attribute='supplier_part__supplier__id', column_name=_('Supplier ID'), readonly=True, widget=widgets.IntegerWidget()) supplier_name = Field(attribute='supplier_part__supplier__name', column_name=_('Supplier Name'), readonly=True) customer = Field(attribute='customer', column_name=_('Customer ID'), widget=widgets.ForeignKeyWidget(Company)) belongs_to = Field(attribute='belongs_to', column_name=_('Installed In'), widget=widgets.ForeignKeyWidget(StockItem)) build = Field(attribute='build', column_name=_('Build ID'), widget=widgets.ForeignKeyWidget(Build)) parent = Field(attribute='parent', column_name=_('Parent ID'), widget=widgets.ForeignKeyWidget(StockItem)) sales_order = Field(attribute='sales_order', column_name=_('Sales Order ID'), widget=widgets.ForeignKeyWidget(SalesOrder)) purchase_order = Field(attribute='purchase_order', column_name=_('Purchase Order ID'), widget=widgets.ForeignKeyWidget(PurchaseOrder)) packaging = Field(attribute='packaging', column_name=_('Packaging')) link = Field(attribute='link', column_name=_('Link')) notes = Field(attribute='notes', column_name=_('Notes')) # Status fields (note that IntegerWidget exports better to excel than BooleanWidget) is_building = Field(attribute='is_building', column_name=_('Building'), widget=widgets.IntegerWidget()) review_needed = Field(attribute='review_needed', column_name=_('Review Needed'), widget=widgets.IntegerWidget()) delete_on_deplete = Field(attribute='delete_on_deplete', column_name=_('Delete on Deplete'), widget=widgets.IntegerWidget()) # Date management updated = Field(attribute='updated', column_name=_('Last Updated'), widget=widgets.DateWidget()) stocktake_date = Field(attribute='stocktake_date', column_name=_('Stocktake'), widget=widgets.DateWidget()) expiry_date = Field(attribute='expiry_date', column_name=_('Expiry Date'), widget=widgets.DateWidget()) def dehydrate_purchase_price(self, item): """Render purchase pric as float""" if item.purchase_price is not None: return float(item.purchase_price.amount) def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): """Rebuild after import to keep tree intact.""" super().after_import(dataset, result, using_transactions, dry_run, **kwargs) # Rebuild the StockItem tree(s) StockItem.objects.rebuild() class StockItemAdmin(ImportExportModelAdmin): """Admin class for StockItem.""" resource_class = StockItemResource list_display = ('part', 'quantity', 'location', 'status', 'updated') # A list of search fields which can be used for lookup on matching 'autocomplete' fields search_fields = [ 'part__name', 'part__description', 'serial', 'batch', ] autocomplete_fields = [ 'belongs_to', 'build', 'customer', 'location', 'parent', 'part', 'purchase_order', 'sales_order', 'stocktake_user', 'supplier_part', ] class StockAttachmentAdmin(admin.ModelAdmin): """Admin class for StockAttachment.""" list_display = ('stock_item', 'attachment', 'comment') autocomplete_fields = [ 'stock_item', ] class StockTrackingAdmin(ImportExportModelAdmin): """Admin class for StockTracking.""" list_display = ('item', 'date', 'label') autocomplete_fields = [ 'item', ] class StockItemTestResultAdmin(admin.ModelAdmin): """Admin class for StockItemTestResult.""" list_display = ('stock_item', 'test', 'result', 'value') autocomplete_fields = [ 'stock_item', ] admin.site.register(StockLocation, LocationAdmin) admin.site.register(StockLocationType, LocationTypeAdmin) admin.site.register(StockItem, StockItemAdmin) admin.site.register(StockItemTracking, StockTrackingAdmin) admin.site.register(StockItemAttachment, StockAttachmentAdmin) admin.site.register(StockItemTestResult, StockItemTestResultAdmin)