├── .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 |
7 |
8 |
9 | Assign Asset
10 |
11 |
12 |
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 |
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 | Object name
30 | Created if missing?
31 | Plugin setting name
32 |
33 |
34 |
35 |
36 | Manufacturer & Device Type
37 | {% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_device_type %}
38 | asset_import_create_device_type
39 |
40 |
41 | Manufacturer & Module Type
42 | {% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_module_type %}
43 | asset_import_create_module_type
44 |
45 |
46 | Manufacturer & InventoryItem Type
47 | {% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_inventoryitem_type %}
48 | asset_import_create_inventoryitem_type
49 |
50 |
51 | Manufacturer & Rack Type
52 | {% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_rack_type %}
53 | asset_import_create_rack_type
54 |
55 |
56 | Supplier & Purchase
57 | {% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_purchase %}
58 | asset_import_create_purchase
59 |
60 |
61 | Owner & Tenant
62 | {% checkmark settings.PLUGINS_CONFIG.netbox_inventory.asset_import_create_tenant %}
63 | asset_import_create_tenant
64 |
65 |
66 |
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 |
8 | Create
9 |
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 |
30 |
31 |
32 | Device
33 |
34 |
35 |
36 |
37 | Module
38 |
39 |
40 |
41 |
42 | Inventory Item
43 |
44 |
45 |
46 |
47 | Rack
48 |
49 |
50 |
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 |
7 |
8 |
9 | Asset
10 |
11 |
12 |
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 |
18 | {{ object }}
19 |
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 |
44 |
50 | {% endblock table_controls %}
51 |
52 | {% block bulk_controls %}
53 | {% if perms.netbox_inventory.add_audittrail %}
54 |
55 |
58 |
59 | {% trans "Mark seen" %}
60 |
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 |
10 |
11 |
12 | {% trans "Name" %}
13 | {{ object.name }}
14 |
15 |
16 | {% trans "Description" %}
17 | {{ object.description }}
18 |
19 |
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 |
8 | {{ object.purchase.supplier }}
9 |
10 |
11 | {{ object.purchase }}
12 |
13 | {% endblock %}
14 |
15 | {% block content %}
16 |
17 |
18 |
19 |
20 |
21 |
22 | Name
23 | {{ object.name }}
24 |
25 |
26 | Purchase
27 | {{ object.purchase|linkify }}
28 |
29 |
30 | Receiving Contact
31 |
32 | {% if object.receiving_contact.group %}
33 | {{ object.receiving_contact.group|linkify }} /
34 | {% endif %}
35 | {{ object.receiving_contact|linkify|placeholder }}
36 |
37 |
38 |
39 | Date
40 | {{ object.date|isodate|placeholder }}
41 |
42 |
43 | Description
44 | {{ object.description|placeholder }}
45 |
46 |
47 | Assets
48 |
49 | {{ asset_count }}
50 |
51 |
52 |
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 |
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 |
10 |
11 |
12 | {% trans "Name" %}
13 | {{ object.name }}
14 |
15 |
16 | {% trans "Description" %}
17 | {{ object.description }}
18 |
19 |
20 | {% trans "Object Type" %}
21 | {{ object.object_type }}
22 |
23 | {% block enabled_fieled %}{% endblock %}
24 |
25 |
26 |
27 |
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 |
26 | {% if asset %}
27 |
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 |
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 |
14 |
15 |
16 | {% trans "Add" %}
17 |
18 |
33 |
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 |
14 |
15 |
16 | {% trans "Audit" %}
17 |
18 |
27 |
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 |
12 |
13 |
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 | {{ group }}
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 |
25 |
26 |
27 | Name
28 | {{ object.name }}
29 |
30 |
31 | Parent
32 | {{ object.parent|linkify|placeholder }}
33 |
34 |
35 | Description
36 | {{ object.description|placeholder }}
37 |
38 |
39 | Assets
40 |
41 | {{ asset_table.rows|length }}
42 |
43 |
44 |
45 |
46 | {% include 'inc/panels/custom_fields.html' %}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Status
55 | Count
56 |
57 |
58 |
59 | {% for sc in status_counts.values %}
60 |
61 | {% badge value=sc.label bg_color=sc.color %}
62 |
63 |
64 | {{ sc.count }}
65 |
66 |
67 |
68 | {% empty %}
69 | — No assets found —
70 | {% endfor %}
71 |
72 |
73 |
74 |
75 |
108 | {% plugin_left_page object %}
109 |
110 |
111 |
112 |
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 |
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 | {{ object.manufacturer }}
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 |
24 |
25 |
26 | Manufacturer
27 | {{ object.manufacturer|linkify }}
28 |
29 |
30 | Model
31 | {{ object.model }}
32 |
33 |
34 | Part number
35 | {{ object.part_number }}
36 |
37 |
38 | Description
39 | {{ object.description|placeholder }}
40 |
41 |
42 | Group
43 | {{ object.inventoryitem_group|linkify|placeholder }}
44 |
45 |
46 | Assets
47 | {{ asset_count }}
48 |
49 |
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 |
8 | {{ object.supplier }}
9 |
10 | {% endblock %}
11 |
12 | {% block content %}
13 |
14 |
15 |
16 |
17 |
18 |
19 | Name
20 | {{ object.name }}
21 |
22 |
23 | Supplier
24 | {{ object.supplier|linkify }}
25 |
26 |
27 | Status
28 | {% badge object.get_status_display bg_color=object.get_status_color %}
29 |
30 |
31 | Date
32 | {{ object.date|isodate|placeholder }}
33 |
34 |
35 | Description
36 | {{ object.description|placeholder }}
37 |
38 |
39 | Deliveries
40 |
41 | {{ delivery_count }}
42 |
43 |
44 |
45 | Assets
46 |
47 | {{ asset_count }}
48 |
49 |
50 |
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 |
74 | {% htmx_table 'plugins:netbox_inventory:delivery_list' purchase_id=object.pk %}
75 |
76 |
77 |
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 | Suppliers
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 |
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 |
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 |
--------------------------------------------------------------------------------