feature/#26_enhance_storage_stock_display #37

Merged
sst merged 18 commits from feature/#26_enhance_storage_stock_display into develop 2025-02-03 22:20:05 +01:00
17 changed files with 325 additions and 61 deletions

View File

@ -44,6 +44,7 @@ class ComponentSerializer(serializers.HyperlinkedModelSerializer):
ro_component_type = serializers.ReadOnlyField(source='component_type.class_name') ro_component_type = serializers.ReadOnlyField(source='component_type.class_name')
ro_parameters = ComponentParameterSerializer(many=True, source='componentparameter_set', read_only=True) ro_parameters = ComponentParameterSerializer(many=True, source='componentparameter_set', read_only=True)
ro_distributor_numbers = ComponentDistributorNumSerializer(many=True, source='distributornum_set', read_only=True) ro_distributor_numbers = ComponentDistributorNumSerializer(many=True, source='distributornum_set', read_only=True)
ro_dynamic_description = serializers.ReadOnlyField(source='dynamic_description')
class Meta: class Meta:
model = parts_models.Component model = parts_models.Component
@ -60,12 +61,14 @@ class ComponentSerializer(serializers.HyperlinkedModelSerializer):
'ro_image', 'ro_image',
'ro_component_type', 'ro_component_type',
'ro_parameters', 'ro_parameters',
'ro_distributor_numbers'] 'ro_distributor_numbers',
'ro_dynamic_description']
class StockSerializer(serializers.HyperlinkedModelSerializer): class StockSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.ReadOnlyField() id = serializers.ReadOnlyField()
ro_package_name = serializers.ReadOnlyField(source='component.package.name') ro_package_name = serializers.ReadOnlyField(source='component.package.name')
ro_component_name = serializers.ReadOnlyField(source='component.name') ro_component_name = serializers.ReadOnlyField(source='component.name')
ro_component_dynamic_description = serializers.ReadOnlyField(source='component.dynamic_description')
ro_manufacturer_name = serializers.ReadOnlyField(source='component.manufacturer.name') ro_manufacturer_name = serializers.ReadOnlyField(source='component.manufacturer.name')
ro_image = serializers.ReadOnlyField(source='component.get_resolved_image') ro_image = serializers.ReadOnlyField(source='component.get_resolved_image')
class Meta: class Meta:
@ -80,7 +83,7 @@ class StorageSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = parts_models.Storage model = parts_models.Storage
fields = ['url', 'id', 'name', 'verbose_name', 'parent_storage', 'responsible', 'template', 'full_path'] fields = ['url', 'id', 'name', 'verbose_name', 'parent_storage', 'responsible', 'template', 'full_path', 'full_path_verbose']
class StorageSerializerStocksExpanded(StorageSerializer): class StorageSerializerStocksExpanded(StorageSerializer):
ro_stocks = StockSerializerExpandComponent(many=True, read_only=True, source='stock_set') ro_stocks = StockSerializerExpandComponent(many=True, read_only=True, source='stock_set')

View File

@ -43,8 +43,8 @@ class GroupViewSet(viewsets.ReadOnlyModelViewSet):
class PartsStorageViewSet(viewsets.ModelViewSet): class PartsStorageViewSet(viewsets.ModelViewSet):
queryset = parts_models.Storage.objects.all() queryset = parts_models.Storage.objects.all()
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [django_filters.rest_framework.DjangoFilterBackend] filter_backends = [django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter]
search_fields = ['id', 'name', 'parent_storage'] search_fields = ['id', 'name', 'verbose_name']
filterset_fields = ['id', 'name', 'parent_storage'] filterset_fields = ['id', 'name', 'parent_storage']
def get_serializer_class(self): def get_serializer_class(self):

View File

@ -53,7 +53,7 @@ class AutoCompleteWidget(widgets.Input):
'name_field_name': self.name_field_name, 'name_field_name': self.name_field_name,
'prepend': self.prepend, 'prepend': self.prepend,
'name': display_name, 'name': display_name,
} }
return context return context
class AutocompleteForeingKeyField(forms.UUIDField): class AutocompleteForeingKeyField(forms.UUIDField):
@ -72,7 +72,7 @@ class AutocompleteForeingKeyField(forms.UUIDField):
prepend) prepend)
self.foreign_model = foreign_model self.foreign_model = foreign_model
def clean(self, value): def clean(self, value):
try: try:
pre_cleaned_uuid = super().clean(value) pre_cleaned_uuid = super().clean(value)
@ -101,7 +101,8 @@ class ChangeStorageForm(forms.Form):
foreign_model=get_user_model(), foreign_model=get_user_model(),
prepend='@') prepend='@')
is_template = forms.BooleanField(label='is_template', required=False) expand_sub_storage_stocks = forms.BooleanField(label='Expand sub storage Stocks', required=False)
is_template = forms.BooleanField(label='Is template', required=False)
class AddSubStorageForm(ChangeStorageForm): class AddSubStorageForm(ChangeStorageForm):
template = AutocompleteForeingKeyField(api_search_url='storage-template-list', template = AutocompleteForeingKeyField(api_search_url='storage-template-list',
@ -112,15 +113,13 @@ class AddSubStorageForm(ChangeStorageForm):
class DeleteStockForm(forms.Form): class DeleteStockForm(forms.Form):
stock_uuid = forms.UUIDField() stock_uuid = forms.UUIDField()
class EditWatermarkForm(forms.Form): class EditStockBaseForm(forms.Form):
stock_uuid = forms.UUIDField() stock_uuid = forms.UUIDField()
watermark_active = forms.BooleanField(required=False) #If it is false, the webbrowser won't send it at all. Therefore we have to set it to required=False
watermark = forms.IntegerField(min_value=0)
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
id = cleaned_data.get("stock_uuid") id = cleaned_data.get("stock_uuid")
if not id: if not id:
raise ValidationError("No stock UUID given") raise ValidationError("No stock UUID given")
@ -133,6 +132,10 @@ class EditWatermarkForm(forms.Form):
return cleaned_data return cleaned_data
class EditWatermarkForm(EditStockBaseForm):
watermark_active = forms.BooleanField(required=False) #If it is false, the webbrowser won't send it at all. Therefore we have to set it to required=False
watermark = forms.IntegerField(min_value=0)
def save(self): def save(self):
stock = self.cleaned_data['stock'] stock = self.cleaned_data['stock']
active = self.cleaned_data['watermark_active'] active = self.cleaned_data['watermark_active']
@ -144,32 +147,36 @@ class EditWatermarkForm(forms.Form):
stock.watermark = watermark stock.watermark = watermark
stock.save() stock.save()
class EditStockAmountForm(forms.Form): class EditLotForm(EditStockBaseForm):
stock_uuid = forms.UUIDField() lot = forms.IntegerField(min_value=0)
def save(self):
stock = self.cleaned_data['stock']
lot = self.cleaned_data['lot']
stock.lot = lot
stock.save()
class RelocateStockForm(forms.ModelForm):
storage = AutocompleteForeingKeyField(api_search_url='storage-list',
foreign_model=parts_models.Storage,
name_field_name='full_path_verbose',
image_field_name=None,
required=True)
class Meta:
model = parts_models.Stock
fields = ['storage']
class EditStockAmountForm(EditStockBaseForm):
amount = forms.IntegerField(min_value=0) amount = forms.IntegerField(min_value=0)
def clean(self):
cleaned_data = super().clean()
id = cleaned_data.get("stock_uuid")
if not id:
raise ValidationError("No stock UUID given")
stock = None
try:
stock = parts_models.Stock.objects.get(id=id)
except:
raise ValidationError("Stock with uuid %s does not exist" % (id))
cleaned_data['stock'] = stock
return cleaned_data
def save(self, increase: bool): def save(self, increase: bool):
stock = self.cleaned_data['stock'] stock = self.cleaned_data['stock']
amount = self.cleaned_data['amount'] amount = self.cleaned_data['amount']
if not increase: if not increase:
amount = -amount amount = -amount
return stock.atomic_increment(amount) return stock.atomic_increment(amount)
@ -194,7 +201,7 @@ class AddStockForm(forms.Form):
cleaned_data['component'] = component cleaned_data['component'] = component
return cleaned_data return cleaned_data
def save(self, storage): def save(self, storage):
component = self.cleaned_data.get('component') component = self.cleaned_data.get('component')
amount = self.cleaned_data.get('amount') amount = self.cleaned_data.get('amount')
@ -253,7 +260,7 @@ class DistributorNumberDeleteForm(forms.Form):
class ComponentParameterDeleteForm(forms.Form): class ComponentParameterDeleteForm(forms.Form):
param_num = forms.UUIDField(required=True) param_num = forms.UUIDField(required=True)
model = parts_models.ComponentParameter model = parts_models.ComponentParameter
def clean_param_num(self): def clean_param_num(self):
my_uuid = self.cleaned_data['param_num'] my_uuid = self.cleaned_data['param_num']
try: try:
@ -322,7 +329,7 @@ class ComponentParameterCreateForm(forms.Form):
if not ptype: if not ptype:
raise ValidationError('No valid parameter type selected') raise ValidationError('No valid parameter type selected')
if not value: if not value:
raise ValidationError('No valid parameter value') raise ValidationError('No valid parameter value')
@ -334,7 +341,7 @@ class ComponentParameterCreateForm(forms.Form):
data['number_value'] = number data['number_value'] = number
else: else:
pass pass
def save(self, component): def save(self, component):
param_type = self.cleaned_data['parameter_type'] param_type = self.cleaned_data['parameter_type']
if param_type.parameter_type == 'F': if param_type.parameter_type == 'F':
@ -363,6 +370,6 @@ class QrSearchForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator]) qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator])

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2025-01-25 14:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0013_packageparameter'),
]
operations = [
migrations.AddField(
model_name='storage',
name='expand_sub_storage_stocks',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2025-01-31 21:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0014_storage_expand_sub_storage_stocks'),
]
operations = [
migrations.AddField(
model_name='componenttype',
name='description_template',
field=models.TextField(blank=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2025-01-31 21:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0015_componenttype_description_template'),
]
operations = [
migrations.AddField(
model_name='componentparametertype',
name='interfix',
field=models.CharField(blank=True, max_length=10),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.1.3 on 2025-02-02 10:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0016_componentparametertype_interfix'),
]
operations = [
migrations.AlterField(
model_name='componentparametertype',
name='interfix',
field=models.CharField(blank=True, help_text='char to be used as decimal point in dynamic description eg. 2R2', max_length=10),
),
migrations.AlterField(
model_name='componenttype',
name='description_template',
field=models.TextField(blank=True, help_text="Template to assemble the dynamic description. Use template syntax, access the component with 'object', parameters with 'param_*'."),
),
]

View File

@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.dispatch import receiver from django.dispatch import receiver
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.template import engines
import os import os
import uuid import uuid
from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter as NumConv from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter as NumConv
@ -29,6 +30,7 @@ class ComponentParameterType(models.Model):
parameter_name = models.CharField(max_length=50, unique=True) parameter_name = models.CharField(max_length=50, unique=True)
parameter_description = models.TextField(null=False, blank=True) parameter_description = models.TextField(null=False, blank=True)
unit = models.CharField(max_length=10, null=False, blank=True) unit = models.CharField(max_length=10, null=False, blank=True)
interfix = models.CharField(max_length=10, null=False, blank=True, help_text="char to be used as decimal point in dynamic description eg. 2R2")
parameter_type = models.CharField(max_length=1, choices=TYPE_CHOICES, default='N') parameter_type = models.CharField(max_length=1, choices=TYPE_CHOICES, default='N')
def __str__(self): def __str__(self):
@ -49,6 +51,9 @@ class ComponentType(models.Model):
class_name = models.CharField(max_length=50, unique=True) class_name = models.CharField(max_length=50, unique=True)
passive = models.BooleanField() passive = models.BooleanField()
possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True) possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True)
description_template = models.TextField(blank=True,
help_text="Template to assemble the dynamic description. "
"Use template syntax, access the component with 'object', parameters with 'param_*'.")
def __str__(self): def __str__(self):
return '[' + self.class_name + ']' return '[' + self.class_name + ']'
@ -65,6 +70,7 @@ class Storage(models.Model):
verbose_name = models.CharField(max_length=100, null=True, blank=True) verbose_name = models.CharField(max_length=100, null=True, blank=True)
parent_storage = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True) parent_storage = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True)
responsible = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, blank=True, null=True) responsible = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, blank=True, null=True)
expand_sub_storage_stocks = models.BooleanField(default=False)
# allow storages to be templates which can be selected when adding new storages # allow storages to be templates which can be selected when adding new storages
is_template = models.BooleanField(default=False) is_template = models.BooleanField(default=False)
@ -74,25 +80,40 @@ class Storage(models.Model):
null=True, null=True,
related_name='template_of') related_name='template_of')
def get_path_components(self): # caching variable for subtrees
storage_list = None
def get_path_components(self, top_level=None):
chain = [] chain = []
iterator = self iterator = self
chain.append(self) chain.append(self)
while iterator.parent_storage is not None: while iterator.parent_storage is not None:
if top_level and iterator.parent_storage == top_level:
break
chain.append(iterator.parent_storage) chain.append(iterator.parent_storage)
iterator = iterator.parent_storage iterator = iterator.parent_storage
return chain return chain
def get_full_path(self): def get_full_path(self, top_level=None):
output = '' output = ''
chain = self.get_path_components() chain = self.get_path_components(top_level)
for i in range(len(chain) - 1, -1, -1): for i in range(len(chain) - 1, -1, -1):
output = output + '/' + chain[i].name output = output + '/' + chain[i].name
return output return output
@property
def full_path_verbose(self):
full_path = f'{self.get_full_path()} ({self.id})'
if self.verbose_name:
full_path += f' ({self.verbose_name})'
return full_path
def get_qr_code(self): def get_qr_code(self):
qrdata = '[stor_uuid]' + str(self.id) qrdata = '[stor_uuid]' + str(self.id)
return qrdata return qrdata
@ -106,6 +127,19 @@ class Storage(models.Model):
else: else:
return self.storage_set.all() return self.storage_set.all()
@classmethod
def get_substorage_list(cls, sub_storages):
sub_sub_storages = cls.objects.filter(parent_storage__in=sub_storages)
final_sub_storages = sub_storages | sub_sub_storages
if sub_sub_storages:
final_sub_storages |= cls.get_substorage_list(sub_sub_storages)
return final_sub_storages
def get_storage_list(self):
return Storage.objects.filter(id=self.id) | self.get_substorage_list(self.storage_set.all())
def validate_unique(self, exclude=None): def validate_unique(self, exclude=None):
if Storage.objects.exclude(id=self.id).filter(name=self.name, parent_storage__isnull=True).exists(): if Storage.objects.exclude(id=self.id).filter(name=self.name, parent_storage__isnull=True).exists():
if self.parent_storage is None: if self.parent_storage is None:
@ -116,12 +150,18 @@ class Storage(models.Model):
raise raise
def get_total_stock_amount(self): def get_total_stock_amount(self):
stocks = Stock.objects.filter(storage=self) stocks = Stock.objects.filter(storage__in=self.storage_list or self.get_storage_list())
sum = stocks.aggregate(Sum('amount'))['amount__sum'] sum = stocks.aggregate(Sum('amount'))['amount__sum']
if sum is None: if sum is None:
sum = 0 sum = 0
return sum return sum
def get_total_stock_count(self):
return Stock.objects.filter(storage__in=self.storage_list or self.get_storage_list()).count()
def get_total_substorage_amount(self):
return len(self.storage_list or self.get_storage_list()) - 1 # -1 as thhe storage list counts the parent storage as well
@classmethod @classmethod
def from_path(cls, path, root_storage=None): def from_path(cls, path, root_storage=None):
''' '''
@ -237,6 +277,22 @@ class Component(models.Model):
if sum is None: if sum is None:
sum = 0 sum = 0
return sum return sum
@property
def dynamic_description(self):
if not self.component_type or not self.component_type.description_template:
return ''
django_engine = engines["django"]
template = django_engine.from_string(self.component_type.description_template)
parameters = list(ComponentParameter.objects.filter(component=self))
parameters += list(PackageParameter.objects.filter(package=self.package))
context = {f'param_{param.parameter_type.parameter_name}': param for param in parameters}
context.update({'object': self})
return template.render(context)
class AbstractParameter(models.Model): class AbstractParameter(models.Model):
class Meta: class Meta:
@ -271,6 +327,25 @@ class AbstractParameter(models.Model):
elif my_type == 'F': elif my_type == 'F':
return self.text_value return self.text_value
def resolved_value_as_short_string(self):
my_type = self.parameter_type.parameter_type
if my_type == 'E' or my_type == 'I':
# Engineering float number
(num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False))
result = f'{round(num, 3):g}'
interpostfix = (prefix if prefix else self.parameter_type.interfix or '.')
if '.' in result:
result = result.replace('.', interpostfix)
else:
result = result + interpostfix
return result
elif my_type == 'N':
# Standard float number
return f'{round(self.value, 3):g}{self.parameter_type.unit}'
else:
return self.resolved_value_as_string()
class ComponentParameter(AbstractParameter): class ComponentParameter(AbstractParameter):
class Meta: class Meta:
unique_together = ('component', 'parameter_type') unique_together = ('component', 'parameter_type')
@ -308,7 +383,7 @@ class Stock(models.Model):
return True return True
def get_qr_code(self): def get_qr_code(self):
qr_data = '[stock]'+str(self.id) qr_data = '[stck_uuid]'+str(self.id)
return qr_data return qr_data
def __str__(self): def __str__(self):
@ -407,5 +482,6 @@ def auto_apply_template_structure(sender, instance, created, **kwargs):
Storage.objects.create(name=sub_storage.name, Storage.objects.create(name=sub_storage.name,
parent_storage=instance, parent_storage=instance,
responsible=instance.responsible, responsible=instance.responsible,
expand_sub_storage_stocks=instance.expand_sub_storage_stocks,
is_template=False, is_template=False,
template=sub_storage) template=sub_storage)

View File

@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.urls import reverse as url_reverse from django.urls import reverse as url_reverse
import re import re
from .models import Storage, Component from .models import Storage, Component, Stock
class QrCode: class QrCode:
prefix = '' prefix = ''
@ -19,6 +19,7 @@ class QrCodeValidator:
qr_patterns = { qr_patterns = {
'stor_uuid': QrCode('stor_uuid', 'parts-stocks-detail', Storage), 'stor_uuid': QrCode('stor_uuid', 'parts-stocks-detail', Storage),
'comp_uuid': QrCode('comp_uuid', 'parts-components-detail', Component), 'comp_uuid': QrCode('comp_uuid', 'parts-components-detail', Component),
'stck_uuid': QrCode('stck_uuid', 'parts-stock-detail', Stock),
} }
def __init__(self): def __init__(self):
@ -32,16 +33,13 @@ class QrCodeValidator:
qr_type = matches.group('prefix') qr_type = matches.group('prefix')
qr_uuid = matches.group('uuid') qr_uuid = matches.group('uuid')
url_name = self.qr_patterns[qr_type].detail_view
model = None
try: try:
url_name = self.qr_patterns[qr_type].detail_view
model = self.qr_patterns[qr_type].model model = self.qr_patterns[qr_type].model
except: except KeyError as ex:
model = None raise ValidationError('QR Pattern not registered') from ex
if model is None: return (model, qr_uuid, url_name)
raise ValidationError('QR Pattern not registered')
return (model,qr_uuid, url_name)
def get_redirect_url(self, data): def get_redirect_url(self, data):
model, uuid, url_name = self._get_model_from_qr(data) model, uuid, url_name = self._get_model_from_qr(data)

View File

@ -0,0 +1,9 @@
import datetime
from django import template
register = template.Library()
@register.filter(name="get_relative_storage_path")
def get_relative_storage_path(storage, top_level):
return f'.{storage.get_full_path(top_level)}'

View File

@ -74,7 +74,13 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
return crumbs return crumbs
def search_stock_queryset(self, search): def search_stock_queryset(self, search):
stocks_in_storage = Stock.objects.filter(storage=self.object).order_by(Lower('component__name'))
if self.object.expand_sub_storage_stocks:
stocks_in_storage = Stock.objects.filter(storage__in=self.object.get_storage_list())
else:
stocks_in_storage = Stock.objects.filter(storage=self.object)
stocks_in_storage = stocks_in_storage.order_by(Lower('component__name'))
if search is None or search == '': if search is None or search == '':
return stocks_in_storage return stocks_in_storage
@ -115,14 +121,16 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
context['storages'] = storage_paginator.get_page(storage_page) context['storages'] = storage_paginator.get_page(storage_page)
stocks = stock_paginator.get_page(componente_stock_page) stocks = stock_paginator.get_page(componente_stock_page)
context['stocks'] = stocks context['stocks'] = stocks
context['stocks_with_forms'] = [{'object': s, 'relocate_form': RelocateStockForm(instance=s, prefix=str(s.id))} for s in stocks]
context['stock_search'] = stock_search_input context['stock_search'] = stock_search_input
add_storage_form = AddSubStorageForm() add_storage_form = AddSubStorageForm()
add_storage_form.fields['responsible'].initial = self.request.user.id add_storage_form.fields['responsible'].initial = self.request.user.id
context['add_storage_form'] = add_storage_form context['add_storage_form'] = add_storage_form
change_storage_form = ChangeStorageForm() change_storage_form = ChangeStorageForm(prefix='change_storage')
change_storage_form.fields['storage_name'].initial = self.object.name change_storage_form.fields['storage_name'].initial = self.object.name
change_storage_form.fields['verbose_name'].initial = self.object.verbose_name change_storage_form.fields['verbose_name'].initial = self.object.verbose_name
change_storage_form.fields['responsible'].initial = self.object.responsible.id change_storage_form.fields['responsible'].initial = self.object.responsible.id
change_storage_form.fields['expand_sub_storage_stocks'].initial = self.object.expand_sub_storage_stocks
change_storage_form.fields['is_template'].initial = self.object.is_template change_storage_form.fields['is_template'].initial = self.object.is_template
context['change_storage_form'] = change_storage_form context['change_storage_form'] = change_storage_form
context['delete_storage_error'] = None context['delete_storage_error'] = None
@ -139,6 +147,7 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
verbose_name=f.cleaned_data.get('verbose_name'), verbose_name=f.cleaned_data.get('verbose_name'),
parent_storage=self.object, parent_storage=self.object,
responsible=f.cleaned_data['responsible'], responsible=f.cleaned_data['responsible'],
expand_sub_storage_stocks=f.cleaned_data['expand_sub_storage_stocks'],
is_template=f.cleaned_data['is_template'], is_template=f.cleaned_data['is_template'],
template=f.cleaned_data.get('template')) template=f.cleaned_data.get('template'))
except ValidationError as v_err: except ValidationError as v_err:
@ -148,12 +157,13 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
return self.render_to_response(context) return self.render_to_response(context)
def handle_change_storage_post(self, request, **kwargs): def handle_change_storage_post(self, request, **kwargs):
f = ChangeStorageForm(data=request.POST) f = ChangeStorageForm(data=request.POST, prefix='change_storage')
if f.is_valid(): if f.is_valid():
try: try:
self.object.name = f.cleaned_data['storage_name'] self.object.name = f.cleaned_data['storage_name']
self.object.verbose_name = f.cleaned_data.get('verbose_name') self.object.verbose_name = f.cleaned_data.get('verbose_name')
self.object.responsible = f.cleaned_data['responsible'] self.object.responsible = f.cleaned_data['responsible']
self.object.expand_sub_storage_stocks = f.cleaned_data['expand_sub_storage_stocks']
self.object.is_template = f.cleaned_data['is_template'] self.object.is_template = f.cleaned_data['is_template']
self.object.save() self.object.save()
except ValidationError as v_err: except ValidationError as v_err:
@ -205,6 +215,27 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
return self.render_to_response(context) return self.render_to_response(context)
def handle_update_lot(self, request, **kwargs):
edit_form = EditLotForm(data=request.POST)
if edit_form.is_valid():
edit_form.save()
else:
pass # Todo: Handle error
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_relocate_stock(self, request, **kwargs):
instance = Stock.objects.get(id=request.POST['prefix'])
edit_form = RelocateStockForm(instance=instance, data=request.POST, prefix=request.POST['prefix'])
if edit_form.is_valid():
edit_form.save()
else:
pass # Todo: Handle error
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def handle_amount_change_post(self, request, increase, **kwargs): def handle_amount_change_post(self, request, increase, **kwargs):
edit_form = EditStockAmountForm(data=request.POST) edit_form = EditStockAmountForm(data=request.POST)
if edit_form.is_valid(): if edit_form.is_valid():
@ -244,6 +275,10 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
return self.handle_del_stock_post(request, **kwargs) return self.handle_del_stock_post(request, **kwargs)
elif 'submit-edit-watermark' in request.POST: elif 'submit-edit-watermark' in request.POST:
return self.handle_update_watermark(request, **kwargs) return self.handle_update_watermark(request, **kwargs)
elif 'submit-edit-lot' in request.POST:
return self.handle_update_lot(request, **kwargs)
elif 'submit-relocate-stock' in request.POST:
return self.handle_relocate_stock(request, **kwargs)
elif 'submit-amount-reduce' in request.POST: elif 'submit-amount-reduce' in request.POST:
return self.handle_amount_change_post(request, False, **kwargs) return self.handle_amount_change_post(request, False, **kwargs)
elif 'submit-amount-increase' in request.POST: elif 'submit-amount-increase' in request.POST:

View File

@ -83,6 +83,13 @@
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]') const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl)) const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
</script> </script>
<!-- Initialize bootstrap tooltips -->
<script type="text/javascript">
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
</script>
<!-- Select search field on start of QR scan if no input is currently selevted([) --> <!-- Select search field on start of QR scan if no input is currently selevted([) -->
<script type="text/javascript"> <script type="text/javascript">
window.addEventListener("keydown", (event)=>{ window.addEventListener("keydown", (event)=>{

View File

@ -76,6 +76,11 @@
No description available No description available
</div> </div>
{% endif %} {% endif %}
{% if component.dynamic_description %}
<h2>Dynamic Description</h2>
<pre>{{ component.dynamic_description }}</pre>
{% endif %}
</div> </div>
<div class="col-4"> <div class="col-4">
{% if component.pref_distri %} {% if component.pref_distri %}

View File

@ -54,7 +54,12 @@
Manufacturer: {{comp.manufacturer}} Manufacturer: {{comp.manufacturer}}
{% endif %} {% endif %}
</div> </div>
<span class="badge bg-primary rounded-pill">{{comp.get_total_amount}}</span> <div class="flex-grow-1 d-block ms-3" style="text-align: right;">
<pre>{{ comp.dynamic_description }}</pre>
</div>
<div style="width: 10%; text-align: right;">
<span class="badge bg-primary rounded-pill me-2">{{comp.get_total_amount}}</span>
</div>
</li> </li>
</a> </a>
{% endfor %} {% endfor %}

View File

@ -39,7 +39,7 @@ needs following context:
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="stock_uuid" value="{{stock.id}}"> <input type="hidden" name="stock_uuid" value="{{stock.id}}">
<div class="input-group mb-3"> <div class="input-group mb-3">
<div class="input-group-text"> <div class="input-group-text">
<input type="checkbox" class="form-check-input mt-0" name="watermark_active" id="ch-stk-watermark-act-{{stock.id}}" {% if stock.watermark >= 0 %}checked{%endif%}> <input type="checkbox" class="form-check-input mt-0" name="watermark_active" id="ch-stk-watermark-act-{{stock.id}}" {% if stock.watermark >= 0 %}checked{%endif%}>
</div> </div>
@ -47,7 +47,22 @@ needs following context:
<input type="submit" class="btn btn-primary" name="submit-edit-watermark" value="Update Watermark"> <input type="submit" class="btn btn-primary" name="submit-edit-watermark" value="Update Watermark">
</div> </div>
</form> </form>
<form method="post">
{% csrf_token %}
<input type="hidden" name="stock_uuid" value="{{stock.id}}">
<div class="input-group mb-3">
<input type="text" name="lot" id="ch-stk-lot-{{stock.id}}" class="form-control" value="{{stock.lot}}" required>
<input type="submit" class="btn btn-primary" name="submit-edit-lot" value="Update Lot">
</div>
</form>
<form method="post">
{% csrf_token %}
<input type="hidden" name="prefix" value="{{relocate_form.prefix}}">
{{ relocate_form|crispy }}
<input type="submit" class="btn btn-warning" name="submit-relocate-stock" value="Relocate Stock">
</form>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@
{% load qr_code %} {% load qr_code %}
{% load static %} {% load static %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load storage_tags %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
@ -21,13 +22,28 @@
{% qr_from_text object.get_qr_code size="m" image_format="svg" %} {% qr_from_text object.get_qr_code size="m" image_format="svg" %}
</div> </div>
<div class="row"> <div class="row">
<h4>{{storage.name}}{% if storage.verbose_name %}<small> ({{storage.verbose_name}})</small>{% endif %}</h4> <h4>{{storage.name}}
{% if storage.verbose_name %}
<small>
({{storage.verbose_name}})
</small>
{% endif %}
{% if storage.is_template %}
<small>
(template)
</small>
{% endif %}
</h4>
</div> </div>
<div class="row"> <div class="row">
{% if object.parent_storage %} {% if object.parent_storage %}
<h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks-detail' uuid=object.parent_storage.id %}">Parent Storage</a> {% else %} <h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks-detail' uuid=object.parent_storage.id %}">Parent Storage</a>
{% else %}
<h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks'%}">Stock Overview</a> <h1>Sub-Storages <a class="btn btn-secondary" href="{% url 'parts-stocks'%}">Stock Overview</a>
{% endif %} {% endif %}
{% if storage.template %}
<a class="btn btn-secondary" href="{% url 'parts-stocks-detail' uuid=storage.template.id %}">Template</a>
{% endif %}
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#delete-storage-modal">Delete</button> <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#delete-storage-modal">Delete</button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-sub-modal"><i class="bi bi-plus-circle"></i></button> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-sub-modal"><i class="bi bi-plus-circle"></i></button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#change-modal"><i class="bi bi-pencil-square"></i></button> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#change-modal"><i class="bi bi-pencil-square"></i></button>
@ -36,11 +52,13 @@
{% for storage in storages %} {% for storage in storages %}
<a href="{% url 'parts-stocks-detail' uuid=storage.id %}" class="text-decoration-none"> <a href="{% url 'parts-stocks-detail' uuid=storage.id %}" class="text-decoration-none">
<li class="list-group-item list-group-item-action justify-content-between align-items-center d-flex"> <li class="list-group-item list-group-item-action justify-content-between align-items-center d-flex">
<div> <div>
<h5>{{storage.name}}{% if storage.verbose_name %}<small> ({{storage.verbose_name}})</small>{% endif %}</h5> <h5>{{storage.name}}{% if storage.verbose_name %}<small> ({{storage.verbose_name}})</small>{% endif %}</h5>
Responsible: {{ storage.responsible }} Responsible: {{ storage.responsible }}
</div> </div>
<span class="badge bg-primary rounded-pill">{{storage.get_total_stock_amount}}</span> <span class="badge ms-1 bg-primary rounded-pill" data-bs-toggle="tooltip" data-bs-placement="top" title="Total number of stored parts">{{storage.get_total_stock_amount}}</span>
<span class="badge ms-1 bg-secondary rounded-pill d-none d-lg-block" data-bs-toggle="tooltip" data-bs-placement="top" title="Number of stored lots">{{storage.get_total_stock_count}}</span>
<span class="badge ms-1 bg-info rounded-pill d-none d-lg-block" data-bs-toggle="tooltip" data-bs-placement="top" title="Number of substorages">{{storage.get_total_substorage_amount}}</span>
</li> </li>
</a> </a>
{% endfor %} {% endfor %}
@ -80,11 +98,20 @@
{% if stock.component.manufacturer %} {% if stock.component.manufacturer %}
Manufacturer: {{stock.component.manufacturer}} Manufacturer: {{stock.component.manufacturer}}
{% endif %} {% endif %}
{% if stock.storage != storage %}
<span class="text-secondary"><br>{{ stock.storage|get_relative_storage_path:storage }}</span>
{% endif %}
{% if stock.lot %} {% if stock.lot %}
<span class="text-secondary"><br>Lot: {{stock.lot}}</span> <span class="text-secondary"><br>Lot: {{stock.lot}}</span>
{% endif %} {% endif %}
</div> </div>
<div class="ms-3"> <div class="flex-grow-2 ms-3 d-none d-lg-block" style="text-align: right;">
<pre>{{ stock.component.dynamic_description }}</pre>
</div>
<div class="flex-grow-2 ms-5 d-none d-lg-block">
{% qr_from_text stock.get_qr_code size="6" image_format="svg" %}
</div>
<div class="ms-3" style="width: 20%;">
Amount: {{stock.amount}} Amount: {{stock.amount}}
{% if stock.watermark >= 0 %} {% if stock.watermark >= 0 %}
<br>Watermark: {{stock.watermark}} <br>Watermark: {{stock.watermark}}
@ -108,14 +135,14 @@
{% include 'paginator.html' with paginator=stocks get_param='stock_page' aria_label='Stock Page Navigation' %} {% include 'paginator.html' with paginator=stocks get_param='stock_page' aria_label='Stock Page Navigation' %}
</div> </div>
</div> </div>
{% for stock in stocks %} {% for stock in stocks_with_forms %}
{% include 'parts/modals/update-stock-modal.html' with stock=stock form=change_stock_form %} {% include 'parts/modals/update-stock-modal.html' with stock=stock.object form=change_stock_form relocate_form=stock.relocate_form %}
{% endfor %} {% endfor %}
<!-- Modal for adding a substorage--> <!-- Modal for adding a substorage-->
{% with add_storage_form as form %} {% with add_storage_form as form %}
{% include 'parts/modals/add-substorage-modal.html' %} {% include 'parts/modals/add-substorage-modal.html' %}
{% endwith %} {% endwith %}
<!-- Modal to change current storag--> <!-- Modal to change current storage-->
{% with change_storage_form as form %} {% with change_storage_form as form %}
{% include 'parts/modals/change-storage-modal.html' %} {% include 'parts/modals/change-storage-modal.html' %}
{% endwith %} {% endwith %}