Compare commits
23 Commits
7e36059605
...
feature/#3
Author | SHA1 | Date | |
---|---|---|---|
3ef9ea3a3b | |||
8a676c096a | |||
65f25e282e | |||
8b3ef1af25 | |||
bbcbf6ab3d | |||
1b48e8f283 | |||
6dc8f3bfef | |||
871086c7b7 | |||
adf152938d | |||
462ed0c101 | |||
032c4fc838 | |||
e0c39d9d6e | |||
6ca0ce483f | |||
35b99e1b18 | |||
6ae94e9ea4 | |||
aefcc472ea | |||
3ec11cf092 | |||
e4e7456a5d | |||
6eaef98c86 | |||
3aa4225acb | |||
39b64aeb71 | |||
f6a878460d | |||
171b6b83f4 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "sly"]
|
||||
path = sly
|
||||
url = git@git.shimatta.de:sst/sly.git
|
@@ -44,6 +44,7 @@ class ComponentSerializer(serializers.HyperlinkedModelSerializer):
|
||||
ro_component_type = serializers.ReadOnlyField(source='component_type.class_name')
|
||||
ro_parameters = ComponentParameterSerializer(many=True, source='componentparameter_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:
|
||||
model = parts_models.Component
|
||||
@@ -60,12 +61,14 @@ class ComponentSerializer(serializers.HyperlinkedModelSerializer):
|
||||
'ro_image',
|
||||
'ro_component_type',
|
||||
'ro_parameters',
|
||||
'ro_distributor_numbers']
|
||||
'ro_distributor_numbers',
|
||||
'ro_dynamic_description']
|
||||
|
||||
class StockSerializer(serializers.HyperlinkedModelSerializer):
|
||||
id = serializers.ReadOnlyField()
|
||||
ro_package_name = serializers.ReadOnlyField(source='component.package.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_image = serializers.ReadOnlyField(source='component.get_resolved_image')
|
||||
class Meta:
|
||||
@@ -80,7 +83,7 @@ class StorageSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
ro_stocks = StockSerializerExpandComponent(many=True, read_only=True, source='stock_set')
|
||||
|
@@ -43,8 +43,8 @@ class GroupViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class PartsStorageViewSet(viewsets.ModelViewSet):
|
||||
queryset = parts_models.Storage.objects.all()
|
||||
permission_classes = [permissions.DjangoModelPermissions]
|
||||
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
|
||||
search_fields = ['id', 'name', 'parent_storage']
|
||||
filter_backends = [django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter]
|
||||
search_fields = ['id', 'name', 'verbose_name']
|
||||
filterset_fields = ['id', 'name', 'parent_storage']
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
@@ -53,7 +53,7 @@ class AutoCompleteWidget(widgets.Input):
|
||||
'name_field_name': self.name_field_name,
|
||||
'prepend': self.prepend,
|
||||
'name': display_name,
|
||||
}
|
||||
}
|
||||
return context
|
||||
|
||||
class AutocompleteForeingKeyField(forms.UUIDField):
|
||||
@@ -72,7 +72,7 @@ class AutocompleteForeingKeyField(forms.UUIDField):
|
||||
prepend)
|
||||
self.foreign_model = foreign_model
|
||||
|
||||
|
||||
|
||||
def clean(self, value):
|
||||
try:
|
||||
pre_cleaned_uuid = super().clean(value)
|
||||
@@ -101,7 +101,8 @@ class ChangeStorageForm(forms.Form):
|
||||
foreign_model=get_user_model(),
|
||||
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):
|
||||
template = AutocompleteForeingKeyField(api_search_url='storage-template-list',
|
||||
@@ -112,15 +113,13 @@ class AddSubStorageForm(ChangeStorageForm):
|
||||
class DeleteStockForm(forms.Form):
|
||||
stock_uuid = forms.UUIDField()
|
||||
|
||||
class EditWatermarkForm(forms.Form):
|
||||
class EditStockBaseForm(forms.Form):
|
||||
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):
|
||||
cleaned_data = super().clean()
|
||||
id = cleaned_data.get("stock_uuid")
|
||||
|
||||
|
||||
if not id:
|
||||
raise ValidationError("No stock UUID given")
|
||||
|
||||
@@ -133,6 +132,10 @@ class EditWatermarkForm(forms.Form):
|
||||
|
||||
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):
|
||||
stock = self.cleaned_data['stock']
|
||||
active = self.cleaned_data['watermark_active']
|
||||
@@ -144,32 +147,36 @@ class EditWatermarkForm(forms.Form):
|
||||
stock.watermark = watermark
|
||||
stock.save()
|
||||
|
||||
class EditStockAmountForm(forms.Form):
|
||||
stock_uuid = forms.UUIDField()
|
||||
class EditLotForm(EditStockBaseForm):
|
||||
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)
|
||||
|
||||
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):
|
||||
stock = self.cleaned_data['stock']
|
||||
amount = self.cleaned_data['amount']
|
||||
|
||||
if not increase:
|
||||
amount = -amount
|
||||
amount = -amount
|
||||
|
||||
return stock.atomic_increment(amount)
|
||||
|
||||
@@ -194,7 +201,7 @@ class AddStockForm(forms.Form):
|
||||
cleaned_data['component'] = component
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
def save(self, storage):
|
||||
component = self.cleaned_data.get('component')
|
||||
amount = self.cleaned_data.get('amount')
|
||||
@@ -253,7 +260,7 @@ class DistributorNumberDeleteForm(forms.Form):
|
||||
class ComponentParameterDeleteForm(forms.Form):
|
||||
param_num = forms.UUIDField(required=True)
|
||||
model = parts_models.ComponentParameter
|
||||
|
||||
|
||||
def clean_param_num(self):
|
||||
my_uuid = self.cleaned_data['param_num']
|
||||
try:
|
||||
@@ -322,7 +329,7 @@ class ComponentParameterCreateForm(forms.Form):
|
||||
|
||||
if not ptype:
|
||||
raise ValidationError('No valid parameter type selected')
|
||||
|
||||
|
||||
if not value:
|
||||
raise ValidationError('No valid parameter value')
|
||||
|
||||
@@ -334,7 +341,7 @@ class ComponentParameterCreateForm(forms.Form):
|
||||
data['number_value'] = number
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def save(self, component):
|
||||
param_type = self.cleaned_data['parameter_type']
|
||||
if param_type.parameter_type == 'F':
|
||||
@@ -363,6 +370,6 @@ class QrSearchForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator])
|
@@ -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),
|
||||
),
|
||||
]
|
@@ -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),
|
||||
),
|
||||
]
|
@@ -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),
|
||||
),
|
||||
]
|
@@ -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_*'."),
|
||||
),
|
||||
]
|
@@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.dispatch import receiver
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.template import engines
|
||||
import os
|
||||
import uuid
|
||||
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_description = models.TextField(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')
|
||||
|
||||
def __str__(self):
|
||||
@@ -49,6 +51,9 @@ class ComponentType(models.Model):
|
||||
class_name = models.CharField(max_length=50, unique=True)
|
||||
passive = models.BooleanField()
|
||||
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):
|
||||
return '[' + self.class_name + ']'
|
||||
@@ -65,6 +70,7 @@ class Storage(models.Model):
|
||||
verbose_name = models.CharField(max_length=100, null=True, blank=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)
|
||||
expand_sub_storage_stocks = models.BooleanField(default=False)
|
||||
|
||||
# allow storages to be templates which can be selected when adding new storages
|
||||
is_template = models.BooleanField(default=False)
|
||||
@@ -74,25 +80,40 @@ class Storage(models.Model):
|
||||
null=True,
|
||||
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 = []
|
||||
iterator = self
|
||||
chain.append(self)
|
||||
while iterator.parent_storage is not None:
|
||||
|
||||
if top_level and iterator.parent_storage == top_level:
|
||||
break
|
||||
|
||||
chain.append(iterator.parent_storage)
|
||||
iterator = iterator.parent_storage
|
||||
|
||||
return chain
|
||||
|
||||
def get_full_path(self):
|
||||
def get_full_path(self, top_level=None):
|
||||
output = ''
|
||||
|
||||
chain = self.get_path_components()
|
||||
chain = self.get_path_components(top_level)
|
||||
|
||||
for i in range(len(chain) - 1, -1, -1):
|
||||
output = output + '/' + chain[i].name
|
||||
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):
|
||||
qrdata = '[stor_uuid]' + str(self.id)
|
||||
return qrdata
|
||||
@@ -106,6 +127,19 @@ class Storage(models.Model):
|
||||
else:
|
||||
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):
|
||||
if Storage.objects.exclude(id=self.id).filter(name=self.name, parent_storage__isnull=True).exists():
|
||||
if self.parent_storage is None:
|
||||
@@ -116,12 +150,18 @@ class Storage(models.Model):
|
||||
raise
|
||||
|
||||
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']
|
||||
if sum is None:
|
||||
sum = 0
|
||||
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
|
||||
def from_path(cls, path, root_storage=None):
|
||||
'''
|
||||
@@ -237,6 +277,22 @@ class Component(models.Model):
|
||||
if sum is None:
|
||||
sum = 0
|
||||
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 Meta:
|
||||
@@ -271,6 +327,25 @@ class AbstractParameter(models.Model):
|
||||
elif my_type == 'F':
|
||||
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 Meta:
|
||||
unique_together = ('component', 'parameter_type')
|
||||
@@ -308,7 +383,7 @@ class Stock(models.Model):
|
||||
return True
|
||||
|
||||
def get_qr_code(self):
|
||||
qr_data = '[stock]'+str(self.id)
|
||||
qr_data = '[stck_uuid]'+str(self.id)
|
||||
return qr_data
|
||||
|
||||
def __str__(self):
|
||||
@@ -407,5 +482,6 @@ def auto_apply_template_structure(sender, instance, created, **kwargs):
|
||||
Storage.objects.create(name=sub_storage.name,
|
||||
parent_storage=instance,
|
||||
responsible=instance.responsible,
|
||||
expand_sub_storage_stocks=instance.expand_sub_storage_stocks,
|
||||
is_template=False,
|
||||
template=sub_storage)
|
||||
|
@@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.urls import reverse as url_reverse
|
||||
import re
|
||||
|
||||
from .models import Storage, Component
|
||||
from .models import Storage, Component, Stock
|
||||
|
||||
class QrCode:
|
||||
prefix = ''
|
||||
@@ -19,6 +19,7 @@ class QrCodeValidator:
|
||||
qr_patterns = {
|
||||
'stor_uuid': QrCode('stor_uuid', 'parts-stocks-detail', Storage),
|
||||
'comp_uuid': QrCode('comp_uuid', 'parts-components-detail', Component),
|
||||
'stck_uuid': QrCode('stck_uuid', 'parts-stock-detail', Stock),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
@@ -32,16 +33,13 @@ class QrCodeValidator:
|
||||
|
||||
qr_type = matches.group('prefix')
|
||||
qr_uuid = matches.group('uuid')
|
||||
url_name = self.qr_patterns[qr_type].detail_view
|
||||
|
||||
model = None
|
||||
try:
|
||||
url_name = self.qr_patterns[qr_type].detail_view
|
||||
model = self.qr_patterns[qr_type].model
|
||||
except:
|
||||
model = None
|
||||
if model is None:
|
||||
raise ValidationError('QR Pattern not registered')
|
||||
return (model,qr_uuid, url_name)
|
||||
except KeyError as ex:
|
||||
raise ValidationError('QR Pattern not registered') from ex
|
||||
return (model, qr_uuid, url_name)
|
||||
|
||||
def get_redirect_url(self, data):
|
||||
model, uuid, url_name = self._get_model_from_qr(data)
|
||||
|
0
shimatta_kenkyusho/parts/templatetags/__init__.py
Normal file
0
shimatta_kenkyusho/parts/templatetags/__init__.py
Normal file
9
shimatta_kenkyusho/parts/templatetags/storage_tags.py
Normal file
9
shimatta_kenkyusho/parts/templatetags/storage_tags.py
Normal 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)}'
|
@@ -1,4 +1,3 @@
|
||||
import uuid
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.views.generic import TemplateView, DetailView
|
||||
@@ -8,6 +7,7 @@ from django.db.models import Q
|
||||
from django.forms import formset_factory
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import ProtectedError
|
||||
from shimatta_modules.ShimattaSearchLanguage import ShimattaSearchLanguage
|
||||
from ..models import Stock, Component, ComponentParameter, DistributorNum, PackageParameter
|
||||
from ..forms import *
|
||||
from .component_import import import_components_from_csv
|
||||
@@ -26,15 +26,23 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
||||
|
||||
def get_component_query_set(self, search_string):
|
||||
queryset = Component.objects.all()
|
||||
error_string = ''
|
||||
|
||||
if not search_string:
|
||||
return queryset
|
||||
return queryset, error_string
|
||||
|
||||
search_parser = ShimattaSearchLanguage()
|
||||
query, errors = search_parser.search_for_components(search_string)
|
||||
|
||||
search_fragments = search_string.strip().split()
|
||||
for search in search_fragments:
|
||||
queryset = queryset.filter(Q(name__icontains = search) | Q(manufacturer__name__icontains = search) | Q(package__name__icontains = search))
|
||||
if query and not errors:
|
||||
try:
|
||||
queryset = queryset.filter(query)
|
||||
except Exception as ex:
|
||||
error_string = str(ex)
|
||||
else:
|
||||
error_string = '<br><br>'.join(errors)
|
||||
|
||||
return queryset
|
||||
return queryset, error_string
|
||||
|
||||
def get_component_queryset_from_advanced_search(self, cleaned_data):
|
||||
queryset = Component.objects.all()
|
||||
@@ -60,6 +68,8 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
||||
def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
errors = ''
|
||||
|
||||
comp_page_num = self.request.GET.get('comp_page', default=1)
|
||||
|
||||
if advanced_search and parameter_formset:
|
||||
@@ -79,7 +89,7 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
||||
|
||||
else:
|
||||
search = self.request.GET.get('search', default=None)
|
||||
paginator_queryset = self.get_component_query_set(search)
|
||||
paginator_queryset, errors = self.get_component_query_set(search)
|
||||
|
||||
comp_paginator = Paginator(paginator_queryset, self.default_page_size)
|
||||
|
||||
@@ -87,11 +97,11 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
||||
context['comp_form'] = ComponentForm()
|
||||
context['import_comp_form'] = ImportComponentForm()
|
||||
context['search_string'] = search
|
||||
context['errors'] = errors
|
||||
|
||||
if not parameter_formset:
|
||||
context['advanced_search_param_formset'] = ParameterSearchFormSet()
|
||||
|
||||
|
||||
if not advanced_search:
|
||||
context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s')
|
||||
|
||||
|
@@ -74,7 +74,13 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
||||
return crumbs
|
||||
|
||||
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 == '':
|
||||
return stocks_in_storage
|
||||
@@ -115,14 +121,16 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
||||
context['storages'] = storage_paginator.get_page(storage_page)
|
||||
stocks = stock_paginator.get_page(componente_stock_page)
|
||||
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
|
||||
add_storage_form = AddSubStorageForm()
|
||||
add_storage_form.fields['responsible'].initial = self.request.user.id
|
||||
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['verbose_name'].initial = self.object.verbose_name
|
||||
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
|
||||
context['change_storage_form'] = change_storage_form
|
||||
context['delete_storage_error'] = None
|
||||
@@ -139,6 +147,7 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
||||
verbose_name=f.cleaned_data.get('verbose_name'),
|
||||
parent_storage=self.object,
|
||||
responsible=f.cleaned_data['responsible'],
|
||||
expand_sub_storage_stocks=f.cleaned_data['expand_sub_storage_stocks'],
|
||||
is_template=f.cleaned_data['is_template'],
|
||||
template=f.cleaned_data.get('template'))
|
||||
except ValidationError as v_err:
|
||||
@@ -148,12 +157,13 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
||||
return self.render_to_response(context)
|
||||
|
||||
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():
|
||||
try:
|
||||
self.object.name = f.cleaned_data['storage_name']
|
||||
self.object.verbose_name = f.cleaned_data.get('verbose_name')
|
||||
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.save()
|
||||
except ValidationError as v_err:
|
||||
@@ -205,6 +215,27 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
||||
context = self.get_context_data(**kwargs)
|
||||
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):
|
||||
edit_form = EditStockAmountForm(data=request.POST)
|
||||
if edit_form.is_valid():
|
||||
@@ -244,6 +275,10 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
||||
return self.handle_del_stock_post(request, **kwargs)
|
||||
elif 'submit-edit-watermark' in request.POST:
|
||||
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:
|
||||
return self.handle_amount_change_post(request, False, **kwargs)
|
||||
elif 'submit-amount-increase' in request.POST:
|
||||
|
184
shimatta_kenkyusho/shimatta_modules/ShimattaSearchLanguage.py
Normal file
184
shimatta_kenkyusho/shimatta_modules/ShimattaSearchLanguage.py
Normal file
@@ -0,0 +1,184 @@
|
||||
import re
|
||||
from django.db.models import Q
|
||||
from sly import Lexer, Parser
|
||||
from shimatta_modules.EngineeringNumberConverter import EngineeringNumberConverter
|
||||
|
||||
class ShimattaSearchConstants():
|
||||
'''
|
||||
Just a bunch of conversions and regular expression stored here.
|
||||
'''
|
||||
|
||||
# convert the prefixes from the engineering number converter be used in the lexer
|
||||
PREFIX_DICT = {k: v for k, v in EngineeringNumberConverter.prefixes if k}
|
||||
PREFIX_DICT.update({k: v for k, v in EngineeringNumberConverter.it_prefixes if k})
|
||||
PREFIX_RULE = r'(' + r'|'.join([rf'(?:{p})' for p in PREFIX_DICT.keys()]) + r')?'
|
||||
|
||||
TEXTUAL_REGEX = r'(?:([^"><=!&\|\(\)\s]+))|(?:"([^"]*)")'
|
||||
VALUE_REGEX = rf'(\-?\d+(?:\.\d+)?){PREFIX_RULE}'
|
||||
|
||||
TEXTUAL_PATTERN = re.compile(TEXTUAL_REGEX)
|
||||
VALUE_PATTERN = re.compile(VALUE_REGEX)
|
||||
|
||||
class ShimattaSearchLexer(Lexer):
|
||||
'''
|
||||
Stupid lexer to tokenize a search string.
|
||||
'''
|
||||
|
||||
tokens = {GT, LT, GTE, LTE, EQ, NEQ, AND, OR, LPAREN, RPAREN, NUMBER, TEXTUAL}
|
||||
|
||||
# ignore whitespace only characters as well as newlines
|
||||
ignore = ' \t\n'
|
||||
|
||||
# Regular expression rules for simple tokens
|
||||
GTE = r'>='
|
||||
LTE = r'<='
|
||||
GT = r'>'
|
||||
LT = r'<'
|
||||
EQ = r'=='
|
||||
NEQ = r'!='
|
||||
AND = r'(?:&{1,2})|(?:and)|(?:AND)'
|
||||
OR = r'(?:\|{1,2})|(?:or)|(?:OR)'
|
||||
LPAREN = r'\('
|
||||
RPAREN = r'\)'
|
||||
|
||||
def __init__(self):
|
||||
self.errors = []
|
||||
super().__init__()
|
||||
|
||||
@_(ShimattaSearchConstants.VALUE_REGEX)
|
||||
def NUMBER(self, t):
|
||||
'''
|
||||
Parse numbers with engineering unit prefixes
|
||||
'''
|
||||
match = ShimattaSearchConstants.VALUE_PATTERN.match(t.value)
|
||||
t.value = float(match.group(1))
|
||||
prefix = match.group(2)
|
||||
if prefix:
|
||||
t.value *= ShimattaSearchConstants.PREFIX_DICT[prefix]
|
||||
return t
|
||||
|
||||
@_(ShimattaSearchConstants.TEXTUAL_REGEX)
|
||||
def TEXTUAL(self, t):
|
||||
'''
|
||||
Find texts with or without param_ prefix (used to filter for parameters)
|
||||
'''
|
||||
match = ShimattaSearchConstants.TEXTUAL_PATTERN.match(t.value)
|
||||
|
||||
# strip the quotation marks
|
||||
value = match.group(1)
|
||||
if match.group(2):
|
||||
value = match.group(2)
|
||||
|
||||
t.value = value
|
||||
return t
|
||||
|
||||
def error(self, t):
|
||||
self.errors.append(f'Line {self.lineno}: Bad character {t.value}')
|
||||
self.index += 1
|
||||
|
||||
class ShimattaSearchParser(Parser):
|
||||
# Get the token list from the lexer (required)
|
||||
tokens = ShimattaSearchLexer.tokens
|
||||
|
||||
def __init__(self):
|
||||
self.errors = []
|
||||
super().__init__()
|
||||
|
||||
@staticmethod
|
||||
def _get_filter(key, value, compare_suffix='', invert=False):
|
||||
'''
|
||||
Assemble a filter to grep data from the relational database structure
|
||||
'''
|
||||
|
||||
# filter for params - stored in two separate tables
|
||||
if key.startswith('param_'):
|
||||
key = key[len('param_'):]
|
||||
|
||||
key_query = Q(Q(**{f'componentparameter__parameter_type__parameter_name': key})| \
|
||||
Q(**{f'package__packageparameter__parameter_type__parameter_name': key}))
|
||||
|
||||
if isinstance(value, str):
|
||||
query = Q(Q(**{f'componentparameter__text_value{compare_suffix}': value})| \
|
||||
Q(**{f'package__packageparameter__text_value{compare_suffix}': value}))&key_query
|
||||
else:
|
||||
query = Q(Q(**{f'componentparameter__value{compare_suffix}': value})| \
|
||||
Q(**{f'package__packageparameter__value{compare_suffix}': value}))&key_query
|
||||
# filter for direct attributes - or whatever the user throws into the search input
|
||||
else:
|
||||
query = Q(**{f'{key}{compare_suffix}': value})
|
||||
|
||||
if invert:
|
||||
query = ~query
|
||||
return Q(query)
|
||||
|
||||
# ruleset
|
||||
|
||||
@_('expression : textual GT number')
|
||||
def expression(self, p):
|
||||
return self._get_filter(p.textual.strip(), p.number, '__gt', False)
|
||||
|
||||
@_('expression : textual LT number')
|
||||
def expression(self, p):
|
||||
return self._get_filter(p.textual.strip(), p.number, '__lt', False)
|
||||
|
||||
@_('expression : textual GTE number')
|
||||
def expression(self, p):
|
||||
return self._get_filter(p.textual.strip(), p.number, '__gte', False)
|
||||
|
||||
@_('expression : textual LTE number')
|
||||
def expression(self, p):
|
||||
return self._get_filter(p.textual.strip(), p.number, '__lte', False)
|
||||
|
||||
@_('expression : textual EQ number')
|
||||
def expression(self, p):
|
||||
return self._get_filter(p.textual.strip(), p.number, '', False)
|
||||
|
||||
@_('expression : textual NEQ number')
|
||||
def expression(self, p):
|
||||
return self._get_filter(p.textual.strip(), p.number, '', True)
|
||||
|
||||
@_('expression : textual EQ textual')
|
||||
def expression(self, p):
|
||||
return self._get_filter(p.textual0.strip(), p.textual1, '', False)
|
||||
|
||||
@_('expression : textual NEQ textual')
|
||||
def expression(self, p):
|
||||
return self._get_filter(p.textual0.strip(), p.textual1, '', True)
|
||||
|
||||
@_('TEXTUAL')
|
||||
def textual(self, p):
|
||||
return p.TEXTUAL
|
||||
|
||||
@_('NUMBER')
|
||||
def number(self, p):
|
||||
return p.NUMBER
|
||||
|
||||
@_('expression : LPAREN expression RPAREN')
|
||||
def expression(self, p):
|
||||
return Q(p.expression)
|
||||
|
||||
@_('expression : expression AND expression')
|
||||
def expression(self, p):
|
||||
return p.expression0&p.expression1
|
||||
|
||||
@_('expression : expression OR expression')
|
||||
def expression(self, p):
|
||||
return p.expression0|p.expression1
|
||||
|
||||
@_('expression')
|
||||
def expression(self, p):
|
||||
return p.expression
|
||||
|
||||
# Error rule for syntax errors
|
||||
def error(self, p):
|
||||
self.errors.append(f'Syntax error in input {p}!')
|
||||
|
||||
class ShimattaSearchLanguage():
|
||||
|
||||
def __init__(self):
|
||||
self.lexer = ShimattaSearchLexer()
|
||||
self.parser = ShimattaSearchParser()
|
||||
|
||||
def search_for_components(self, search_string):
|
||||
query = self.parser.parse(self.lexer.tokenize(search_string))
|
||||
return query, self.lexer.errors + self.parser.errors
|
@@ -83,6 +83,13 @@
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
|
||||
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
|
||||
</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([) -->
|
||||
<script type="text/javascript">
|
||||
window.addEventListener("keydown", (event)=>{
|
||||
|
@@ -76,6 +76,11 @@
|
||||
No description available
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if component.dynamic_description %}
|
||||
<h2>Dynamic Description</h2>
|
||||
<pre>{{ component.dynamic_description }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{% if component.pref_distri %}
|
||||
|
@@ -6,16 +6,26 @@
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h2>Components</h2>
|
||||
<form action="" method="get">
|
||||
<div class="input-group mb-3">
|
||||
<input class="form-control" name="search" type="search" placeholder="Search Component..." {% if search_string %}value="{{search_string}}"{% endif %}>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" href="#advanced-search-collapse">Advanced <i class="bi bi-search"></i></button>
|
||||
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-edit-modal"><i class="bi bi-plus-circle"></i> Add Component</button>
|
||||
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-import-modal"><i class="bi bi-plus-circle"></i> Import CSV</button>
|
||||
<form class="needs-validation" action="" method="get">
|
||||
{% if errors %}
|
||||
<div class="card mb-3">
|
||||
{% endif %}
|
||||
<div class="input-group mb-3">
|
||||
<input class="form-control" name="search" type="search" placeholder="Search Component..." {% if search_string %}value="{{search_string}}"{% endif %}>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" href="#advanced-search-collapse">Advanced <i class="bi bi-search"></i></button>
|
||||
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-edit-modal"><i class="bi bi-plus-circle"></i> Add Component</button>
|
||||
<button class="btn btn-success" type="button" data-bs-toggle="modal" data-bs-target="#comp-import-modal"><i class="bi bi-plus-circle"></i> Import CSV</button>
|
||||
</div>
|
||||
{% if errors %}
|
||||
<div class="card-body text-danger">
|
||||
<h5>Error in search query:</h5>
|
||||
{{ errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="collapse mb-3{% if advanced_search_shown %} show{% endif %}" id="advanced-search-collapse" aria-expanded="{% if advanced_search_shown %}true{% else %}false{% endif %}">
|
||||
<form method="POST">
|
||||
@@ -54,7 +64,12 @@
|
||||
Manufacturer: {{comp.manufacturer}}
|
||||
{% endif %}
|
||||
</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>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
@@ -39,7 +39,7 @@ needs following context:
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<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">
|
||||
<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>
|
||||
@@ -47,7 +47,22 @@ needs following context:
|
||||
<input type="submit" class="btn btn-primary" name="submit-edit-watermark" value="Update Watermark">
|
||||
</div>
|
||||
</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>
|
@@ -2,6 +2,7 @@
|
||||
{% load qr_code %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load storage_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
@@ -21,13 +22,28 @@
|
||||
{% qr_from_text object.get_qr_code size="m" image_format="svg" %}
|
||||
</div>
|
||||
<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 class="row">
|
||||
{% 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>
|
||||
{% 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-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>
|
||||
@@ -36,11 +52,13 @@
|
||||
{% for storage in storages %}
|
||||
<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">
|
||||
<div>
|
||||
<div>
|
||||
<h5>{{storage.name}}{% if storage.verbose_name %}<small> ({{storage.verbose_name}})</small>{% endif %}</h5>
|
||||
Responsible: {{ storage.responsible }}
|
||||
</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>
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -80,11 +98,20 @@
|
||||
{% if stock.component.manufacturer %}
|
||||
Manufacturer: {{stock.component.manufacturer}}
|
||||
{% endif %}
|
||||
{% if stock.storage != storage %}
|
||||
<span class="text-secondary"><br>{{ stock.storage|get_relative_storage_path:storage }}</span>
|
||||
{% endif %}
|
||||
{% if stock.lot %}
|
||||
<span class="text-secondary"><br>Lot: {{stock.lot}}</span>
|
||||
{% endif %}
|
||||
</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}}
|
||||
{% if stock.watermark >= 0 %}
|
||||
<br>Watermark: {{stock.watermark}}
|
||||
@@ -108,14 +135,14 @@
|
||||
{% include 'paginator.html' with paginator=stocks get_param='stock_page' aria_label='Stock Page Navigation' %}
|
||||
</div>
|
||||
</div>
|
||||
{% for stock in stocks %}
|
||||
{% include 'parts/modals/update-stock-modal.html' with stock=stock form=change_stock_form %}
|
||||
{% for stock in stocks_with_forms %}
|
||||
{% include 'parts/modals/update-stock-modal.html' with stock=stock.object form=change_stock_form relocate_form=stock.relocate_form %}
|
||||
{% endfor %}
|
||||
<!-- Modal for adding a substorage-->
|
||||
{% with add_storage_form as form %}
|
||||
{% include 'parts/modals/add-substorage-modal.html' %}
|
||||
{% endwith %}
|
||||
<!-- Modal to change current storag-->
|
||||
<!-- Modal to change current storage-->
|
||||
{% with change_storage_form as form %}
|
||||
{% include 'parts/modals/change-storage-modal.html' %}
|
||||
{% endwith %}
|
||||
|
1
sly
Submodule
1
sly
Submodule
Submodule sly added at 539a85a5d5
Reference in New Issue
Block a user