PyInventory 0.19.0__py3-none-any.whl
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.
- PyInventory-0.19.0.dist-info/AUTHORS +14 -0
- PyInventory-0.19.0.dist-info/LICENSE +674 -0
- PyInventory-0.19.0.dist-info/METADATA +347 -0
- PyInventory-0.19.0.dist-info/RECORD +101 -0
- PyInventory-0.19.0.dist-info/WHEEL +5 -0
- PyInventory-0.19.0.dist-info/entry_points.txt +2 -0
- PyInventory-0.19.0.dist-info/top_level.txt +2 -0
- inventory/__init__.py +7 -0
- inventory/admin/__init__.py +3 -0
- inventory/admin/base.py +104 -0
- inventory/admin/item.py +169 -0
- inventory/admin/location.py +78 -0
- inventory/admin/memo.py +76 -0
- inventory/admin/tagulous_fix.py +45 -0
- inventory/apps.py +18 -0
- inventory/ckeditor_upload.py +15 -0
- inventory/context_processors.py +5 -0
- inventory/forms.py +36 -0
- inventory/locale/ca/LC_MESSAGES/django.mo +0 -0
- inventory/locale/ca/LC_MESSAGES/django.po +297 -0
- inventory/locale/de/LC_MESSAGES/django.mo +0 -0
- inventory/locale/de/LC_MESSAGES/django.po +294 -0
- inventory/locale/en/LC_MESSAGES/django.mo +0 -0
- inventory/locale/en/LC_MESSAGES/django.po +294 -0
- inventory/locale/es/LC_MESSAGES/django.mo +0 -0
- inventory/locale/es/LC_MESSAGES/django.po +297 -0
- inventory/management/__init__.py +0 -0
- inventory/management/commands/__init__.py +0 -0
- inventory/management/commands/seed_data.py +135 -0
- inventory/management/commands/tree.py +62 -0
- inventory/middlewares.py +21 -0
- inventory/migrations/0001_initial.py +596 -0
- inventory/migrations/0002_auto_20201017_2211.py +87 -0
- inventory/migrations/0003_auto_20201024_1830.py +23 -0
- inventory/migrations/0004_item_user_images.py +129 -0
- inventory/migrations/0005_serve_uploads_by_django_tools.py +77 -0
- inventory/migrations/0006_refactor_image_model.py +46 -0
- inventory/migrations/0007_add_file_attachment.py +128 -0
- inventory/migrations/0008_last_check_datetime.py +23 -0
- inventory/migrations/0009_add_memo.py +517 -0
- inventory/migrations/0010_version_protect_models.py +37 -0
- inventory/migrations/0011_parent_tree1.py +97 -0
- inventory/migrations/0012_parent_tree2.py +20 -0
- inventory/migrations/0013_alter_itemmodel_location.py +26 -0
- inventory/migrations/__init__.py +0 -0
- inventory/models/__init__.py +3 -0
- inventory/models/base.py +239 -0
- inventory/models/item.py +228 -0
- inventory/models/links.py +104 -0
- inventory/models/location.py +24 -0
- inventory/models/memo.py +109 -0
- inventory/parent_tree.py +71 -0
- inventory/permissions.py +60 -0
- inventory/request_dict.py +16 -0
- inventory/signals.py +15 -0
- inventory/string_utils.py +15 -0
- inventory/templates/admin/item/related_items.html +18 -0
- inventory/templates/admin/location/items.html +18 -0
- inventory/tests/__init__.py +0 -0
- inventory/tests/fixtures/__init__.py +0 -0
- inventory/tests/fixtures/users.py +11 -0
- inventory/tests/test_admin_location.py +34 -0
- inventory/tests/test_admin_location_empty_change_list_1.snapshot.html +84 -0
- inventory/tests/test_item_images.py +76 -0
- inventory/tests/test_link_model.py +72 -0
- inventory/tests/test_management_command_seed_data.py +49 -0
- inventory/tests/test_management_command_tree.py +27 -0
- inventory/tests/test_parent_tree.py +40 -0
- inventory/tests/test_parent_tree_model.py +139 -0
- inventory_project/__init__.py +12 -0
- inventory_project/__main__.py +17 -0
- inventory_project/manage.py +41 -0
- inventory_project/middlewares.py +23 -0
- inventory_project/publish.py +21 -0
- inventory_project/settings/__init__.py +0 -0
- inventory_project/settings/local.py +74 -0
- inventory_project/settings/prod.py +393 -0
- inventory_project/settings/tests.py +45 -0
- inventory_project/templates/admin/base_site.html +22 -0
- inventory_project/templates/admin/login.html +32 -0
- inventory_project/tests/__init__.py +0 -0
- inventory_project/tests/fixtures.py +40 -0
- inventory_project/tests/mocks.py +15 -0
- inventory_project/tests/playwright_utils.py +22 -0
- inventory_project/tests/test_admin.py +15 -0
- inventory_project/tests/test_admin_item.py +240 -0
- inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html +349 -0
- inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html +232 -0
- inventory_project/tests/test_admin_item_login_1.snapshot.html +40 -0
- inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html +637 -0
- inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html +930 -0
- inventory_project/tests/test_admin_memo.py +153 -0
- inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html +365 -0
- inventory_project/tests/test_command_shell_help_django4.2.3.snapshot.txt +60 -0
- inventory_project/tests/test_inventory_commands.py +26 -0
- inventory_project/tests/test_migrations.py +22 -0
- inventory_project/tests/test_models_item.py +24 -0
- inventory_project/tests/test_playwright_admin.py +157 -0
- inventory_project/tests/test_project_setup.py +102 -0
- inventory_project/urls.py +21 -0
- inventory_project/wsgi.py +9 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Generated by Django 3.2.15 on 2022-09-30 18:30
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
('inventory', '0012_parent_tree2'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name='itemmodel',
|
|
15
|
+
name='location',
|
|
16
|
+
field=models.ForeignKey(
|
|
17
|
+
blank=True,
|
|
18
|
+
help_text='ItemModel.location.help_text',
|
|
19
|
+
null=True,
|
|
20
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
21
|
+
related_name='items',
|
|
22
|
+
to='inventory.locationmodel',
|
|
23
|
+
verbose_name='ItemModel.location.verbose_name',
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
]
|
|
File without changes
|
inventory/models/base.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
import unicodedata
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
import tagulous.models
|
|
8
|
+
from bx_django_utils.models.timetracking import TimetrackingBaseModel
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.db import models
|
|
11
|
+
from django.db.models import QuerySet
|
|
12
|
+
from django.utils.translation import gettext_lazy as _
|
|
13
|
+
|
|
14
|
+
from inventory.parent_tree import ValuesListTree
|
|
15
|
+
from inventory.string_utils import ltruncatechars
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseModel(TimetrackingBaseModel):
|
|
22
|
+
id = models.UUIDField(
|
|
23
|
+
primary_key=True,
|
|
24
|
+
default=uuid.uuid4,
|
|
25
|
+
editable=False,
|
|
26
|
+
verbose_name=_('BaseModel.id.verbose_name'),
|
|
27
|
+
help_text=_('BaseModel.id.help_text'),
|
|
28
|
+
)
|
|
29
|
+
user = models.ForeignKey( # "Owner" of this entry
|
|
30
|
+
settings.AUTH_USER_MODEL,
|
|
31
|
+
related_name='+',
|
|
32
|
+
on_delete=models.CASCADE,
|
|
33
|
+
editable=False, # Must be set automatically and never changed
|
|
34
|
+
verbose_name=_('BaseModel.user.verbose_name'),
|
|
35
|
+
help_text=_('BaseModel.user.help_text'),
|
|
36
|
+
)
|
|
37
|
+
name = models.CharField(
|
|
38
|
+
max_length=255, verbose_name=_('BaseModel.name.verbose_name'), help_text=_('BaseModel.name.help_text')
|
|
39
|
+
)
|
|
40
|
+
tags = tagulous.models.TagField(
|
|
41
|
+
blank=True,
|
|
42
|
+
case_sensitive=False,
|
|
43
|
+
force_lowercase=False,
|
|
44
|
+
space_delimiter=False,
|
|
45
|
+
max_count=10,
|
|
46
|
+
verbose_name=_('BaseModel.tags.verbose_name'),
|
|
47
|
+
help_text=_('BaseModel.tags.help_text'),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def __str__(self):
|
|
51
|
+
return self.name
|
|
52
|
+
|
|
53
|
+
class Meta:
|
|
54
|
+
abstract = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def nomalize_text(text):
|
|
58
|
+
"""
|
|
59
|
+
>>> nomalize_text('Foo Bar 1 §$% äö-üß +')
|
|
60
|
+
'foobar1aou'
|
|
61
|
+
"""
|
|
62
|
+
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
|
|
63
|
+
text = text.lower()
|
|
64
|
+
text = re.sub(r'[^\w]', '', text)
|
|
65
|
+
return text
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def generate_path_str(path):
|
|
69
|
+
"""
|
|
70
|
+
>>> generate_path_str(['Foo', 'B a r', '1 §$% äö-üß +'])
|
|
71
|
+
'foo 0 bar 0 1aou'
|
|
72
|
+
"""
|
|
73
|
+
# The choice of the separator is very important for the correct sorting by the database!
|
|
74
|
+
# Use 0, because this character is used for sorting and is the first character in the charset.
|
|
75
|
+
# The spaces are only visual separators ;)
|
|
76
|
+
return ' 0 '.join(nomalize_text(part) for part in path)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ParentTreeModelManager(models.Manager):
|
|
80
|
+
def update_tree_info(self) -> None:
|
|
81
|
+
start_time = time.monotonic()
|
|
82
|
+
|
|
83
|
+
values = self.all().values('pk', 'name', 'parent__pk', 'path')
|
|
84
|
+
tree = ValuesListTree(values=values)
|
|
85
|
+
tree_path = tree.get_tree_path()
|
|
86
|
+
logger.debug('Tree path: %r', tree_path)
|
|
87
|
+
update_path_info = tree.get_update_path_info()
|
|
88
|
+
|
|
89
|
+
duration = (time.monotonic() - start_time) * 1000
|
|
90
|
+
logger.info('Get update_path_info: %r in %ims', update_path_info, duration)
|
|
91
|
+
|
|
92
|
+
if not update_path_info:
|
|
93
|
+
logger.info('No tree path changed, ok')
|
|
94
|
+
else:
|
|
95
|
+
start_time = time.monotonic()
|
|
96
|
+
|
|
97
|
+
entries = self.filter(pk__in=update_path_info.keys())
|
|
98
|
+
for entry in entries:
|
|
99
|
+
path = update_path_info[entry.pk]
|
|
100
|
+
entry.path = path
|
|
101
|
+
entry.path_str = generate_path_str(path)
|
|
102
|
+
entry.level = len(path)
|
|
103
|
+
|
|
104
|
+
self.bulk_update(entries, ['path', 'path_str', 'level'])
|
|
105
|
+
|
|
106
|
+
duration = (time.monotonic() - start_time) * 1000
|
|
107
|
+
logger.info('Update %i entries in %ims', len(entries), duration)
|
|
108
|
+
|
|
109
|
+
def related_objects(self, instance: 'BaseParentTreeModel') -> QuerySet:
|
|
110
|
+
"""
|
|
111
|
+
Returns a QuerySet with relation section of the tree
|
|
112
|
+
"""
|
|
113
|
+
path = instance.path
|
|
114
|
+
if path is None:
|
|
115
|
+
# Not saved -> Can't have related objects ;)
|
|
116
|
+
return self.none()
|
|
117
|
+
|
|
118
|
+
root_entry = path[0]
|
|
119
|
+
qs = self.all()
|
|
120
|
+
qs = qs.filter(path__0=root_entry)
|
|
121
|
+
return qs
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class BaseParentTreeModel(BaseModel):
|
|
125
|
+
path = models.JSONField(
|
|
126
|
+
blank=True,
|
|
127
|
+
null=True,
|
|
128
|
+
editable=False,
|
|
129
|
+
)
|
|
130
|
+
path_str = models.TextField(
|
|
131
|
+
blank=True,
|
|
132
|
+
null=True,
|
|
133
|
+
editable=False,
|
|
134
|
+
)
|
|
135
|
+
level = models.PositiveSmallIntegerField(
|
|
136
|
+
blank=True,
|
|
137
|
+
null=True,
|
|
138
|
+
editable=False,
|
|
139
|
+
)
|
|
140
|
+
parent = models.ForeignKey(
|
|
141
|
+
'self',
|
|
142
|
+
on_delete=models.SET_NULL,
|
|
143
|
+
blank=True,
|
|
144
|
+
null=True,
|
|
145
|
+
verbose_name=_('LocationModel.parent.verbose_name'),
|
|
146
|
+
help_text=_('LocationModel.parent.help_text'),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
objects = models.Manager()
|
|
150
|
+
tree_objects = ParentTreeModelManager()
|
|
151
|
+
|
|
152
|
+
def save(self, **kwargs):
|
|
153
|
+
if not self.path:
|
|
154
|
+
if self.parent:
|
|
155
|
+
path = self.parent.path
|
|
156
|
+
if path:
|
|
157
|
+
self.path = [*path, self.name]
|
|
158
|
+
else:
|
|
159
|
+
self.path = [self.name]
|
|
160
|
+
self.path_str = generate_path_str(self.path)
|
|
161
|
+
self.level = len(self.path)
|
|
162
|
+
logger.info('Init path with: %r', self.path)
|
|
163
|
+
|
|
164
|
+
self.full_clean()
|
|
165
|
+
super().save(**kwargs)
|
|
166
|
+
self.__class__.tree_objects.update_tree_info()
|
|
167
|
+
|
|
168
|
+
def __str__(self):
|
|
169
|
+
if self.path:
|
|
170
|
+
text = ' › '.join(self.path)
|
|
171
|
+
text = ltruncatechars(text, max_length=settings.TREE_PATH_STR_MAX_LENGTH)
|
|
172
|
+
return text
|
|
173
|
+
|
|
174
|
+
return self.name
|
|
175
|
+
|
|
176
|
+
class Meta:
|
|
177
|
+
abstract = True
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class BaseAttachmentModel(BaseModel):
|
|
181
|
+
"""
|
|
182
|
+
Base model to store files or images to Items
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
name = models.CharField(
|
|
186
|
+
null=True,
|
|
187
|
+
blank=True,
|
|
188
|
+
max_length=255,
|
|
189
|
+
verbose_name=_('BaseItemAttachmentModel.name.verbose_name'),
|
|
190
|
+
help_text=_('BaseItemAttachmentModel.name.help_text'),
|
|
191
|
+
)
|
|
192
|
+
position = models.PositiveSmallIntegerField(
|
|
193
|
+
# Note: Will be set in admin via adminsortable2
|
|
194
|
+
# The JavaScript which performs the sorting is 1-indexed !
|
|
195
|
+
default=0,
|
|
196
|
+
blank=False,
|
|
197
|
+
null=False,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def __str__(self):
|
|
201
|
+
return self.name
|
|
202
|
+
|
|
203
|
+
def full_clean(self, *, parent_instance, **kwargs):
|
|
204
|
+
if self.user_id is None:
|
|
205
|
+
# inherit owner of this link from parent model instance
|
|
206
|
+
self.user_id = parent_instance.user_id
|
|
207
|
+
|
|
208
|
+
return super().full_clean(**kwargs)
|
|
209
|
+
|
|
210
|
+
class Meta:
|
|
211
|
+
abstract = True
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class BaseItemAttachmentModel(BaseAttachmentModel):
|
|
215
|
+
"""
|
|
216
|
+
Base model to store files or images to Items
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
item = models.ForeignKey('ItemModel', on_delete=models.CASCADE)
|
|
220
|
+
|
|
221
|
+
def full_clean(self, **kwargs):
|
|
222
|
+
return super().full_clean(parent_instance=self.item, **kwargs)
|
|
223
|
+
|
|
224
|
+
class Meta:
|
|
225
|
+
abstract = True
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class BaseMemoAttachmentModel(BaseAttachmentModel):
|
|
229
|
+
"""
|
|
230
|
+
Base model to store files or images to Memos
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
memo = models.ForeignKey('MemoModel', on_delete=models.CASCADE)
|
|
234
|
+
|
|
235
|
+
def full_clean(self, **kwargs):
|
|
236
|
+
return super().full_clean(parent_instance=self.memo, **kwargs)
|
|
237
|
+
|
|
238
|
+
class Meta:
|
|
239
|
+
abstract = True
|
inventory/models/item.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import tagulous.models
|
|
5
|
+
from bx_django_utils.filename import clean_filename
|
|
6
|
+
from ckeditor_uploader.fields import RichTextUploadingField
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.urls import reverse
|
|
9
|
+
from django.utils.translation import gettext_lazy as _
|
|
10
|
+
from django_tools.model_version_protect.models import VersionProtectBaseModel
|
|
11
|
+
from django_tools.serve_media_app.models import user_directory_path
|
|
12
|
+
|
|
13
|
+
from inventory.models.base import BaseItemAttachmentModel, BaseParentTreeModel
|
|
14
|
+
from inventory.models.links import BaseLink
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ItemQuerySet(models.QuerySet):
|
|
21
|
+
def sort(self):
|
|
22
|
+
return self.order_by('kind', 'producer', 'name')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ItemModel(BaseParentTreeModel, VersionProtectBaseModel):
|
|
26
|
+
"""
|
|
27
|
+
A Item that can be described and store somewhere ;)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
objects = ItemQuerySet.as_manager()
|
|
31
|
+
|
|
32
|
+
kind = tagulous.models.TagField(
|
|
33
|
+
case_sensitive=False,
|
|
34
|
+
force_lowercase=False,
|
|
35
|
+
space_delimiter=False,
|
|
36
|
+
max_count=3,
|
|
37
|
+
verbose_name=_('ItemModel.kind.verbose_name'),
|
|
38
|
+
help_text=_('ItemModel.kind.help_text'),
|
|
39
|
+
)
|
|
40
|
+
producer = tagulous.models.TagField(
|
|
41
|
+
blank=True,
|
|
42
|
+
case_sensitive=False,
|
|
43
|
+
force_lowercase=False,
|
|
44
|
+
space_delimiter=False,
|
|
45
|
+
max_count=1,
|
|
46
|
+
verbose_name=_('ItemModel.producer.verbose_name'),
|
|
47
|
+
help_text=_('ItemModel.producer.help_text'),
|
|
48
|
+
)
|
|
49
|
+
description = RichTextUploadingField(
|
|
50
|
+
blank=True,
|
|
51
|
+
null=True,
|
|
52
|
+
config_name='ItemModel.description',
|
|
53
|
+
verbose_name=_('ItemModel.description.verbose_name'),
|
|
54
|
+
help_text=_('ItemModel.description.help_text'),
|
|
55
|
+
)
|
|
56
|
+
fcc_id = models.CharField(
|
|
57
|
+
max_length=20,
|
|
58
|
+
blank=True,
|
|
59
|
+
null=True,
|
|
60
|
+
verbose_name=_('ItemModel.fcc_id.verbose_name'),
|
|
61
|
+
help_text=_('ItemModel.fcc_id.help_text'),
|
|
62
|
+
)
|
|
63
|
+
location = models.ForeignKey(
|
|
64
|
+
'inventory.LocationModel',
|
|
65
|
+
blank=True,
|
|
66
|
+
null=True,
|
|
67
|
+
on_delete=models.SET_NULL,
|
|
68
|
+
related_name='items',
|
|
69
|
+
verbose_name=_('ItemModel.location.verbose_name'),
|
|
70
|
+
help_text=_('ItemModel.location.help_text'),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# ________________________________________________________________________
|
|
74
|
+
# lent
|
|
75
|
+
|
|
76
|
+
lent_to = models.CharField(
|
|
77
|
+
max_length=64,
|
|
78
|
+
blank=True,
|
|
79
|
+
null=True,
|
|
80
|
+
verbose_name=_('ItemModel.lent_to.verbose_name'),
|
|
81
|
+
help_text=_('ItemModel.lent_to.help_text'),
|
|
82
|
+
)
|
|
83
|
+
lent_from_date = models.DateField(
|
|
84
|
+
blank=True,
|
|
85
|
+
null=True,
|
|
86
|
+
verbose_name=_('ItemModel.lent_from_date.verbose_name'),
|
|
87
|
+
help_text=_('ItemModel.lent_from_date.help_text'),
|
|
88
|
+
)
|
|
89
|
+
lent_until_date = models.DateField(
|
|
90
|
+
blank=True,
|
|
91
|
+
null=True,
|
|
92
|
+
verbose_name=_('ItemModel.lent_until_date.verbose_name'),
|
|
93
|
+
help_text=_('ItemModel.lent_until_date.help_text'),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# ________________________________________________________________________
|
|
97
|
+
# received
|
|
98
|
+
|
|
99
|
+
received_from = models.CharField(
|
|
100
|
+
max_length=64,
|
|
101
|
+
blank=True,
|
|
102
|
+
null=True,
|
|
103
|
+
verbose_name=_('ItemModel.received_from.verbose_name'),
|
|
104
|
+
help_text=_('ItemModel.received_from.help_text'),
|
|
105
|
+
)
|
|
106
|
+
received_date = models.DateField(
|
|
107
|
+
blank=True,
|
|
108
|
+
null=True,
|
|
109
|
+
verbose_name=_('ItemModel.received_date.verbose_name'),
|
|
110
|
+
help_text=_('ItemModel.received_date.help_text'),
|
|
111
|
+
)
|
|
112
|
+
received_price = models.DecimalField(
|
|
113
|
+
decimal_places=2,
|
|
114
|
+
max_digits=6, # up to 9999 with a resolution of 2 decimal places
|
|
115
|
+
blank=True,
|
|
116
|
+
null=True,
|
|
117
|
+
verbose_name=_('ItemModel.received_price.verbose_name'),
|
|
118
|
+
help_text=_('ItemModel.received_price.help_text'),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ________________________________________________________________________
|
|
122
|
+
# handed over
|
|
123
|
+
|
|
124
|
+
handed_over_to = models.CharField(
|
|
125
|
+
max_length=64,
|
|
126
|
+
blank=True,
|
|
127
|
+
null=True,
|
|
128
|
+
verbose_name=_('ItemModel.handed_over_to.verbose_name'),
|
|
129
|
+
help_text=_('ItemModel.handed_over_to.help_text'),
|
|
130
|
+
)
|
|
131
|
+
handed_over_date = models.DateField(
|
|
132
|
+
blank=True,
|
|
133
|
+
null=True,
|
|
134
|
+
verbose_name=_('ItemModel.handed_over_date.verbose_name'),
|
|
135
|
+
help_text=_('ItemModel.handed_over_date.help_text'),
|
|
136
|
+
)
|
|
137
|
+
handed_over_price = models.DecimalField(
|
|
138
|
+
decimal_places=2,
|
|
139
|
+
max_digits=6, # up to 9999 with a resolution of 2 decimal places
|
|
140
|
+
blank=True,
|
|
141
|
+
null=True,
|
|
142
|
+
verbose_name=_('ItemModel.handed_over_price.verbose_name'),
|
|
143
|
+
help_text=_('ItemModel.handed_over_price.help_text'),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def local_admin_link(self):
|
|
147
|
+
url = reverse('admin:inventory_itemmodel_change', args=[self.id])
|
|
148
|
+
return url
|
|
149
|
+
|
|
150
|
+
def verbose_name(self):
|
|
151
|
+
parts = [str(part) for part in (self.kind, self.producer, self.name)]
|
|
152
|
+
return ' - '.join(part for part in parts if part)
|
|
153
|
+
|
|
154
|
+
class Meta:
|
|
155
|
+
ordering = ('path_str',)
|
|
156
|
+
verbose_name = _('ItemModel.verbose_name')
|
|
157
|
+
verbose_name_plural = _('ItemModel.verbose_name_plural')
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ItemLinkModel(BaseLink):
|
|
161
|
+
item = models.ForeignKey(ItemModel, on_delete=models.CASCADE)
|
|
162
|
+
|
|
163
|
+
def full_clean(self, **kwargs):
|
|
164
|
+
if self.user_id is None:
|
|
165
|
+
# inherit owner of this link from item instance
|
|
166
|
+
self.user_id = self.item.user_id
|
|
167
|
+
return super().full_clean(**kwargs)
|
|
168
|
+
|
|
169
|
+
class Meta:
|
|
170
|
+
verbose_name = _('ItemLinkModel.verbose_name')
|
|
171
|
+
verbose_name_plural = _('ItemLinkModel.verbose_name_plural')
|
|
172
|
+
ordering = ('position',)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ItemImageModel(BaseItemAttachmentModel):
|
|
176
|
+
"""
|
|
177
|
+
Store images to Items
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
image = models.ImageField(
|
|
181
|
+
upload_to=user_directory_path,
|
|
182
|
+
verbose_name=_('ItemImageModel.image.verbose_name'),
|
|
183
|
+
help_text=_('ItemImageModel.image.help_text'),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def __str__(self):
|
|
187
|
+
return self.name or self.image.name
|
|
188
|
+
|
|
189
|
+
def full_clean(self, **kwargs):
|
|
190
|
+
# Set name by image filename:
|
|
191
|
+
if not self.name:
|
|
192
|
+
filename = Path(self.image.name).name
|
|
193
|
+
self.name = clean_filename(filename)
|
|
194
|
+
|
|
195
|
+
return super().full_clean(**kwargs)
|
|
196
|
+
|
|
197
|
+
class Meta:
|
|
198
|
+
verbose_name = _('ItemImageModel.verbose_name')
|
|
199
|
+
verbose_name_plural = _('ItemImageModel.verbose_name_plural')
|
|
200
|
+
ordering = ('position',)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ItemFileModel(BaseItemAttachmentModel):
|
|
204
|
+
"""
|
|
205
|
+
Store files to Items
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
file = models.FileField(
|
|
209
|
+
upload_to=user_directory_path,
|
|
210
|
+
verbose_name=_('ItemFileModel.file.verbose_name'),
|
|
211
|
+
help_text=_('ItemFileModel.file.help_text'),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def __str__(self):
|
|
215
|
+
return self.name or self.file.name
|
|
216
|
+
|
|
217
|
+
def full_clean(self, **kwargs):
|
|
218
|
+
# Set name by filename:
|
|
219
|
+
if not self.name:
|
|
220
|
+
filename = Path(self.file.name).name
|
|
221
|
+
self.name = clean_filename(filename)
|
|
222
|
+
|
|
223
|
+
return super().full_clean(**kwargs)
|
|
224
|
+
|
|
225
|
+
class Meta:
|
|
226
|
+
verbose_name = _('ItemFileModel.verbose_name')
|
|
227
|
+
verbose_name_plural = _('ItemFileModel.verbose_name_plural')
|
|
228
|
+
ordering = ('position',)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from django.db import models
|
|
7
|
+
from django.template.defaultfilters import striptags
|
|
8
|
+
from django.utils import timezone
|
|
9
|
+
from django.utils.translation import gettext_lazy as _
|
|
10
|
+
|
|
11
|
+
from inventory.models.base import BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseLink(BaseModel):
|
|
18
|
+
name = models.CharField(
|
|
19
|
+
max_length=255,
|
|
20
|
+
blank=True,
|
|
21
|
+
null=True,
|
|
22
|
+
verbose_name=_('BaseLink.name.verbose_name'),
|
|
23
|
+
help_text=_('BaseLink.name.help_text'),
|
|
24
|
+
)
|
|
25
|
+
url = models.URLField(verbose_name=_('Link.url.verbose_name'), help_text=_('Link.url.help_text'))
|
|
26
|
+
last_check = models.DateTimeField(
|
|
27
|
+
blank=True,
|
|
28
|
+
null=True,
|
|
29
|
+
editable=False,
|
|
30
|
+
verbose_name=_('Link.url.verbose_name'),
|
|
31
|
+
help_text=_('Link.url.help_text'),
|
|
32
|
+
)
|
|
33
|
+
status_code = models.PositiveSmallIntegerField(
|
|
34
|
+
blank=True,
|
|
35
|
+
null=True,
|
|
36
|
+
editable=False,
|
|
37
|
+
verbose_name=_('Link.status_code.verbose_name'),
|
|
38
|
+
help_text=_('Link.status_code.help_text'),
|
|
39
|
+
)
|
|
40
|
+
page_title = models.CharField(
|
|
41
|
+
max_length=255,
|
|
42
|
+
blank=True,
|
|
43
|
+
null=True,
|
|
44
|
+
editable=False,
|
|
45
|
+
verbose_name=_('Link.page_title.verbose_name'),
|
|
46
|
+
help_text=_('Link.page_title.help_text'),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
position = models.PositiveSmallIntegerField(
|
|
50
|
+
# Note: Will be set in admin via adminsortable2
|
|
51
|
+
# The JavaScript which performs the sorting is 1-indexed !
|
|
52
|
+
default=0,
|
|
53
|
+
blank=False,
|
|
54
|
+
null=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def update_response_info(self):
|
|
58
|
+
if self.name:
|
|
59
|
+
logger.debug('Skip link request: because we have a name: %r', self.name)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if self.last_check:
|
|
63
|
+
delta = timezone.now() - self.last_check
|
|
64
|
+
logger.debug('Last check is %s ago.', delta)
|
|
65
|
+
if delta < datetime.timedelta(minutes=1):
|
|
66
|
+
logger.info('Skip request for: %r', self.url)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
r = requests.get(url=self.url, allow_redirects=True, timeout=10)
|
|
71
|
+
except Exception as err:
|
|
72
|
+
logger.exception('Error get %s: %s', self.url, err)
|
|
73
|
+
self.status_code = None
|
|
74
|
+
self.page_title = None
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
logger.debug('%r: %r', self.url, r.headers)
|
|
78
|
+
|
|
79
|
+
self.last_check = timezone.now()
|
|
80
|
+
self.status_code = r.status_code
|
|
81
|
+
|
|
82
|
+
if r.status_code == 200:
|
|
83
|
+
titles = re.findall(r'<title>(.+?)</title>', r.text)
|
|
84
|
+
if not titles:
|
|
85
|
+
logger.warning('No title found in %r', self.url)
|
|
86
|
+
else:
|
|
87
|
+
title = titles[0]
|
|
88
|
+
logger.info('Found title: %r', title)
|
|
89
|
+
|
|
90
|
+
self.page_title = striptags(title) # TODO: remove with a better clean method!
|
|
91
|
+
if not self.name:
|
|
92
|
+
logger.debug('set name to: %r', self.page_title)
|
|
93
|
+
self.name = self.page_title
|
|
94
|
+
|
|
95
|
+
def full_clean(self, **kwargs):
|
|
96
|
+
if self.url is not None:
|
|
97
|
+
self.update_response_info()
|
|
98
|
+
return super().full_clean(**kwargs)
|
|
99
|
+
|
|
100
|
+
def __str__(self):
|
|
101
|
+
return self.url
|
|
102
|
+
|
|
103
|
+
class Meta:
|
|
104
|
+
abstract = True
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ckeditor_uploader.fields import RichTextUploadingField
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from django_tools.model_version_protect.models import VersionProtectBaseModel
|
|
4
|
+
|
|
5
|
+
from inventory.models.base import BaseParentTreeModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LocationModel(BaseParentTreeModel, VersionProtectBaseModel):
|
|
9
|
+
"""
|
|
10
|
+
A Storage for items.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
description = RichTextUploadingField(
|
|
14
|
+
blank=True,
|
|
15
|
+
null=True,
|
|
16
|
+
config_name='LocationModel.description',
|
|
17
|
+
verbose_name=_('LocationModel.description.verbose_name'),
|
|
18
|
+
help_text=_('LocationModel.description.help_text'),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class Meta:
|
|
22
|
+
ordering = ('path_str',)
|
|
23
|
+
verbose_name = _('LocationModel.verbose_name')
|
|
24
|
+
verbose_name_plural = _('LocationModel.verbose_name_plural')
|