├── .editorconfig ├── .github └── workflows │ ├── lint.yml │ ├── publish.yaml │ └── test.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs └── img │ ├── asset.png │ ├── asset_edit.png │ ├── asset_filters.png │ ├── asset_list.png │ ├── data_model.drawio.png │ ├── inventoryitem_type_list.png │ ├── netbox_attachments_example.png │ └── supplier.png ├── netbox_inventory ├── __init__.py ├── analyzers.py ├── api │ ├── __init__.py │ ├── serializers.py │ ├── serializers_ │ │ ├── __init__.py │ │ ├── assets.py │ │ ├── audit.py │ │ ├── deliveries.py │ │ └── nested.py │ ├── urls.py │ └── views.py ├── choices.py ├── constants.py ├── filtersets.py ├── forms │ ├── __init__.py │ ├── assign.py │ ├── bulk_add.py │ ├── bulk_edit.py │ ├── bulk_import.py │ ├── create.py │ ├── filters.py │ ├── models.py │ └── reassign.py ├── graphql │ ├── __init__.py │ ├── filters.py │ ├── schema.py │ └── types.py ├── managers.py ├── migrations │ ├── 0001_initial_prod.py │ ├── 0002_alter_asset_serial.py │ ├── 0003_add_inventoryitemgroup.py │ ├── 0004_inventoryitemgroup_tree.py │ ├── 0005_delivery_asset_delivery.py │ ├── 0006_purchase_status.py │ ├── 0007_alter_asset_unique_together_and_more.py │ ├── 0008_alter_asset_device_type_alter_asset_module_type.py │ ├── 0009_add_rack.py │ ├── 0010_asset_description_inventoryitemtype_description.py │ ├── 0011_alter_supplier_options.py │ ├── 0012_add_auditflow.py │ ├── 0013_add_audittrail.py │ ├── 0014_alter_audittrail_object_type.py │ ├── 0015_alter_asset_storage_location.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── assets.py │ ├── audit.py │ ├── deliveries.py │ └── mixins.py ├── navigation.py ├── search.py ├── signals.py ├── tables.py ├── template_content.py ├── templates │ └── netbox_inventory │ │ ├── asset.html │ │ ├── asset_assign.html │ │ ├── asset_bulk_add.html │ │ ├── asset_bulk_import.html │ │ ├── asset_create.html │ │ ├── asset_edit.html │ │ ├── asset_reassign.html │ │ ├── auditflow.html │ │ ├── auditflow_pages.html │ │ ├── auditflow_run.html │ │ ├── auditflowpage.html │ │ ├── audittrailsource.html │ │ ├── delivery.html │ │ ├── generic │ │ └── baseflow.html │ │ ├── inc │ │ ├── asset_edit_header.html │ │ ├── asset_info.html │ │ ├── asset_stats_counts.html │ │ └── buttons │ │ │ ├── auditflow_add_object.html │ │ │ ├── auditflow_run.html │ │ │ └── audittrail_seen.html │ │ ├── inventoryitemgroup.html │ │ ├── inventoryitemtype.html │ │ ├── purchase.html │ │ └── supplier.html ├── tests │ ├── __init__.py │ ├── asset │ │ ├── __init__.py │ │ ├── test_api.py │ │ ├── test_models.py │ │ ├── test_views.py │ │ ├── test_views_create.py │ │ └── test_views_reassign.py │ ├── auditflow │ │ ├── __init__.py │ │ ├── test_api.py │ │ ├── test_filtersets.py │ │ ├── test_models.py │ │ └── test_views.py │ ├── auditflowpage │ │ ├── __init__.py │ │ ├── test_api.py │ │ ├── test_filtersets.py │ │ ├── test_models.py │ │ └── test_views.py │ ├── auditflowpageassignment │ │ ├── __init__.py │ │ ├── test_api.py │ │ ├── test_models.py │ │ └── test_views.py │ ├── audittrail │ │ ├── __init__.py │ │ ├── test_api.py │ │ ├── test_filterset.py │ │ └── test_views.py │ ├── audittrailsource │ │ ├── __init__.py │ │ ├── test_api.py │ │ ├── test_filtersets.py │ │ └── test_views.py │ ├── custom.py │ ├── delivery │ │ ├── __init__.py │ │ ├── test_api.py │ │ └── test_views.py │ ├── inventoryitem_group │ │ ├── __init__.py │ │ ├── test_api.py │ │ └── test_views.py │ ├── inventoryitem_type │ │ ├── __init__.py │ │ ├── test_api.py │ │ └── test_views.py │ ├── purchase │ │ ├── __init__.py │ │ ├── test_api.py │ │ └── test_views.py │ ├── settings.py │ ├── supplier │ │ ├── __init__.py │ │ ├── test_api.py │ │ └── test_views.py │ └── test_load.py ├── urls.py ├── utils.py ├── version.py └── views │ ├── __init__.py │ ├── asset.py │ ├── asset_assign.py │ ├── asset_create.py │ ├── asset_reassign.py │ ├── auditflow.py │ ├── auditflowpage.py │ ├── auditflowpageassignments.py │ ├── audittrail.py │ ├── audittrailsource.py │ ├── delivery.py │ ├── inventoryitem_group.py │ ├── inventoryitem_type.py │ ├── purchase.py │ └── supplier.py ├── pyproject.toml └── testing └── configuration.testing.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{md,py,toml}] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@main 14 | 15 | - uses: astral-sh/ruff-action@v3 16 | with: 17 | src: "./netbox_inventory" 18 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish Python package to PyPI 3 | 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | environment: release 13 | permissions: 14 | id-token: write 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@main 18 | - name: Setup Python 19 | uses: actions/setup-python@main 20 | with: 21 | python-version: "3.12" 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install 'build[virtualenv]' 26 | - name: Build package 27 | run: python -m build 28 | - name: Publish package 29 | uses: pypa/gh-action-pypi-publish@release/v1 30 | with: 31 | verbose: true 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests under netbox 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test-netbox: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | netbox-version: ["v4.4.4"] 14 | services: 15 | redis: 16 | image: redis 17 | ports: 18 | - 6379:6379 19 | postgres: 20 | image: postgres 21 | env: 22 | POSTGRES_USER: netbox 23 | POSTGRES_PASSWORD: netbox 24 | options: >- 25 | --health-cmd pg_isready 26 | --health-interval 10s 27 | --health-timeout 5s 28 | --health-retries 5 29 | ports: 30 | - 5432:5432 31 | 32 | steps: 33 | - name: Checkout repo 34 | uses: actions/checkout@main 35 | with: 36 | path: netbox-inventory 37 | 38 | - name: Set up Python 39 | uses: actions/setup-python@main 40 | with: 41 | python-version: "3.10" 42 | 43 | - name: Checkout netbox ${{ matrix.netbox-version }} 44 | uses: actions/checkout@v4 45 | with: 46 | repository: "netbox-community/netbox" 47 | ref: ${{ matrix.netbox-version }} 48 | path: netbox 49 | 50 | - name: install netbox_inventory 51 | working-directory: netbox-inventory 52 | run: | 53 | pip install . 54 | 55 | - name: Install dependencies & set up configuration 56 | working-directory: netbox 57 | run: | 58 | ln -s $(pwd)/../netbox-inventory/testing/configuration.testing.py netbox/netbox/configuration.py 59 | python -m pip install --upgrade pip 60 | pip install -r requirements.txt -U 61 | 62 | - name: Check for missing migrations 63 | working-directory: netbox 64 | run: python netbox/manage.py makemigrations --check 65 | 66 | - name: Run tests 67 | working-directory: netbox 68 | run: | 69 | python netbox/manage.py test netbox_inventory.tests --parallel -v 2 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .idea 8 | .coverage 9 | .vscode 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Arnes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include netbox_inventory/templates *.html 2 | -------------------------------------------------------------------------------- /docs/img/asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/docs/img/asset.png -------------------------------------------------------------------------------- /docs/img/asset_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/docs/img/asset_edit.png -------------------------------------------------------------------------------- /docs/img/asset_filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/docs/img/asset_filters.png -------------------------------------------------------------------------------- /docs/img/asset_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/docs/img/asset_list.png -------------------------------------------------------------------------------- /docs/img/data_model.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/docs/img/data_model.drawio.png -------------------------------------------------------------------------------- /docs/img/inventoryitem_type_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/docs/img/inventoryitem_type_list.png -------------------------------------------------------------------------------- /docs/img/netbox_attachments_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/docs/img/netbox_attachments_example.png -------------------------------------------------------------------------------- /docs/img/supplier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/docs/img/supplier.png -------------------------------------------------------------------------------- /netbox_inventory/__init__.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | 3 | from netbox.plugins import PluginConfig 4 | 5 | from .version import __version__ 6 | 7 | 8 | class NetBoxInventoryConfig(PluginConfig): 9 | name = 'netbox_inventory' 10 | verbose_name = 'NetBox Inventory' 11 | version = __version__ 12 | description = 'Inventory asset management in NetBox' 13 | author = 'Matej Vadnjal' 14 | author_email = 'matej.vadnjal@arnes.si' 15 | base_url = 'inventory' 16 | min_version = '4.4.0' 17 | default_settings = { 18 | 'top_level_menu': True, 19 | 'used_status_name': 'used', 20 | 'used_additional_status_names': [], 21 | 'stored_status_name': 'stored', 22 | 'stored_additional_status_names': [ 23 | 'retired', 24 | ], 25 | 'sync_hardware_serial_asset_tag': False, 26 | 'asset_import_create_purchase': False, 27 | 'asset_import_create_device_type': False, 28 | 'asset_import_create_module_type': False, 29 | 'asset_import_create_inventoryitem_type': False, 30 | 'asset_import_create_rack_type': False, 31 | 'asset_import_create_tenant': False, 32 | 'asset_disable_editing_fields_for_tags': {}, 33 | 'asset_disable_deletion_for_tags': [], 34 | 'asset_custom_fields_search_filters': {}, 35 | 'asset_warranty_expire_warning_days': 90, 36 | 'prefill_asset_name_create_inventoryitem': False, 37 | 'prefill_asset_tag_create_inventoryitem': False, 38 | 'audit_window': 4 * 60, # 4 hours 39 | } 40 | 41 | def register_feature_views(self) -> None: 42 | """ 43 | Register feature views for all available models. 44 | """ 45 | from utilities.views import register_model_view 46 | 47 | for model in apps.get_models(): 48 | register_model_view(model, 'audit-trails', kwargs={'model': model})( 49 | 'netbox_inventory.views.ObjectAuditTrailView', 50 | ) 51 | 52 | def ready(self): 53 | super().ready() 54 | from . import signals # noqa: F401 55 | 56 | self.register_feature_views() 57 | 58 | 59 | config = NetBoxInventoryConfig 60 | -------------------------------------------------------------------------------- /netbox_inventory/analyzers.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | from django.db.models import Count, F 4 | 5 | from .choices import AssetStatusChoices 6 | from .models import Asset 7 | 8 | 9 | def asset_counts_type_status(inventoryitem_group, assets=None): # noqa: C901 10 | """ 11 | Return counts of assets based on combinations of inventoryitem type 12 | and status values for assets that belong to an inventoryitem group. 13 | Can optionally accept pre-filtered queryset with assets. 14 | Return value is a list of dicts, each having keys: 15 | - inventoryitem_type__manufacturer__name 16 | - inventoryitem_type__model 17 | - inventoryitem_type (ID) 18 | - status 19 | - label (display label of status) 20 | - color (color of status) 21 | """ 22 | if assets is None: 23 | assets = Asset.objects.all() 24 | assets = assets.filter( 25 | inventoryitem_type__inventoryitem_group__in=inventoryitem_group.get_descendants( 26 | include_self=True 27 | ) 28 | ) 29 | # generate counts of assets grouped by type and status 30 | asset_counts = ( 31 | assets.values( 32 | 'inventoryitem_type__manufacturer__name', 33 | 'inventoryitem_type__model', 34 | 'inventoryitem_type', 35 | 'status', 36 | ) 37 | .annotate(count=Count('pk')) 38 | .order_by('inventoryitem_type', 'status') 39 | ) 40 | 41 | def _update_status_meta(entry): 42 | """adds color and label keys based on status value""" 43 | entry['color'] = AssetStatusChoices.colors.get(entry['status'], 'gray') 44 | entry['label'] = dict(AssetStatusChoices).get(entry['status'], entry['status']) 45 | 46 | def _generate_entry(entry_from, status, count=0): 47 | t = copy(entry_from) 48 | t['status'] = status 49 | t['count'] = count 50 | _update_status_meta(t) 51 | return t 52 | 53 | # for each inventoryitem_type keep track of seen statues and add any that are 54 | # missing with count:0 55 | zero_counts = [] 56 | all_statuses = set(AssetStatusChoices.values()) 57 | last_iid_pk = None 58 | seen_statues = set() 59 | seen_iit_pks = set() 60 | for idx, iit_status_count in enumerate(asset_counts): 61 | _update_status_meta(iit_status_count) 62 | seen_iit_pks.add(iit_status_count['inventoryitem_type']) 63 | if last_iid_pk is None: 64 | last_iid_pk = iit_status_count['inventoryitem_type'] 65 | if last_iid_pk != iit_status_count['inventoryitem_type']: 66 | # next iit_pk, add unseen statuses of previous pk 67 | for missing_status in all_statuses - seen_statues: 68 | zero_counts.append( 69 | _generate_entry(asset_counts[idx - 1], missing_status) 70 | ) 71 | # reset 72 | seen_statues = set() 73 | last_iid_pk = iit_status_count['inventoryitem_type'] 74 | seen_statues.add(iit_status_count['status']) 75 | # complete missing statues for the last inventoryitem_type in asset_counts 76 | if last_iid_pk: 77 | for missing_status in all_statuses - seen_statues: 78 | zero_counts.append(_generate_entry(iit_status_count, missing_status)) 79 | 80 | # now add entries for inventory item types that have no assets at all 81 | for iit in ( 82 | inventoryitem_group.inventoryitem_types.exclude(pk__in=seen_iit_pks) 83 | .annotate( 84 | inventoryitem_type__manufacturer__name=F('manufacturer__name'), 85 | inventoryitem_type__model=F('model'), 86 | inventoryitem_type=F('pk'), 87 | ) 88 | .values( 89 | 'inventoryitem_type__manufacturer__name', 90 | 'inventoryitem_type__model', 91 | 'inventoryitem_type', 92 | ) 93 | ): 94 | for status in all_statuses: 95 | zero_counts.append(_generate_entry(iit, status)) 96 | 97 | # combine non-zero and zero counts and sort 98 | asset_counts = sorted( 99 | list(asset_counts) + zero_counts, 100 | key=lambda k: ( 101 | k['inventoryitem_type__manufacturer__name'], 102 | k['inventoryitem_type__model'], 103 | AssetStatusChoices.values().index(k['status']), 104 | ), 105 | ) 106 | return asset_counts 107 | 108 | 109 | def asset_counts_status(asset_counts): 110 | """ 111 | Aggregates asset counts broken down by inventory item type and status 112 | (as returned by asset_counts_type_status) to counts on just status valuies. 113 | """ 114 | status_counts = { 115 | key: { 116 | 'value': key, 117 | 'label': label, 118 | 'color': AssetStatusChoices.colors[key], 119 | 'count': sum(e['count'] for e in asset_counts if e['status'] == key), 120 | } 121 | for key, label in list(AssetStatusChoices) 122 | } 123 | return status_counts 124 | -------------------------------------------------------------------------------- /netbox_inventory/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/api/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/api/serializers.py: -------------------------------------------------------------------------------- 1 | from .serializers_.assets import * 2 | from .serializers_.audit import * 3 | from .serializers_.deliveries import * 4 | -------------------------------------------------------------------------------- /netbox_inventory/api/serializers_/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/api/serializers_/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/api/serializers_/audit.py: -------------------------------------------------------------------------------- 1 | from drf_spectacular.types import OpenApiTypes 2 | from drf_spectacular.utils import extend_schema_field 3 | from rest_framework import serializers 4 | 5 | from core.models import ObjectType 6 | from netbox.api.fields import ContentTypeField 7 | from netbox.api.serializers import NetBoxModelSerializer 8 | from utilities.api import get_serializer_for_model 9 | 10 | from netbox_inventory.models import ( 11 | AuditFlow, 12 | AuditFlowPage, 13 | AuditFlowPageAssignment, 14 | AuditTrail, 15 | AuditTrailSource, 16 | ) 17 | 18 | __all__ = ( 19 | 'AuditFlowPageAssignmentSerializer', 20 | 'AuditFlowPageSerializer', 21 | 'AuditFlowSerializer', 22 | 'AuditTrailSerializer', 23 | 'AuditTrailSourceSerializer', 24 | ) 25 | 26 | 27 | class BaseFlowSerializer(NetBoxModelSerializer): 28 | """ 29 | Internal base serializer for audit flow models. 30 | """ 31 | 32 | object_type = ContentTypeField( 33 | queryset=ObjectType.objects.public(), 34 | ) 35 | 36 | class Meta: 37 | fields = ( 38 | 'id', 39 | 'url', 40 | 'display_url', 41 | 'display', 42 | 'name', 43 | 'description', 44 | 'object_type', 45 | 'object_filter', 46 | 'comments', 47 | 'tags', 48 | 'custom_fields', 49 | 'created', 50 | 'last_updated', 51 | ) 52 | brief_fields = ( 53 | 'id', 54 | 'url', 55 | 'display', 56 | 'name', 57 | ) 58 | 59 | 60 | class AuditFlowPageSerializer(BaseFlowSerializer): 61 | class Meta(BaseFlowSerializer.Meta): 62 | model = AuditFlowPage 63 | 64 | 65 | class AuditFlowSerializer(BaseFlowSerializer): 66 | class Meta(BaseFlowSerializer.Meta): 67 | model = AuditFlow 68 | fields = BaseFlowSerializer.Meta.fields + ('enabled',) 69 | 70 | 71 | class AuditFlowPageAssignmentSerializer(NetBoxModelSerializer): 72 | flow = AuditFlowSerializer( 73 | nested=True, 74 | ) 75 | page = AuditFlowPageSerializer( 76 | nested=True, 77 | ) 78 | 79 | class Meta: 80 | model = AuditFlowPageAssignment 81 | fields = ( 82 | 'id', 83 | 'url', 84 | 'display', 85 | 'flow', 86 | 'page', 87 | 'weight', 88 | 'created', 89 | 'last_updated', 90 | ) 91 | brief_fields = ( 92 | 'id', 93 | 'url', 94 | 'display', 95 | 'flow', 96 | 'page', 97 | ) 98 | 99 | 100 | class AuditTrailSourceSerializer(NetBoxModelSerializer): 101 | class Meta: 102 | model = AuditTrailSource 103 | fields = ( 104 | 'id', 105 | 'url', 106 | 'display', 107 | 'display_url', 108 | 'name', 109 | 'slug', 110 | 'description', 111 | 'comments', 112 | 'tags', 113 | 'custom_fields', 114 | 'created', 115 | 'last_updated', 116 | ) 117 | brief_fields = ( 118 | 'id', 119 | 'url', 120 | 'display', 121 | 'name', 122 | 'slug', 123 | ) 124 | 125 | 126 | class AuditTrailSerializer(NetBoxModelSerializer): 127 | object_type = ContentTypeField( 128 | queryset=ObjectType.objects.public(), 129 | ) 130 | object = serializers.SerializerMethodField( 131 | read_only=True, 132 | ) 133 | source = AuditTrailSourceSerializer( 134 | nested=True, 135 | required=False, 136 | allow_null=True, 137 | ) 138 | 139 | class Meta: 140 | model = AuditTrail 141 | fields = ( 142 | 'id', 143 | 'url', 144 | 'display', 145 | 'object_type', 146 | 'object_id', 147 | 'object', 148 | 'source', 149 | 'created', 150 | 'last_updated', 151 | ) 152 | brief_fields = ( 153 | 'id', 154 | 'url', 155 | 'display', 156 | 'object', 157 | ) 158 | 159 | @extend_schema_field(OpenApiTypes.OBJECT) 160 | def get_object(self, instance): 161 | serializer = get_serializer_for_model(instance.object_type.model_class()) 162 | context = {'request': self.context['request']} 163 | return serializer(instance.object, nested=True, context=context).data 164 | -------------------------------------------------------------------------------- /netbox_inventory/api/serializers_/deliveries.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from netbox.api.serializers import NetBoxModelSerializer 4 | from tenancy.api.serializers import ContactSerializer 5 | 6 | from .nested import * 7 | from netbox_inventory.models import Delivery, Purchase, Supplier 8 | 9 | 10 | class SupplierSerializer(NetBoxModelSerializer): 11 | asset_count = serializers.IntegerField(read_only=True) 12 | purchase_count = serializers.IntegerField(read_only=True) 13 | delivery_count = serializers.IntegerField(read_only=True) 14 | 15 | class Meta: 16 | model = Supplier 17 | fields = ( 18 | 'id', 19 | 'url', 20 | 'display', 21 | 'name', 22 | 'slug', 23 | 'description', 24 | 'comments', 25 | 'tags', 26 | 'custom_fields', 27 | 'created', 28 | 'last_updated', 29 | 'asset_count', 30 | 'purchase_count', 31 | 'delivery_count', 32 | ) 33 | brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') 34 | 35 | 36 | class PurchaseSerializer(NetBoxModelSerializer): 37 | supplier = SupplierSerializer(nested=True) 38 | asset_count = serializers.IntegerField(read_only=True) 39 | delivery_count = serializers.IntegerField(read_only=True) 40 | 41 | class Meta: 42 | model = Purchase 43 | fields = ( 44 | 'id', 45 | 'url', 46 | 'display', 47 | 'supplier', 48 | 'name', 49 | 'status', 50 | 'date', 51 | 'description', 52 | 'comments', 53 | 'tags', 54 | 'custom_fields', 55 | 'created', 56 | 'last_updated', 57 | 'asset_count', 58 | 'delivery_count', 59 | ) 60 | brief_fields = ( 61 | 'id', 62 | 'url', 63 | 'display', 64 | 'supplier', 65 | 'name', 66 | 'status', 67 | 'date', 68 | 'description', 69 | ) 70 | 71 | 72 | class DeliverySerializer(NetBoxModelSerializer): 73 | purchase = PurchaseSerializer(nested=True) 74 | receiving_contact = ContactSerializer( 75 | nested=True, required=False, allow_null=True, default=None 76 | ) 77 | asset_count = serializers.IntegerField(read_only=True) 78 | 79 | class Meta: 80 | model = Delivery 81 | fields = ( 82 | 'id', 83 | 'url', 84 | 'display', 85 | 'purchase', 86 | 'name', 87 | 'date', 88 | 'description', 89 | 'comments', 90 | 'receiving_contact', 91 | 'tags', 92 | 'custom_fields', 93 | 'created', 94 | 'last_updated', 95 | 'asset_count', 96 | ) 97 | brief_fields = ('id', 'url', 'display', 'name', 'date', 'description') 98 | -------------------------------------------------------------------------------- /netbox_inventory/api/serializers_/nested.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from netbox.api.serializers import WritableNestedSerializer 4 | 5 | from netbox_inventory.models import InventoryItemGroup 6 | 7 | __all__ = ('NestedInventoryItemGroupSerializer',) 8 | 9 | 10 | class NestedInventoryItemGroupSerializer(WritableNestedSerializer): 11 | _depth = serializers.IntegerField(source='level', read_only=True) 12 | 13 | class Meta: 14 | model = InventoryItemGroup 15 | fields = ('id', 'url', 'display', 'name', 'description', '_depth') 16 | -------------------------------------------------------------------------------- /netbox_inventory/api/urls.py: -------------------------------------------------------------------------------- 1 | from netbox.api.routers import NetBoxRouter 2 | 3 | from . import views 4 | 5 | app_name = 'netbox_inventory' 6 | 7 | router = NetBoxRouter() 8 | 9 | # Assets 10 | router.register('assets', views.AssetViewSet) 11 | router.register('inventory-item-types', views.InventoryItemTypeViewSet) 12 | router.register('inventory-item-groups', views.InventoryItemGroupViewSet) 13 | router.register('dcim/devices', views.DeviceAssetViewSet) 14 | router.register('dcim/modules', views.ModuleAssetViewSet) 15 | router.register('dcim/inventory-items', views.InventoryItemAssetViewSet) 16 | 17 | # Deliveries 18 | router.register('suppliers', views.SupplierViewSet) 19 | router.register('purchases', views.PurchaseViewSet) 20 | router.register('deliveries', views.DeliveryViewSet) 21 | 22 | # Audit 23 | router.register('audit-flows', views.AuditFlowViewSet) 24 | router.register('audit-flowpages', views.AuditFlowPageViewSet) 25 | router.register('audit-flowpage-assignments', views.AuditFlowPageAssignmentViewSet) 26 | router.register('audit-trail-sources', views.AuditTrailSourceViewSet) 27 | router.register('audit-trails', views.AuditTrailViewSet) 28 | 29 | 30 | urlpatterns = router.urls 31 | -------------------------------------------------------------------------------- /netbox_inventory/api/views.py: -------------------------------------------------------------------------------- 1 | from dcim.api.views import DeviceViewSet, InventoryItemViewSet, ModuleViewSet 2 | from netbox.api.viewsets import NetBoxModelViewSet 3 | from utilities.query import count_related 4 | 5 | from .. import filtersets, models 6 | from .serializers import * 7 | 8 | __all__ = ( 9 | 'AssetViewSet', 10 | 'AuditFlowPageAssignmentViewSet', 11 | 'AuditFlowPageViewSet', 12 | 'AuditFlowViewSet', 13 | 'AuditTrailSourceViewSet', 14 | 'AuditTrailViewSet', 15 | 'DeliveryViewSet', 16 | 'DeviceAssetViewSet', 17 | 'InventoryItemAssetViewSet', 18 | 'InventoryItemGroupViewSet', 19 | 'InventoryItemTypeViewSet', 20 | 'ModuleAssetViewSet', 21 | 'PurchaseViewSet', 22 | 'SupplierViewSet', 23 | ) 24 | 25 | # 26 | # Assets 27 | # 28 | 29 | 30 | class InventoryItemGroupViewSet(NetBoxModelViewSet): 31 | queryset = models.InventoryItemGroup.objects.add_related_count( 32 | models.InventoryItemGroup.objects.all(), 33 | models.Asset, 34 | 'inventoryitem_type__inventoryitem_group', 35 | 'asset_count', 36 | cumulative=True, 37 | ).prefetch_related('tags') 38 | serializer_class = InventoryItemGroupSerializer 39 | filterset_class = filtersets.InventoryItemGroupFilterSet 40 | 41 | 42 | class InventoryItemTypeViewSet(NetBoxModelViewSet): 43 | queryset = models.InventoryItemType.objects.prefetch_related('tags').annotate( 44 | asset_count=count_related(models.Asset, 'inventoryitem_type') 45 | ) 46 | serializer_class = InventoryItemTypeSerializer 47 | filterset_class = filtersets.InventoryItemTypeFilterSet 48 | 49 | 50 | class AssetViewSet(NetBoxModelViewSet): 51 | queryset = models.Asset.objects.prefetch_related( 52 | 'device_type', 53 | 'device', 54 | 'module_type', 55 | 'module', 56 | 'rack_type', 57 | 'rack', 58 | 'storage_location', 59 | 'delivery', 60 | 'purchase__supplier', 61 | 'tags', 62 | ) 63 | serializer_class = AssetSerializer 64 | filterset_class = filtersets.AssetFilterSet 65 | 66 | 67 | class DeviceAssetViewSet(DeviceViewSet): 68 | """ 69 | Adds option to filter on asset assignemnet 70 | """ 71 | 72 | filterset_class = filtersets.DeviceAssetFilterSet 73 | 74 | 75 | class ModuleAssetViewSet(ModuleViewSet): 76 | """ 77 | Adds option to filter on asset assignemnet 78 | """ 79 | 80 | filterset_class = filtersets.ModuleAssetFilterSet 81 | 82 | 83 | class InventoryItemAssetViewSet(InventoryItemViewSet): 84 | """ 85 | Adds option to filter on asset assignemnet 86 | """ 87 | 88 | filterset_class = filtersets.InventoryItemAssetFilterSet 89 | 90 | 91 | # 92 | # Deliveries 93 | # 94 | 95 | 96 | class SupplierViewSet(NetBoxModelViewSet): 97 | queryset = models.Supplier.objects.prefetch_related('tags').annotate( 98 | asset_count=count_related(models.Asset, 'purchase__supplier'), 99 | purchase_count=count_related(models.Purchase, 'supplier'), 100 | delivery_count=count_related(models.Delivery, 'purchase__supplier'), 101 | ) 102 | serializer_class = SupplierSerializer 103 | filterset_class = filtersets.SupplierFilterSet 104 | 105 | 106 | class PurchaseViewSet(NetBoxModelViewSet): 107 | queryset = models.Purchase.objects.prefetch_related('tags').annotate( 108 | asset_count=count_related(models.Asset, 'purchase'), 109 | delivery_count=count_related(models.Delivery, 'purchase'), 110 | ) 111 | serializer_class = PurchaseSerializer 112 | filterset_class = filtersets.PurchaseFilterSet 113 | 114 | 115 | class DeliveryViewSet(NetBoxModelViewSet): 116 | queryset = models.Delivery.objects.prefetch_related('tags').annotate( 117 | asset_count=count_related(models.Asset, 'delivery') 118 | ) 119 | serializer_class = DeliverySerializer 120 | filterset_class = filtersets.DeliveryFilterSet 121 | 122 | 123 | # 124 | # Audit 125 | # 126 | 127 | 128 | class AuditFlowPageViewSet(NetBoxModelViewSet): 129 | queryset = models.AuditFlowPage.objects.prefetch_related('object_type', 'tags') 130 | serializer_class = AuditFlowPageSerializer 131 | 132 | 133 | class AuditFlowViewSet(NetBoxModelViewSet): 134 | queryset = models.AuditFlow.objects.prefetch_related('object_type', 'pages', 'tags') 135 | serializer_class = AuditFlowSerializer 136 | 137 | 138 | class AuditFlowPageAssignmentViewSet(NetBoxModelViewSet): 139 | queryset = models.AuditFlowPageAssignment.objects.prefetch_related('flow', 'page') 140 | serializer_class = AuditFlowPageAssignmentSerializer 141 | 142 | 143 | class AuditTrailSourceViewSet(NetBoxModelViewSet): 144 | queryset = models.AuditTrailSource.objects.prefetch_related('tags') 145 | serializer_class = AuditTrailSourceSerializer 146 | 147 | 148 | class AuditTrailViewSet(NetBoxModelViewSet): 149 | queryset = models.AuditTrail.objects.prefetch_related('object') 150 | serializer_class = AuditTrailSerializer 151 | -------------------------------------------------------------------------------- /netbox_inventory/choices.py: -------------------------------------------------------------------------------- 1 | from utilities.choices import ChoiceSet 2 | 3 | # 4 | # Assets 5 | # 6 | 7 | 8 | class AssetStatusChoices(ChoiceSet): 9 | key = 'Asset.status' 10 | 11 | CHOICES = [ 12 | ('stored', 'Stored', 'green'), 13 | ('used', 'Used', 'blue'), 14 | ('retired', 'Retired', 'gray'), 15 | ] 16 | 17 | 18 | class HardwareKindChoices(ChoiceSet): 19 | CHOICES = [ 20 | ('device', 'Device'), 21 | ('module', 'Module'), 22 | ('inventoryitem', 'Inventory Item'), 23 | ('rack', 'Rack'), 24 | ] 25 | 26 | 27 | # 28 | # Deliveries 29 | # 30 | 31 | 32 | class PurchaseStatusChoices(ChoiceSet): 33 | key = 'Purchase.status' 34 | 35 | CHOICES = [ 36 | ('open', 'Open', 'cyan'), 37 | ('partial', 'Partial', 'blue'), 38 | ('closed', 'Closed', 'green'), 39 | ] 40 | -------------------------------------------------------------------------------- /netbox_inventory/constants.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | 3 | AUDITFLOW_OBJECT_TYPE_CHOICES = Q( 4 | app_label='dcim', 5 | model__in=( 6 | 'site', 7 | 'location', 8 | 'rack', 9 | ), 10 | ) 11 | -------------------------------------------------------------------------------- /netbox_inventory/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .assign import * 2 | from .bulk_add import * 3 | from .bulk_edit import * 4 | from .bulk_import import * 5 | from .create import * 6 | from .filters import * 7 | from .models import * 8 | from .reassign import * 9 | -------------------------------------------------------------------------------- /netbox_inventory/forms/bulk_add.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import AssetForm 4 | 5 | __all__ = ( 6 | 'AssetBulkAddForm', 7 | 'AssetBulkAddModelForm', 8 | ) 9 | 10 | 11 | class AssetBulkAddForm(forms.Form): 12 | """Form for creating multiple Assets by count""" 13 | 14 | count = forms.IntegerField( 15 | min_value=1, 16 | required=True, 17 | help_text='How many assets to create', 18 | ) 19 | 20 | 21 | class AssetBulkAddModelForm(AssetForm): 22 | def __init__(self, *args, **kwargs): 23 | super().__init__(*args, **kwargs) 24 | self.fields['asset_tag'].disabled = True 25 | self.fields['serial'].disabled = True 26 | -------------------------------------------------------------------------------- /netbox_inventory/graphql/__init__.py: -------------------------------------------------------------------------------- 1 | from .schema import ( 2 | AssetQuery, 3 | DeliveryQuery, 4 | InventoryItemGroupQuery, 5 | InventoryItemTypeQuery, 6 | PurchaseQuery, 7 | SupplierQuery, 8 | ) 9 | 10 | schema = [ 11 | AssetQuery, 12 | SupplierQuery, 13 | PurchaseQuery, 14 | DeliveryQuery, 15 | InventoryItemTypeQuery, 16 | InventoryItemGroupQuery, 17 | ] 18 | -------------------------------------------------------------------------------- /netbox_inventory/graphql/filters.py: -------------------------------------------------------------------------------- 1 | import strawberry_django 2 | 3 | from netbox.graphql.filter_mixins import BaseFilterMixin 4 | 5 | from netbox_inventory import models 6 | 7 | __all__ = ( 8 | 'AssetFilter', 9 | 'SupplierFilter', 10 | 'PurchaseFilter', 11 | 'DeliveryFilter', 12 | 'InventoryItemTypeFilter', 13 | 'InventoryItemGroupFilter', 14 | ) 15 | 16 | 17 | @strawberry_django.filter(models.Asset, lookups=True) 18 | class AssetFilter(BaseFilterMixin): 19 | pass 20 | 21 | 22 | @strawberry_django.filter(models.Supplier, lookups=True) 23 | class SupplierFilter(BaseFilterMixin): 24 | pass 25 | 26 | 27 | @strawberry_django.filter(models.Purchase, lookups=True) 28 | class PurchaseFilter(BaseFilterMixin): 29 | pass 30 | 31 | 32 | @strawberry_django.filter(models.Delivery, lookups=True) 33 | class DeliveryFilter(BaseFilterMixin): 34 | pass 35 | 36 | 37 | @strawberry_django.filter(models.InventoryItemType, lookups=True) 38 | class InventoryItemTypeFilter(BaseFilterMixin): 39 | pass 40 | 41 | 42 | @strawberry_django.filter(models.InventoryItemGroup, lookups=True) 43 | class InventoryItemGroupFilter(BaseFilterMixin): 44 | pass 45 | -------------------------------------------------------------------------------- /netbox_inventory/graphql/schema.py: -------------------------------------------------------------------------------- 1 | import strawberry 2 | import strawberry_django 3 | 4 | from .types import ( 5 | AssetType, 6 | DeliveryType, 7 | InventoryItemGroupType, 8 | InventoryItemTypeType, 9 | PurchaseType, 10 | SupplierType, 11 | ) 12 | from netbox_inventory.models import ( 13 | Asset, 14 | Delivery, 15 | InventoryItemGroup, 16 | InventoryItemType, 17 | Purchase, 18 | Supplier, 19 | ) 20 | 21 | 22 | @strawberry.type 23 | class AssetQuery: 24 | @strawberry.field 25 | def asset(self, id: int) -> AssetType: 26 | return Asset.objects.get(pk=id) 27 | 28 | asset_list: list[AssetType] = strawberry_django.field() 29 | 30 | 31 | @strawberry.type 32 | class SupplierQuery: 33 | @strawberry.field 34 | def supplier(self, id: int) -> SupplierType: 35 | return Supplier.objects.get(pk=id) 36 | 37 | supplier_list: list[SupplierType] = strawberry_django.field() 38 | 39 | 40 | @strawberry.type 41 | class PurchaseQuery: 42 | @strawberry.field 43 | def purchase(self, id: int) -> PurchaseType: 44 | return Purchase.objects.get(pk=id) 45 | 46 | purchase_list: list[PurchaseType] = strawberry_django.field() 47 | 48 | 49 | @strawberry.type 50 | class DeliveryQuery: 51 | @strawberry.field 52 | def delivery(self, id: int) -> DeliveryType: 53 | return Delivery.objects.get(pk=id) 54 | 55 | delivery_list: list[DeliveryType] = strawberry_django.field() 56 | 57 | 58 | @strawberry.type 59 | class InventoryItemTypeQuery: 60 | @strawberry.field 61 | def inventory_item_type(self, id: int) -> InventoryItemTypeType: 62 | return InventoryItemType.objects.get(pk=id) 63 | 64 | inventory_item_type_list: list[InventoryItemTypeType] = strawberry_django.field() 65 | 66 | 67 | @strawberry.type 68 | class InventoryItemGroupQuery: 69 | @strawberry.field 70 | def inventory_item_group(self, id: int) -> InventoryItemGroupType: 71 | return InventoryItemGroup.objects.get(pk=id) 72 | 73 | inventory_item_group_list: list[InventoryItemGroupType] = strawberry_django.field() 74 | -------------------------------------------------------------------------------- /netbox_inventory/graphql/types.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | import strawberry 4 | import strawberry_django 5 | 6 | from dcim.graphql.types import ( 7 | DeviceType, 8 | DeviceTypeType, 9 | LocationType, 10 | ManufacturerType, 11 | ModuleType, 12 | ModuleTypeType, 13 | RackType, 14 | RackTypeType, 15 | ) 16 | from extras.graphql.mixins import ContactsMixin, ImageAttachmentsMixin 17 | from netbox.graphql.types import NetBoxObjectType, OrganizationalObjectType 18 | from tenancy.graphql.types import ContactType, TenantType 19 | 20 | from .filters import ( 21 | AssetFilter, 22 | DeliveryFilter, 23 | InventoryItemGroupFilter, 24 | InventoryItemTypeFilter, 25 | PurchaseFilter, 26 | SupplierFilter, 27 | ) 28 | from netbox_inventory.models import ( 29 | Asset, 30 | Delivery, 31 | InventoryItemGroup, 32 | InventoryItemType, 33 | Purchase, 34 | Supplier, 35 | ) 36 | 37 | 38 | @strawberry_django.type(Asset, fields='__all__', filters=AssetFilter) 39 | class AssetType(ImageAttachmentsMixin, NetBoxObjectType): 40 | device_type: ( 41 | Annotated['DeviceTypeType', strawberry.lazy('dcim.graphql.types')] | None 42 | ) 43 | module_type: ( 44 | Annotated['ModuleTypeType', strawberry.lazy('dcim.graphql.types')] | None 45 | ) 46 | inventoryitem_type: ( 47 | Annotated[ 48 | 'InventoryItemTypeType', strawberry.lazy('netbox_inventory.graphql.types') 49 | ] 50 | | None 51 | ) 52 | rack_type: Annotated['RackTypeType', strawberry.lazy('dcim.graphql.types')] | None 53 | tenant: Annotated['TenantType', strawberry.lazy('tenancy.graphql.types')] | None 54 | device: Annotated['DeviceType', strawberry.lazy('dcim.graphql.types')] | None 55 | module: Annotated['ModuleType', strawberry.lazy('dcim.graphql.types')] | None 56 | contact: Annotated['ContactType', strawberry.lazy('tenancy.graphql.types')] | None 57 | inventoryitem: ( 58 | Annotated['InventoryItemType', strawberry.lazy('dcim.graphql.types')] | None 59 | ) 60 | rack: Annotated['RackType', strawberry.lazy('dcim.graphql.types')] | None 61 | storage_location: ( 62 | Annotated['LocationType', strawberry.lazy('dcim.graphql.types')] | None 63 | ) 64 | owner: Annotated['TenantType', strawberry.lazy('tenancy.graphql.types')] | None 65 | delivery: ( 66 | Annotated['DeliveryType', strawberry.lazy('netbox_inventory.graphql.types')] 67 | | None 68 | ) 69 | purchase: ( 70 | Annotated['PurchaseType', strawberry.lazy('netbox_inventory.graphql.types')] 71 | | None 72 | ) 73 | 74 | 75 | @strawberry_django.type(Supplier, fields='__all__', filters=SupplierFilter) 76 | class SupplierType(ContactsMixin, NetBoxObjectType): 77 | purchases: list[ 78 | Annotated['PurchaseType', strawberry.lazy('netbox_inventory.graphql.types')] 79 | ] 80 | 81 | 82 | @strawberry_django.type(Purchase, fields='__all__', filters=PurchaseFilter) 83 | class PurchaseType(NetBoxObjectType): 84 | supplier: Annotated[ 85 | 'SupplierType', strawberry.lazy('netbox_inventory.graphql.types') 86 | ] 87 | assets: list[ 88 | Annotated['AssetType', strawberry.lazy('netbox_inventory.graphql.types')] 89 | ] 90 | orders: list[ 91 | Annotated['DeliveryType', strawberry.lazy('netbox_inventory.graphql.types')] 92 | ] 93 | 94 | 95 | @strawberry_django.type(Delivery, fields='__all__', filters=DeliveryFilter) 96 | class DeliveryType(NetBoxObjectType): 97 | purchase: Annotated[ 98 | 'PurchaseType', strawberry.lazy('netbox_inventory.graphql.types') 99 | ] 100 | receiving_contact: ( 101 | Annotated['ContactType', strawberry.lazy('tenancy.graphql.types')] | None 102 | ) 103 | assets: list[ 104 | Annotated['AssetType', strawberry.lazy('netbox_inventory.graphql.types')] 105 | ] 106 | 107 | 108 | @strawberry_django.type( 109 | InventoryItemType, fields='__all__', filters=InventoryItemTypeFilter 110 | ) 111 | class InventoryItemTypeType(ImageAttachmentsMixin, NetBoxObjectType): 112 | manufacturer: Annotated['ManufacturerType', strawberry.lazy('dcim.graphql.types')] 113 | inventoryitem_group: ( 114 | Annotated[ 115 | 'InventoryItemGroupType', strawberry.lazy('netbox_inventory.graphql.types') 116 | ] 117 | | None 118 | ) 119 | 120 | 121 | @strawberry_django.type( 122 | InventoryItemGroup, fields='__all__', filters=InventoryItemGroupFilter 123 | ) 124 | class InventoryItemGroupType(OrganizationalObjectType): 125 | parent: ( 126 | Annotated[ 127 | 'InventoryItemGroupType', strawberry.lazy('netbox_inventory.graphql.types') 128 | ] 129 | | None 130 | ) 131 | inventoryitem_types: list[ 132 | Annotated[ 133 | 'InventoryItemTypeType', strawberry.lazy('netbox_inventory.graphql.types') 134 | ] 135 | ] 136 | children: list[ 137 | Annotated[ 138 | 'InventoryItemGroupType', strawberry.lazy('netbox_inventory.graphql.types') 139 | ] 140 | ] 141 | -------------------------------------------------------------------------------- /netbox_inventory/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from utilities.querysets import RestrictedQuerySet 4 | 5 | 6 | class AssetManager(models.Manager.from_queryset(RestrictedQuerySet)): 7 | def count_with_children(self): 8 | """ """ 9 | if hasattr(self, 'instance'): 10 | assets = self.model.objects.filter( 11 | storage_location__in=self.instance.get_descendants(include_self=True) 12 | ) 13 | else: 14 | assets = self.get_queryset() 15 | return assets.count() 16 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0002_alter_asset_serial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.8 on 2022-10-27 16:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ('netbox_inventory', '0001_initial_prod'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name='asset', 14 | name='serial', 15 | field=models.CharField(blank=True, default=None, max_length=60, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0003_add_inventoryitemgroup.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.8 on 2022-12-11 12:44 2 | 3 | import django.db.models.deletion 4 | import taggit.managers 5 | from django.db import migrations, models 6 | 7 | from utilities.json import CustomFieldJSONEncoder 8 | 9 | 10 | class Migration(migrations.Migration): 11 | dependencies = [ 12 | ('extras', '0077_customlink_extend_text_and_url'), 13 | ('netbox_inventory', '0002_alter_asset_serial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='InventoryItemGroup', 19 | fields=[ 20 | ( 21 | 'id', 22 | models.BigAutoField( 23 | auto_created=True, primary_key=True, serialize=False 24 | ), 25 | ), 26 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 27 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 28 | ( 29 | 'custom_field_data', 30 | models.JSONField( 31 | blank=True, default=dict, encoder=CustomFieldJSONEncoder 32 | ), 33 | ), 34 | ('name', models.CharField(max_length=100, unique=True)), 35 | ('comments', models.TextField(blank=True)), 36 | ( 37 | 'tags', 38 | taggit.managers.TaggableManager( 39 | through='extras.TaggedItem', to='extras.Tag' 40 | ), 41 | ), 42 | ], 43 | options={ 44 | 'ordering': ['name'], 45 | }, 46 | ), 47 | migrations.AddField( 48 | model_name='inventoryitemtype', 49 | name='inventoryitem_group', 50 | field=models.ForeignKey( 51 | blank=True, 52 | null=True, 53 | on_delete=django.db.models.deletion.SET_NULL, 54 | related_name='inventoryitem_types', 55 | to='netbox_inventory.inventoryitemgroup', 56 | ), 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0004_inventoryitemgroup_tree.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-26 14:56 2 | 3 | import django.db.models.deletion 4 | import mptt.fields 5 | from django.db import migrations, models 6 | 7 | 8 | def rebuild_tree(apps, schema_editor): 9 | from ..models import InventoryItemGroup 10 | 11 | if hasattr(InventoryItemGroup, '_tree_manager'): 12 | InventoryItemGroup._tree_manager.rebuild() 13 | 14 | 15 | class Migration(migrations.Migration): 16 | dependencies = [ 17 | ('netbox_inventory', '0003_add_inventoryitemgroup'), 18 | ] 19 | 20 | operations = [ 21 | migrations.AddField( 22 | model_name='inventoryitemgroup', 23 | name='description', 24 | field=models.CharField(blank=True, max_length=200), 25 | ), 26 | migrations.AddField( 27 | model_name='inventoryitemgroup', 28 | name='level', 29 | field=models.PositiveIntegerField(default=0, editable=False), 30 | preserve_default=False, 31 | ), 32 | migrations.AddField( 33 | model_name='inventoryitemgroup', 34 | name='lft', 35 | field=models.PositiveIntegerField(default=0, editable=False), 36 | preserve_default=False, 37 | ), 38 | migrations.AddField( 39 | model_name='inventoryitemgroup', 40 | name='parent', 41 | field=mptt.fields.TreeForeignKey( 42 | blank=True, 43 | null=True, 44 | on_delete=django.db.models.deletion.CASCADE, 45 | related_name='children', 46 | to='netbox_inventory.inventoryitemgroup', 47 | ), 48 | ), 49 | migrations.AddField( 50 | model_name='inventoryitemgroup', 51 | name='rght', 52 | field=models.PositiveIntegerField(default=0, editable=False), 53 | preserve_default=False, 54 | ), 55 | migrations.AddField( 56 | model_name='inventoryitemgroup', 57 | name='tree_id', 58 | field=models.PositiveIntegerField(db_index=True, default=0, editable=False), 59 | preserve_default=False, 60 | ), 61 | migrations.AlterField( 62 | model_name='inventoryitemgroup', 63 | name='name', 64 | field=models.CharField(max_length=100), 65 | ), 66 | migrations.RunPython(rebuild_tree, migrations.RunPython.noop), 67 | migrations.AddConstraint( 68 | model_name='inventoryitemgroup', 69 | constraint=models.UniqueConstraint( 70 | fields=('parent', 'name'), 71 | name='netbox_inventory_inventoryitemgroup_parent_name', 72 | ), 73 | ), 74 | migrations.AddConstraint( 75 | model_name='inventoryitemgroup', 76 | constraint=models.UniqueConstraint( 77 | condition=models.Q(('parent__isnull', True)), 78 | fields=('name',), 79 | name='netbox_inventory_inventoryitemgroup_name', 80 | violation_error_message='A top-level group with this name already exists.', 81 | ), 82 | ), 83 | ] 84 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0005_delivery_asset_delivery.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.9 on 2023-07-25 14:34 2 | 3 | import django.db.models.deletion 4 | import taggit.managers 5 | from django.db import migrations, models 6 | 7 | import utilities.json 8 | 9 | 10 | class Migration(migrations.Migration): 11 | dependencies = [ 12 | ('tenancy', '0010_tenant_relax_uniqueness'), 13 | ('extras', '0092_delete_jobresult'), 14 | ('netbox_inventory', '0004_inventoryitemgroup_tree'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Delivery', 20 | fields=[ 21 | ( 22 | 'id', 23 | models.BigAutoField( 24 | auto_created=True, primary_key=True, serialize=False 25 | ), 26 | ), 27 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 28 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 29 | ( 30 | 'custom_field_data', 31 | models.JSONField( 32 | blank=True, 33 | default=dict, 34 | encoder=utilities.json.CustomFieldJSONEncoder, 35 | ), 36 | ), 37 | ('name', models.CharField(max_length=100)), 38 | ('date', models.DateField(blank=True, null=True)), 39 | ('description', models.CharField(blank=True, max_length=200)), 40 | ('comments', models.TextField(blank=True)), 41 | ( 42 | 'purchase', 43 | models.ForeignKey( 44 | on_delete=django.db.models.deletion.PROTECT, 45 | related_name='orders', 46 | to='netbox_inventory.purchase', 47 | ), 48 | ), 49 | ( 50 | 'receiving_contact', 51 | models.ForeignKey( 52 | blank=True, 53 | null=True, 54 | on_delete=django.db.models.deletion.PROTECT, 55 | related_name='deliveries', 56 | to='tenancy.contact', 57 | ), 58 | ), 59 | ( 60 | 'tags', 61 | taggit.managers.TaggableManager( 62 | through='extras.TaggedItem', to='extras.Tag' 63 | ), 64 | ), 65 | ], 66 | options={ 67 | 'verbose_name': 'delivery', 68 | 'verbose_name_plural': 'deliveries', 69 | 'ordering': ['purchase', 'name'], 70 | 'unique_together': {('purchase', 'name')}, 71 | }, 72 | ), 73 | migrations.AddField( 74 | model_name='asset', 75 | name='delivery', 76 | field=models.ForeignKey( 77 | blank=True, 78 | null=True, 79 | on_delete=django.db.models.deletion.PROTECT, 80 | related_name='assets', 81 | to='netbox_inventory.delivery', 82 | ), 83 | ), 84 | ] 85 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0006_purchase_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-02-15 08:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ('netbox_inventory', '0005_delivery_asset_delivery'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name='purchase', 14 | name='status', 15 | field=models.CharField(default='closed', max_length=30), 16 | preserve_default=False, 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0007_alter_asset_unique_together_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-04-18 13:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ('netbox_inventory', '0006_purchase_status'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterUniqueTogether( 13 | name='asset', 14 | unique_together=set(), 15 | ), 16 | migrations.AddConstraint( 17 | model_name='asset', 18 | constraint=models.UniqueConstraint( 19 | fields=('device_type', 'serial'), name='unique_device_type_serial' 20 | ), 21 | ), 22 | migrations.AddConstraint( 23 | model_name='asset', 24 | constraint=models.UniqueConstraint( 25 | fields=('module_type', 'serial'), name='unique_module_type_serial' 26 | ), 27 | ), 28 | migrations.AddConstraint( 29 | model_name='asset', 30 | constraint=models.UniqueConstraint( 31 | fields=('inventoryitem_type', 'serial'), 32 | name='unique_inventoryitem_type_serial', 33 | ), 34 | ), 35 | migrations.AddConstraint( 36 | model_name='asset', 37 | constraint=models.UniqueConstraint( 38 | fields=('owner', 'asset_tag'), name='unique_owner_asset_tag' 39 | ), 40 | ), 41 | migrations.AddConstraint( 42 | model_name='asset', 43 | constraint=models.UniqueConstraint( 44 | models.F('asset_tag'), 45 | condition=models.Q(('owner__isnull', True)), 46 | name='unique_asset_tag', 47 | violation_error_message='Asset with this Asset Tag and no Owner already exists.', 48 | ), 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0008_alter_asset_device_type_alter_asset_module_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.7 on 2024-07-16 09:59 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ('dcim', '0187_alter_device_vc_position'), 10 | ('netbox_inventory', '0007_alter_asset_unique_together_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='asset', 16 | name='device_type', 17 | field=models.ForeignKey( 18 | blank=True, 19 | null=True, 20 | on_delete=django.db.models.deletion.PROTECT, 21 | related_name='assets', 22 | to='dcim.devicetype', 23 | ), 24 | ), 25 | migrations.AlterField( 26 | model_name='asset', 27 | name='module_type', 28 | field=models.ForeignKey( 29 | blank=True, 30 | null=True, 31 | on_delete=django.db.models.deletion.PROTECT, 32 | related_name='assets', 33 | to='dcim.moduletype', 34 | ), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0009_add_rack.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.9 on 2024-12-11 16:47 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ('netbox_inventory', '0008_alter_asset_device_type_alter_asset_module_type'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='asset', 15 | options={ 16 | 'ordering': ( 17 | 'device_type', 18 | 'module_type', 19 | 'inventoryitem_type', 20 | 'rack_type', 21 | 'serial', 22 | ) 23 | }, 24 | ), 25 | migrations.AddField( 26 | model_name='asset', 27 | name='rack', 28 | field=models.OneToOneField( 29 | blank=True, 30 | null=True, 31 | on_delete=django.db.models.deletion.SET_NULL, 32 | related_name='assigned_asset', 33 | to='dcim.rack', 34 | ), 35 | ), 36 | migrations.AddField( 37 | model_name='asset', 38 | name='rack_type', 39 | field=models.ForeignKey( 40 | blank=True, 41 | null=True, 42 | on_delete=django.db.models.deletion.PROTECT, 43 | related_name='assets', 44 | to='dcim.racktype', 45 | ), 46 | ), 47 | migrations.AddConstraint( 48 | model_name='asset', 49 | constraint=models.UniqueConstraint( 50 | fields=('rack_type', 'serial'), name='unique_rack_type_serial' 51 | ), 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0010_asset_description_inventoryitemtype_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.6 on 2025-03-12 14:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('netbox_inventory', '0009_add_rack'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='asset', 15 | name='description', 16 | field=models.CharField(blank=True, max_length=200), 17 | ), 18 | migrations.AddField( 19 | model_name='inventoryitemtype', 20 | name='description', 21 | field=models.CharField(blank=True, max_length=200), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0011_alter_supplier_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-16 15:51 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('netbox_inventory', '0010_asset_description_inventoryitemtype_description'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='supplier', 15 | options={'ordering': ('name',)}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0012_add_auditflow.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-04-13 14:56 2 | 3 | import django.db.models.deletion 4 | import taggit.managers 5 | import utilities.json 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('core', '0012_job_object_type_optional'), 13 | ('extras', '0122_charfield_null_choices'), 14 | ('netbox_inventory', '0011_alter_supplier_options'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='AuditFlowPage', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 22 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 23 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 24 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 25 | ('name', models.CharField(max_length=100, unique=True)), 26 | ('description', models.CharField(blank=True, max_length=200)), 27 | ('comments', models.TextField(blank=True)), 28 | ('object_filter', models.JSONField(blank=True, null=True)), 29 | ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.objecttype')), 30 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 31 | ], 32 | options={ 33 | 'ordering': ('name',), 34 | 'abstract': False, 35 | }, 36 | ), 37 | migrations.CreateModel( 38 | name='AuditFlow', 39 | fields=[ 40 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 41 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 42 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 43 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 44 | ('name', models.CharField(max_length=100, unique=True)), 45 | ('description', models.CharField(blank=True, max_length=200)), 46 | ('comments', models.TextField(blank=True)), 47 | ('object_filter', models.JSONField(blank=True, null=True)), 48 | ('enabled', models.BooleanField(default=True)), 49 | ('object_type', models.ForeignKey(limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ('site', 'location', 'rack'))), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.objecttype')), 50 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 51 | ], 52 | options={ 53 | 'ordering': ('name',), 54 | 'abstract': False, 55 | }, 56 | ), 57 | migrations.CreateModel( 58 | name='AuditFlowPageAssignment', 59 | fields=[ 60 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 61 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 62 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 63 | ('weight', models.PositiveSmallIntegerField(default=100)), 64 | ('flow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='netbox_inventory.auditflow')), 65 | ('page', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='netbox_inventory.auditflowpage')), 66 | ], 67 | options={ 68 | 'ordering': ('weight',), 69 | }, 70 | ), 71 | migrations.AddField( 72 | model_name='auditflow', 73 | name='pages', 74 | field=models.ManyToManyField(related_name='assigned_flows', through='netbox_inventory.AuditFlowPageAssignment', to='netbox_inventory.auditflowpage'), 75 | ), 76 | migrations.AddConstraint( 77 | model_name='auditflowpageassignment', 78 | constraint=models.UniqueConstraint(fields=('flow', 'page'), name='netbox_inventory_auditflowpageassignment_unique_flow_page'), 79 | ), 80 | migrations.AlterField( 81 | model_name='auditflowpageassignment', 82 | name='flow', 83 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assigned_pages', to='netbox_inventory.auditflow'), 84 | ), 85 | ] 86 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0013_add_audittrail.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.8 on 2025-05-04 16:11 2 | 3 | import django.db.models.deletion 4 | import taggit.managers 5 | import utilities.json 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('contenttypes', '0002_remove_content_type_name'), 13 | ('extras', '0122_charfield_null_choices'), 14 | ('netbox_inventory', '0012_add_auditflow'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='AuditTrailSource', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 22 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 23 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 24 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 25 | ('name', models.CharField(max_length=100, unique=True)), 26 | ('description', models.CharField(blank=True, max_length=200)), 27 | ('comments', models.TextField(blank=True)), 28 | ('slug', models.SlugField(max_length=100, unique=True)), 29 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 30 | ], 31 | options={ 32 | 'ordering': ('name',), 33 | 'abstract': False, 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='AuditTrail', 38 | fields=[ 39 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 40 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 41 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 42 | ('object_id', models.PositiveBigIntegerField()), 43 | ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), 44 | ('source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='audit_trails', to='netbox_inventory.audittrailsource')), 45 | ], 46 | options={ 47 | 'ordering': ('-created', 'object_type'), 48 | 'indexes': [models.Index(fields=['object_type', 'object_id'], name='netbox_inve_object__12f05c_idx')], 49 | }, 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0014_alter_audittrail_object_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.5 on 2025-08-31 13:46 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('contenttypes', '0002_remove_content_type_name'), 11 | ('netbox_inventory', '0013_add_audittrail'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='audittrail', 17 | name='object_type', 18 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/0015_alter_asset_storage_location.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.7 on 2025-10-22 06:50 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('netbox_inventory', '0014_alter_audittrail_object_type'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='asset', 16 | name='storage_location', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='assets', to='dcim.location'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /netbox_inventory/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/migrations/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .assets import * 2 | from .audit import * 3 | from .deliveries import * 4 | -------------------------------------------------------------------------------- /netbox_inventory/models/deliveries.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from netbox.models.features import ContactsMixin 4 | 5 | from ..choices import PurchaseStatusChoices 6 | from .mixins import NamedModel 7 | 8 | 9 | class Supplier(NamedModel, ContactsMixin): 10 | """ 11 | Supplier is a legal entity that sold some assets that we keep track of. 12 | This can be the same entity as Manufacturer or a separate one. However 13 | netbox_inventory keeps track of Suppliers separate from Manufacturers. 14 | """ 15 | 16 | slug = models.SlugField( 17 | max_length=100, 18 | unique=True, 19 | ) 20 | 21 | clone_fields = ['description', 'comments'] 22 | 23 | 24 | class Purchase(NamedModel): 25 | """ 26 | Represents a purchase of a set of Assets from a Supplier. 27 | """ 28 | 29 | name = models.CharField(max_length=100) 30 | supplier = models.ForeignKey( 31 | help_text='Legal entity this purchase was made at', 32 | to='netbox_inventory.Supplier', 33 | on_delete=models.PROTECT, 34 | related_name='purchases', 35 | blank=False, 36 | null=False, 37 | ) 38 | status = models.CharField( 39 | max_length=30, 40 | choices=PurchaseStatusChoices, 41 | help_text='Status of purchase', 42 | ) 43 | date = models.DateField( 44 | help_text='Date when this purchase was made', 45 | blank=True, 46 | null=True, 47 | ) 48 | 49 | clone_fields = ['supplier', 'date', 'status', 'description', 'comments'] 50 | 51 | class Meta: 52 | ordering = ['supplier', 'name'] 53 | unique_together = (('supplier', 'name'),) 54 | 55 | def get_status_color(self): 56 | return PurchaseStatusChoices.colors.get(self.status) 57 | 58 | def __str__(self): 59 | return f'{self.supplier} {self.name}' 60 | 61 | 62 | class Delivery(NamedModel): 63 | """ 64 | Delivery is a stage in Purchase. Purchase can have multiple deliveries. 65 | In each Delivery one or more Assets were delivered. 66 | """ 67 | 68 | name = models.CharField(max_length=100) 69 | purchase = models.ForeignKey( 70 | help_text='Purchase that this delivery is part of', 71 | to='netbox_inventory.Purchase', 72 | on_delete=models.PROTECT, 73 | related_name='orders', 74 | blank=False, 75 | null=False, 76 | ) 77 | date = models.DateField( 78 | help_text='Date when this delivery was made', 79 | blank=True, 80 | null=True, 81 | ) 82 | receiving_contact = models.ForeignKey( 83 | help_text='Contact that accepted this delivery', 84 | to='tenancy.Contact', 85 | on_delete=models.PROTECT, 86 | related_name='deliveries', 87 | blank=True, 88 | null=True, 89 | ) 90 | 91 | clone_fields = ['purchase', 'date', 'receiving_contact', 'description', 'comments'] 92 | 93 | class Meta: 94 | ordering = ['purchase', 'name'] 95 | unique_together = (('purchase', 'name'),) 96 | verbose_name = 'delivery' 97 | verbose_name_plural = 'deliveries' 98 | 99 | def __str__(self): 100 | return f'{self.purchase} {self.name}' 101 | -------------------------------------------------------------------------------- /netbox_inventory/models/mixins.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from netbox.models import NetBoxModel 5 | 6 | 7 | class NamedModel(NetBoxModel): 8 | """ 9 | Named models represent something that can be identified by its name. An additional 10 | description and comments can be set. 11 | """ 12 | 13 | name = models.CharField( 14 | verbose_name=_('name'), 15 | max_length=100, 16 | unique=True, 17 | ) 18 | description = models.CharField( 19 | verbose_name=_('description'), 20 | max_length=200, 21 | blank=True, 22 | ) 23 | comments = models.TextField( 24 | verbose_name=_('comments'), 25 | blank=True, 26 | ) 27 | 28 | class Meta: 29 | abstract = True 30 | ordering = ('name',) 31 | 32 | def __str__(self): 33 | return self.name 34 | -------------------------------------------------------------------------------- /netbox_inventory/search.py: -------------------------------------------------------------------------------- 1 | from netbox.search import SearchIndex 2 | 3 | from .models import ( 4 | Asset, 5 | AuditTrailSource, 6 | Delivery, 7 | InventoryItemGroup, 8 | InventoryItemType, 9 | Purchase, 10 | Supplier, 11 | ) 12 | 13 | # 14 | # Assets 15 | # 16 | 17 | 18 | class InventoryItemGroupIndex(SearchIndex): 19 | model = InventoryItemGroup 20 | fields = ( 21 | ('name', 100), 22 | ('description', 500), 23 | ('comments', 5000), 24 | ) 25 | 26 | 27 | class InventoryItemTypeIndex(SearchIndex): 28 | model = InventoryItemType 29 | fields = ( 30 | ('model', 100), 31 | ('part_number', 100), 32 | ('description', 500), 33 | ('comments', 5000), 34 | ) 35 | 36 | 37 | class AssetIndex(SearchIndex): 38 | model = Asset 39 | fields = ( 40 | ('name', 100), 41 | ('asset_tag', 50), 42 | ('serial', 60), 43 | ('description', 500), 44 | ('comments', 5000), 45 | ) 46 | display_attrs = ('name', 'asset_tag', 'status') 47 | 48 | 49 | # 50 | # Deliveries 51 | # 52 | 53 | 54 | class SupplierIndex(SearchIndex): 55 | model = Supplier 56 | fields = ( 57 | ('name', 100), 58 | ('description', 500), 59 | ('comments', 5000), 60 | ) 61 | 62 | 63 | class PurchaseIndex(SearchIndex): 64 | model = Purchase 65 | fields = ( 66 | ('name', 100), 67 | ('description', 500), 68 | ('comments', 5000), 69 | ) 70 | 71 | 72 | class DeliveryIndex(SearchIndex): 73 | model = Delivery 74 | fields = ( 75 | ('name', 100), 76 | ('description', 500), 77 | ('comments', 5000), 78 | ) 79 | 80 | 81 | # 82 | # Audit 83 | # 84 | 85 | 86 | class AuditTrailSourceIndex(SearchIndex): 87 | model = AuditTrailSource 88 | fields = ( 89 | ('name', 100), 90 | ('slug', 110), 91 | ('description', 500), 92 | ('comments', 5000), 93 | ) 94 | 95 | 96 | indexes = [ 97 | InventoryItemGroupIndex, 98 | InventoryItemTypeIndex, 99 | AssetIndex, 100 | SupplierIndex, 101 | PurchaseIndex, 102 | DeliveryIndex, 103 | AuditTrailSourceIndex, 104 | ] 105 | -------------------------------------------------------------------------------- /netbox_inventory/signals.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.db.models.signals import post_save, pre_delete, pre_save 4 | from django.dispatch import receiver 5 | 6 | from dcim.models import Device, InventoryItem, Module, Rack 7 | from utilities.exceptions import AbortRequest 8 | 9 | from .models import Asset, Delivery 10 | from .utils import get_plugin_setting, get_status_for, is_equal_none 11 | 12 | logger = logging.getLogger('netbox.netbox_inventory.signals') 13 | 14 | 15 | @receiver(pre_save, sender=Device) 16 | @receiver(pre_save, sender=Module) 17 | @receiver(pre_save, sender=InventoryItem) 18 | @receiver(pre_save, sender=Rack) 19 | def prevent_update_serial_asset_tag(instance, **kwargs): 20 | """ 21 | When a hardware (Device, Module, InventoryItem, Rack) has an Asset assigned and 22 | user changes serial or asset_tag on hardware, prevent that change 23 | and inform that change must be made on Asset instance instead. 24 | 25 | Only enforces if `sync_hardware_serial_asset_tag` setting is true. 26 | """ 27 | try: 28 | # will raise RelatedObjectDoesNotExist if not set 29 | asset = instance.assigned_asset 30 | except Asset.DoesNotExist: 31 | return 32 | if not get_plugin_setting('sync_hardware_serial_asset_tag'): 33 | # don't enforce if sync not enabled 34 | return 35 | if instance.pk and ( 36 | not is_equal_none(asset.serial, instance.serial) 37 | or not is_equal_none(asset.asset_tag, instance.asset_tag) 38 | ): 39 | raise AbortRequest( 40 | f'Cannot change {asset.kind} serial and asset tag if asset is assigned. Please update via inventory > asset instead.' 41 | ) 42 | 43 | 44 | @receiver(pre_delete, sender=Device) 45 | @receiver(pre_delete, sender=Module) 46 | @receiver(pre_delete, sender=InventoryItem) 47 | @receiver(pre_delete, sender=Rack) 48 | def free_assigned_asset(instance, **kwargs): 49 | """ 50 | If a hardware (Device, Module, InventoryItem, Rack) has an Asset assigned and 51 | that hardware is deleted, update Asset.status to stored_status. 52 | 53 | Netbox handles deletion in a DB transaction, so if deletion failes for any 54 | reason, this status change will also be reverted. 55 | """ 56 | stored_status = get_status_for('stored') 57 | if not stored_status: 58 | return 59 | try: 60 | # will raise RelatedObjectDoesNotExist if not set 61 | asset = instance.assigned_asset 62 | except Asset.DoesNotExist: 63 | return 64 | asset.snapshot() 65 | asset.status = stored_status 66 | # also unassign that item from asset 67 | setattr(asset, asset.kind, None) 68 | asset.full_clean() 69 | asset.save(clear_old_hw=False) 70 | logger.info(f'Asset marked as stored {asset}') 71 | 72 | 73 | @receiver(post_save, sender=Delivery) 74 | def handle_delivery_purchase_change(instance, created, **kwargs): 75 | """ 76 | Update child Assets if Delivery Purchase has changed. 77 | """ 78 | if not created: 79 | Asset.objects.filter(delivery=instance).update(purchase=instance.purchase) 80 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/asset_assign.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_edit.html' %} 2 | 3 | {% block title %}Assign {{ object.hardware_type.manufacturer }} {{ object }}{% endblock %} 4 | 5 | {% block tabs %} 6 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/asset_bulk_add.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'netbox_inventory/asset_edit.html' %} 3 | {% load static %} 4 | {% load form_helpers %} 5 | 6 | {% block title %}Add multiple assets{% endblock %} 7 | 8 | {% block tabs %} 9 | {% include 'netbox_inventory/inc/asset_edit_header.html' with active_tab='bulk_add' %} 10 | {% endblock %} 11 | 12 | 13 | {% block form %} 14 | {% render_field form.count %} 15 | {% with model_form as form %} 16 | {{ block.super }} 17 | {% endwith %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/asset_bulk_import.html: -------------------------------------------------------------------------------- 1 | {% extends "generic/bulk_import.html" %} 2 | 3 | {% load helpers %} 4 | {% load form_helpers %} 5 | 6 | {% block content %} 7 | {{ block.super }} 8 | 9 |
10 |
11 |
12 |
13 |
14 | Import settings 15 |
16 |
17 |

18 | CSV import can automatically create objects related to each Asset being imported. 19 | This is controlled via plugin's settings. 20 | If a related object is missing and is not allowed to be created autmatically, import will fail. 21 |

22 |

23 | Values bellow show your current config values. 24 |

25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
Object nameCreated if missing?Plugin setting name
Manufacturer & Device Type{% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_device_type %}asset_import_create_device_type
Manufacturer & Module Type{% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_module_type %}asset_import_create_module_type
Manufacturer & InventoryItem Type{% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_inventoryitem_type %}asset_import_create_inventoryitem_type
Manufacturer & Rack Type{% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_rack_type %}asset_import_create_rack_type
Supplier & Purchase{% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_purchase %}asset_import_create_purchase
Owner & Tenant{% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_tenant %}asset_import_create_tenant
67 |
68 |
69 |
70 |
71 | {% endblock content %} 72 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/asset_create.html: -------------------------------------------------------------------------------- 1 | {% extends template_extends|default:'generic/object_edit.html' %} 2 | 3 | {% block title %}Add a new {{ asset.get_kind_display }} and associate with asset {{ asset }}{% endblock %} 4 | 5 | {% block buttons %} 6 | Cancel 7 | 10 | {% endblock buttons %} 11 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/asset_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_edit.html' %} 2 | {% load static %} 3 | {% load form_helpers %} 4 | {% load helpers %} 5 | 6 | {% block tabs %} 7 | {% include 'netbox_inventory/inc/asset_edit_header.html' with active_tab='add' %} 8 | {% endblock tabs %} 9 | 10 | {% block form %} 11 |
12 |
13 |
General
14 |
15 | {% render_field form.name %} 16 | {% render_field form.asset_tag %} 17 | {% render_field form.description %} 18 | {% render_field form.tags %} 19 | {% render_field form.status %} 20 |
21 | 22 |
23 |
24 |
Hardware
25 |
26 | {% render_field form.serial %} 27 | {% render_field form.manufacturer %} 28 |
29 | 51 |
52 |
53 |
54 | {% render_field form.device_type %} 55 |
56 |
57 | {% render_field form.module_type %} 58 |
59 |
60 | {% render_field form.inventoryitem_type %} 61 |
62 |
63 | {% render_field form.rack_type %} 64 |
65 |
66 |
67 | 68 |
69 |
70 |
Purchase
71 |
72 | {% render_field form.owner %} 73 | {% render_field form.purchase %} 74 | {% render_field form.delivery %} 75 | {% render_field form.warranty_start %} 76 | {% render_field form.warranty_end %} 77 |
78 | 79 |
80 |
81 |
Assigned to
82 |
83 | {% render_field form.tenant %} 84 | {% render_field form.contact_group %} 85 | {% render_field form.contact %} 86 |
87 | 88 |
89 |
90 |
Location
91 |
92 | {% render_field form.storage_site %} 93 | {% render_field form.storage_location %} 94 |
95 | 96 | {% if form.custom_fields %} 97 |
98 |
99 |
Custom Fields
100 |
101 | {% render_custom_fields form %} 102 |
103 | {% endif %} 104 | 105 |
106 |
Comments
107 | {% render_field form.comments %} 108 |
109 | 110 | {% endblock %} 111 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/asset_reassign.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_edit.html' %} 2 | 3 | {% block title %}Assign to {{ object.manufacturer }} {{ object }}{% endblock %} 4 | 5 | {% block tabs %} 6 | 13 | {% endblock %} 14 | 15 | {% block form %} 16 |
Select a different asset to assign to {{ object }}
17 | {{ block.super }} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/auditflow.html: -------------------------------------------------------------------------------- 1 | {% extends 'netbox_inventory/generic/baseflow.html' %} 2 | {% load i18n %} 3 | 4 | {% block flow_title %} 5 | {% trans "Audit Flow" %} 6 | {% endblock %} 7 | 8 | {% block enabled_fieled %} 9 | 10 | {% trans "Enabled" %} 11 | {% checkmark object.enabled %} 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/auditflow_pages.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_inventory.add_auditflowpageassignment %} 7 | {% with viewname=object|viewname:"pages" %} 8 | 9 | 10 | {% trans "Assign page" %} 11 | 12 | {% endwith %} 13 | {% endif %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/auditflow_run.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | {% load perms %} 5 | 6 | {% block subtitle %}{% endblock subtitle %} 7 | 8 | {% block object_identifier %} 9 | {% with object=start_object %} 10 | {{ block.super }} 11 | {% endwith %} 12 | {% endblock object_identifier %} 13 | 14 | {% block breadcrumbs %} 15 | {% with object=start_object %} 16 | {{ block.super }} 17 | 20 | {% endwith %} 21 | {% endblock breadcrumbs %} 22 | 23 | {% block controls %} 24 | {% if request.user|can_add:child_model %} 25 | {% include "netbox_inventory/inc/buttons/auditflow_add_object.html" %} 26 | {% endif %} 27 | {% endblock %} 28 | 29 | {% block tabs %} 30 | 37 | {% endblock tabs %} 38 | 39 | {% block table_controls %} 40 |
41 | {% csrf_token %} 42 | {{ block.super }} 43 |
44 | 50 | {% endblock table_controls %} 51 | 52 | {% block bulk_controls %} 53 | {% if perms.netbox_inventory.add_audittrail %} 54 | 55 | 61 | {% endif %} 62 | {{ block.super }} 63 | {% endblock bulk_controls %} 64 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/auditflowpage.html: -------------------------------------------------------------------------------- 1 | {% extends 'netbox_inventory/generic/baseflow.html' %} 2 | {% load i18n %} 3 | 4 | {% block flow_title %} 5 | {% trans "Audit Page Flow" %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/audittrailsource.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load plugins %} 3 | {% load i18n %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
{% trans "Audit Trail Source" %}
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description }}
20 |
21 | {% include 'inc/panels/custom_fields.html' %} 22 | {% plugin_left_page object %} 23 |
24 |
25 | {% include 'inc/panels/tags.html' %} 26 | {% include 'inc/panels/comments.html' %} 27 | {% plugin_right_page object %} 28 |
29 |
30 | {% endblock content %} 31 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/delivery.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load helpers %} 3 | {% load plugins %} 4 | 5 | {% block breadcrumbs %} 6 | {{ block.super }} 7 | 10 | 13 | {% endblock %} 14 | 15 | {% block content %} 16 |
17 |
18 |
19 |
Delivery
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 |
Name{{ object.name }}
Purchase{{ object.purchase|linkify }}
Receiving Contact 32 | {% if object.receiving_contact.group %} 33 | {{ object.receiving_contact.group|linkify }} / 34 | {% endif %} 35 | {{ object.receiving_contact|linkify|placeholder }} 36 |
Date{{ object.date|isodate|placeholder }}
Description{{ object.description|placeholder }}
Assets 49 | {{ asset_count }} 50 |
53 |
54 | {% include 'inc/panels/tags.html' %} 55 | {% plugin_left_page object %} 56 |
57 |
58 | {% include 'inc/panels/custom_fields.html' %} 59 | {% include 'inc/panels/comments.html' %} 60 | {% plugin_right_page object %} 61 |
62 |
63 |
64 |
65 |
66 |
67 | Delivered Assets 68 | {% if perms.netbox_inventory.add_asset %} 69 | 74 | {% endif %} 75 |
76 | {% htmx_table 'plugins:netbox_inventory:asset_list' delivery_id=object.pk %} 77 |
78 | {% plugin_full_width_page object %} 79 |
80 |
81 | {% endblock content %} 82 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/generic/baseflow.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load plugins %} 3 | {% load i18n %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
{% block flow_title %}{% endblock %}
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% block enabled_fieled %}{% endblock %} 24 |
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description }}
{% trans "Object Type" %}{{ object.object_type }}
25 |
26 |
27 |
{% trans "Object Filter" %}
28 |
29 | {% if object.object_filter %} 30 |
{{ object.object_filter|json }}
31 | {% else %} 32 | {% trans "No filter defined" %} 33 | {% endif %} 34 |
35 |
36 | {% include 'inc/panels/custom_fields.html' %} 37 | {% plugin_left_page object %} 38 |
39 |
40 | {% include 'inc/panels/tags.html' %} 41 | {% include 'inc/panels/comments.html' %} 42 | {% plugin_right_page object %} 43 |
44 |
45 | {% endblock content %} 46 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/inc/asset_edit_header.html: -------------------------------------------------------------------------------- 1 | {% load helpers %} 2 | {# renders tab navbar for asset form #} 3 | 4 | 24 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/inc/asset_info.html: -------------------------------------------------------------------------------- 1 | {% load helpers %} 2 | {# renders panel on object (device, module, inventory_item) with asset info assigned to it #} 3 | 4 |
5 |
6 | Asset 7 | {# only show reassign button if user has change permissions on asset #} 8 | {% if perms.netbox_inventory.change_asset %} 9 | 24 | {% endif %} 25 |
26 | {% if asset %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 |
Name{{ asset.hardware_type.manufacturer }} {{ asset }}{% if asset.name %} ({{ asset.name }}){% endif %}
Status{% badge asset.get_status_display bg_color=asset.get_status_color %}
Owner{{ asset.owner|linkify|placeholder }}
Purchase{{ asset.purchase|linkify|placeholder }}
Warranty remaining 47 | {% include warranty_progressbar with record=asset %} 48 |
51 | {% else %} 52 |
None assigned
53 | {% endif %} 54 |
55 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/inc/asset_stats_counts.html: -------------------------------------------------------------------------------- 1 | {% load helpers %} 2 | {# renders panel simmilar to Related Objects with counts of assets related to object #} 3 | 4 |
5 |
Assets
6 | 19 |
20 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/inc/buttons/auditflow_add_object.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load helpers %} 3 | 4 | {% if buttons|length == 1 %} 5 | {% with button=buttons|first %} 6 | 7 | 8 | {% trans "Add" %} 9 | 10 | {% endwith %} 11 | 12 | {% else %} 13 | 34 | {% endif %} 35 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/inc/buttons/auditflow_run.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load helpers %} 3 | 4 | {% if flows|length == 1 %} 5 | {% with flow=flows|first %} 6 | 7 | 8 | {% trans "Audit" %} 9 | 10 | {% endwith %} 11 | 12 | {% else %} 13 | 28 | {% endif %} 29 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/inc/buttons/audittrail_seen.html: -------------------------------------------------------------------------------- 1 | {% load helpers %} 2 | {% load perms %} 3 | 4 | {% if not record.audit_trail %} 5 | {% if perms.netbox_inventory.add_audittrail %} 6 | 14 | {% endif %} 15 | {% else %} 16 | {% if perms.netbox_inventory.delete_audittrail %} 17 | 21 | 22 | 23 | {% endif %} 24 | {% endif %} 25 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/inventoryitemgroup.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load helpers %} 3 | {% load plugins %} 4 | 5 | {% block breadcrumbs %} 6 | {{ block.super }} 7 | {% for group in object.get_ancestors %} 8 | 9 | {% endfor %} 10 | {% endblock %} 11 | 12 | {% block extra_controls %} 13 | {% if perms.netbox_inventory.add_inventoryitemtype %} 14 | 15 | Add inventory item type 16 | 17 | {% endif %} 18 | {% endblock extra_controls %} 19 | 20 | {% block content %} 21 |
22 |
23 |
24 |
Inventory Item Group
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 |
Name{{ object.name }}
Parent{{ object.parent|linkify|placeholder }}
Description{{ object.description|placeholder }}
Assets 41 | {{ asset_table.rows|length }} 42 |
45 |
46 | {% include 'inc/panels/custom_fields.html' %} 47 | 48 | 49 |
50 |
Asset count by status
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% for sc in status_counts.values %} 60 | 61 | 62 | 67 | 68 | {% empty %} 69 | 70 | {% endfor %} 71 | 72 |
StatusCount
{% badge value=sc.label bg_color=sc.color %} 63 | 64 | {{ sc.count }} 65 | 66 |
— No assets found —
73 |
74 | 75 |
76 |
Asset count by type & status
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {% for tsc in type_status_objects %} 86 | 87 | 92 | 101 | 102 | {% empty %} 103 | 104 | {% endfor %} 105 | 106 |
Inventory Item TypeStatus - Count
88 | 89 | {{ tsc.inventoryitem_type__manufacturer__name }} {{ tsc.inventoryitem_type__model }} 90 | 91 | 93 | 100 |
— No assets found —
107 |
108 | {% plugin_left_page object %} 109 |
110 |
111 |
112 |
113 | Child Groups 114 | {% if perms.netbox_inventory.add_inventoryitemgroup %} 115 | 120 | {% endif %} 121 |
122 | {% htmx_table 'plugins:netbox_inventory:inventoryitemgroup_list' ancestor_id=object.pk %} 123 |
124 | {% include 'inc/panels/tags.html' %} 125 | {% include 'inc/panels/comments.html' %} 126 | {% plugin_right_page object %} 127 |
128 |
129 |
130 |
131 |
132 |
Assets
133 | {% htmx_table 'plugins:netbox_inventory:asset_list' inventoryitem_group_id=object.pk %} 134 |
135 | {% plugin_full_width_page object %} 136 |
137 |
138 | {% endblock content %} 139 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/inventoryitemtype.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load plugins %} 3 | 4 | {% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | {% endblock %} 10 | 11 | {% block extra_controls %} 12 | {% if perms.netbox_inventory.add_asset %} 13 | 14 | Add asset 15 | 16 | {% endif %} 17 | {% endblock extra_controls %} 18 | 19 | {% block content %} 20 |
21 |
22 |
23 |
Inventory Item Type
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
Manufacturer{{ object.manufacturer|linkify }}
Model{{ object.model }}
Part number{{ object.part_number }}
Description{{ object.description|placeholder }}
Group{{ object.inventoryitem_group|linkify|placeholder }}
Assets{{ asset_count }}
50 |
51 | {% include 'inc/panels/custom_fields.html' %} 52 | {% plugin_left_page object %} 53 |
54 |
55 | {% include 'inc/panels/tags.html' %} 56 | {% include 'inc/panels/comments.html' %} 57 | {% include 'inc/panels/image_attachments.html' %} 58 | {% plugin_right_page object %} 59 |
60 |
61 |
62 |
63 | {% plugin_full_width_page object %} 64 |
65 |
66 | {% endblock content %} 67 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/purchase.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load helpers %} 3 | {% load plugins %} 4 | 5 | {% block breadcrumbs %} 6 | {{ block.super }} 7 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 |
Purchase
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 49 | 50 |
Name{{ object.name }}
Supplier{{ object.supplier|linkify }}
Status{% badge object.get_status_display bg_color=object.get_status_color %}
Date{{ object.date|isodate|placeholder }}
Description{{ object.description|placeholder }}
Deliveries 41 | {{ delivery_count }} 42 |
Assets 47 | {{ asset_count }} 48 |
51 |
52 | {% include 'inc/panels/tags.html' %} 53 | {% plugin_left_page object %} 54 |
55 |
56 | {% include 'inc/panels/custom_fields.html' %} 57 | {% include 'inc/panels/comments.html' %} 58 | {% plugin_right_page object %} 59 |
60 |
61 |
62 |
63 |
64 |
65 | Deliveries 66 | {% if perms.netbox_inventory.add_delivery %} 67 | 72 | {% endif %} 73 |
74 | {% htmx_table 'plugins:netbox_inventory:delivery_list' purchase_id=object.pk %} 75 |
76 |
77 |
78 | Purchased Assets 79 | {% if perms.netbox_inventory.add_asset %} 80 | 85 | {% endif %} 86 |
87 | {% htmx_table 'plugins:netbox_inventory:asset_list' purchase_id=object.pk %} 88 |
89 | {% plugin_full_width_page object %} 90 |
91 |
92 | {% endblock content %} 93 | -------------------------------------------------------------------------------- /netbox_inventory/templates/netbox_inventory/supplier.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load plugins %} 3 | 4 | {% block breadcrumbs %} 5 | 6 | {% endblock %} 7 | 8 | {% block extra_controls %} 9 | {% if perms.netbox_inventory.add_purchase %} 10 | 11 | Add purchase 12 | 13 | {% endif %} 14 | {% endblock extra_controls %} 15 | 16 | {% block content %} 17 |
18 |
19 |
20 |
Supplier
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 47 | 48 |
Name{{ object.name }}
Description{{ object.description }}
Purchases 33 | {{ purchase_count }} 34 |
Deliveries 39 | {{ delivery_count }} 40 |
Assets 45 | {{ asset_count }} 46 |
49 |
50 | {% include 'inc/panels/tags.html' %} 51 | {% plugin_left_page object %} 52 |
53 |
54 | {% include 'inc/panels/custom_fields.html' %} 55 | {% include 'inc/panels/comments.html' %} 56 | {% plugin_right_page object %} 57 |
58 |
59 |
60 |
61 |
62 |
Supplied Assets
63 | {% htmx_table 'plugins:netbox_inventory:asset_list' supplier_id=object.pk %} 64 |
65 | {% plugin_full_width_page object %} 66 |
67 |
68 | {% endblock content %} 69 | -------------------------------------------------------------------------------- /netbox_inventory/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/asset/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/asset/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/auditflow/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflow/test_api.py: -------------------------------------------------------------------------------- 1 | from core.models import ObjectType 2 | from dcim.models import Site 3 | from utilities.object_types import object_type_identifier 4 | from utilities.testing import APIViewTestCases 5 | 6 | from netbox_inventory.models import AuditFlow 7 | from netbox_inventory.tests.custom import APITestCase 8 | 9 | 10 | class AuditFlowTest( 11 | APITestCase, 12 | APIViewTestCases.GetObjectViewTestCase, 13 | APIViewTestCases.ListObjectsViewTestCase, 14 | APIViewTestCases.CreateObjectViewTestCase, 15 | APIViewTestCases.UpdateObjectViewTestCase, 16 | APIViewTestCases.DeleteObjectViewTestCase, 17 | ): 18 | model = AuditFlow 19 | 20 | brief_fields = [ 21 | 'display', 22 | 'id', 23 | 'name', 24 | 'url', 25 | ] 26 | 27 | bulk_update_data = { 28 | 'description': 'new description', 29 | } 30 | 31 | @classmethod 32 | def setUpTestData(cls) -> None: 33 | object_type = ObjectType.objects.get_for_model(Site) 34 | 35 | AuditFlow.objects.create( 36 | name='Flow 1', 37 | object_type=object_type, 38 | ) 39 | AuditFlow.objects.create( 40 | name='Flow 2', 41 | object_type=object_type, 42 | ) 43 | AuditFlow.objects.create( 44 | name='Flow 3', 45 | object_type=object_type, 46 | ) 47 | 48 | cls.create_data = [ 49 | { 50 | 'name': 'Flow 4', 51 | 'object_type': object_type_identifier(object_type), 52 | }, 53 | { 54 | 'name': 'Flow 5', 55 | 'object_type': object_type_identifier(object_type), 56 | }, 57 | { 58 | 'name': 'Flow 6', 59 | 'object_type': object_type_identifier(object_type), 60 | }, 61 | ] 62 | -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflow/test_filtersets.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from core.models import ObjectType 4 | from dcim.models import Location, Site 5 | from tenancy.filtersets import * 6 | from tenancy.models import * 7 | from utilities.testing import ChangeLoggedFilterSetTests 8 | 9 | from netbox_inventory.filtersets import AuditFlowFilterSet 10 | from netbox_inventory.models import ( 11 | Asset, 12 | AuditFlow, 13 | AuditFlowPage, 14 | AuditFlowPageAssignment, 15 | ) 16 | 17 | 18 | class AuditFlowTestCase(TestCase, ChangeLoggedFilterSetTests): 19 | queryset = AuditFlow.objects.all() 20 | filterset = AuditFlowFilterSet 21 | ignore_fields = ( 22 | 'object_filter', 23 | 'pages', 24 | ) 25 | 26 | @classmethod 27 | def setUpTestData(cls): 28 | cls.object_types = ( 29 | ObjectType.objects.get_for_model(Site), 30 | ObjectType.objects.get_for_model(Location), 31 | ) 32 | 33 | cls.audit_flows = ( 34 | AuditFlow( 35 | name='Flow 1', 36 | description='Description 1', 37 | object_type=cls.object_types[0], 38 | ), 39 | AuditFlow( 40 | name='Flow 2', 41 | description='Description 2', 42 | object_type=cls.object_types[1], 43 | ), 44 | AuditFlow( 45 | name='Flow 3', 46 | description='Description 3', 47 | object_type=cls.object_types[1], 48 | ), 49 | ) 50 | AuditFlow.objects.bulk_create(cls.audit_flows) 51 | 52 | def test_q(self): 53 | params = {'q': 'Flow 1'} 54 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 55 | 56 | def test_name(self): 57 | params = {'name': ['Flow 1', 'Flow 2']} 58 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 59 | 60 | def test_description(self): 61 | params = {'description': ['Description 1', 'Description 2']} 62 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 63 | 64 | def test_object_type(self): 65 | params = {'object_type': 'dcim.site'} 66 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 67 | 68 | params = {'object_type_id': [self.object_types[0].pk]} 69 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 70 | 71 | def test_page(self): 72 | audit_flow_pages = ( 73 | AuditFlowPage.objects.create( 74 | name='Page 1', 75 | object_type=ObjectType.objects.get_for_model(Asset), 76 | ), 77 | ) 78 | AuditFlowPageAssignment.objects.create( 79 | flow=self.audit_flows[0], 80 | page=audit_flow_pages[0], 81 | ) 82 | 83 | params = {'page_id': [audit_flow_pages[0].pk]} 84 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 85 | -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflow/test_models.py: -------------------------------------------------------------------------------- 1 | from dcim.models import Site 2 | 3 | from netbox_inventory.models import AuditFlow 4 | from netbox_inventory.tests.auditflowpage.test_models import BaseFlowModelTestCases 5 | 6 | 7 | class TestAuditFlowModel(BaseFlowModelTestCases.ObjectFilterTestCase): 8 | model = AuditFlow 9 | 10 | model_data = { 11 | 'name': 'Flow', 12 | 'description': 'Flow description', 13 | 'comments': 'Flow comments', 14 | } 15 | object_type = Site 16 | 17 | @classmethod 18 | def setUpTestData(cls) -> None: 19 | sites = ( 20 | Site( 21 | name='Site 1', 22 | slug='site-1', 23 | ), 24 | Site( 25 | name='Site 2', 26 | slug='site-2', 27 | ), 28 | ) 29 | Site.objects.bulk_create(sites) 30 | 31 | cls.object_filter = { 32 | 'name': sites[0].name, 33 | } 34 | -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflowpage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/auditflowpage/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflowpage/test_api.py: -------------------------------------------------------------------------------- 1 | from core.models import ObjectType 2 | from utilities.object_types import object_type_identifier 3 | from utilities.testing import APIViewTestCases 4 | 5 | from netbox_inventory.models import Asset, AuditFlowPage 6 | from netbox_inventory.tests.custom import APITestCase 7 | 8 | 9 | class AuditFlowPageTest( 10 | APITestCase, 11 | APIViewTestCases.GetObjectViewTestCase, 12 | APIViewTestCases.ListObjectsViewTestCase, 13 | APIViewTestCases.CreateObjectViewTestCase, 14 | APIViewTestCases.UpdateObjectViewTestCase, 15 | APIViewTestCases.DeleteObjectViewTestCase, 16 | ): 17 | model = AuditFlowPage 18 | 19 | brief_fields = [ 20 | 'display', 21 | 'id', 22 | 'name', 23 | 'url', 24 | ] 25 | 26 | bulk_update_data = { 27 | 'description': 'new description', 28 | } 29 | 30 | @classmethod 31 | def setUpTestData(cls) -> None: 32 | object_type = ObjectType.objects.get_for_model(Asset) 33 | 34 | AuditFlowPage.objects.create( 35 | name='Page 1', 36 | object_type=object_type, 37 | ) 38 | AuditFlowPage.objects.create( 39 | name='Page 2', 40 | object_type=object_type, 41 | ) 42 | AuditFlowPage.objects.create( 43 | name='Page 3', 44 | object_type=object_type, 45 | ) 46 | 47 | cls.create_data = [ 48 | { 49 | 'name': 'Page 4', 50 | 'object_type': object_type_identifier(object_type), 51 | }, 52 | { 53 | 'name': 'Page 5', 54 | 'object_type': object_type_identifier(object_type), 55 | }, 56 | { 57 | 'name': 'Page 6', 58 | 'object_type': object_type_identifier(object_type), 59 | }, 60 | ] 61 | -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflowpage/test_filtersets.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from core.models import ObjectType 4 | from dcim.models import Device, Site 5 | from tenancy.filtersets import * 6 | from tenancy.models import * 7 | from utilities.testing import ChangeLoggedFilterSetTests 8 | 9 | from netbox_inventory.filtersets import AuditFlowPageFilterSet 10 | from netbox_inventory.models import ( 11 | Asset, 12 | AuditFlow, 13 | AuditFlowPage, 14 | AuditFlowPageAssignment, 15 | ) 16 | 17 | 18 | class AuditFlowPageTestCase(TestCase, ChangeLoggedFilterSetTests): 19 | queryset = AuditFlowPage.objects.all() 20 | filterset = AuditFlowPageFilterSet 21 | ignore_fields = ( 22 | 'object_filter', 23 | 'assigned_flows', 24 | ) 25 | 26 | @classmethod 27 | def setUpTestData(cls): 28 | cls.object_types = ( 29 | ObjectType.objects.get_for_model(Asset), 30 | ObjectType.objects.get_for_model(Device), 31 | ) 32 | 33 | cls.audit_flow_pages = ( 34 | AuditFlowPage( 35 | name='Page 1', 36 | description='Description 1', 37 | object_type=cls.object_types[0], 38 | ), 39 | AuditFlowPage( 40 | name='Page 2', 41 | description='Description 2', 42 | object_type=cls.object_types[1], 43 | ), 44 | AuditFlowPage( 45 | name='Page 3', 46 | description='Description 3', 47 | object_type=cls.object_types[1], 48 | ), 49 | ) 50 | AuditFlowPage.objects.bulk_create(cls.audit_flow_pages) 51 | 52 | def test_q(self): 53 | params = {'q': 'Page 1'} 54 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 55 | 56 | def test_name(self): 57 | params = {'name': ['Page 1', 'Page 2']} 58 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 59 | 60 | def test_description(self): 61 | params = {'description': ['Description 1', 'Description 2']} 62 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 63 | 64 | def test_object_type(self): 65 | params = {'object_type': 'netbox_inventory.asset'} 66 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 67 | 68 | params = {'object_type_id': [self.object_types[0].pk]} 69 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 70 | 71 | def test_assigned_flow(self): 72 | audit_flows = ( 73 | AuditFlow.objects.create( 74 | name='Flow 1', 75 | object_type=ObjectType.objects.get_for_model(Site), 76 | ), 77 | ) 78 | AuditFlowPageAssignment.objects.create( 79 | flow=audit_flows[0], 80 | page=self.audit_flow_pages[0], 81 | ) 82 | 83 | params = {'assigned_flow_id': [audit_flows[0].pk]} 84 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 85 | -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflowpage/test_models.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.test import TestCase 3 | 4 | from core.models import ObjectType 5 | from dcim.models import DeviceType, InterfaceTemplate, Manufacturer 6 | 7 | from netbox_inventory.models import Asset, AuditFlowPage 8 | from netbox_inventory.models.audit import BaseFlow 9 | 10 | 11 | class BaseFlowModelTestCases: 12 | class ObjectFilterTestCase(TestCase): 13 | def _get_flow_object(self) -> BaseFlow: 14 | return self.model( 15 | object_type=ObjectType.objects.get_for_model(self.object_type), 16 | **self.model_data, 17 | ) 18 | 19 | def test_clean_object_filter(self) -> None: 20 | # No error 21 | obj = self._get_flow_object() 22 | obj.object_filter = self.object_filter 23 | obj.full_clean() 24 | 25 | # Filter is not a dictionary 26 | obj.object_filter = 'foo' 27 | self.assertRaises(ValidationError, obj.full_clean) 28 | 29 | # Filter is invalid 30 | obj.object_filter = {'field-does-not-exist': 'foo'} 31 | self.assertRaises(ValidationError, obj.full_clean) 32 | 33 | def test_get_objects_type(self) -> None: 34 | obj = self._get_flow_object() 35 | self.assertEqual(obj.get_objects().model, self.object_type) 36 | 37 | def test_get_objects_filter(self) -> None: 38 | # No filter 39 | obj = self._get_flow_object() 40 | self.assertEqual( 41 | obj.get_objects().count(), 42 | self.object_type.objects.count(), 43 | ) 44 | 45 | # Filter objects 46 | obj.object_filter = self.object_filter 47 | self.assertEqual( 48 | obj.get_objects().count(), 49 | self.object_type.objects.filter(**self.object_filter).count(), 50 | ) 51 | 52 | 53 | class TestAuditFlowPageModel(BaseFlowModelTestCases.ObjectFilterTestCase): 54 | model = AuditFlowPage 55 | 56 | model_data = { 57 | 'name': 'Page', 58 | 'description': 'Page description', 59 | 'comments': 'Page comments', 60 | } 61 | object_type = Asset 62 | 63 | @classmethod 64 | def setUpTestData(cls) -> None: 65 | manufacturer = Manufacturer.objects.create( 66 | name='manufacturer 1', 67 | slug='manufacturer-1', 68 | ) 69 | device_type = DeviceType.objects.create( 70 | manufacturer=manufacturer, 71 | model='DeviceType 1', 72 | slug='devicetype-1', 73 | ) 74 | 75 | assets = ( 76 | Asset( 77 | asset_tag='asset1', 78 | serial='asset1', 79 | status='stored', 80 | device_type=device_type, 81 | ), 82 | Asset( 83 | asset_tag='asset2', 84 | serial='asset2', 85 | status='stored', 86 | device_type=device_type, 87 | ), 88 | ) 89 | Asset.objects.bulk_create(assets) 90 | 91 | cls.object_filter = { 92 | 'asset_tag': assets[0].asset_tag, 93 | } 94 | 95 | def test_clean_object_type(self) -> None: 96 | # No error 97 | page1 = AuditFlowPage( 98 | name='Page 1', 99 | object_type=ObjectType.objects.get_for_model(Asset), 100 | ) 101 | page1.full_clean() 102 | 103 | # No list view 104 | page2 = AuditFlowPage( 105 | name='Page 1', 106 | object_type=ObjectType.objects.get_for_model(InterfaceTemplate), 107 | ) 108 | self.assertRaises(ValidationError, page2.full_clean) 109 | -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflowpage/test_views.py: -------------------------------------------------------------------------------- 1 | from core.models import ObjectType 2 | from utilities.object_types import object_type_identifier 3 | from utilities.testing import ViewTestCases 4 | 5 | from netbox_inventory.models import Asset, AuditFlowPage 6 | from netbox_inventory.tests.custom import ModelViewTestCase 7 | 8 | 9 | class AuditFlowPageViewTestCase( 10 | ModelViewTestCase, 11 | ViewTestCases.GetObjectViewTestCase, 12 | ViewTestCases.GetObjectChangelogViewTestCase, 13 | ViewTestCases.CreateObjectViewTestCase, 14 | ViewTestCases.EditObjectViewTestCase, 15 | ViewTestCases.DeleteObjectViewTestCase, 16 | ViewTestCases.ListObjectsViewTestCase, 17 | ViewTestCases.BulkImportObjectsViewTestCase, 18 | ViewTestCases.BulkDeleteObjectsViewTestCase, 19 | ): 20 | model = AuditFlowPage 21 | 22 | @classmethod 23 | def setUpTestData(cls) -> None: 24 | object_type = ObjectType.objects.get_for_model(Asset) 25 | 26 | page1 = AuditFlowPage.objects.create( 27 | name='Page 1', 28 | object_type=object_type, 29 | ) 30 | page2 = AuditFlowPage.objects.create( 31 | name='Page 2', 32 | object_type=object_type, 33 | ) 34 | page3 = AuditFlowPage.objects.create( 35 | name='Page 3', 36 | object_type=object_type, 37 | ) 38 | 39 | cls.form_data = { 40 | 'name': 'Page', 41 | 'description': 'Page description', 42 | 'object_type': object_type.pk, 43 | 'object_filter': '{"status": "stored"}', 44 | 'comments': 'Page comments', 45 | } 46 | cls.csv_data = ( 47 | 'name,object_type', 48 | f'Page 4,{object_type_identifier(object_type)}', 49 | f'Page 5,{object_type_identifier(object_type)}', 50 | f'Page 6,{object_type_identifier(object_type)}', 51 | ) 52 | cls.csv_update_data = ( 53 | 'id,description', 54 | f'{page1.pk},description 1', 55 | f'{page2.pk},description 2', 56 | f'{page3.pk},description 3', 57 | ) 58 | -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflowpageassignment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/auditflowpageassignment/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflowpageassignment/test_api.py: -------------------------------------------------------------------------------- 1 | from core.models import ObjectType 2 | from dcim.models import Site 3 | from utilities.testing import APIViewTestCases 4 | 5 | from netbox_inventory.models import ( 6 | Asset, 7 | AuditFlow, 8 | AuditFlowPage, 9 | AuditFlowPageAssignment, 10 | ) 11 | from netbox_inventory.tests.custom import APITestCase 12 | 13 | 14 | class AuditFlowPageAssignmentTest( 15 | APITestCase, 16 | APIViewTestCases.GetObjectViewTestCase, 17 | APIViewTestCases.ListObjectsViewTestCase, 18 | APIViewTestCases.CreateObjectViewTestCase, 19 | APIViewTestCases.UpdateObjectViewTestCase, 20 | APIViewTestCases.DeleteObjectViewTestCase, 21 | ): 22 | model = AuditFlowPageAssignment 23 | 24 | brief_fields = [ 25 | 'display', 26 | 'flow', 27 | 'id', 28 | 'page', 29 | 'url', 30 | ] 31 | 32 | bulk_update_data = { 33 | 'weight': 500, 34 | } 35 | 36 | @classmethod 37 | def setUpTestData(cls) -> None: 38 | audit_flows = ( 39 | AuditFlow( 40 | name='Flow 1', 41 | object_type=ObjectType.objects.get_for_model(Site), 42 | ), 43 | ) 44 | AuditFlow.objects.bulk_create(audit_flows) 45 | 46 | object_type = ObjectType.objects.get_for_model(Asset) 47 | audit_flow_pages = ( 48 | AuditFlowPage(name='Page 1', object_type=object_type), 49 | AuditFlowPage(name='Page 2', object_type=object_type), 50 | AuditFlowPage(name='Page 3', object_type=object_type), 51 | AuditFlowPage(name='Page 4', object_type=object_type), 52 | AuditFlowPage(name='Page 5', object_type=object_type), 53 | AuditFlowPage(name='Page 6', object_type=object_type), 54 | ) 55 | AuditFlowPage.objects.bulk_create(audit_flow_pages) 56 | 57 | audit_flow_page_assignments = ( 58 | AuditFlowPageAssignment(flow=audit_flows[0], page=audit_flow_pages[0]), 59 | AuditFlowPageAssignment(flow=audit_flows[0], page=audit_flow_pages[1]), 60 | AuditFlowPageAssignment(flow=audit_flows[0], page=audit_flow_pages[2]), 61 | ) 62 | AuditFlowPageAssignment.objects.bulk_create(audit_flow_page_assignments) 63 | 64 | cls.create_data = ( 65 | { 66 | 'flow': audit_flows[0].pk, 67 | 'page': audit_flow_pages[3].pk, 68 | }, 69 | { 70 | 'flow': audit_flows[0].pk, 71 | 'page': audit_flow_pages[4].pk, 72 | }, 73 | { 74 | 'flow': audit_flows[0].pk, 75 | 'page': audit_flow_pages[5].pk, 76 | }, 77 | ) 78 | -------------------------------------------------------------------------------- /netbox_inventory/tests/auditflowpageassignment/test_views.py: -------------------------------------------------------------------------------- 1 | from core.models import ObjectType 2 | from dcim.models import Site 3 | from utilities.testing import ViewTestCases 4 | 5 | from netbox_inventory.models import ( 6 | Asset, 7 | AuditFlow, 8 | AuditFlowPage, 9 | AuditFlowPageAssignment, 10 | ) 11 | from netbox_inventory.tests.custom import ModelViewTestCase 12 | 13 | 14 | class AuditFlowPageAssignmentViewTestCase( 15 | ModelViewTestCase, 16 | ViewTestCases.CreateObjectViewTestCase, 17 | ViewTestCases.EditObjectViewTestCase, 18 | ViewTestCases.DeleteObjectViewTestCase, 19 | ViewTestCases.BulkEditObjectsViewTestCase, 20 | ViewTestCases.BulkDeleteObjectsViewTestCase, 21 | ): 22 | model = AuditFlowPageAssignment 23 | 24 | @classmethod 25 | def setUpTestData(cls) -> None: 26 | audit_flows = ( 27 | AuditFlow( 28 | name='Flow 1', 29 | object_type=ObjectType.objects.get_for_model(Site), 30 | ), 31 | ) 32 | AuditFlow.objects.bulk_create(audit_flows) 33 | 34 | object_type = ObjectType.objects.get_for_model(Asset) 35 | audit_flow_pages = ( 36 | AuditFlowPage(name='Page 1', object_type=object_type), 37 | AuditFlowPage(name='Page 2', object_type=object_type), 38 | AuditFlowPage(name='Page 3', object_type=object_type), 39 | AuditFlowPage(name='Page 4', object_type=object_type), 40 | ) 41 | AuditFlowPage.objects.bulk_create(audit_flow_pages) 42 | 43 | audit_flow_page_assignments = ( 44 | AuditFlowPageAssignment(flow=audit_flows[0], page=audit_flow_pages[0]), 45 | AuditFlowPageAssignment(flow=audit_flows[0], page=audit_flow_pages[1]), 46 | AuditFlowPageAssignment(flow=audit_flows[0], page=audit_flow_pages[2]), 47 | ) 48 | AuditFlowPageAssignment.objects.bulk_create(audit_flow_page_assignments) 49 | 50 | cls.form_data = { 51 | 'flow': audit_flows[0].pk, 52 | 'page': audit_flow_pages[3].pk, 53 | 'weight': 250, 54 | } 55 | cls.bulk_edit_data = { 56 | 'weight': 500, 57 | } 58 | -------------------------------------------------------------------------------- /netbox_inventory/tests/audittrail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/audittrail/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/audittrail/test_api.py: -------------------------------------------------------------------------------- 1 | from dcim.models import DeviceType, Manufacturer 2 | from utilities.testing import APIViewTestCases 3 | 4 | from netbox_inventory.models import Asset, AuditTrail, AuditTrailSource 5 | from netbox_inventory.tests.custom import APITestCase 6 | 7 | 8 | class AuditTrailTest( 9 | APITestCase, 10 | APIViewTestCases.GetObjectViewTestCase, 11 | APIViewTestCases.ListObjectsViewTestCase, 12 | APIViewTestCases.CreateObjectViewTestCase, 13 | APIViewTestCases.UpdateObjectViewTestCase, 14 | APIViewTestCases.DeleteObjectViewTestCase, 15 | ): 16 | model = AuditTrail 17 | 18 | brief_fields = [ 19 | 'display', 20 | 'id', 21 | 'object', 22 | 'url', 23 | ] 24 | 25 | @classmethod 26 | def setUpTestData(cls) -> None: 27 | manufacturer = Manufacturer.objects.create( 28 | name='manufacturer 1', 29 | slug='manufacturer-1', 30 | ) 31 | device_type = DeviceType.objects.create( 32 | manufacturer=manufacturer, 33 | model='DeviceType 1', 34 | slug='devicetype-1', 35 | ) 36 | 37 | assets = ( 38 | Asset( 39 | asset_tag='asset1', 40 | serial='asset1', 41 | status='stored', 42 | device_type=device_type, 43 | ), 44 | Asset( 45 | asset_tag='asset2', 46 | serial='asset2', 47 | status='stored', 48 | device_type=device_type, 49 | ), 50 | Asset( 51 | asset_tag='asset3', 52 | serial='asset3', 53 | status='stored', 54 | device_type=device_type, 55 | ), 56 | Asset( 57 | asset_tag='asset4', 58 | serial='asset4', 59 | status='stored', 60 | device_type=device_type, 61 | ), 62 | ) 63 | Asset.objects.bulk_create(assets) 64 | 65 | audit_trails = ( 66 | AuditTrail(object=assets[0]), 67 | AuditTrail(object=assets[1]), 68 | AuditTrail(object=assets[2]), 69 | ) 70 | AuditTrail.objects.bulk_create(audit_trails) 71 | 72 | audit_trail_source = AuditTrailSource.objects.create( 73 | name='Source 1', 74 | slug='source-1', 75 | ) 76 | 77 | cls.create_data = [ 78 | { 79 | 'object_type': 'netbox_inventory.asset', 80 | 'object_id': assets[0].pk, 81 | }, 82 | { 83 | 'object_type': 'netbox_inventory.asset', 84 | 'object_id': assets[1].pk, 85 | }, 86 | { 87 | 'object_type': 'netbox_inventory.asset', 88 | 'object_id': assets[2].pk, 89 | }, 90 | # With source 91 | { 92 | 'object_type': 'netbox_inventory.asset', 93 | 'object_id': assets[0].pk, 94 | 'source': audit_trail_source.pk, 95 | }, 96 | ] 97 | 98 | cls.bulk_update_data = { 99 | 'object_id': assets[3].pk, 100 | } 101 | -------------------------------------------------------------------------------- /netbox_inventory/tests/audittrail/test_filterset.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from core.models import ObjectType 4 | from dcim.models import DeviceType, Manufacturer 5 | from tenancy.filtersets import * 6 | from tenancy.models import * 7 | from utilities.testing import ChangeLoggedFilterSetTests 8 | 9 | from netbox_inventory.filtersets import AuditTrailFilterSet 10 | from netbox_inventory.models import Asset, AuditTrail, AuditTrailSource 11 | 12 | 13 | class AuditFlowTestCase(TestCase, ChangeLoggedFilterSetTests): 14 | queryset = AuditTrail.objects.all() 15 | filterset = AuditTrailFilterSet 16 | 17 | @classmethod 18 | def setUpTestData(cls) -> None: 19 | manufacturer = Manufacturer.objects.create( 20 | name='manufacturer 1', 21 | slug='manufacturer-1', 22 | ) 23 | device_type = DeviceType.objects.create( 24 | manufacturer=manufacturer, 25 | model='DeviceType 1', 26 | slug='devicetype-1', 27 | ) 28 | 29 | assets = ( 30 | Asset( 31 | asset_tag='asset1', 32 | serial='asset1', 33 | status='stored', 34 | device_type=device_type, 35 | ), 36 | Asset( 37 | asset_tag='asset2', 38 | serial='asset2', 39 | status='stored', 40 | device_type=device_type, 41 | ), 42 | Asset( 43 | asset_tag='asset3', 44 | serial='asset3', 45 | status='stored', 46 | device_type=device_type, 47 | ), 48 | ) 49 | Asset.objects.bulk_create(assets) 50 | 51 | audit_trail_sources = ( 52 | AuditTrailSource( 53 | name='Source 1', 54 | slug='source-1', 55 | ), 56 | ) 57 | AuditTrailSource.objects.bulk_create(audit_trail_sources) 58 | 59 | audit_trails = ( 60 | AuditTrail(object=assets[0], source=audit_trail_sources[0]), 61 | AuditTrail(object=assets[1]), 62 | AuditTrail(object=assets[2]), 63 | AuditTrail(object=device_type), 64 | ) 65 | AuditTrail.objects.bulk_create(audit_trails) 66 | 67 | def test_object_type(self): 68 | params = {'object_type': 'netbox_inventory.asset'} 69 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) 70 | 71 | object_type = ObjectType.objects.get_for_model(Asset) 72 | params = {'object_type_id': object_type.pk} 73 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) 74 | 75 | def test_source(self): 76 | audit_trail_source = AuditTrailSource.objects.first() 77 | 78 | params = {'source': [audit_trail_source.slug]} 79 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 80 | 81 | params = {'source_id': [audit_trail_source.pk]} 82 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 83 | -------------------------------------------------------------------------------- /netbox_inventory/tests/audittrailsource/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/audittrailsource/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/audittrailsource/test_api.py: -------------------------------------------------------------------------------- 1 | from utilities.testing import APIViewTestCases 2 | 3 | from netbox_inventory.models import AuditTrailSource 4 | from netbox_inventory.tests.custom import APITestCase 5 | 6 | 7 | class AuditTrailSourceTest( 8 | APITestCase, 9 | APIViewTestCases.GetObjectViewTestCase, 10 | APIViewTestCases.ListObjectsViewTestCase, 11 | APIViewTestCases.CreateObjectViewTestCase, 12 | APIViewTestCases.UpdateObjectViewTestCase, 13 | APIViewTestCases.DeleteObjectViewTestCase, 14 | ): 15 | model = AuditTrailSource 16 | 17 | brief_fields = [ 18 | 'display', 19 | 'id', 20 | 'name', 21 | 'slug', 22 | 'url', 23 | ] 24 | 25 | create_data = [ 26 | {'name': 'Source 4', 'slug': 'source-4'}, 27 | {'name': 'Source 5', 'slug': 'source-5'}, 28 | {'name': 'Source 6', 'slug': 'source-6'}, 29 | ] 30 | 31 | bulk_update_data = { 32 | 'description': 'new description', 33 | } 34 | 35 | @classmethod 36 | def setUpTestData(cls) -> None: 37 | audit_trail_sources = ( 38 | AuditTrailSource(name='Source 1', slug='source-1'), 39 | AuditTrailSource(name='Source 2', slug='source-2'), 40 | AuditTrailSource(name='Source 3', slug='source-3'), 41 | ) 42 | AuditTrailSource.objects.bulk_create(audit_trail_sources) 43 | -------------------------------------------------------------------------------- /netbox_inventory/tests/audittrailsource/test_filtersets.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from tenancy.filtersets import * 4 | from tenancy.models import * 5 | from utilities.testing import ChangeLoggedFilterSetTests 6 | 7 | from netbox_inventory.filtersets import AuditTrailSourceFilterSet 8 | from netbox_inventory.models import ( 9 | AuditTrailSource, 10 | ) 11 | 12 | 13 | class AuditTrailSourceTestCase(TestCase, ChangeLoggedFilterSetTests): 14 | queryset = AuditTrailSource.objects.all() 15 | filterset = AuditTrailSourceFilterSet 16 | 17 | @classmethod 18 | def setUpTestData(cls): 19 | audit_trail_sources = ( 20 | AuditTrailSource( 21 | name='Source 1', 22 | slug='source-1', 23 | description='Description 1', 24 | ), 25 | AuditTrailSource( 26 | name='Source 2', 27 | slug='source-2', 28 | description='Description 2', 29 | ), 30 | AuditTrailSource( 31 | name='Source 3', 32 | slug='source-3', 33 | description='Description 3', 34 | ), 35 | ) 36 | AuditTrailSource.objects.bulk_create(audit_trail_sources) 37 | 38 | def test_q(self): 39 | params = {'q': 'Source 1'} 40 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 41 | 42 | def test_name(self): 43 | params = {'name': ['Source 1', 'Source 2']} 44 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 45 | 46 | def test_description(self): 47 | params = {'description': ['Description 1', 'Description 2']} 48 | self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) 49 | -------------------------------------------------------------------------------- /netbox_inventory/tests/audittrailsource/test_views.py: -------------------------------------------------------------------------------- 1 | from django.test import override_settings 2 | from django.urls import reverse 3 | 4 | from utilities.testing import ViewTestCases 5 | 6 | from netbox_inventory.models import AuditTrailSource 7 | from netbox_inventory.tests.custom import ModelViewTestCase 8 | 9 | 10 | class AuditTrailSourceViewTestCase( 11 | ModelViewTestCase, 12 | ViewTestCases.GetObjectViewTestCase, 13 | ViewTestCases.GetObjectChangelogViewTestCase, 14 | ViewTestCases.CreateObjectViewTestCase, 15 | ViewTestCases.EditObjectViewTestCase, 16 | ViewTestCases.DeleteObjectViewTestCase, 17 | ViewTestCases.ListObjectsViewTestCase, 18 | ViewTestCases.BulkImportObjectsViewTestCase, 19 | ViewTestCases.BulkDeleteObjectsViewTestCase, 20 | ): 21 | model = AuditTrailSource 22 | 23 | form_data = { 24 | 'name': 'Source', 25 | 'slug': 'source', 26 | 'description': 'Source description', 27 | 'comments': 'Source comments', 28 | } 29 | 30 | csv_data = ( 31 | 'name,slug', 32 | 'Source 4,source-4', 33 | 'Source 5,source-5', 34 | 'Source 6,source-6', 35 | ) 36 | 37 | bulk_edit_data = { 38 | 'description': 'Bulk description', 39 | } 40 | 41 | @classmethod 42 | def setUpTestData(cls) -> None: 43 | audit_trail_sources = ( 44 | AuditTrailSource(name='Source 1', slug='source-1'), 45 | AuditTrailSource(name='Source 2', slug='source-2'), 46 | AuditTrailSource(name='Source 3', slug='source-3'), 47 | ) 48 | AuditTrailSource.objects.bulk_create(audit_trail_sources) 49 | 50 | cls.csv_update_data = ( 51 | 'id,description', 52 | f'{audit_trail_sources[0].pk},description 1', 53 | f'{audit_trail_sources[1].pk},description 2', 54 | f'{audit_trail_sources[2].pk},description 3', 55 | ) 56 | 57 | @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) 58 | def test_view_audittrailsource_trails(self): 59 | audit_trail_source = AuditTrailSource.objects.first() 60 | 61 | url = reverse( 62 | 'plugins:netbox_inventory:audittrailsource_trails', 63 | kwargs={'pk': audit_trail_source.pk}, 64 | ) 65 | self.assertHttpStatus(self.client.get(url), 200) 66 | -------------------------------------------------------------------------------- /netbox_inventory/tests/custom.py: -------------------------------------------------------------------------------- 1 | #### 2 | #### Taken from https://github.com/auroraresearchlab/netbox-dns/blob/main/netbox_dns/tests/custom.py 3 | #### 4 | #### Makes it so Netbox test utils work with plugins 5 | #### 6 | 7 | from django.urls import reverse 8 | 9 | from utilities.testing.api import APITestCase as NetBoxAPITestCase 10 | from utilities.testing.views import ModelViewTestCase as NetBoxModelViewTestCase 11 | 12 | 13 | class ModelViewTestCase(NetBoxModelViewTestCase): 14 | """ 15 | Customized ModelViewTestCase for work with plugins 16 | """ 17 | 18 | def _get_base_url(self): 19 | """ 20 | Return the base format for a URL for the test's model. Override this to test for a model which belongs 21 | to a different app (e.g. testing Interfaces within the virtualization app). 22 | """ 23 | return ( 24 | f'plugins:{self.model._meta.app_label}:{self.model._meta.model_name}_{{}}' 25 | ) 26 | 27 | 28 | class APITestCase(NetBoxAPITestCase): 29 | """ 30 | Customized APITestCase for work with plugins 31 | """ 32 | 33 | def _get_detail_url(self, instance): 34 | viewname = f'plugins-api:{self._get_view_namespace()}:{instance._meta.model_name}-detail' 35 | return reverse(viewname, kwargs={'pk': instance.pk}) 36 | 37 | def _get_list_url(self): 38 | viewname = f'plugins-api:{self._get_view_namespace()}:{self.model._meta.model_name}-list' 39 | return reverse(viewname) 40 | -------------------------------------------------------------------------------- /netbox_inventory/tests/delivery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/delivery/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/delivery/test_api.py: -------------------------------------------------------------------------------- 1 | from utilities.testing import APIViewTestCases 2 | 3 | from ...models import Delivery, Purchase, Supplier 4 | from ..custom import APITestCase 5 | 6 | 7 | class DeliveryTest( 8 | APITestCase, 9 | APIViewTestCases.GetObjectViewTestCase, 10 | APIViewTestCases.ListObjectsViewTestCase, 11 | APIViewTestCases.CreateObjectViewTestCase, 12 | APIViewTestCases.UpdateObjectViewTestCase, 13 | APIViewTestCases.DeleteObjectViewTestCase, 14 | ): 15 | model = Delivery 16 | brief_fields = ['date', 'description', 'display', 'id', 'name', 'url'] 17 | 18 | bulk_update_data = { 19 | 'description': 'new description', 20 | } 21 | 22 | @classmethod 23 | def setUpTestData(cls) -> None: 24 | supplier1 = Supplier.objects.create(name='Supplier1', slug='supplier1') 25 | purchase1 = Purchase.objects.create( 26 | name='Purchase1', supplier=supplier1, status='closed' 27 | ) 28 | Delivery.objects.create(name='Delivery 1', purchase=purchase1) 29 | Delivery.objects.create(name='Delivery 2', purchase=purchase1) 30 | Delivery.objects.create(name='Delivery 3', purchase=purchase1) 31 | cls.create_data = [ 32 | { 33 | 'name': 'Delivery 4', 34 | 'purchase': purchase1.pk, 35 | }, 36 | { 37 | 'name': 'Delivery 5', 38 | 'purchase': purchase1.pk, 39 | }, 40 | { 41 | 'name': 'Delivery 6', 42 | 'purchase': purchase1.pk, 43 | }, 44 | ] 45 | -------------------------------------------------------------------------------- /netbox_inventory/tests/delivery/test_views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from utilities.testing import ViewTestCases 4 | 5 | from netbox_inventory.models import Delivery, Purchase, Supplier 6 | from netbox_inventory.tests.custom import ModelViewTestCase 7 | 8 | 9 | class DeliveryTestCase( 10 | ModelViewTestCase, 11 | ViewTestCases.PrimaryObjectViewTestCase, 12 | ): 13 | model = Delivery 14 | 15 | @classmethod 16 | def setUpTestData(cls): 17 | supplier1 = Supplier.objects.create( 18 | name='Supplier 1', 19 | slug='supplier1', 20 | ) 21 | supplier2 = Supplier.objects.create( 22 | name='Supplier 2', 23 | slug='supplier2', 24 | ) 25 | purchase1 = Purchase.objects.create( 26 | name='Purchase 1', 27 | supplier=supplier1, 28 | status='closed', 29 | ) 30 | purchase2 = Purchase.objects.create( 31 | name='Purchase 1', 32 | supplier=supplier2, 33 | status='closed', 34 | ) 35 | delivery1 = Delivery.objects.create( 36 | name='Delivery 1', 37 | purchase=purchase1, 38 | ) 39 | delivery2 = Delivery.objects.create( 40 | name='Delivery 2', 41 | purchase=purchase1, 42 | ) 43 | delivery3 = Delivery.objects.create( 44 | name='Delivery 1', 45 | purchase=purchase2, 46 | ) 47 | cls.form_data = { 48 | 'name': 'Delivery', 49 | 'purchase': purchase1.pk, 50 | 'description': 'Delivery description', 51 | 'date': datetime.date(day=1, month=1, year=2023), 52 | } 53 | cls.csv_data = ( 54 | 'name,purchase,date', 55 | f'Delivery 4,{purchase1.pk},2023-03-26', 56 | f'Delivery 5,{purchase1.pk},2023-03-26', 57 | f'Delivery 6,{purchase1.pk},2023-03-26', 58 | ) 59 | cls.csv_update_data = ( 60 | 'id,description,purchase', 61 | f'{delivery1.pk},description 1,{delivery1.purchase.pk}', 62 | f'{delivery2.pk},description 2,{delivery2.purchase.pk}', 63 | f'{delivery3.pk},description 3,{delivery3.purchase.pk}', 64 | ) 65 | cls.bulk_edit_data = { 66 | 'description': 'bulk description', 67 | 'date': datetime.date(day=1, month=1, year=2022), 68 | } 69 | -------------------------------------------------------------------------------- /netbox_inventory/tests/inventoryitem_group/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/inventoryitem_group/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/inventoryitem_group/test_api.py: -------------------------------------------------------------------------------- 1 | from utilities.testing import APIViewTestCases 2 | 3 | from ...models import InventoryItemGroup 4 | from ..custom import APITestCase 5 | 6 | 7 | class InventoryItemGroupTest( 8 | APITestCase, 9 | APIViewTestCases.GetObjectViewTestCase, 10 | APIViewTestCases.ListObjectsViewTestCase, 11 | APIViewTestCases.CreateObjectViewTestCase, 12 | APIViewTestCases.UpdateObjectViewTestCase, 13 | APIViewTestCases.DeleteObjectViewTestCase, 14 | ): 15 | model = InventoryItemGroup 16 | brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'url'] 17 | create_data = [ 18 | { 19 | 'name': 'InventoryItemGroup 4', 20 | }, 21 | { 22 | 'name': 'InventoryItemGroup 5', 23 | }, 24 | { 25 | 'name': 'InventoryItemGroup 6', 26 | }, 27 | ] 28 | 29 | @classmethod 30 | def setUpTestData(cls) -> None: 31 | InventoryItemGroup.objects.create(name='InventoryItemGroup 1') 32 | InventoryItemGroup.objects.create(name='InventoryItemGroup 2') 33 | InventoryItemGroup.objects.create(name='InventoryItemGroup 3') 34 | -------------------------------------------------------------------------------- /netbox_inventory/tests/inventoryitem_group/test_views.py: -------------------------------------------------------------------------------- 1 | from utilities.testing import ViewTestCases 2 | 3 | from netbox_inventory.models import InventoryItemGroup 4 | from netbox_inventory.tests.custom import ModelViewTestCase 5 | 6 | 7 | class InventoryItemGroupTestCase( 8 | ModelViewTestCase, 9 | ViewTestCases.PrimaryObjectViewTestCase, 10 | ): 11 | model = InventoryItemGroup 12 | 13 | @classmethod 14 | def setUpTestData(cls): 15 | iig_parent = InventoryItemGroup.objects.create(name='parent group') 16 | iig1 = InventoryItemGroup.objects.create(name='IIG1') 17 | iig2 = InventoryItemGroup.objects.create(name='IIG2') 18 | iig3 = InventoryItemGroup.objects.create(name='IIG3') 19 | 20 | cls.form_data = { 21 | 'name': 'InventoryItemGroup', 22 | } 23 | cls.csv_data = ( 24 | 'name,comments', 25 | 'IIG4,a comment', 26 | 'IIG5,a comment', 27 | 'IIG6,a comment', 28 | ) 29 | cls.csv_update_data = ( 30 | 'id,name,parent', 31 | f'{iig1.pk},IIG1_update,{iig_parent.name}', 32 | f'{iig2.pk},IIG2_update,{iig_parent.name}', 33 | f'{iig3.pk},IIG3_update,{iig_parent.name}', 34 | ) 35 | cls.bulk_edit_data = { 36 | 'comments': 'updated', 37 | } 38 | -------------------------------------------------------------------------------- /netbox_inventory/tests/inventoryitem_type/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/inventoryitem_type/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/inventoryitem_type/test_api.py: -------------------------------------------------------------------------------- 1 | from dcim.models import Manufacturer 2 | from utilities.testing import APIViewTestCases 3 | 4 | from ...models import InventoryItemGroup, InventoryItemType 5 | from ..custom import APITestCase 6 | 7 | 8 | class InventoryItemTypeTest( 9 | APITestCase, 10 | APIViewTestCases.GetObjectViewTestCase, 11 | APIViewTestCases.ListObjectsViewTestCase, 12 | APIViewTestCases.CreateObjectViewTestCase, 13 | APIViewTestCases.UpdateObjectViewTestCase, 14 | APIViewTestCases.DeleteObjectViewTestCase, 15 | ): 16 | model = InventoryItemType 17 | brief_fields = [ 18 | 'description', 19 | 'display', 20 | 'id', 21 | 'manufacturer', 22 | 'model', 23 | 'slug', 24 | 'url', 25 | ] 26 | 27 | @classmethod 28 | def setUpTestData(cls) -> None: 29 | manufacturer1 = Manufacturer.objects.create( 30 | name='Manufacturer 1', slug='manufacturer1' 31 | ) 32 | ig1 = InventoryItemGroup.objects.create(name='IG1') 33 | InventoryItemType.objects.create( 34 | model='InventoryItemType 1', 35 | slug='inventoryitemtype1', 36 | manufacturer=manufacturer1, 37 | ) 38 | InventoryItemType.objects.create( 39 | model='InventoryItemType 2', 40 | slug='inventoryitemtype2', 41 | manufacturer=manufacturer1, 42 | ) 43 | InventoryItemType.objects.create( 44 | model='InventoryItemType 3', 45 | slug='inventoryitemtype3', 46 | manufacturer=manufacturer1, 47 | ) 48 | cls.create_data = [ 49 | { 50 | 'model': 'InventoryItemType 4', 51 | 'slug': 'inventoryitemtype4', 52 | 'manufacturer': manufacturer1.pk, 53 | }, 54 | { 55 | 'model': 'InventoryItemType 5', 56 | 'slug': 'inventoryitemtype5', 57 | 'manufacturer': manufacturer1.pk, 58 | }, 59 | { 60 | 'model': 'InventoryItemType 6', 61 | 'slug': 'inventoryitemtype6', 62 | 'manufacturer': manufacturer1.pk, 63 | }, 64 | ] 65 | cls.bulk_update_data = { 66 | 'inventoryitem_group': ig1.pk, 67 | } 68 | -------------------------------------------------------------------------------- /netbox_inventory/tests/inventoryitem_type/test_views.py: -------------------------------------------------------------------------------- 1 | from dcim.models import Manufacturer 2 | from utilities.testing import ViewTestCases 3 | 4 | from netbox_inventory.models import InventoryItemGroup, InventoryItemType 5 | from netbox_inventory.tests.custom import ModelViewTestCase 6 | 7 | 8 | class InventoryItemTypeTestCase( 9 | ModelViewTestCase, 10 | ViewTestCases.PrimaryObjectViewTestCase, 11 | ): 12 | model = InventoryItemType 13 | 14 | @classmethod 15 | def setUpTestData(cls): 16 | manufacturer1 = Manufacturer.objects.create( 17 | name='Manufacturer 1', 18 | slug='manufacturer1', 19 | ) 20 | manufacturer2 = Manufacturer.objects.create( 21 | name='Manufacturer 2', 22 | slug='manufacturer2', 23 | ) 24 | inventoryitem_group1 = InventoryItemGroup.objects.create(name='IIG1') 25 | inventoryitemtype1 = InventoryItemType.objects.create( 26 | model='InventoryItemType 1', 27 | slug='inventoryitemtype1', 28 | manufacturer=manufacturer1, 29 | ) 30 | inventoryitemtype2 = InventoryItemType.objects.create( 31 | model='InventoryItemType 2', 32 | slug='inventoryitemtype2', 33 | manufacturer=manufacturer1, 34 | ) 35 | inventoryitemtype3 = InventoryItemType.objects.create( 36 | model='InventoryItemType 3', 37 | slug='inventoryitemtype3', 38 | manufacturer=manufacturer1, 39 | ) 40 | cls.form_data = { 41 | 'model': 'InventoryItemType', 42 | 'slug': 'inventoryitemtype', 43 | 'manufacturer': manufacturer1.pk, 44 | 'part_number': 'InventoryItemType PN', 45 | } 46 | cls.csv_data = ( 47 | 'model,slug,manufacturer', 48 | f'InventoryItemType 4,inventoryitemtype4,{manufacturer1.name}', 49 | f'InventoryItemType 5,inventoryitemtype5,{manufacturer1.name}', 50 | f'InventoryItemType 6,inventoryitemtype6,{manufacturer1.name}', 51 | ) 52 | cls.csv_update_data = ( 53 | 'id,manufacturer', 54 | f'{inventoryitemtype1.pk},{manufacturer2.name}', 55 | f'{inventoryitemtype2.pk},{manufacturer2.name}', 56 | f'{inventoryitemtype3.pk},{manufacturer2.name}', 57 | ) 58 | cls.bulk_edit_data = { 59 | 'inventoryitem_group': inventoryitem_group1.pk, 60 | } 61 | -------------------------------------------------------------------------------- /netbox_inventory/tests/purchase/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/purchase/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/purchase/test_api.py: -------------------------------------------------------------------------------- 1 | from utilities.testing import APIViewTestCases 2 | 3 | from ...models import Purchase, Supplier 4 | from ..custom import APITestCase 5 | 6 | 7 | class PurchaseTest( 8 | APITestCase, 9 | APIViewTestCases.GetObjectViewTestCase, 10 | APIViewTestCases.ListObjectsViewTestCase, 11 | APIViewTestCases.CreateObjectViewTestCase, 12 | APIViewTestCases.UpdateObjectViewTestCase, 13 | APIViewTestCases.DeleteObjectViewTestCase, 14 | ): 15 | model = Purchase 16 | brief_fields = [ 17 | 'date', 18 | 'description', 19 | 'display', 20 | 'id', 21 | 'name', 22 | 'status', 23 | 'supplier', 24 | 'url', 25 | ] 26 | 27 | bulk_update_data = { 28 | 'description': 'new description', 29 | } 30 | 31 | @classmethod 32 | def setUpTestData(cls) -> None: 33 | supplier1 = Supplier.objects.create(name='Supplier 1') 34 | Purchase.objects.create(name='Purchase 1', supplier=supplier1, status='closed') 35 | Purchase.objects.create(name='Purchase 2', supplier=supplier1, status='closed') 36 | Purchase.objects.create(name='Purchase 3', supplier=supplier1, status='closed') 37 | cls.create_data = [ 38 | { 39 | 'name': 'Purchase 4', 40 | 'supplier': supplier1.pk, 41 | 'status': 'closed', 42 | }, 43 | { 44 | 'name': 'Purchase 5', 45 | 'supplier': supplier1.pk, 46 | 'status': 'closed', 47 | }, 48 | { 49 | 'name': 'Purchase 6', 50 | 'supplier': supplier1.pk, 51 | 'status': 'closed', 52 | }, 53 | ] 54 | -------------------------------------------------------------------------------- /netbox_inventory/tests/purchase/test_views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from utilities.testing import ViewTestCases 4 | 5 | from netbox_inventory.models import Purchase, Supplier 6 | from netbox_inventory.tests.custom import ModelViewTestCase 7 | 8 | 9 | class PurchaseTestCase( 10 | ModelViewTestCase, 11 | ViewTestCases.PrimaryObjectViewTestCase, 12 | ): 13 | model = Purchase 14 | 15 | @classmethod 16 | def setUpTestData(cls): 17 | supplier1 = Supplier.objects.create( 18 | name='Supplier 1', 19 | slug='supplier1', 20 | ) 21 | supplier2 = Supplier.objects.create( 22 | name='Supplier 2', 23 | slug='supplier2', 24 | ) 25 | purchase1 = Purchase.objects.create( 26 | name='Purchase 1', 27 | supplier=supplier1, 28 | status='closed', 29 | ) 30 | purchase2 = Purchase.objects.create( 31 | name='Purchase 2', 32 | supplier=supplier1, 33 | status='closed', 34 | ) 35 | purchase3 = Purchase.objects.create( 36 | name='Purchase 3', 37 | supplier=supplier1, 38 | status='closed', 39 | ) 40 | cls.form_data = { 41 | 'name': 'Purchase', 42 | 'supplier': supplier1.pk, 43 | 'description': 'Purchase description', 44 | 'status': 'open', 45 | 'date': datetime.date(day=1, month=1, year=2023), 46 | } 47 | cls.csv_data = ( 48 | 'name,supplier,date,status', 49 | f'Purchase 4,{supplier1.name},2023-03-26,open', 50 | f'Purchase 5,{supplier1.name},2023-03-26,open', 51 | f'Purchase 6,{supplier1.name},2023-03-26,open', 52 | ) 53 | cls.csv_update_data = ( 54 | 'id,description,supplier,status', 55 | f'{purchase1.pk},description 1,{supplier2.name},closed', 56 | f'{purchase2.pk},description 2,{supplier2.name},closed', 57 | f'{purchase3.pk},description 3,{supplier2.name},closed', 58 | ) 59 | cls.bulk_edit_data = { 60 | 'description': 'bulk description', 61 | 'date': datetime.date(day=1, month=1, year=2022), 62 | 'status': 'partial', 63 | } 64 | -------------------------------------------------------------------------------- /netbox_inventory/tests/settings.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from django.conf import settings 4 | 5 | """ Custom settings to use with override_settings in tests """ 6 | 7 | 8 | CONFIG_ALLOW_CREATE_DEVICE_TYPE = deepcopy(settings.PLUGINS_CONFIG) 9 | CONFIG_ALLOW_CREATE_DEVICE_TYPE['netbox_inventory'][ 10 | 'asset_import_create_device_type' 11 | ] = True 12 | 13 | CONFIG_SYNC_ON = deepcopy(settings.PLUGINS_CONFIG) 14 | CONFIG_SYNC_ON['netbox_inventory']['sync_hardware_serial_asset_tag'] = True 15 | 16 | CONFIG_SYNC_OFF = deepcopy(settings.PLUGINS_CONFIG) 17 | CONFIG_SYNC_OFF['netbox_inventory']['sync_hardware_serial_asset_tag'] = False 18 | -------------------------------------------------------------------------------- /netbox_inventory/tests/supplier/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnesSI/netbox-inventory/b409e3034cba7e750e92dc29a336a2872143574c/netbox_inventory/tests/supplier/__init__.py -------------------------------------------------------------------------------- /netbox_inventory/tests/supplier/test_api.py: -------------------------------------------------------------------------------- 1 | from utilities.testing import APIViewTestCases 2 | 3 | from ...models import Supplier 4 | from ..custom import APITestCase 5 | 6 | 7 | class SupplierTest( 8 | APITestCase, 9 | APIViewTestCases.GetObjectViewTestCase, 10 | APIViewTestCases.ListObjectsViewTestCase, 11 | APIViewTestCases.CreateObjectViewTestCase, 12 | APIViewTestCases.UpdateObjectViewTestCase, 13 | APIViewTestCases.DeleteObjectViewTestCase, 14 | ): 15 | model = Supplier 16 | brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url'] 17 | create_data = [ 18 | { 19 | 'name': 'Supplier 4', 20 | 'slug': 'supplier4', 21 | }, 22 | { 23 | 'name': 'Supplier 5', 24 | 'slug': 'supplier5', 25 | }, 26 | { 27 | 'name': 'Supplier 6', 28 | 'slug': 'supplier6', 29 | }, 30 | ] 31 | bulk_update_data = { 32 | 'description': 'new description', 33 | } 34 | 35 | @classmethod 36 | def setUpTestData(cls) -> None: 37 | Supplier.objects.create(name='Supplier 1', slug='supplier1') 38 | Supplier.objects.create(name='Supplier 2', slug='supplier2') 39 | Supplier.objects.create(name='Supplier 3', slug='supplier3') 40 | -------------------------------------------------------------------------------- /netbox_inventory/tests/supplier/test_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.urls import reverse 3 | 4 | from tenancy.choices import ContactPriorityChoices 5 | from tenancy.models import Contact, ContactAssignment, ContactRole 6 | from utilities.testing import ViewTestCases, create_tags 7 | 8 | from netbox_inventory.models import Supplier 9 | from netbox_inventory.tests.custom import ModelViewTestCase 10 | 11 | 12 | class SupplierTestCase( 13 | ModelViewTestCase, 14 | ViewTestCases.PrimaryObjectViewTestCase, 15 | ): 16 | model = Supplier 17 | 18 | form_data = { 19 | 'name': 'Supplier', 20 | 'slug': 'supplier', 21 | 'description': 'supplier description', 22 | } 23 | csv_data = ( 24 | 'name,slug', 25 | 'Supplier 4,supplier4', 26 | 'Supplier 5,supplier5', 27 | 'Supplier 6,supplier6', 28 | ) 29 | bulk_edit_data = { 30 | 'description': 'bulk description', 31 | } 32 | 33 | @classmethod 34 | def setUpTestData(cls): 35 | supplier1 = Supplier.objects.create( 36 | name='Supplier 1', 37 | slug='supplier1', 38 | ) 39 | supplier2 = Supplier.objects.create( 40 | name='Supplier 2', 41 | slug='supplier2', 42 | ) 43 | supplier3 = Supplier.objects.create( 44 | name='Supplier 3', 45 | slug='supplier3', 46 | ) 47 | cls.csv_update_data = ( 48 | 'id,description', 49 | f'{supplier1.pk},description 1', 50 | f'{supplier2.pk},description 2', 51 | f'{supplier3.pk},description 3', 52 | ) 53 | 54 | 55 | class ContactAssignmentTestCase( 56 | ViewTestCases.CreateObjectViewTestCase, 57 | ViewTestCases.EditObjectViewTestCase, 58 | ViewTestCases.DeleteObjectViewTestCase, 59 | ViewTestCases.ListObjectsViewTestCase, 60 | ViewTestCases.BulkEditObjectsViewTestCase, 61 | ViewTestCases.BulkDeleteObjectsViewTestCase, 62 | ): 63 | model = ContactAssignment 64 | 65 | @classmethod 66 | def setUpTestData(cls): 67 | suppliers = ( 68 | Supplier(name='Supplier 1', slug='supplier-1'), 69 | Supplier(name='Supplier 2', slug='supplier-2'), 70 | Supplier(name='Supplier 3', slug='supplier-3'), 71 | Supplier(name='Supplier 4', slug='supplier-4'), 72 | ) 73 | Supplier.objects.bulk_create(suppliers) 74 | 75 | contacts = ( 76 | Contact(name='Contact 1'), 77 | Contact(name='Contact 2'), 78 | Contact(name='Contact 3'), 79 | Contact(name='Contact 4'), 80 | ) 81 | Contact.objects.bulk_create(contacts) 82 | 83 | contact_roles = ( 84 | ContactRole(name='Contact Role 1', slug='contact-role-1'), 85 | ContactRole(name='Contact Role 2', slug='contact-role-2'), 86 | ContactRole(name='Contact Role 3', slug='contact-role-3'), 87 | ContactRole(name='Contact Role 4', slug='contact-role-4'), 88 | ) 89 | ContactRole.objects.bulk_create(contact_roles) 90 | 91 | assignments = ( 92 | ContactAssignment( 93 | object=suppliers[0], 94 | contact=contacts[0], 95 | role=contact_roles[0], 96 | priority=ContactPriorityChoices.PRIORITY_PRIMARY, 97 | ), 98 | ContactAssignment( 99 | object=suppliers[1], 100 | contact=contacts[1], 101 | role=contact_roles[1], 102 | priority=ContactPriorityChoices.PRIORITY_SECONDARY, 103 | ), 104 | ContactAssignment( 105 | object=suppliers[2], 106 | contact=contacts[2], 107 | role=contact_roles[2], 108 | priority=ContactPriorityChoices.PRIORITY_TERTIARY, 109 | ), 110 | ) 111 | ContactAssignment.objects.bulk_create(assignments) 112 | 113 | tags = create_tags('Alpha', 'Bravo', 'Charlie') 114 | 115 | cls.form_data = { 116 | 'object_type': ContentType.objects.get_for_model(Supplier).pk, 117 | 'object_id': suppliers[3].pk, 118 | 'contact': contacts[3].pk, 119 | 'role': contact_roles[3].pk, 120 | 'priority': ContactPriorityChoices.PRIORITY_INACTIVE, 121 | 'tags': [t.pk for t in tags], 122 | } 123 | 124 | cls.bulk_edit_data = { 125 | 'role': contact_roles[3].pk, 126 | 'priority': ContactPriorityChoices.PRIORITY_INACTIVE, 127 | } 128 | 129 | def _get_url(self, action, instance=None): 130 | # Override creation URL to append content_type & object_id parameters 131 | if action == 'add': 132 | url = reverse('tenancy:contactassignment_add') 133 | content_type = ContentType.objects.get_for_model(Supplier).pk 134 | object_id = Supplier.objects.first().pk 135 | return f'{url}?object_type={content_type}&object_id={object_id}' 136 | 137 | return super()._get_url(action, instance=instance) 138 | -------------------------------------------------------------------------------- /netbox_inventory/tests/test_load.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | from django.urls import reverse 3 | 4 | from netbox_inventory import __version__ 5 | from netbox_inventory.tests.custom import APITestCase 6 | 7 | 8 | class NetboxInventoryVersionTestCase(SimpleTestCase): 9 | """ 10 | Test for netbox_inventory package 11 | """ 12 | 13 | def test_version(self): 14 | assert __version__ == '2.4.1' 15 | 16 | 17 | class AppTest(APITestCase): 18 | """ 19 | Test the availability of the plugin API root 20 | """ 21 | 22 | def test_root(self): 23 | url = reverse('plugins-api:netbox_inventory-api:api-root') 24 | response = self.client.get(f'{url}?format=api', **self.header) 25 | 26 | self.assertEqual(response.status_code, 200) 27 | -------------------------------------------------------------------------------- /netbox_inventory/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from utilities.urls import get_model_urls 4 | 5 | from . import views 6 | 7 | urlpatterns = ( 8 | # InventoryItemGroups 9 | path( 10 | 'inventory-item-groups/', 11 | include(get_model_urls('netbox_inventory', 'inventoryitemgroup', detail=False)), 12 | ), 13 | path( 14 | 'inventory-item-groups//', 15 | include(get_model_urls('netbox_inventory', 'inventoryitemgroup')), 16 | ), 17 | # InventoryItemTypes 18 | path( 19 | 'inventory-item-types/', 20 | include(get_model_urls('netbox_inventory', 'inventoryitemtype', detail=False)), 21 | ), 22 | path( 23 | 'inventory-item-types//', 24 | include(get_model_urls('netbox_inventory', 'inventoryitemtype')), 25 | ), 26 | # Assets 27 | path( 28 | 'assets/', 29 | include(get_model_urls('netbox_inventory', 'asset', detail=False)), 30 | ), 31 | path( 32 | 'assets//', 33 | include(get_model_urls('netbox_inventory', 'asset')), 34 | ), 35 | path( 36 | 'assets//assign/', 37 | views.AssetAssignView.as_view(), 38 | name='asset_assign', 39 | ), 40 | path( 41 | 'assets/device/create/', 42 | views.AssetDeviceCreateView.as_view(), 43 | name='asset_device_create', 44 | ), 45 | path( 46 | 'assets/module/create/', 47 | views.AssetModuleCreateView.as_view(), 48 | name='asset_module_create', 49 | ), 50 | path( 51 | 'assets/inventory-item/create/', 52 | views.AssetInventoryItemCreateView.as_view(), 53 | name='asset_inventoryitem_create', 54 | ), 55 | path( 56 | 'assets/rack/create/', 57 | views.AssetRackCreateView.as_view(), 58 | name='asset_rack_create', 59 | ), 60 | path( 61 | 'assets/device//reassign/', 62 | views.AssetDeviceReassignView.as_view(), 63 | name='asset_device_reassign', 64 | ), 65 | path( 66 | 'assets/module//reassign/', 67 | views.AssetModuleReassignView.as_view(), 68 | name='asset_module_reassign', 69 | ), 70 | path( 71 | 'assets/inventoryitem//reassign/', 72 | views.AssetInventoryItemReassignView.as_view(), 73 | name='asset_inventoryitem_reassign', 74 | ), 75 | path( 76 | 'assets/rack//reassign/', 77 | views.AssetRackReassignView.as_view(), 78 | name='asset_rack_reassign', 79 | ), 80 | # Suppliers 81 | path( 82 | 'suppliers/', 83 | include(get_model_urls('netbox_inventory', 'supplier', detail=False)), 84 | ), 85 | path( 86 | 'suppliers//', 87 | include(get_model_urls('netbox_inventory', 'supplier')), 88 | ), 89 | # Purchases 90 | path( 91 | 'purchases/', 92 | include(get_model_urls('netbox_inventory', 'purchase', detail=False)), 93 | ), 94 | path( 95 | 'purchases//', 96 | include(get_model_urls('netbox_inventory', 'purchase')), 97 | ), 98 | # Deliveries 99 | path( 100 | 'deliveries/', 101 | include(get_model_urls('netbox_inventory', 'delivery', detail=False)), 102 | ), 103 | path( 104 | 'deliveries//', 105 | include(get_model_urls('netbox_inventory', 'delivery')), 106 | ), 107 | # AuditFlows (for clarity above AuditFlowPages) 108 | path( 109 | 'audit-flows/', 110 | include(get_model_urls('netbox_inventory', 'auditflow', detail=False)), 111 | ), 112 | path( 113 | 'audit-flows//', 114 | include(get_model_urls('netbox_inventory', 'auditflow')), 115 | ), 116 | # AuditFlowPages 117 | path( 118 | 'audit-flowpages/', 119 | include(get_model_urls('netbox_inventory', 'auditflowpage', detail=False)), 120 | ), 121 | path( 122 | 'audit-flowpages//', 123 | include(get_model_urls('netbox_inventory', 'auditflowpage')), 124 | ), 125 | # AuditFlowPageAssignments 126 | path( 127 | 'audit-flowpage-assignments/', 128 | include( 129 | get_model_urls('netbox_inventory', 'auditflowpageassignment', detail=False) 130 | ), 131 | ), 132 | path( 133 | 'audit-flowpage-assignments//', 134 | include(get_model_urls('netbox_inventory', 'auditflowpageassignment')), 135 | ), 136 | # AuditTrailSources 137 | path( 138 | 'audit-trail-sources/', 139 | include(get_model_urls('netbox_inventory', 'audittrailsource', detail=False)), 140 | ), 141 | path( 142 | 'audit-trail-sources//', 143 | include(get_model_urls('netbox_inventory', 'audittrailsource')), 144 | ), 145 | # AuditTrails 146 | path( 147 | 'audit-trails/', 148 | include(get_model_urls('netbox_inventory', 'audittrail', detail=False)), 149 | ), 150 | path( 151 | 'audit-trails//', 152 | include(get_model_urls('netbox_inventory', 'audittrail')), 153 | ), 154 | ) 155 | -------------------------------------------------------------------------------- /netbox_inventory/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.4.1' 2 | -------------------------------------------------------------------------------- /netbox_inventory/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .asset import * 2 | from .asset_assign import * 3 | from .asset_create import * 4 | from .asset_reassign import * 5 | from .auditflow import * 6 | from .auditflowpage import * 7 | from .auditflowpageassignments import * 8 | from .audittrail import * 9 | from .audittrailsource import * 10 | from .delivery import * 11 | from .inventoryitem_group import * 12 | from .inventoryitem_type import * 13 | from .purchase import * 14 | from .supplier import * 15 | -------------------------------------------------------------------------------- /netbox_inventory/views/asset_assign.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | 3 | from ..forms.assign import * 4 | from ..models import Asset 5 | 6 | __all__ = ('AssetAssignView',) 7 | 8 | 9 | class AssetAssignView(generic.ObjectEditView): 10 | queryset = Asset.objects.all() 11 | template_name = 'netbox_inventory/asset_assign.html' 12 | 13 | def dispatch(self, request, *args, **kwargs): 14 | # Set the form class based on the type of hardware being assigned 15 | obj = self.get_object(**kwargs) 16 | self.form = { 17 | 'device': AssetDeviceAssignForm, 18 | 'module': AssetModuleAssignForm, 19 | 'inventoryitem': AssetInventoryItemAssignForm, 20 | 'rack': AssetRackAssignForm, 21 | }[obj.kind] 22 | return super().dispatch(request, *args, **kwargs) 23 | -------------------------------------------------------------------------------- /netbox_inventory/views/asset_create.py: -------------------------------------------------------------------------------- 1 | from dcim.models import Device, InventoryItem, Module, Rack 2 | from netbox.views import generic 3 | 4 | from ..forms.create import * 5 | from ..models import Asset 6 | 7 | __all__ = ( 8 | 'AssetDeviceCreateView', 9 | 'AssetModuleCreateView', 10 | 'AssetInventoryItemCreateView', 11 | 'AssetRackCreateView', 12 | ) 13 | 14 | 15 | class AssetCreateView(generic.ObjectEditView): 16 | template_name = 'netbox_inventory/asset_create.html' 17 | asset = None 18 | 19 | def _load_asset(self, request): 20 | asset_id = request.GET.get('asset_id') 21 | if asset_id: 22 | self.asset = Asset.objects.get(pk=asset_id) 23 | 24 | def dispatch(self, request, *args, **kwargs): 25 | self._load_asset(request) 26 | return super().dispatch(request, *args, **kwargs) 27 | 28 | def alter_object(self, obj, request, url_args, url_kwargs): 29 | obj.assigned_asset = self.asset 30 | return super().alter_object(obj, request, url_args, url_kwargs) 31 | 32 | def get_extra_context(self, request, instance): 33 | context = super().get_extra_context(request, instance) 34 | context['asset'] = self.asset 35 | return context 36 | 37 | 38 | class AssetDeviceCreateView(AssetCreateView): 39 | queryset = Device.objects.all() 40 | form = AssetDeviceCreateForm 41 | 42 | def get_object(self, **kwargs): 43 | return Device(assigned_asset=self.asset) 44 | 45 | def get_extra_context(self, request, instance): 46 | context = super().get_extra_context(request, instance) 47 | context['template_extends'] = 'dcim/device_edit.html' 48 | return context 49 | 50 | 51 | class AssetModuleCreateView(AssetCreateView): 52 | queryset = Module.objects.all() 53 | form = AssetModuleCreateForm 54 | 55 | def get_object(self, **kwargs): 56 | return Module(assigned_asset=self.asset) 57 | 58 | 59 | class AssetInventoryItemCreateView(AssetCreateView): 60 | queryset = InventoryItem.objects.all() 61 | form = AssetInventoryItemCreateForm 62 | 63 | def get_object(self, **kwargs): 64 | return InventoryItem(assigned_asset=self.asset) 65 | 66 | 67 | class AssetRackCreateView(AssetCreateView): 68 | queryset = Rack.objects.all() 69 | form = AssetRackCreateForm 70 | 71 | def get_object(self, **kwargs): 72 | return Rack(assigned_asset=self.asset) 73 | -------------------------------------------------------------------------------- /netbox_inventory/views/asset_reassign.py: -------------------------------------------------------------------------------- 1 | from dcim.models import Device, InventoryItem, Module, Rack 2 | from netbox.views import generic 3 | 4 | from ..forms.reassign import * 5 | 6 | __all__ = ( 7 | 'AssetDeviceReassignView', 8 | 'AssetModuleReassignView', 9 | 'AssetInventoryItemReassignView', 10 | 'AssetRackReassignView', 11 | ) 12 | 13 | 14 | class AssetDeviceReassignView(generic.ObjectEditView): 15 | queryset = Device.objects.all() 16 | template_name = 'netbox_inventory/asset_reassign.html' 17 | form = AssetDeviceReassignForm 18 | 19 | 20 | class AssetModuleReassignView(generic.ObjectEditView): 21 | queryset = Module.objects.all() 22 | template_name = 'netbox_inventory/asset_reassign.html' 23 | form = AssetModuleReassignForm 24 | 25 | 26 | class AssetInventoryItemReassignView(generic.ObjectEditView): 27 | queryset = InventoryItem.objects.all() 28 | template_name = 'netbox_inventory/asset_reassign.html' 29 | form = AssetInventoryItemReassignForm 30 | 31 | 32 | class AssetRackReassignView(generic.ObjectEditView): 33 | queryset = Rack.objects.all() 34 | template_name = 'netbox_inventory/asset_reassign.html' 35 | form = AssetRackReassignForm 36 | -------------------------------------------------------------------------------- /netbox_inventory/views/auditflowpage.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from utilities.views import register_model_view 3 | 4 | from .. import filtersets, forms, models, tables 5 | 6 | __all__ = ( 7 | 'AuditFlowPageView', 8 | 'AuditFlowPageListView', 9 | 'AuditFlowPageEditView', 10 | 'AuditFlowPageDeleteView', 11 | 'AuditFlowPageBulkImportView', 12 | 'AuditFlowPageBulkDeleteView', 13 | ) 14 | 15 | 16 | @register_model_view(models.AuditFlowPage) 17 | class AuditFlowPageView(generic.ObjectView): 18 | queryset = models.AuditFlowPage.objects.all() 19 | 20 | 21 | @register_model_view(models.AuditFlowPage, 'list', path='', detail=False) 22 | class AuditFlowPageListView(generic.ObjectListView): 23 | queryset = models.AuditFlowPage.objects.all() 24 | table = tables.AuditFlowPageTable 25 | filterset = filtersets.AuditFlowPageFilterSet 26 | filterset_form = forms.AuditFlowPageFilterForm 27 | 28 | 29 | @register_model_view(models.AuditFlowPage, 'add', detail=False) 30 | @register_model_view(models.AuditFlowPage, 'edit') 31 | class AuditFlowPageEditView(generic.ObjectEditView): 32 | queryset = models.AuditFlowPage.objects.all() 33 | form = forms.AuditFlowPageForm 34 | 35 | 36 | @register_model_view(models.AuditFlowPage, 'delete') 37 | class AuditFlowPageDeleteView(generic.ObjectDeleteView): 38 | queryset = models.AuditFlowPage.objects.all() 39 | 40 | 41 | @register_model_view(models.AuditFlowPage, 'bulk_import', path='import', detail=False) 42 | class AuditFlowPageBulkImportView(generic.BulkImportView): 43 | queryset = models.AuditFlowPage.objects.all() 44 | model_form = forms.AuditFlowPageImportForm 45 | 46 | 47 | @register_model_view(models.AuditFlowPage, 'bulk_delete', path='delete', detail=False) 48 | class AuditFlowPageBulkDeleteView(generic.BulkDeleteView): 49 | queryset = models.AuditFlowPage.objects.all() 50 | table = tables.AuditFlowPageTable 51 | -------------------------------------------------------------------------------- /netbox_inventory/views/auditflowpageassignments.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from utilities.views import register_model_view 3 | 4 | from .. import forms, models, tables 5 | 6 | __all__ = ( 7 | 'AuditFlowPageAssignmentEditView', 8 | 'AuditFlowPageAssignmentDeleteView', 9 | 'AuditFlowPageAssignmentBulkEditView', 10 | 'AuditFlowPageAssignmentBulkDeleteView', 11 | ) 12 | 13 | 14 | @register_model_view(models.AuditFlowPageAssignment, 'add', detail=False) 15 | @register_model_view(models.AuditFlowPageAssignment, 'edit') 16 | class AuditFlowPageAssignmentEditView(generic.ObjectEditView): 17 | queryset = models.AuditFlowPageAssignment.objects.all() 18 | form = forms.AuditFlowPageAssignmentForm 19 | 20 | 21 | @register_model_view(models.AuditFlowPageAssignment, 'delete') 22 | class AuditFlowPageAssignmentDeleteView(generic.ObjectDeleteView): 23 | queryset = models.AuditFlowPageAssignment.objects.all() 24 | 25 | 26 | @register_model_view( 27 | models.AuditFlowPageAssignment, 28 | 'bulk_edit', 29 | path='edit', 30 | detail=False, 31 | ) 32 | class AuditFlowPageAssignmentBulkEditView(generic.BulkEditView): 33 | queryset = models.AuditFlowPageAssignment.objects.all() 34 | table = tables.AuditFlowPageAssignmentTable 35 | form = forms.AuditFlowPageAssignmentBulkEditForm 36 | 37 | 38 | @register_model_view( 39 | models.AuditFlowPageAssignment, 40 | 'bulk_delete', 41 | path='delete', 42 | detail=False, 43 | ) 44 | class AuditFlowPageAssignmentBulkDeleteView(generic.BulkDeleteView): 45 | queryset = models.AuditFlowPageAssignment.objects.all() 46 | table = tables.AuditFlowPageAssignmentTable 47 | -------------------------------------------------------------------------------- /netbox_inventory/views/audittrail.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.db import transaction 3 | from django.db.models import Model, QuerySet 4 | from django.http import HttpRequest, HttpResponse 5 | from django.shortcuts import get_object_or_404, redirect, render 6 | from django.utils.translation import gettext_lazy as _ 7 | from django.views.generic import View 8 | 9 | from core.models import ObjectType 10 | from netbox.views import generic 11 | from utilities.views import ( 12 | ConditionalLoginRequiredMixin, 13 | GetReturnURLMixin, 14 | ObjectPermissionRequiredMixin, 15 | ViewTab, 16 | register_model_view, 17 | ) 18 | 19 | from .. import filtersets, forms, models, tables 20 | 21 | __all__ = ( 22 | # AuditTrail 23 | 'AuditTrailListView', 24 | 'AuditTrailDeleteView', 25 | 'AuditTrailBulkImportView', 26 | 'AuditTrailBulkDeleteView', 27 | # Objects 28 | 'ObjectAuditTrailView', 29 | ) 30 | 31 | 32 | # 33 | # AuditTrail 34 | # 35 | 36 | 37 | @register_model_view(models.AuditTrail, 'list', path='', detail=False) 38 | class AuditTrailListView(generic.ObjectListView): 39 | queryset = models.AuditTrail.objects.prefetch_related('object_changes__user') 40 | table = tables.AuditTrailTable 41 | filterset = filtersets.AuditTrailFilterSet 42 | filterset_form = forms.AuditTrailFilterForm 43 | 44 | 45 | @register_model_view(models.AuditTrail, 'delete') 46 | class AuditTrailDeleteView(generic.ObjectDeleteView): 47 | queryset = models.AuditTrail.objects.all() 48 | 49 | 50 | @register_model_view(models.AuditTrail, 'bulk_add', path='bulk-add', detail=False) 51 | class AuditTrailBulkAddView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): 52 | queryset = models.AuditTrail.objects.all() 53 | 54 | def get_required_permission(self): 55 | return 'netbox_inventory.add_audittrail' 56 | 57 | def post(self, request: HttpRequest) -> HttpResponse: 58 | object_type = get_object_or_404( 59 | ObjectType, 60 | pk=request.POST.get('object_type_id'), 61 | ) 62 | model = object_type.model_class() 63 | 64 | with transaction.atomic(): 65 | count = 0 66 | for obj in model.objects.filter( 67 | pk__in=request.POST.getlist('pk'), 68 | ): 69 | models.AuditTrail.objects.create(object=obj) 70 | count += 1 71 | 72 | if count > 0: 73 | messages.success( 74 | request, 75 | _('Marked {count} {type} as seen').format( 76 | count=count, 77 | type=model._meta.verbose_name_plural, 78 | ), 79 | ) 80 | 81 | return redirect(self.get_return_url(request)) 82 | 83 | 84 | @register_model_view(models.AuditTrail, 'bulk_import', path='import', detail=False) 85 | class AuditTrailBulkImportView(generic.BulkImportView): 86 | queryset = models.AuditTrail.objects.all() 87 | model_form = forms.AuditTrailImportForm 88 | 89 | 90 | @register_model_view(models.AuditTrail, 'bulk_delete', path='delete', detail=False) 91 | class AuditTrailBulkDeleteView(generic.BulkDeleteView): 92 | queryset = models.AuditTrail.objects.all() 93 | table = tables.AuditTrailTable 94 | 95 | 96 | # 97 | # Objects 98 | # 99 | 100 | 101 | class ObjectAuditTrailView(ConditionalLoginRequiredMixin, View): 102 | """ 103 | List audit trails of an object. 104 | """ 105 | 106 | tab = ViewTab( 107 | label=_('Audit'), 108 | badge=lambda obj: ObjectAuditTrailView.get_audit_trails(obj).count(), 109 | permission='netbox_inventory.view_audittrail', 110 | weight=4000, 111 | hide_if_empty=True, 112 | ) 113 | 114 | @staticmethod 115 | def get_audit_trails(obj: Model) -> QuerySet: 116 | return models.AuditTrail.objects.filter( 117 | object_type=ObjectType.objects.get_for_model(obj), 118 | object_id=obj.pk, 119 | ) 120 | 121 | def get(self, request, model, **kwargs) -> HttpResponse: 122 | # Get parent object and handle QuerySet restriction if needed. 123 | if hasattr(model.objects, 'restrict'): 124 | obj = get_object_or_404( 125 | model.objects.restrict(request.user, 'view'), 126 | **kwargs, 127 | ) 128 | else: 129 | obj = get_object_or_404(model, **kwargs) 130 | 131 | # Prepare table for listing all audit trails of this object. 132 | table = tables.AuditTrailTable( 133 | data=self.get_audit_trails(obj).prefetch_related('object_changes__user'), 134 | user=request.user, 135 | ) 136 | table.configure(request) 137 | 138 | return render( 139 | request, 140 | 'generic/object_children.html', 141 | { 142 | 'object': obj, 143 | 'model': model, 144 | 'child_model': models.AuditTrail, 145 | 'base_template': f'{model._meta.app_label}/{model._meta.model_name}.html', 146 | 'table': table, 147 | 'table_config': f'{table.name}_config', 148 | 'tab': self.tab, 149 | 'return_url': request.get_full_path(), 150 | }, 151 | ) 152 | -------------------------------------------------------------------------------- /netbox_inventory/views/audittrailsource.py: -------------------------------------------------------------------------------- 1 | from django.db.models import QuerySet 2 | from django.http import HttpRequest 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from netbox.views import generic 6 | from utilities.views import ViewTab, register_model_view 7 | 8 | from .. import filtersets, forms, models, tables 9 | 10 | __all__ = ( 11 | 'AuditTrailSourceView', 12 | 'AuditTrailSourceListView', 13 | 'AuditTrailSourceEditView', 14 | 'AuditTrailSourceDeleteView', 15 | 'AuditTrailSourceBulkImportView', 16 | 'AuditTrailSourceBulkDeleteView', 17 | ) 18 | 19 | 20 | @register_model_view(models.AuditTrailSource) 21 | class AuditTrailSourceView(generic.ObjectView): 22 | queryset = models.AuditTrailSource.objects.all() 23 | 24 | 25 | @register_model_view(models.AuditTrailSource, 'trails') 26 | class AuditTrailSourceTrailsView(generic.ObjectChildrenView): 27 | queryset = models.AuditTrailSource.objects.all() 28 | child_model = models.AuditTrail 29 | table = tables.AuditTrailTable 30 | tab = ViewTab( 31 | label=_('Trails'), 32 | badge=lambda obj: obj.audit_trails.count(), 33 | permission='netbox_inventory.view_audittrail', 34 | weight=1000, 35 | ) 36 | 37 | def get_children( 38 | self, 39 | request: HttpRequest, 40 | parent: models.AuditTrailSource, 41 | ) -> QuerySet: 42 | return parent.audit_trails.restrict(request.user, 'view') 43 | 44 | 45 | @register_model_view(models.AuditTrailSource, 'list', path='', detail=False) 46 | class AuditTrailSourceListView(generic.ObjectListView): 47 | queryset = models.AuditTrailSource.objects.all() 48 | table = tables.AuditTrailSourceTable 49 | filterset = filtersets.AuditTrailSourceFilterSet 50 | filterset_form = forms.AuditTrailSourceFilterForm 51 | 52 | 53 | @register_model_view(models.AuditTrailSource, 'add', detail=False) 54 | @register_model_view(models.AuditTrailSource, 'edit') 55 | class AuditTrailSourceEditView(generic.ObjectEditView): 56 | queryset = models.AuditTrailSource.objects.all() 57 | form = forms.AuditTrailSourceForm 58 | 59 | 60 | @register_model_view(models.AuditTrailSource, 'delete') 61 | class AuditTrailSourceDeleteView(generic.ObjectDeleteView): 62 | queryset = models.AuditTrailSource.objects.all() 63 | 64 | 65 | @register_model_view( 66 | models.AuditTrailSource, 67 | 'bulk_import', 68 | path='import', 69 | detail=False, 70 | ) 71 | class AuditTrailSourceBulkImportView(generic.BulkImportView): 72 | queryset = models.AuditTrailSource.objects.all() 73 | model_form = forms.AuditTrailSourceImportForm 74 | 75 | 76 | @register_model_view( 77 | models.AuditTrailSource, 78 | 'bulk_delete', 79 | path='delete', 80 | detail=False, 81 | ) 82 | class AuditTrailSourceBulkDeleteView(generic.BulkDeleteView): 83 | queryset = models.AuditTrailSource.objects.all() 84 | table = tables.AuditTrailSourceTable 85 | -------------------------------------------------------------------------------- /netbox_inventory/views/delivery.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from utilities.query import count_related 3 | from utilities.views import register_model_view 4 | 5 | from .. import filtersets, forms, models, tables 6 | 7 | __all__ = ( 8 | 'DeliveryView', 9 | 'DeliveryListView', 10 | 'DeliveryEditView', 11 | 'DeliveryDeleteView', 12 | 'DeliveryBulkImportView', 13 | 'DeliveryBulkEditView', 14 | 'DeliveryBulkDeleteView', 15 | ) 16 | 17 | 18 | @register_model_view(models.Delivery) 19 | class DeliveryView(generic.ObjectView): 20 | queryset = models.Delivery.objects.all() 21 | 22 | def get_extra_context(self, request, instance): 23 | return { 24 | 'asset_count': models.Asset.objects.filter(delivery=instance).count(), 25 | } 26 | 27 | 28 | @register_model_view(models.Delivery, 'list', path='', detail=False) 29 | class DeliveryListView(generic.ObjectListView): 30 | queryset = models.Delivery.objects.annotate( 31 | asset_count=count_related(models.Asset, 'delivery'), 32 | ) 33 | table = tables.DeliveryTable 34 | filterset = filtersets.DeliveryFilterSet 35 | filterset_form = forms.DeliveryFilterForm 36 | 37 | 38 | @register_model_view(models.Delivery, 'edit') 39 | @register_model_view(models.Delivery, 'add', detail=False) 40 | class DeliveryEditView(generic.ObjectEditView): 41 | queryset = models.Delivery.objects.all() 42 | form = forms.DeliveryForm 43 | 44 | 45 | @register_model_view(models.Delivery, 'delete') 46 | class DeliveryDeleteView(generic.ObjectDeleteView): 47 | queryset = models.Delivery.objects.all() 48 | 49 | 50 | @register_model_view(models.Delivery, 'bulk_import', path='import', detail=False) 51 | class DeliveryBulkImportView(generic.BulkImportView): 52 | queryset = models.Delivery.objects.all() 53 | model_form = forms.DeliveryImportForm 54 | 55 | 56 | @register_model_view(models.Delivery, 'bulk_edit', path='edit', detail=False) 57 | class DeliveryBulkEditView(generic.BulkEditView): 58 | queryset = models.Delivery.objects.all() 59 | filterset = filtersets.DeliveryFilterSet 60 | table = tables.DeliveryTable 61 | form = forms.DeliveryBulkEditForm 62 | 63 | 64 | @register_model_view(models.Delivery, 'bulk_delete', path='delete', detail=False) 65 | class DeliveryBulkDeleteView(generic.BulkDeleteView): 66 | queryset = models.Delivery.objects.all() 67 | filterset = filtersets.DeliveryFilterSet 68 | table = tables.DeliveryTable 69 | -------------------------------------------------------------------------------- /netbox_inventory/views/inventoryitem_type.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from utilities.query import count_related 3 | from utilities.views import register_model_view 4 | 5 | from .. import filtersets, forms, models, tables 6 | 7 | __all__ = ( 8 | 'InventoryItemTypeView', 9 | 'InventoryItemTypeListView', 10 | 'InventoryItemTypeEditView', 11 | 'InventoryItemTypeDeleteView', 12 | 'InventoryItemTypeBulkImportView', 13 | 'InventoryItemTypeBulkEditView', 14 | 'InventoryItemTypeBulkDeleteView', 15 | ) 16 | 17 | 18 | @register_model_view(models.InventoryItemType) 19 | class InventoryItemTypeView(generic.ObjectView): 20 | queryset = models.InventoryItemType.objects.all() 21 | 22 | def get_extra_context(self, request, instance): 23 | context = super().get_extra_context(request, instance) 24 | context['asset_count'] = ( 25 | models.Asset.objects.restrict(request.user, 'view') 26 | .filter(inventoryitem_type=instance) 27 | .count() 28 | ) 29 | return context 30 | 31 | 32 | @register_model_view(models.InventoryItemType, 'list', path='', detail=False) 33 | class InventoryItemTypeListView(generic.ObjectListView): 34 | queryset = models.InventoryItemType.objects.annotate( 35 | asset_count=count_related(models.Asset, 'inventoryitem_type'), 36 | ) 37 | table = tables.InventoryItemTypeTable 38 | filterset = filtersets.InventoryItemTypeFilterSet 39 | filterset_form = forms.InventoryItemTypeFilterForm 40 | 41 | 42 | @register_model_view(models.InventoryItemType, 'edit') 43 | @register_model_view(models.InventoryItemType, 'add', detail=False) 44 | class InventoryItemTypeEditView(generic.ObjectEditView): 45 | queryset = models.InventoryItemType.objects.all() 46 | form = forms.InventoryItemTypeForm 47 | 48 | 49 | @register_model_view(models.InventoryItemType, 'delete') 50 | class InventoryItemTypeDeleteView(generic.ObjectDeleteView): 51 | queryset = models.InventoryItemType.objects.all() 52 | 53 | 54 | @register_model_view( 55 | models.InventoryItemType, 'bulk_import', path='import', detail=False 56 | ) 57 | class InventoryItemTypeBulkImportView(generic.BulkImportView): 58 | queryset = models.InventoryItemType.objects.all() 59 | model_form = forms.InventoryItemTypeImportForm 60 | 61 | 62 | @register_model_view(models.InventoryItemType, 'bulk_edit', path='edit', detail=False) 63 | class InventoryItemTypeBulkEditView(generic.BulkEditView): 64 | queryset = models.InventoryItemType.objects.all() 65 | filterset = filtersets.InventoryItemTypeFilterSet 66 | table = tables.InventoryItemTypeTable 67 | form = forms.InventoryItemTypeBulkEditForm 68 | 69 | 70 | @register_model_view( 71 | models.InventoryItemType, 'bulk_delete', path='delete', detail=False 72 | ) 73 | class InventoryItemTypeBulkDeleteView(generic.BulkDeleteView): 74 | queryset = models.InventoryItemType.objects.all() 75 | filterset = filtersets.InventoryItemTypeFilterSet 76 | table = tables.InventoryItemTypeTable 77 | -------------------------------------------------------------------------------- /netbox_inventory/views/purchase.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from utilities.query import count_related 3 | from utilities.views import register_model_view 4 | 5 | from .. import filtersets, forms, models, tables 6 | 7 | __all__ = ( 8 | 'PurchaseView', 9 | 'PurchaseListView', 10 | 'PurchaseEditView', 11 | 'PurchaseDeleteView', 12 | 'PurchaseBulkImportView', 13 | 'PurchaseBulkEditView', 14 | 'PurchaseBulkDeleteView', 15 | ) 16 | 17 | 18 | @register_model_view(models.Purchase) 19 | class PurchaseView(generic.ObjectView): 20 | queryset = models.Purchase.objects.all() 21 | 22 | def get_extra_context(self, request, instance): 23 | return { 24 | 'asset_count': models.Asset.objects.filter(purchase=instance).count(), 25 | 'delivery_count': models.Delivery.objects.filter(purchase=instance).count(), 26 | } 27 | 28 | 29 | @register_model_view(models.Purchase, 'list', path='', detail=False) 30 | class PurchaseListView(generic.ObjectListView): 31 | queryset = models.Purchase.objects.annotate( 32 | asset_count=count_related(models.Asset, 'purchase'), 33 | delivery_count=count_related(models.Delivery, 'purchase'), 34 | ) 35 | table = tables.PurchaseTable 36 | filterset = filtersets.PurchaseFilterSet 37 | filterset_form = forms.PurchaseFilterForm 38 | 39 | 40 | @register_model_view(models.Purchase, 'edit') 41 | @register_model_view(models.Purchase, 'add', detail=False) 42 | class PurchaseEditView(generic.ObjectEditView): 43 | queryset = models.Purchase.objects.all() 44 | form = forms.PurchaseForm 45 | 46 | 47 | @register_model_view(models.Purchase, 'delete') 48 | class PurchaseDeleteView(generic.ObjectDeleteView): 49 | queryset = models.Purchase.objects.all() 50 | 51 | 52 | @register_model_view(models.Purchase, 'bulk_import', path='import', detail=False) 53 | class PurchaseBulkImportView(generic.BulkImportView): 54 | queryset = models.Purchase.objects.all() 55 | model_form = forms.PurchaseImportForm 56 | 57 | 58 | @register_model_view(models.Purchase, 'bulk_edit', path='edit', detail=False) 59 | class PurchaseBulkEditView(generic.BulkEditView): 60 | queryset = models.Purchase.objects.all() 61 | filterset = filtersets.PurchaseFilterSet 62 | table = tables.PurchaseTable 63 | form = forms.PurchaseBulkEditForm 64 | 65 | 66 | @register_model_view(models.Purchase, 'bulk_delete', path='delete', detail=False) 67 | class PurchaseBulkDeleteView(generic.BulkDeleteView): 68 | queryset = models.Purchase.objects.all() 69 | filterset = filtersets.PurchaseFilterSet 70 | table = tables.PurchaseTable 71 | -------------------------------------------------------------------------------- /netbox_inventory/views/supplier.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from utilities.query import count_related 3 | from utilities.views import register_model_view 4 | 5 | from .. import filtersets, forms, models, tables 6 | 7 | __all__ = ( 8 | 'SupplierView', 9 | 'SupplierListView', 10 | 'SupplierEditView', 11 | 'SupplierDeleteView', 12 | 'SupplierBulkImportView', 13 | 'SupplierBulkEditView', 14 | 'SupplierBulkDeleteView', 15 | ) 16 | 17 | 18 | @register_model_view(models.Supplier) 19 | class SupplierView(generic.ObjectView): 20 | queryset = models.Supplier.objects.all() 21 | 22 | def get_extra_context(self, request, instance): 23 | return { 24 | 'asset_count': models.Asset.objects.filter( 25 | purchase__supplier=instance 26 | ).count(), 27 | 'purchase_count': models.Purchase.objects.filter(supplier=instance).count(), 28 | 'delivery_count': models.Delivery.objects.filter( 29 | purchase__supplier=instance 30 | ).count(), 31 | } 32 | 33 | 34 | @register_model_view(models.Supplier, 'list', path='', detail=False) 35 | class SupplierListView(generic.ObjectListView): 36 | queryset = models.Supplier.objects.annotate( 37 | purchase_count=count_related(models.Purchase, 'supplier'), 38 | delivery_count=count_related(models.Delivery, 'purchase__supplier'), 39 | asset_count=count_related(models.Asset, 'purchase__supplier'), 40 | ) 41 | table = tables.SupplierTable 42 | filterset = filtersets.SupplierFilterSet 43 | filterset_form = forms.SupplierFilterForm 44 | 45 | 46 | @register_model_view(models.Supplier, 'edit') 47 | @register_model_view(models.Supplier, 'add', detail=False) 48 | class SupplierEditView(generic.ObjectEditView): 49 | queryset = models.Supplier.objects.all() 50 | form = forms.SupplierForm 51 | 52 | 53 | @register_model_view(models.Supplier, 'delete') 54 | class SupplierDeleteView(generic.ObjectDeleteView): 55 | queryset = models.Supplier.objects.all() 56 | 57 | 58 | @register_model_view(models.Supplier, 'bulk_import', path='import', detail=False) 59 | class SupplierBulkImportView(generic.BulkImportView): 60 | queryset = models.Supplier.objects.all() 61 | model_form = forms.SupplierImportForm 62 | 63 | 64 | @register_model_view(models.Supplier, 'bulk_edit', path='edit', detail=False) 65 | class SupplierBulkEditView(generic.BulkEditView): 66 | queryset = models.Supplier.objects.all() 67 | filterset = filtersets.SupplierFilterSet 68 | table = tables.SupplierTable 69 | form = forms.SupplierBulkEditForm 70 | 71 | 72 | @register_model_view(models.Supplier, 'bulk_delete', path='delete', detail=False) 73 | class SupplierBulkDeleteView(generic.BulkDeleteView): 74 | queryset = models.Supplier.objects.all() 75 | filterset = filtersets.SupplierFilterSet 76 | table = tables.SupplierTable 77 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "netbox-inventory" 7 | version = "2.4.1" 8 | authors = [ 9 | { name="Matej Vadnjal", email="matej.vadnjal@arnes.si" }, 10 | ] 11 | description = "Inventory asset management in NetBox" 12 | readme = "README.md" 13 | license = "MIT" 14 | requires-python = ">=3.10" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "Operating System :: OS Independent", 18 | ] 19 | keywords = ["netbox", "netbox-plugin", "inventory"] 20 | 21 | [project.urls] 22 | "Homepage" = "https://github.com/ArnesSI/netbox-inventory/" 23 | "Bug Tracker" = "https://github.com/ArnesSI/netbox-inventory/issues/" 24 | 25 | [tool.setuptools] 26 | include-package-data = true 27 | 28 | [tool.setuptools.packages.find] 29 | include = ["netbox_inventory*"] 30 | 31 | [tool.ruff] 32 | exclude = [ 33 | "netbox_inventory/migrations", 34 | ] 35 | 36 | [tool.ruff.lint] 37 | extend-select = ["E4", "E7", "E9", "F", "W", "C", "I"] 38 | ignore = ["F403", "F405"] 39 | 40 | [tool.ruff.lint.isort] 41 | known-local-folder = ["netbox_inventory"] 42 | known-first-party = [ 43 | "netbox", 44 | "core", 45 | "dcim", 46 | "extras", 47 | "tenancy", 48 | "users", 49 | "utilities", 50 | ] 51 | 52 | [tool.ruff.format] 53 | quote-style = "single" 54 | -------------------------------------------------------------------------------- /testing/configuration.testing.py: -------------------------------------------------------------------------------- 1 | ##################################### 2 | # # 3 | # Config used to run unit tests # 4 | # # 5 | ##################################### 6 | 7 | ALLOWED_HOSTS = ["*",] 8 | 9 | DATABASE = { 10 | 'NAME': 'netbox', # Database name 11 | 'USER': 'netbox', # PostgreSQL username 12 | 'PASSWORD': 'netbox', # PostgreSQL password 13 | 'HOST': 'localhost', # Database server 14 | 'PORT': '', # Database port (leave blank for default) 15 | 'CONN_MAX_AGE': 300, # Max database connection age 16 | } 17 | 18 | REDIS = { 19 | 'tasks': { 20 | 'HOST': 'localhost', 21 | 'PORT': 6379, 22 | 'PASSWORD': '', 23 | 'DATABASE': 0, 24 | 'SSL': False, 25 | }, 26 | 'caching': { 27 | 'HOST': 'localhost', 28 | 'PORT': 6379, 29 | 'PASSWORD': '', 30 | 'DATABASE': 1, 31 | 'SSL': False, 32 | } 33 | } 34 | 35 | SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 36 | 37 | PLUGINS = [ 38 | 'netbox_inventory', 39 | ] 40 | --------------------------------------------------------------------------------