netbox-contract 2.3.2__tar.gz → 2.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netbox_contract-2.3.2/netbox_contract.egg-info → netbox_contract-2.4.1}/PKG-INFO +22 -4
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/README.md +9 -2
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/__init__.py +2 -2
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/api/serializers.py +42 -22
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/api/urls.py +1 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/api/views.py +6 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/constants.py +1 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/filtersets.py +25 -4
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/forms.py +113 -37
- netbox_contract-2.4.1/netbox_contract/locale/en/LC_MESSAGES/django.mo +0 -0
- netbox_contract-2.4.1/netbox_contract/locale/en/LC_MESSAGES/django.po +499 -0
- netbox_contract-2.4.1/netbox_contract/locale/fr/LC_MESSAGES/django.mo +0 -0
- netbox_contract-2.4.1/netbox_contract/locale/fr/LC_MESSAGES/django.po +503 -0
- netbox_contract-2.4.1/netbox_contract/migrations/0041_contracttype_contract_contract_type.py +48 -0
- netbox_contract-2.4.1/netbox_contract/migrations/0042_remove_contract_netbox_cont_externa_6343fb_idx_and_more.py +42 -0
- netbox_contract-2.4.1/netbox_contract/migrations/0043_invoice_status.py +21 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/models.py +65 -12
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/navigation.py +17 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/search.py +10 -1
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/tables.py +48 -21
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/template_content.py +27 -5
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/templates/netbox_contract/contract.html +13 -7
- netbox_contract-2.4.1/netbox_contract/templates/netbox_contract/contracttype.html +33 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/templates/netbox_contract/invoice.html +4 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/urls.py +48 -6
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/views.py +52 -22
- {netbox_contract-2.3.2 → netbox_contract-2.4.1/netbox_contract.egg-info}/PKG-INFO +22 -4
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract.egg-info/SOURCES.txt +9 -4
- netbox_contract-2.4.1/netbox_contract.egg-info/requires.txt +14 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/pyproject.toml +17 -1
- netbox_contract-2.3.2/netbox_contract/tests/__init__.py +0 -1
- netbox_contract-2.3.2/netbox_contract/tests/custom.py +0 -18
- netbox_contract-2.3.2/netbox_contract/tests/test_views.py +0 -449
- netbox_contract-2.3.2/netbox_contract.egg-info/requires.txt +0 -2
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/CONTRIBUTING.md +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/LICENSE +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/MANIFEST.in +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/docs/img/accounting_dimensions.png +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/docs/img/contract.png +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/docs/img/contract_linked_objects.png +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/docs/img/invoice.png +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/docs/img/invoice_line.png +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/docs/img/invoice_linked_objects.png +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/api/__init__.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0001_initial.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0002_alter_contract_circuit_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0003_alter_contract_custom_field_data_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0004_contract_currency_invoice_currency.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0005_contract_accounting_dimensions_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0006_alter_contract_circuit.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0007_invoice_date.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0008_invoice_comments.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0009_contract_external_reference.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0010_invoice_contracts.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0011_auto_20230122_2112.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0012_remove_invoice_contract.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0013_contract_documents_invoice_documents.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0014_contract_end_date.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0015_contractassignement.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0016_contract_parent.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0017_alter_contract_accounting_dimensions.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0018_contract_external_partie_object_id_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0019_auto_20230924_1813.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0020_alter_contract_external_partie.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0021_alter_contract_external_partie.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0022_alter_contract_internal_partie_alter_contract_parent.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0023_rename_contractassignement_contractassignment_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0024_remove_contract_external_partie.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0025_remove_contract_circuit_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0026_auto_20240421_1550.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0027_invoiceline.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0033_remove_contract_invoice_template.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0035_accountingdimension_status_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0036_alter_invoiceline_accounting_dimensions.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0037_contract_notice_period_alter_contract_currency_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0038_alter_accountingdimension_options_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0039_alter_accountingdimension_status_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/0040_alter_contract_currency_alter_invoice_currency_and_more.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/migrations/__init__.py +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/templates/contract_assignments_bottom.html +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/templates/contract_list_bottom.html +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/templates/netbox_contract/accountingdimension.html +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/templates/netbox_contract/contractassignment.html +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/templates/netbox_contract/invoiceline.html +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract/templates/netbox_contract/serviceprovider.html +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract.egg-info/dependency_links.txt +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/netbox_contract.egg-info/top_level.txt +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/setup.cfg +0 -0
- {netbox_contract-2.3.2 → netbox_contract-2.4.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: netbox-contract
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: Contract management plugin for Netbox
|
|
5
5
|
Author-email: Marc Lebreuil <marc@famillelebreuil.net>
|
|
6
6
|
Project-URL: Homepage, https://github.com/mlebreuil/netbox-contract
|
|
@@ -13,6 +13,17 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: python-dateutil
|
|
15
15
|
Requires-Dist: drf_yasg
|
|
16
|
+
Requires-Dist: Django
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: check-manifest; extra == "dev"
|
|
19
|
+
Requires-Dist: mkdocs; extra == "dev"
|
|
20
|
+
Requires-Dist: mkdocs-material; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff; extra == "dev"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: coverage; extra == "test"
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
26
|
+
Dynamic: license-file
|
|
16
27
|
|
|
17
28
|
# Contract plugin
|
|
18
29
|
|
|
@@ -36,6 +47,7 @@ Accounting dimensions can be linked with invoice lines.
|
|
|
36
47
|
|----------------|----------------|
|
|
37
48
|
| 4.1 | 2.2 |
|
|
38
49
|
| 4.2 | 2.3 |
|
|
50
|
+
| 4.3 | 2.4 |
|
|
39
51
|
|
|
40
52
|
## Installing
|
|
41
53
|
|
|
@@ -89,13 +101,14 @@ PLUGINS_CONFIG = {
|
|
|
89
101
|
|
|
90
102
|
### Customize the plugin fields choices
|
|
91
103
|
|
|
92
|
-
Internal
|
|
104
|
+
Internal party reference the legal entity of your organization that is a party to the contract.
|
|
93
105
|
The first currency will also be the default currency for contracts.
|
|
94
106
|
|
|
95
107
|
```python
|
|
96
108
|
# configuration.py
|
|
97
109
|
FIELD_CHOICES = {
|
|
98
|
-
'netbox_contract.Contract.
|
|
110
|
+
'netbox_contract.Contract.internal_party': (
|
|
111
|
+
('default', 'Default entity', 'green'),
|
|
99
112
|
('entity1', 'Entity 1', 'green'),
|
|
100
113
|
('entity2', 'Entity 2', 'yellow'),
|
|
101
114
|
),
|
|
@@ -109,6 +122,11 @@ FIELD_CHOICES = {
|
|
|
109
122
|
('active', 'Active', 'green'),
|
|
110
123
|
('canceled', 'Canceled', 'red'),
|
|
111
124
|
)
|
|
125
|
+
'netbox_contract.Invoice.status': (
|
|
126
|
+
('draft', 'Draft', 'yellow'),
|
|
127
|
+
('posted', 'Posted', 'green'),
|
|
128
|
+
('canceled', 'Canceled', 'red'),
|
|
129
|
+
)
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
```
|
|
@@ -20,6 +20,7 @@ Accounting dimensions can be linked with invoice lines.
|
|
|
20
20
|
|----------------|----------------|
|
|
21
21
|
| 4.1 | 2.2 |
|
|
22
22
|
| 4.2 | 2.3 |
|
|
23
|
+
| 4.3 | 2.4 |
|
|
23
24
|
|
|
24
25
|
## Installing
|
|
25
26
|
|
|
@@ -73,13 +74,14 @@ PLUGINS_CONFIG = {
|
|
|
73
74
|
|
|
74
75
|
### Customize the plugin fields choices
|
|
75
76
|
|
|
76
|
-
Internal
|
|
77
|
+
Internal party reference the legal entity of your organization that is a party to the contract.
|
|
77
78
|
The first currency will also be the default currency for contracts.
|
|
78
79
|
|
|
79
80
|
```python
|
|
80
81
|
# configuration.py
|
|
81
82
|
FIELD_CHOICES = {
|
|
82
|
-
'netbox_contract.Contract.
|
|
83
|
+
'netbox_contract.Contract.internal_party': (
|
|
84
|
+
('default', 'Default entity', 'green'),
|
|
83
85
|
('entity1', 'Entity 1', 'green'),
|
|
84
86
|
('entity2', 'Entity 2', 'yellow'),
|
|
85
87
|
),
|
|
@@ -93,6 +95,11 @@ FIELD_CHOICES = {
|
|
|
93
95
|
('active', 'Active', 'green'),
|
|
94
96
|
('canceled', 'Canceled', 'red'),
|
|
95
97
|
)
|
|
98
|
+
'netbox_contract.Invoice.status': (
|
|
99
|
+
('draft', 'Draft', 'yellow'),
|
|
100
|
+
('posted', 'Posted', 'green'),
|
|
101
|
+
('canceled', 'Canceled', 'red'),
|
|
102
|
+
)
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
```
|
|
@@ -5,11 +5,11 @@ class ContractsConfig(PluginConfig):
|
|
|
5
5
|
name = 'netbox_contract'
|
|
6
6
|
verbose_name = 'Netbox contract'
|
|
7
7
|
description = 'Contract management plugin for Netbox'
|
|
8
|
-
version = '2.
|
|
8
|
+
version = '2.4.1'
|
|
9
9
|
author = 'Marc Lebreuil'
|
|
10
10
|
author_email = 'marc@famillelebreuil.net'
|
|
11
11
|
base_url = 'contracts'
|
|
12
|
-
min_version = '4.
|
|
12
|
+
min_version = '4.3.0'
|
|
13
13
|
required_settings = []
|
|
14
14
|
default_settings = {
|
|
15
15
|
'top_level_menu': False,
|
|
@@ -11,6 +11,7 @@ from ..models import (
|
|
|
11
11
|
AccountingDimension,
|
|
12
12
|
Contract,
|
|
13
13
|
ContractAssignment,
|
|
14
|
+
ContractType,
|
|
14
15
|
Invoice,
|
|
15
16
|
InvoiceLine,
|
|
16
17
|
ServiceProvider,
|
|
@@ -23,8 +24,8 @@ class NestedContractSerializer(WritableNestedSerializer):
|
|
|
23
24
|
)
|
|
24
25
|
yrc = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
|
|
25
26
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
external_party_object_type = ContentTypeField(queryset=ContentType.objects.all())
|
|
28
|
+
external_party_object = serializers.SerializerMethodField(read_only=True)
|
|
28
29
|
|
|
29
30
|
class Meta:
|
|
30
31
|
model = Contract
|
|
@@ -33,11 +34,11 @@ class NestedContractSerializer(WritableNestedSerializer):
|
|
|
33
34
|
'url',
|
|
34
35
|
'display',
|
|
35
36
|
'name',
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
37
|
+
'external_party_object_type',
|
|
38
|
+
'external_party_object_id',
|
|
39
|
+
'external_party_object',
|
|
39
40
|
'external_reference',
|
|
40
|
-
'
|
|
41
|
+
'internal_party',
|
|
41
42
|
'tenant',
|
|
42
43
|
'status',
|
|
43
44
|
'start_date',
|
|
@@ -53,13 +54,13 @@ class NestedContractSerializer(WritableNestedSerializer):
|
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
|
56
|
-
def
|
|
57
|
+
def get_external_party_object(self, instance):
|
|
57
58
|
serializer = get_serializer_for_model(
|
|
58
|
-
instance.
|
|
59
|
+
instance.external_party_object_type.model_class()
|
|
59
60
|
)
|
|
60
61
|
context = {'request': self.context['request']}
|
|
61
62
|
return serializer(
|
|
62
|
-
instance.
|
|
63
|
+
instance.external_party_object, nested=True, context=context
|
|
63
64
|
).data
|
|
64
65
|
|
|
65
66
|
|
|
@@ -92,8 +93,8 @@ class ContractSerializer(NetBoxModelSerializer):
|
|
|
92
93
|
yrc = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
|
|
93
94
|
parent = NestedContractSerializer(many=False, required=False)
|
|
94
95
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
external_party_object_type = ContentTypeField(queryset=ContentType.objects.all())
|
|
97
|
+
external_party_object = serializers.SerializerMethodField(read_only=True)
|
|
97
98
|
|
|
98
99
|
class Meta:
|
|
99
100
|
model = Contract
|
|
@@ -102,11 +103,11 @@ class ContractSerializer(NetBoxModelSerializer):
|
|
|
102
103
|
'url',
|
|
103
104
|
'display',
|
|
104
105
|
'name',
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
106
|
+
'external_party_object_type',
|
|
107
|
+
'external_party_object_id',
|
|
108
|
+
'external_party_object',
|
|
108
109
|
'external_reference',
|
|
109
|
-
'
|
|
110
|
+
'internal_party',
|
|
110
111
|
'tenant',
|
|
111
112
|
'status',
|
|
112
113
|
'start_date',
|
|
@@ -130,11 +131,11 @@ class ContractSerializer(NetBoxModelSerializer):
|
|
|
130
131
|
'url',
|
|
131
132
|
'display',
|
|
132
133
|
'name',
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
134
|
+
'external_party_object_type',
|
|
135
|
+
'external_party_object_id',
|
|
136
|
+
'external_party_object',
|
|
136
137
|
'external_reference',
|
|
137
|
-
'
|
|
138
|
+
'internal_party',
|
|
138
139
|
'tenant',
|
|
139
140
|
'status',
|
|
140
141
|
'start_date',
|
|
@@ -151,13 +152,13 @@ class ContractSerializer(NetBoxModelSerializer):
|
|
|
151
152
|
)
|
|
152
153
|
|
|
153
154
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
|
154
|
-
def
|
|
155
|
+
def get_external_party_object(self, instance):
|
|
155
156
|
serializer = get_serializer_for_model(
|
|
156
|
-
instance.
|
|
157
|
+
instance.external_party_object_type.model_class()
|
|
157
158
|
)
|
|
158
159
|
context = {'request': self.context['request']}
|
|
159
160
|
return serializer(
|
|
160
|
-
instance.
|
|
161
|
+
instance.external_party_object, nested=True, context=context
|
|
161
162
|
).data
|
|
162
163
|
|
|
163
164
|
|
|
@@ -395,3 +396,22 @@ class AccountingDimensionSerializer(NetBoxModelSerializer):
|
|
|
395
396
|
'last_updated',
|
|
396
397
|
)
|
|
397
398
|
brief_fields = ('id', 'name', 'value', 'url', 'display')
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class ContractTypeSerializer(NetBoxModelSerializer):
|
|
402
|
+
url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_contract-api:contracttype-detail')
|
|
403
|
+
|
|
404
|
+
class Meta:
|
|
405
|
+
model = ContractType
|
|
406
|
+
fields = (
|
|
407
|
+
'id',
|
|
408
|
+
'url',
|
|
409
|
+
'display',
|
|
410
|
+
'name',
|
|
411
|
+
'description',
|
|
412
|
+
'tags',
|
|
413
|
+
'custom_fields',
|
|
414
|
+
'created',
|
|
415
|
+
'last_updated',
|
|
416
|
+
)
|
|
417
|
+
brief_fields = ('id', 'name', 'description', 'url', 'display')
|
|
@@ -6,6 +6,7 @@ app_name = 'netbox_contract'
|
|
|
6
6
|
|
|
7
7
|
router = NetBoxRouter()
|
|
8
8
|
router.register('contracts', views.ContractViewSet)
|
|
9
|
+
router.register('contracttype', views.ContractTypeViewSet)
|
|
9
10
|
router.register('invoices', views.InvoiceViewSet)
|
|
10
11
|
router.register('serviceproviders', views.ServiceProviderViewSet)
|
|
11
12
|
router.register('contractassignment', views.ContractAssignmentViewSet)
|
|
@@ -7,6 +7,7 @@ from .serializers import (
|
|
|
7
7
|
AccountingDimensionSerializer,
|
|
8
8
|
ContractAssignmentSerializer,
|
|
9
9
|
ContractSerializer,
|
|
10
|
+
ContractTypeSerializer,
|
|
10
11
|
InvoiceLineSerializer,
|
|
11
12
|
InvoiceSerializer,
|
|
12
13
|
ServiceProviderSerializer,
|
|
@@ -51,3 +52,8 @@ class InvoiceLineViewSet(NetBoxModelViewSet):
|
|
|
51
52
|
class AccountingDimensionViewSet(NetBoxModelViewSet):
|
|
52
53
|
queryset = models.AccountingDimension.objects.prefetch_related('tags')
|
|
53
54
|
serializer_class = AccountingDimensionSerializer
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ContractTypeViewSet(NetBoxModelViewSet):
|
|
58
|
+
queryset = models.ContractType.objects.prefetch_related('tags')
|
|
59
|
+
serializer_class = ContractTypeSerializer
|
|
@@ -7,6 +7,7 @@ SERVICE_PROVIDER_MODELS = Q(
|
|
|
7
7
|
|
|
8
8
|
ASSIGNEMENT_MODELS = Q(
|
|
9
9
|
Q(app_label='circuits', model__in=('circuit',))
|
|
10
|
+
| Q(app_label='circuits', model__in=('virtualcircuit',))
|
|
10
11
|
| Q(app_label='dcim', model__in=('site',))
|
|
11
12
|
| Q(app_label='dcim', model__in=('device',))
|
|
12
13
|
| Q(app_label='virtualization', model__in=('virtualmachine',))
|
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
import django_filters
|
|
2
2
|
from django.db.models import Q
|
|
3
3
|
from netbox.filtersets import NetBoxModelFilterSet
|
|
4
|
-
from tenancy.filtersets import TenancyFilterSet
|
|
4
|
+
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
|
5
5
|
|
|
6
6
|
from .models import (
|
|
7
7
|
AccountingDimension,
|
|
8
8
|
AccountingDimensionStatusChoices,
|
|
9
9
|
Contract,
|
|
10
10
|
ContractAssignment,
|
|
11
|
+
ContractType,
|
|
11
12
|
CurrencyChoices,
|
|
12
13
|
InternalEntityChoices,
|
|
13
14
|
Invoice,
|
|
14
15
|
InvoiceLine,
|
|
15
16
|
ServiceProvider,
|
|
16
17
|
StatusChoices,
|
|
18
|
+
InvoiceStatusChoices,
|
|
17
19
|
)
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
class ContractFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|
22
|
+
class ContractFilterSet(ContactModelFilterSet, NetBoxModelFilterSet, TenancyFilterSet):
|
|
21
23
|
status = django_filters.MultipleChoiceFilter(choices=StatusChoices, null_value=None)
|
|
22
|
-
|
|
24
|
+
internal_party = django_filters.MultipleChoiceFilter(
|
|
23
25
|
choices=InternalEntityChoices, null_value=None
|
|
24
26
|
)
|
|
25
27
|
currency = django_filters.MultipleChoiceFilter(
|
|
26
28
|
choices=CurrencyChoices, null_value=None
|
|
27
29
|
)
|
|
30
|
+
contract_type = django_filters.ModelMultipleChoiceFilter(
|
|
31
|
+
field_name='contract_type__name', to_field_name='name', queryset=ContractType.objects.all()
|
|
32
|
+
)
|
|
28
33
|
|
|
29
34
|
class Meta:
|
|
30
35
|
model = Contract
|
|
31
36
|
fields = (
|
|
32
37
|
'id',
|
|
33
38
|
'name',
|
|
39
|
+
'contract_type',
|
|
34
40
|
'external_reference',
|
|
35
41
|
'start_date',
|
|
36
42
|
'end_date',
|
|
@@ -48,9 +54,15 @@ class ContractFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|
|
48
54
|
|
|
49
55
|
|
|
50
56
|
class InvoiceFilterSet(NetBoxModelFilterSet):
|
|
57
|
+
status = django_filters.MultipleChoiceFilter(choices=InvoiceStatusChoices, null_value=None)
|
|
51
58
|
currency = django_filters.MultipleChoiceFilter(
|
|
52
59
|
choices=CurrencyChoices, null_value=None
|
|
53
60
|
)
|
|
61
|
+
accounting_dimensions = django_filters.ModelChoiceFilter(
|
|
62
|
+
field_name='invoicelines__accounting_dimensions',
|
|
63
|
+
queryset=AccountingDimension.objects.all(),
|
|
64
|
+
label='Accounting Dimension'
|
|
65
|
+
)
|
|
54
66
|
|
|
55
67
|
class Meta:
|
|
56
68
|
model = Invoice
|
|
@@ -71,7 +83,7 @@ class InvoiceFilterSet(NetBoxModelFilterSet):
|
|
|
71
83
|
)
|
|
72
84
|
|
|
73
85
|
|
|
74
|
-
class ServiceProviderFilterSet(NetBoxModelFilterSet):
|
|
86
|
+
class ServiceProviderFilterSet(ContactModelFilterSet, NetBoxModelFilterSet):
|
|
75
87
|
class Meta:
|
|
76
88
|
model = ServiceProvider
|
|
77
89
|
fields = ('id', 'name')
|
|
@@ -80,6 +92,15 @@ class ServiceProviderFilterSet(NetBoxModelFilterSet):
|
|
|
80
92
|
return queryset.filter(name__icontains=value)
|
|
81
93
|
|
|
82
94
|
|
|
95
|
+
class ContractTypeFilterSet(NetBoxModelFilterSet):
|
|
96
|
+
class Meta:
|
|
97
|
+
model = ContractType
|
|
98
|
+
fields = ('name', 'description', 'color')
|
|
99
|
+
|
|
100
|
+
def search(self, queryset, name, value):
|
|
101
|
+
return queryset.filter(name__icontains=value)
|
|
102
|
+
|
|
103
|
+
|
|
83
104
|
class ContractAssignmentFilterSet(NetBoxModelFilterSet):
|
|
84
105
|
class Meta:
|
|
85
106
|
model = ContractAssignment
|
|
@@ -9,10 +9,11 @@ from netbox.forms import (
|
|
|
9
9
|
NetBoxModelForm,
|
|
10
10
|
NetBoxModelImportForm,
|
|
11
11
|
)
|
|
12
|
+
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
|
12
13
|
from tenancy.models import Tenant
|
|
13
|
-
from tenancy.forms import TenancyFilterForm
|
|
14
14
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
|
|
15
15
|
from utilities.forms.fields import (
|
|
16
|
+
ColorField,
|
|
16
17
|
CommentField,
|
|
17
18
|
ContentTypeChoiceField,
|
|
18
19
|
CSVChoiceField,
|
|
@@ -26,18 +27,20 @@ from utilities.forms.fields import (
|
|
|
26
27
|
)
|
|
27
28
|
from utilities.forms.widgets import DatePicker, HTMXSelect
|
|
28
29
|
|
|
29
|
-
from .constants import
|
|
30
|
+
from .constants import ASSIGNEMENT_MODELS, SERVICE_PROVIDER_MODELS
|
|
30
31
|
from .models import (
|
|
31
32
|
AccountingDimension,
|
|
32
33
|
AccountingDimensionStatusChoices,
|
|
33
34
|
Contract,
|
|
34
35
|
ContractAssignment,
|
|
36
|
+
ContractType,
|
|
35
37
|
CurrencyChoices,
|
|
36
38
|
InternalEntityChoices,
|
|
37
39
|
Invoice,
|
|
38
40
|
InvoiceLine,
|
|
39
41
|
ServiceProvider,
|
|
40
42
|
StatusChoices,
|
|
43
|
+
InvoiceStatusChoices,
|
|
41
44
|
)
|
|
42
45
|
|
|
43
46
|
plugin_settings = settings.PLUGINS_CONFIG['netbox_contract']
|
|
@@ -49,13 +52,13 @@ plugin_settings = settings.PLUGINS_CONFIG['netbox_contract']
|
|
|
49
52
|
class ContractForm(NetBoxModelForm):
|
|
50
53
|
comments = CommentField(label=_('Comments'))
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
external_party_object_type = ContentTypeChoiceField(
|
|
53
56
|
queryset=ContentType.objects.all(),
|
|
54
57
|
limit_choices_to=SERVICE_PROVIDER_MODELS,
|
|
55
58
|
widget=HTMXSelect(),
|
|
56
|
-
label=_('External
|
|
59
|
+
label=_('External party object type'),
|
|
57
60
|
)
|
|
58
|
-
|
|
61
|
+
external_party_object = forms.ModelChoiceField(queryset=None, label=_('External party object'))
|
|
59
62
|
tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False, selector=True, label=_('Tenant'))
|
|
60
63
|
parent = DynamicModelChoiceField(
|
|
61
64
|
queryset=Contract.objects.all(),
|
|
@@ -63,30 +66,33 @@ class ContractForm(NetBoxModelForm):
|
|
|
63
66
|
selector=True,
|
|
64
67
|
label=_('Parent'),
|
|
65
68
|
)
|
|
69
|
+
contract_type = DynamicModelChoiceField(
|
|
70
|
+
queryset=ContractType.objects.all(), required=False, selector=True, label=_('Contract type')
|
|
71
|
+
)
|
|
66
72
|
|
|
67
73
|
def __init__(self, *args, **kwargs):
|
|
68
74
|
initial = kwargs.get('initial', None)
|
|
69
75
|
super().__init__(*args, **kwargs)
|
|
70
76
|
|
|
71
77
|
# Initialize the external party object gfk
|
|
72
|
-
if initial and '
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
self.fields['
|
|
78
|
+
if initial and 'external_party_object_type' in initial:
|
|
79
|
+
external_party_object_type = ContentType.objects.get_for_id(initial['external_party_object_type'])
|
|
80
|
+
external_party_class = external_party_object_type.model_class()
|
|
81
|
+
self.fields['external_party_object'].queryset = external_party_class.objects.all()
|
|
76
82
|
if (
|
|
77
|
-
self.instance.
|
|
78
|
-
and self.instance.
|
|
83
|
+
self.instance.external_party_object_type
|
|
84
|
+
and self.instance.external_party_object_type.id == external_party_object_type.id
|
|
79
85
|
):
|
|
80
|
-
self.fields['
|
|
86
|
+
self.fields['external_party_object'].initial = self.instance.external_party_object
|
|
81
87
|
else:
|
|
82
|
-
self.fields['
|
|
83
|
-
elif self.instance.
|
|
84
|
-
|
|
85
|
-
self.fields['
|
|
86
|
-
self.fields['
|
|
88
|
+
self.fields['external_party_object'].initial = None
|
|
89
|
+
elif self.instance.external_party_object_type:
|
|
90
|
+
external_party_class = self.instance.external_party_object_type.model_class()
|
|
91
|
+
self.fields['external_party_object'].queryset = external_party_class.objects.all()
|
|
92
|
+
self.fields['external_party_object'].initial = self.instance.external_party_object
|
|
87
93
|
else:
|
|
88
|
-
self.fields['
|
|
89
|
-
self.fields['
|
|
94
|
+
self.fields['external_party_object'].queryset = ServiceProvider.objects.all()
|
|
95
|
+
self.fields['external_party_object'].initial = None
|
|
90
96
|
|
|
91
97
|
# Initialise fields settings
|
|
92
98
|
mandatory_fields = plugin_settings.get('mandatory_contract_fields')
|
|
@@ -101,10 +107,11 @@ class ContractForm(NetBoxModelForm):
|
|
|
101
107
|
model = Contract
|
|
102
108
|
fields = (
|
|
103
109
|
'name',
|
|
104
|
-
'
|
|
105
|
-
'
|
|
110
|
+
'contract_type',
|
|
111
|
+
'external_party_object_type',
|
|
112
|
+
'external_party_object',
|
|
106
113
|
'external_reference',
|
|
107
|
-
'
|
|
114
|
+
'internal_party',
|
|
108
115
|
'tenant',
|
|
109
116
|
'status',
|
|
110
117
|
'start_date',
|
|
@@ -135,11 +142,17 @@ class ContractForm(NetBoxModelForm):
|
|
|
135
142
|
raise ValidationError('you should set monthly OR yearly recuring costs not both')
|
|
136
143
|
|
|
137
144
|
|
|
138
|
-
class ContractFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|
145
|
+
class ContractFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
|
|
139
146
|
model = Contract
|
|
140
147
|
|
|
148
|
+
contract_type = DynamicModelChoiceField(
|
|
149
|
+
queryset=ContractType.objects.all(),
|
|
150
|
+
required=False,
|
|
151
|
+
selector=True,
|
|
152
|
+
label=_('Contract type'),
|
|
153
|
+
)
|
|
141
154
|
external_reference = forms.CharField(required=False, label=_('External reference'))
|
|
142
|
-
|
|
155
|
+
internal_party = forms.ChoiceField(choices=InternalEntityChoices, required=False, label=_('Internal party'))
|
|
143
156
|
status = forms.ChoiceField(choices=StatusChoices, required=False, label=_('Status'))
|
|
144
157
|
currency = forms.ChoiceField(choices=CurrencyChoices, required=False, label=_('Currency'))
|
|
145
158
|
parent = DynamicModelChoiceField(
|
|
@@ -152,13 +165,13 @@ class ContractFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|
|
152
165
|
|
|
153
166
|
|
|
154
167
|
class ContractCSVForm(NetBoxModelImportForm):
|
|
155
|
-
|
|
168
|
+
external_party_object_type = CSVContentTypeField(
|
|
156
169
|
queryset=ContentType.objects.all(),
|
|
157
170
|
limit_choices_to=SERVICE_PROVIDER_MODELS,
|
|
158
171
|
help_text='service provider object type in the form <app>.<model>',
|
|
159
172
|
)
|
|
160
|
-
|
|
161
|
-
help_text='service provider object name', label=_('External
|
|
173
|
+
external_party_object_id = forms.CharField(
|
|
174
|
+
help_text='service provider object name', label=_('External party name')
|
|
162
175
|
)
|
|
163
176
|
tenant = CSVModelChoiceField(
|
|
164
177
|
queryset=Tenant.objects.all(),
|
|
@@ -175,15 +188,23 @@ class ContractCSVForm(NetBoxModelImportForm):
|
|
|
175
188
|
required=False,
|
|
176
189
|
label=_('Parent'),
|
|
177
190
|
)
|
|
191
|
+
contract_type = CSVModelChoiceField(
|
|
192
|
+
queryset=ContractType.objects.all(),
|
|
193
|
+
to_field_name='name',
|
|
194
|
+
help_text='Contract type name',
|
|
195
|
+
required=False,
|
|
196
|
+
label=_('Contract type'),
|
|
197
|
+
)
|
|
178
198
|
|
|
179
199
|
class Meta:
|
|
180
200
|
model = Contract
|
|
181
201
|
fields = [
|
|
182
202
|
'name',
|
|
183
|
-
'
|
|
184
|
-
'
|
|
203
|
+
'contract_type',
|
|
204
|
+
'external_party_object_type',
|
|
205
|
+
'external_party_object_id',
|
|
185
206
|
'external_reference',
|
|
186
|
-
'
|
|
207
|
+
'internal_party',
|
|
187
208
|
'tenant',
|
|
188
209
|
'status',
|
|
189
210
|
'start_date',
|
|
@@ -200,18 +221,24 @@ class ContractCSVForm(NetBoxModelImportForm):
|
|
|
200
221
|
'parent',
|
|
201
222
|
]
|
|
202
223
|
|
|
203
|
-
def
|
|
204
|
-
name = self.cleaned_data.get('
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
def clean_external_party_object_id(self):
|
|
225
|
+
name = self.cleaned_data.get('external_party_object_id')
|
|
226
|
+
external_party_object_type = self.cleaned_data.get('external_party_object_type')
|
|
227
|
+
external_party_object = external_party_object_type.get_object_for_this_type(name=name)
|
|
207
228
|
|
|
208
|
-
return
|
|
229
|
+
return external_party_object.id
|
|
209
230
|
|
|
210
231
|
|
|
211
232
|
class ContractBulkEditForm(NetBoxModelBulkEditForm):
|
|
212
233
|
name = forms.CharField(max_length=100, required=False, label=_('Name'))
|
|
234
|
+
contract_type = DynamicModelChoiceField(
|
|
235
|
+
queryset=ContractType.objects.all(),
|
|
236
|
+
required=False,
|
|
237
|
+
selector=True,
|
|
238
|
+
label=_('Contract Type')
|
|
239
|
+
)
|
|
213
240
|
external_reference = forms.CharField(max_length=100, required=False, label=_('External reference'))
|
|
214
|
-
|
|
241
|
+
internal_party = forms.ChoiceField(choices=InternalEntityChoices, required=False, label=_('Internal party'))
|
|
215
242
|
tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False, selector=True, label=_('Tenant'))
|
|
216
243
|
comments = CommentField(required=False, label=_('Comments'))
|
|
217
244
|
parent = DynamicModelChoiceField(
|
|
@@ -225,6 +252,44 @@ class ContractBulkEditForm(NetBoxModelBulkEditForm):
|
|
|
225
252
|
model = Contract
|
|
226
253
|
|
|
227
254
|
|
|
255
|
+
# ContractType
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class ContractTypeForm(NetBoxModelForm):
|
|
259
|
+
color = ColorField(label=_('Color'))
|
|
260
|
+
|
|
261
|
+
class Meta:
|
|
262
|
+
model = ContractType
|
|
263
|
+
fields = (
|
|
264
|
+
'name',
|
|
265
|
+
'description',
|
|
266
|
+
'color',
|
|
267
|
+
'tags',
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class ContractTypeCSVForm(NetBoxModelImportForm):
|
|
272
|
+
name = forms.CharField(max_length=100, label=_('Name'))
|
|
273
|
+
description = CommentField(label=_('Description'))
|
|
274
|
+
color = ColorField(label=_('Color'))
|
|
275
|
+
|
|
276
|
+
class Meta:
|
|
277
|
+
model = ContractType
|
|
278
|
+
fields = ['name', 'description', 'color']
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ContractTypeBulkEditForm(NetBoxModelBulkEditForm):
|
|
282
|
+
description = CommentField(label=_('Description'))
|
|
283
|
+
nullable_fields = ('comments',)
|
|
284
|
+
color = ColorField(label=_('Color'), required=False,)
|
|
285
|
+
model = ContractType
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ContractTypeFilterForm(NetBoxModelFilterSetForm):
|
|
289
|
+
model = ContractType
|
|
290
|
+
name = forms.CharField(required=False, label=_('Name'))
|
|
291
|
+
description = CommentField(label=_('Description'))
|
|
292
|
+
|
|
228
293
|
# Invoice
|
|
229
294
|
|
|
230
295
|
|
|
@@ -316,6 +381,7 @@ class InvoiceForm(NetBoxModelForm):
|
|
|
316
381
|
'date',
|
|
317
382
|
'contracts',
|
|
318
383
|
'template',
|
|
384
|
+
'status',
|
|
319
385
|
'period_start',
|
|
320
386
|
'period_end',
|
|
321
387
|
'currency',
|
|
@@ -342,6 +408,7 @@ class InvoiceFilterForm(NetBoxModelFilterSetForm):
|
|
|
342
408
|
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES),
|
|
343
409
|
label=_('Template'),
|
|
344
410
|
)
|
|
411
|
+
status = forms.ChoiceField(choices=InvoiceStatusChoices, required=False, label=_('Status'))
|
|
345
412
|
currency = forms.ChoiceField(choices=CurrencyChoices, required=False, label=_('Currency'))
|
|
346
413
|
contracts = DynamicModelMultipleChoiceField(
|
|
347
414
|
queryset=Contract.objects.all(),
|
|
@@ -349,6 +416,13 @@ class InvoiceFilterForm(NetBoxModelFilterSetForm):
|
|
|
349
416
|
selector=True,
|
|
350
417
|
label=_('Contracts'),
|
|
351
418
|
)
|
|
419
|
+
accounting_dimensions = DynamicModelMultipleChoiceField(
|
|
420
|
+
queryset=AccountingDimension.objects.all(),
|
|
421
|
+
required=False,
|
|
422
|
+
selector=True,
|
|
423
|
+
label=_('Accounting Dimensions'),
|
|
424
|
+
)
|
|
425
|
+
|
|
352
426
|
tag = TagFilterField(model)
|
|
353
427
|
|
|
354
428
|
|
|
@@ -359,6 +433,7 @@ class InvoiceCSVForm(NetBoxModelImportForm):
|
|
|
359
433
|
help_text='Related Contracts',
|
|
360
434
|
label=_('Contracts'),
|
|
361
435
|
)
|
|
436
|
+
status = CSVChoiceField(choices=InvoiceStatusChoices, help_text='Invoice status', label=_('Status'))
|
|
362
437
|
|
|
363
438
|
class Meta:
|
|
364
439
|
model = Invoice
|
|
@@ -367,6 +442,7 @@ class InvoiceCSVForm(NetBoxModelImportForm):
|
|
|
367
442
|
'date',
|
|
368
443
|
'contracts',
|
|
369
444
|
'template',
|
|
445
|
+
'status',
|
|
370
446
|
'period_start',
|
|
371
447
|
'period_end',
|
|
372
448
|
'currency',
|
|
@@ -437,7 +513,7 @@ class ServiceProviderForm(NetBoxModelForm):
|
|
|
437
513
|
fields = ('name', 'slug', 'portal_url', 'comments', 'tags')
|
|
438
514
|
|
|
439
515
|
|
|
440
|
-
class ServiceProviderFilterForm(NetBoxModelFilterSetForm):
|
|
516
|
+
class ServiceProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|
441
517
|
model = ServiceProvider
|
|
442
518
|
name = forms.CharField(required=False, label=_('Name'))
|
|
443
519
|
tag = TagFilterField(model)
|
|
Binary file
|