Compare commits
63 Commits
a54cd33880
...
develop
Author | SHA1 | Date | |
---|---|---|---|
8b3ef1af25 | |||
1b48e8f283 | |||
6dc8f3bfef | |||
871086c7b7 | |||
adf152938d | |||
462ed0c101 | |||
032c4fc838 | |||
e0c39d9d6e | |||
6ca0ce483f | |||
35b99e1b18 | |||
6ae94e9ea4 | |||
aefcc472ea | |||
7e36059605 | |||
ed1508f0ed | |||
2d78b4dcdd | |||
19852dd5ad | |||
3ec11cf092 | |||
e4e7456a5d | |||
6eaef98c86 | |||
3aa4225acb | |||
39b64aeb71 | |||
f6a878460d | |||
171b6b83f4 | |||
550e996ae7 | |||
122ae4a731 | |||
0b26a81b94 | |||
befd5e452f | |||
cfb9970c26 | |||
146c2da4f3 | |||
e74a28b0a8 | |||
46a94c9688 | |||
21ac4a8c06 | |||
f2a8166874 | |||
15b4257c73 | |||
c19f4a8159 | |||
26b001d983 | |||
207b5c3fb5 | |||
3450f7475a | |||
2fdcfe8baf | |||
c1b9c966dd | |||
f202896c92 | |||
50cfe0a2c6 | |||
2d718c5e3a | |||
57b475cbe1 | |||
25b592ee39 | |||
08a5f97fd4 | |||
511dacf54a | |||
0c4f1f9dba | |||
08bae61fc0 | |||
4ff71d2b21 | |||
b47c7ad38d | |||
63b8a66ebb | |||
b873b1fd0f | |||
6e51085210 | |||
5163834de4 | |||
39e40d62f4 | |||
841be4f1bb | |||
0d4019832f | |||
fed63fd45f | |||
d10c48f2b9 | |||
7c1465428f | |||
9afa7d709b | |||
f2ea45cd19 |
@@ -1 +1,2 @@
|
|||||||
start_server.sh
|
start_server.sh
|
||||||
|
run/*
|
||||||
|
192
README.md
Normal file
192
README.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# Shimatta Kenkyusho Parts Database
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
Shimatta Kenkyusho (しまった・研究所) is a Django based web application. It is highly recommended to run it using the supplied docker setup. This removes the need of any special installation on the host system. This guide assumes, that `nginx` is running on the host system and can serve as a reverse proxy and webserver. For easiest download, it is recommended to clone the desired release with `git`.
|
||||||
|
|
||||||
|
Install the requirements:
|
||||||
|
|
||||||
|
**For Debian / Ubuntu:**
|
||||||
|
```
|
||||||
|
# apt-get update
|
||||||
|
# apt-get install docker docker-compose-plugin nginx git
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Arch based Systems:**
|
||||||
|
```
|
||||||
|
# pacman -S nginx docker docker-compose git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup Shimatta Kenkyusho
|
||||||
|
|
||||||
|
Clone this repository:
|
||||||
|
```
|
||||||
|
$ git clone https://git.shimatta.de/mhu/shimatta-kenkyusho.git
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: Shimatta Kenkyusho is currently not stable yet and the newest verison is in the `develop` branch. This will change once actual releases are done and merged to the `master` branch. You will be able to get the latest stable version from the `master` branch or a respective tag. For now, the `develop` is recommended.
|
||||||
|
|
||||||
|
|
||||||
|
Change directory into the `shimatta-kenkyusho` folder cloned by git.
|
||||||
|
|
||||||
|
Copy the `example.env` file to `.env` and edit it according to your needs:
|
||||||
|
|
||||||
|
The following settings are required to be adapted:
|
||||||
|
- `DJANGO_STATIC_VOL`: The directory the application will extract its static data into, which needs to be served by your webserver. See the example reverse proxy setup for more details.
|
||||||
|
- `DJANGO_MEDIA_VOL`: The directory all media files like images uploaded to the application are stored here. This folder must be served by your webserver on the configured media URL.
|
||||||
|
- `PGDATA_VOL`: The directory, the postgres database will store its files.
|
||||||
|
- `PORT`: The TCP/IP port that the whole setup will listen on. Use a reverse proxy to forward to this port. *Do not directly expose it to the internet!*
|
||||||
|
- `DJANGO_SECRET_KEY`: Provide a secret, and randomly generated key. Do not share this with anybody!
|
||||||
|
- `DJANGO_ALLOWED_HOST`: Set this to the domain, the application will be reached at. E.g: `lab.example.com`
|
||||||
|
- `DJANGO_MEDIA_URL`: Set this to the media URL at which your webserver serves the `DJANGO_MEDIA_VOL` diretory. E.g: `media.lab.example.com/` Note the **slash at the end**. It is important.
|
||||||
|
- `DJANGO_USER_ID`: The user ID to run the application inside the docker container. This is the user id, that is used to write the to `DJANGO_STATIC_VOL` and `DJANGO_MEDIA_VOL`. Make sure the user has access.
|
||||||
|
- `DJANGO_USER_GID`: The group ID to run the application inside the docker container. This is the group id, that is used to write the to `DJANGO_STATIC_VOL` and `DJANGO_MEDIA_VOL`.
|
||||||
|
|
||||||
|
> Note: It is not recommended to run the docker container without a set `DJANGO_USER_ID` and `DJANGO_USER_GID`. It will default to `0 (root)`.
|
||||||
|
|
||||||
|
Once the environment is set up, the docker containers can be built and started. Run
|
||||||
|
```
|
||||||
|
$ docker compose build
|
||||||
|
```
|
||||||
|
This will generate two container images:
|
||||||
|
1. `shimatta-kenkyusho-shimatta-kenkyusho-web`: The django application
|
||||||
|
2. `postgres`: A alpine based docker container containing the postgres database.
|
||||||
|
|
||||||
|
Start the application as a service with
|
||||||
|
```
|
||||||
|
$ docker compose up -d
|
||||||
|
```
|
||||||
|
> Note: The initial startup might need a minute because the whole database etc. needs to be initialized first.
|
||||||
|
|
||||||
|
Use
|
||||||
|
```
|
||||||
|
$ docker ps
|
||||||
|
```
|
||||||
|
to check if the `shimatta-kenkyusho-shimatta-kenkyusho-db` and the `shimatta-kenkyusho-shimatta-kenkyusho-web` container are running and report a *healthy status*.
|
||||||
|
|
||||||
|
### Setup Initial Login User
|
||||||
|
When started for the first time with a fresh database without any superuser configured, a superuser `admin` with password `admin` will be automatically generated.
|
||||||
|
Use this user to login for the first time. In the django admin panel you can then either change the password of the `admin` user or create a new superuser with your own username and delete the `admin` user.
|
||||||
|
As long as there is at least one superuser configured, no admin user will be regenerated upon startup.
|
||||||
|
|
||||||
|
|
||||||
|
### Example Reverse Proxy Setup Using nginx
|
||||||
|
Once the setup is configured the reverse proxy setup is needed. This setup serves three purposes:
|
||||||
|
1. Redirect incoming requests to the django application running on the port `PORT` configured in the `.env`
|
||||||
|
2. Serve static files at the URL: (e.g. `lab.example.com/static`). See `ALLOWED_HOST` configuration.
|
||||||
|
3. Serve the media volume at the media URL (e.g. `media.lab.example.com`). See `DJANGO_MEDIA_URL`
|
||||||
|
|
||||||
|
Example nginx configuration for `nginx >v2.25` with SSL and http2 / http3 support:
|
||||||
|
> Note: This is by no means a replacement for the documentation of nginx and only serves as an example. Consult the documentation of your nginx version reagrding security and other issues.
|
||||||
|
```
|
||||||
|
# Force redirection from http to https for application
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name lab.example.com; # This must match your ALLOWED_HOST. Adapt domain.
|
||||||
|
allow all;
|
||||||
|
|
||||||
|
return 301 https://lab.example.com$request_uri; # Adapt domain
|
||||||
|
}
|
||||||
|
|
||||||
|
# Force redirection from http to https for media url
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name media.lab.example.com; # Adapt domain name according to DJANGO_MEDIA_URL
|
||||||
|
allow all;
|
||||||
|
return 301 https://media.lab.example.com$request_uri; # Adapt domain name
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse Proxy for application
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
|
||||||
|
# Add this for HTTP3. If your nginx is older than 2.25 this might not be available
|
||||||
|
######################################################################################
|
||||||
|
# listen 443 quic reuseport;
|
||||||
|
# listen [::]:443 quic reuseport;
|
||||||
|
# Enable QUIC and HTTP/3
|
||||||
|
# ssl_early_data on;
|
||||||
|
# add_header Alt-Svc 'h3=":443"; ma=86400';
|
||||||
|
#######################################################################################
|
||||||
|
|
||||||
|
server_name lab.example.com; # Adapt domain
|
||||||
|
|
||||||
|
# Use letsencrypt as SSL certificate provider.
|
||||||
|
ssl_certificate /etc/letsencrypt/live/lab.example.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/lab.example.com/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
ssl_session_cache shared:SSL:1m;
|
||||||
|
ssl_session_timeout 5m;
|
||||||
|
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
allow all;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_pass http://127.0.0.1:8000; # Adapt PORT from .env
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
# Adapt path to static volume here. Note the slash at the end
|
||||||
|
alias /path/to/DJANGO_STATIC_VOL/;
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_max_body_size 60m;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serve the media files
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
# Add this for HTTP3. If your nginx is older than 2.25 this might not be available
|
||||||
|
######################################################################################
|
||||||
|
# listen 443 quic reuseport;
|
||||||
|
# listen [::]:443 quic reuseport;
|
||||||
|
# Enable QUIC and HTTP/3
|
||||||
|
# ssl_early_data on;
|
||||||
|
# add_header Alt-Svc 'h3=":443"; ma=86400';
|
||||||
|
#######################################################################################
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
server_name media.lab.example.com; # Adapt according to DJANGO_MEDIA_URL
|
||||||
|
|
||||||
|
# Use letsencrypt as SSL certificate provider.
|
||||||
|
ssl_certificate /etc/letsencrypt/live/media.lab.example.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/media.lab.example.com/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
ssl_session_cache shared:SSL:1m;
|
||||||
|
ssl_session_timeout 5m;
|
||||||
|
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
error_page 502 /lab_down.html;
|
||||||
|
|
||||||
|
allow all;
|
||||||
|
root /path/to /DJANGO_MEDIA_VOL/; # Adapt this to the volume provided.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Congratulations. Your shimatta kenkyusho installation is now fully setup.
|
||||||
|
> Note that, the `compose.yaml` contains a restart-policy. By default the containers will restart automatically, even after a reboot of the host machine, if the docker service is enabled.
|
||||||
|
|
||||||
|
|
||||||
|
## Backup and Restore
|
||||||
|
> TODO
|
||||||
|
|
||||||
|
## Debugging and Development
|
||||||
|
> Todo
|
||||||
|
|
@@ -1,6 +1,11 @@
|
|||||||
|
x-op-restart-policy: &restart_policy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
services:
|
services:
|
||||||
shimatta-kenkyusho-web:
|
shimatta-kenkyusho-web:
|
||||||
|
<<: *restart_policy
|
||||||
build: .
|
build: .
|
||||||
|
user: "${DJANGO_USER_ID:-0}:${DJANGO_USER_GID:-0}"
|
||||||
volumes:
|
volumes:
|
||||||
- "${DJANGO_STATIC_VOL:-./run/static}:/var/static"
|
- "${DJANGO_STATIC_VOL:-./run/static}:/var/static"
|
||||||
- "${DJANGO_MEDIA_VOL:-./run/media}:/var/media"
|
- "${DJANGO_MEDIA_VOL:-./run/media}:/var/media"
|
||||||
@@ -30,6 +35,7 @@ services:
|
|||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
shimatta-kenkyusho-db:
|
shimatta-kenkyusho-db:
|
||||||
|
<<: *restart_policy
|
||||||
image: postgres:16.5-alpine
|
image: postgres:16.5-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: "${DJANGO_POSTGRESQL_PW:-p4ssw0rd}"
|
POSTGRES_PASSWORD: "${DJANGO_POSTGRESQL_PW:-p4ssw0rd}"
|
||||||
|
@@ -3,4 +3,6 @@ source /home/shimatta/kenkyusho/.venv/bin/activate
|
|||||||
cd /home/shimatta/kenkyusho/shimatta_kenkyusho
|
cd /home/shimatta/kenkyusho/shimatta_kenkyusho
|
||||||
python manage.py migrate --settings shimatta_kenkyusho.settings_production
|
python manage.py migrate --settings shimatta_kenkyusho.settings_production
|
||||||
python manage.py collectstatic --settings shimatta_kenkyusho.settings_production --noinput
|
python manage.py collectstatic --settings shimatta_kenkyusho.settings_production --noinput
|
||||||
|
python manage.py create_kenkyusho_admin_user --settings shimatta_kenkyusho.settings_production
|
||||||
|
|
||||||
gunicorn -w 4 --bind 0.0.0.0:8000 shimatta_kenkyusho.wsgi:application
|
gunicorn -w 4 --bind 0.0.0.0:8000 shimatta_kenkyusho.wsgi:application
|
||||||
|
@@ -2,5 +2,6 @@
|
|||||||
source /home/shimatta/kenkyusho/.venv/bin/activate
|
source /home/shimatta/kenkyusho/.venv/bin/activate
|
||||||
cd /home/shimatta/kenkyusho/shimatta_kenkyusho
|
cd /home/shimatta/kenkyusho/shimatta_kenkyusho
|
||||||
python manage.py migrate --settings shimatta_kenkyusho.settings_production
|
python manage.py migrate --settings shimatta_kenkyusho.settings_production
|
||||||
|
python manage.py create_kenkyusho_admin_user --settings shimatta_kenkyusho.settings_production
|
||||||
|
|
||||||
python manage.py runserver 0.0.0.0:8000 --settings shimatta_kenkyusho.settings_production
|
python manage.py runserver 0.0.0.0:8000 --settings shimatta_kenkyusho.settings_production
|
||||||
|
@@ -2,6 +2,12 @@
|
|||||||
# Example configuration. Must be edited and copied to ".env" next to the compose.yaml
|
# Example configuration. Must be edited and copied to ".env" next to the compose.yaml
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
|
||||||
|
# User id to use for the web application. This determines the user id, the media and static files are written to the volumes.
|
||||||
|
# Make sure the user has rw access to these directories.
|
||||||
|
DJANGO_USER_ID=1000
|
||||||
|
|
||||||
|
# Group id to use for the web application
|
||||||
|
DJANGO_USER_GID=1000
|
||||||
|
|
||||||
# Path to to mount as the directory for static data. Must be served by a webserver on the /static path
|
# Path to to mount as the directory for static data. Must be served by a webserver on the /static path
|
||||||
DJANGO_STATIC_VOL=/path/to/static/root
|
DJANGO_STATIC_VOL=/path/to/static/root
|
@@ -1,12 +1,13 @@
|
|||||||
annotated-types==0.7.0
|
annotated-types==0.7.0
|
||||||
asgiref==3.8.1
|
asgiref==3.8.1
|
||||||
astroid==2.6.5
|
astroid==3.3.5
|
||||||
certifi==2024.8.30
|
certifi==2024.8.30
|
||||||
charset-normalizer==3.4.0
|
charset-normalizer==3.4.0
|
||||||
crispy-bootstrap5==2024.10
|
crispy-bootstrap5==2024.10
|
||||||
|
dill==0.3.9
|
||||||
Django==5.1.3
|
Django==5.1.3
|
||||||
django-crispy-forms==2.3
|
django-crispy-forms==2.3
|
||||||
django-filter==2.4.0
|
django-filter==24.3
|
||||||
django-qr-code==4.1.0
|
django-qr-code==4.1.0
|
||||||
django-rest-framework==0.1.0
|
django-rest-framework==0.1.0
|
||||||
django-tex==1.1.10
|
django-tex==1.1.10
|
||||||
@@ -21,15 +22,18 @@ mccabe==0.6.1
|
|||||||
packaging==24.2
|
packaging==24.2
|
||||||
Pillow==8.3.1
|
Pillow==8.3.1
|
||||||
pipdeptree==2.23.4
|
pipdeptree==2.23.4
|
||||||
|
platformdirs==4.3.6
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
pydantic==2.9.2
|
pydantic==2.9.2
|
||||||
pydantic_core==2.23.4
|
pydantic_core==2.23.4
|
||||||
pylint==2.9.6
|
pylint==3.3.1
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
segno==1.6.1
|
segno==1.6.1
|
||||||
setuptools==75.3.0
|
setuptools==75.3.0
|
||||||
sqlparse==0.4.1
|
sqlparse==0.4.1
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
|
tomlkit==0.13.2
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.12.2
|
||||||
|
tzdata==2024.2
|
||||||
urllib3==2.2.3
|
urllib3==2.2.3
|
||||||
wrapt==1.12.1
|
wrapt==1.12.1
|
||||||
|
@@ -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')
|
||||||
|
@@ -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):
|
||||||
|
@@ -9,6 +9,7 @@ admin.site.register(parts_models.Manufacturer)
|
|||||||
admin.site.register(parts_models.Storage)
|
admin.site.register(parts_models.Storage)
|
||||||
admin.site.register(parts_models.Stock)
|
admin.site.register(parts_models.Stock)
|
||||||
admin.site.register(parts_models.ComponentParameter)
|
admin.site.register(parts_models.ComponentParameter)
|
||||||
|
admin.site.register(parts_models.PackageParameter)
|
||||||
admin.site.register(parts_models.ComponentParameterType)
|
admin.site.register(parts_models.ComponentParameterType)
|
||||||
admin.site.register(parts_models.ComponentType)
|
admin.site.register(parts_models.ComponentType)
|
||||||
admin.site.register(parts_models.Distributor)
|
admin.site.register(parts_models.Distributor)
|
||||||
|
@@ -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,10 +113,8 @@ 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()
|
||||||
@@ -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,26 +147,30 @@ 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']
|
||||||
@@ -252,15 +259,19 @@ 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
|
||||||
|
|
||||||
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:
|
||||||
param = parts_models.ComponentParameter.objects.get(id=my_uuid)
|
param = self.model.objects.get(id=my_uuid)
|
||||||
except:
|
except:
|
||||||
raise ValidationError('Parameter Number Invalid')
|
raise ValidationError('Parameter Number Invalid')
|
||||||
return param
|
return param
|
||||||
|
|
||||||
|
class PackageParameterDeleteForm(ComponentParameterDeleteForm):
|
||||||
|
model = parts_models.PackageParameter
|
||||||
|
|
||||||
class AdvancedComponentSearchForm(forms.Form):
|
class AdvancedComponentSearchForm(forms.Form):
|
||||||
name = forms.CharField(max_length=255, label='Component Name', required=False)
|
name = forms.CharField(max_length=255, label='Component Name', required=False)
|
||||||
package = AutocompleteForeingKeyField(required=False, api_search_url='package-list', foreign_model=parts_models.Package)
|
package = AutocompleteForeingKeyField(required=False, api_search_url='package-list', foreign_model=parts_models.Package)
|
||||||
@@ -309,6 +320,7 @@ class ComponentParameterSearchForm(forms.Form):
|
|||||||
class ComponentParameterCreateForm(forms.Form):
|
class ComponentParameterCreateForm(forms.Form):
|
||||||
parameter_type = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='descriptive_name')
|
parameter_type = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='descriptive_name')
|
||||||
value = forms.CharField(required=True, max_length=256)
|
value = forms.CharField(required=True, max_length=256)
|
||||||
|
model = parts_models.ComponentParameter
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
@@ -338,7 +350,20 @@ class ComponentParameterCreateForm(forms.Form):
|
|||||||
else:
|
else:
|
||||||
text_value = ''
|
text_value = ''
|
||||||
value = self.cleaned_data['number_value']
|
value = self.cleaned_data['number_value']
|
||||||
parts_models.ComponentParameter.objects.create(parameter_type=param_type, component=component, value=value, text_value=text_value)
|
self.model.objects.create(parameter_type=param_type, component=component, value=value, text_value=text_value)
|
||||||
|
|
||||||
|
class PackageParameterCreateForm(ComponentParameterCreateForm):
|
||||||
|
model = parts_models.PackageParameter
|
||||||
|
|
||||||
|
def save(self, package):
|
||||||
|
param_type = self.cleaned_data['parameter_type']
|
||||||
|
if param_type.parameter_type == 'F':
|
||||||
|
text_value = self.cleaned_data['value']
|
||||||
|
value = 0
|
||||||
|
else:
|
||||||
|
text_value = ''
|
||||||
|
value = self.cleaned_data['number_value']
|
||||||
|
self.model.objects.create(parameter_type=param_type, package=package, value=value, text_value=text_value)
|
||||||
|
|
||||||
class QrSearchForm(forms.Form):
|
class QrSearchForm(forms.Form):
|
||||||
my_qr_validator = QrCodeValidator()
|
my_qr_validator = QrCodeValidator()
|
||||||
|
@@ -0,0 +1,43 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandParser
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from parts.models import Component, ComponentParameter, ComponentParameterType, PackageParameter, Package
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Remove component parameters, that are also set on the package with the same value"
|
||||||
|
|
||||||
|
def add_arguments(self, parser: CommandParser):
|
||||||
|
parser.add_argument('--dry-run',
|
||||||
|
help='Do not perform parameter deletion. Print only',
|
||||||
|
action='store_true')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# Get all components with set packages. Ignore the ones without packages
|
||||||
|
all_comps = Component.objects.exclude(package__isnull=True)
|
||||||
|
|
||||||
|
for component in all_comps:
|
||||||
|
package_parameters = PackageParameter.objects.filter(package=component.package)
|
||||||
|
component_parameters = ComponentParameter.objects.filter(component=component)
|
||||||
|
package_param_ids = package_parameters.values_list('parameter_type_id', flat=True)
|
||||||
|
component_param_ids = component_parameters.values_list('parameter_type_id', flat=True)
|
||||||
|
|
||||||
|
# Skip trivial cases
|
||||||
|
if len(package_param_ids) == 0 or len(component_param_ids) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.stdout.write(f'Comp: {str(component)} Found {len(component_param_ids)} different parameters')
|
||||||
|
self.stdout.write(f'\tPackage: {str(component.package)} Found {len(package_param_ids)} different parameters')
|
||||||
|
|
||||||
|
commontypes = ctypes = ComponentParameterType.objects.filter(id__in=component_param_ids).filter(id__in=package_param_ids)
|
||||||
|
self.stdout.write(f'\tCommon parameter count: {len(commontypes)}')
|
||||||
|
|
||||||
|
# Check if values are the same when rendered as a string. This avoids float comparison problems
|
||||||
|
for common_type in commontypes:
|
||||||
|
s1 = package_parameters.filter(parameter_type=common_type).first().resolved_value_as_string()
|
||||||
|
comp_param = component_parameters.filter(parameter_type=common_type).first()
|
||||||
|
s2 = comp_param.resolved_value_as_string()
|
||||||
|
if s1 == s2:
|
||||||
|
self.stdout.write(f'\tParameter {common_type.parameter_name} is the same value for component and package: {s1}. Removing from component')
|
||||||
|
if not options['dry_run']:
|
||||||
|
comp_param.delete()
|
||||||
|
else:
|
||||||
|
self.stderr.write(f'\tParameter {common_type.parameter_name} is set on component {str(component)} and its package with different values: "{s1}" vs "{s2}"')
|
@@ -0,0 +1,23 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandParser
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Create a default superuser if no superuser is already present. This aids automatic deployment inside a container."
|
||||||
|
|
||||||
|
def add_arguments(self, parser: CommandParser):
|
||||||
|
parser.add_argument('--user',
|
||||||
|
help='Username to create if no admin account is present',
|
||||||
|
default='admin')
|
||||||
|
parser.add_argument('--password',
|
||||||
|
help='Password to set for newly created user. Ignored, if any admin user is already present',
|
||||||
|
default='admin')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
# Query if there is any admin user
|
||||||
|
if not User.objects.filter(is_superuser=True).exists():
|
||||||
|
self.stdout.write(f'No superuser present. Creating {options['user']} with supplied password')
|
||||||
|
User.objects.create_superuser(username=options['user'], password=options['password'])
|
||||||
|
else:
|
||||||
|
self.stdout.write('At least one superuser already exists. Skipping superuser creation')
|
49
shimatta_kenkyusho/parts/migrations/0013_packageparameter.py
Normal file
49
shimatta_kenkyusho/parts/migrations/0013_packageparameter.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2024-11-19 20:26
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("parts", "0012_storage_verbose_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PackageParameter",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("value", models.FloatField(default=0)),
|
||||||
|
("text_value", models.TextField(blank=True)),
|
||||||
|
(
|
||||||
|
"package",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="parts.package"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"parameter_type",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="parts.componentparametertype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["id"],
|
||||||
|
"unique_together": {("package", "parameter_type")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@@ -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.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):
|
||||||
'''
|
'''
|
||||||
@@ -238,24 +278,41 @@ class Component(models.Model):
|
|||||||
sum = 0
|
sum = 0
|
||||||
return sum
|
return sum
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dynamic_description(self):
|
||||||
|
|
||||||
class ComponentParameter(models.Model):
|
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:
|
class Meta:
|
||||||
unique_together = ('component', 'parameter_type')
|
abstract = True
|
||||||
ordering = ['id']
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
|
||||||
component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component is required!
|
|
||||||
parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE)
|
parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE)
|
||||||
value = models.FloatField(default=0)
|
value = models.FloatField(default=0)
|
||||||
text_value = models.TextField(null=False, blank=True)
|
text_value = models.TextField(null=False, blank=True)
|
||||||
|
|
||||||
|
def _get_object_of_param(self):
|
||||||
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.parameter_type.parameter_type == 'F':
|
if self.parameter_type.parameter_type == 'F':
|
||||||
value = self.text_value
|
value = self.text_value
|
||||||
else:
|
else:
|
||||||
value = str(self.value)
|
value = str(self.value)
|
||||||
|
|
||||||
return str(self.component)+ ': '+ str(self.parameter_type) + ': ' + value
|
return str(self._get_object_of_param())+ ': '+ str(self.parameter_type) + ': ' + value
|
||||||
|
|
||||||
def resolved_value_as_string(self):
|
def resolved_value_as_string(self):
|
||||||
my_type = self.parameter_type.parameter_type
|
my_type = self.parameter_type.parameter_type
|
||||||
@@ -270,6 +327,43 @@ class ComponentParameter(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 Meta:
|
||||||
|
unique_together = ('component', 'parameter_type')
|
||||||
|
ordering = ['id']
|
||||||
|
component = models.ForeignKey(Component, on_delete=models.CASCADE) # A target component is required!
|
||||||
|
|
||||||
|
def _get_object_of_param(self):
|
||||||
|
return self.component
|
||||||
|
|
||||||
|
class PackageParameter(AbstractParameter):
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('package', 'parameter_type')
|
||||||
|
ordering = ['id']
|
||||||
|
package = models.ForeignKey(Package, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def _get_object_of_param(self):
|
||||||
|
return self.package
|
||||||
|
|
||||||
class Stock(models.Model):
|
class Stock(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('component', 'storage')
|
unique_together = ('component', 'storage')
|
||||||
@@ -289,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):
|
||||||
@@ -376,11 +470,18 @@ def auto_apply_template_structure(sender, instance, created, **kwargs):
|
|||||||
If there are nested sub-storages these will be added when the sub-storages
|
If there are nested sub-storages these will be added when the sub-storages
|
||||||
are created automatically.
|
are created automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Skip recursion if the model is saved 'raw' e.g. when imported
|
||||||
|
if 'raw' in kwargs:
|
||||||
|
if kwargs['raw']:
|
||||||
|
return
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
if instance.template:
|
if instance.template:
|
||||||
for sub_storage in instance.template.storage_set.all():
|
for sub_storage in instance.template.storage_set.all():
|
||||||
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)
|
||||||
|
@@ -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)
|
||||||
|
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)}'
|
@@ -28,7 +28,7 @@ def _set_additional_parameters(component, type, value):
|
|||||||
|
|
||||||
param = ComponentParameter.objects.create(component=component,
|
param = ComponentParameter.objects.create(component=component,
|
||||||
parameter_type=param_type)
|
parameter_type=param_type)
|
||||||
if param_type == 'F':
|
if param_type.parameter_type == 'F':
|
||||||
param.text_value = value
|
param.text_value = value
|
||||||
else:
|
else:
|
||||||
param.value = float(value)
|
param.value = float(value)
|
||||||
|
@@ -8,7 +8,7 @@ from django.db.models import Q
|
|||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import ProtectedError
|
from django.db.models import ProtectedError
|
||||||
from ..models import Stock, Component, ComponentParameter, DistributorNum
|
from ..models import Stock, Component, ComponentParameter, DistributorNum, PackageParameter
|
||||||
from ..forms import *
|
from ..forms import *
|
||||||
from .component_import import import_components_from_csv
|
from .component_import import import_components_from_csv
|
||||||
from .generic_views import BaseTemplateMixin
|
from .generic_views import BaseTemplateMixin
|
||||||
@@ -174,6 +174,8 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
|||||||
'distributor__name')
|
'distributor__name')
|
||||||
context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by(
|
context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by(
|
||||||
'parameter_type__parameter_name')
|
'parameter_type__parameter_name')
|
||||||
|
context['package_parameters'] = PackageParameter.objects.filter(package=self.object.package).order_by(
|
||||||
|
'parameter_type__parameter_name')
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ from django.core.paginator import Paginator
|
|||||||
from django.db.models import ProtectedError
|
from django.db.models import ProtectedError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from ..forms import *
|
from ..forms import *
|
||||||
from ..models import Package
|
from ..models import Package, PackageParameter
|
||||||
from .generic_views import BaseTemplateMixin
|
from .generic_views import BaseTemplateMixin
|
||||||
|
|
||||||
class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView):
|
||||||
@@ -82,6 +82,9 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['package'] = self.object
|
context['package'] = self.object
|
||||||
context['edit_form'] = PackageForm(instance=self.object)
|
context['edit_form'] = PackageForm(instance=self.object)
|
||||||
|
context['new_param_form'] = PackageParameterCreateForm()
|
||||||
|
context['parameters'] = PackageParameter.objects.filter(package=self.object).order_by(
|
||||||
|
'parameter_type__parameter_name')
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -117,6 +120,27 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
|||||||
context['edit_form'] = edit_form
|
context['edit_form'] = edit_form
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
def handle_submit_delete_param_post(self, request, **kwargs):
|
||||||
|
form = PackageParameterDeleteForm(data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.cleaned_data['param_num'].delete()
|
||||||
|
|
||||||
|
context = self.get_context_data(**kwargs)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
def handle_submit_new_param_post(self, request, **kwargs):
|
||||||
|
form = PackageParameterCreateForm(data=request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
try:
|
||||||
|
form.save(self.object)
|
||||||
|
except IntegrityError:
|
||||||
|
form.add_error('__all__', 'This parameter is already set')
|
||||||
|
context = self.get_context_data(**kwargs)
|
||||||
|
if not form.is_valid():
|
||||||
|
context['new_param_form'] = form
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
|
||||||
@@ -124,5 +148,9 @@ class PackageDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
|
|||||||
return self.handle_delete_package(request)
|
return self.handle_delete_package(request)
|
||||||
elif 'submit-pkg-edit' in request.POST:
|
elif 'submit-pkg-edit' in request.POST:
|
||||||
return self.edit_package(request)
|
return self.edit_package(request)
|
||||||
|
elif 'submit-delete-param' in request.POST:
|
||||||
|
return self.handle_submit_delete_param_post(request, **kwargs)
|
||||||
|
elif 'submit-create-new-param' in request.POST:
|
||||||
|
return self.handle_submit_new_param_post(request, **kwargs)
|
||||||
|
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
@@ -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:
|
||||||
|
@@ -56,7 +56,6 @@ if get_env_value('DJANGO_FORCE_DEV_MODE', default=False) == 'True':
|
|||||||
|
|
||||||
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', get_env_value('DJANGO_ALLOWED_HOST')]
|
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', get_env_value('DJANGO_ALLOWED_HOST')]
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
@@ -239,4 +238,7 @@ CSRF_COOKIE_SECURE = True
|
|||||||
|
|
||||||
SECURE_SSL_REDIRECT = False
|
SECURE_SSL_REDIRECT = False
|
||||||
|
|
||||||
|
# allow detection of https behind "old" nginx
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
||||||
SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120)
|
SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120)
|
||||||
|
@@ -27,6 +27,17 @@ function initialize_autocompletion_foreign_key_field(search_element) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// select first match if any on enter
|
||||||
|
search_element.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
first_search_result = search_element.parentElement.querySelector('li')
|
||||||
|
if (first_search_result) {
|
||||||
|
first_search_result.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
new AutocompleteCustomUi(
|
new AutocompleteCustomUi(
|
||||||
base_id, base_id+'-ac-ul', function(search_query, autocomplete_obj) {
|
base_id, base_id+'-ac-ul', function(search_query, autocomplete_obj) {
|
||||||
api_ajax_request_without_send('GET', search_url+`?search=${encodeURIComponent(search_query)}`, function(method, url, json) {
|
api_ajax_request_without_send('GET', search_url+`?search=${encodeURIComponent(search_query)}`, function(method, url, json) {
|
||||||
|
@@ -38,7 +38,9 @@ class AutocompleteCustomUi {
|
|||||||
this.query_callback = query_function.bind(this);
|
this.query_callback = query_function.bind(this);
|
||||||
|
|
||||||
document.getElementById(text_id).addEventListener("keyup", this.ac_delay(function(event) {
|
document.getElementById(text_id).addEventListener("keyup", this.ac_delay(function(event) {
|
||||||
|
if (event.key != 'Enter') {
|
||||||
this.query_callback(document.getElementById(this.text_id).value, this);
|
this.query_callback(document.getElementById(this.text_id).value, this);
|
||||||
|
}
|
||||||
}, autocomplete_query_delay_ms).bind(this));
|
}, autocomplete_query_delay_ms).bind(this));
|
||||||
|
|
||||||
this.dropdown_data = {};
|
this.dropdown_data = {};
|
||||||
|
@@ -75,7 +75,7 @@
|
|||||||
'component-parameter-type-list': '{% url 'componentparametertype-list' %}',
|
'component-parameter-type-list': '{% url 'componentparametertype-list' %}',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" src="{% static 'js/kenyusho-api-v1.js' %}"></script>
|
<script type="text/javascript" src="{% static 'js/kenkyusho-api-v1.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'js/autocomplete.js' %}"></script>
|
<script type="text/javascript" src="{% static 'js/autocomplete.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'js/autocomplete-foreign-key-field.js' %}"></script>
|
<script type="text/javascript" src="{% static 'js/autocomplete-foreign-key-field.js' %}"></script>
|
||||||
<!-- Initialize bootstrap popovers -->
|
<!-- Initialize bootstrap popovers -->
|
||||||
@@ -83,6 +83,21 @@
|
|||||||
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([) -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener("keydown", (event)=>{
|
||||||
|
if (document.activeElement.nodeName != 'INPUT' && event.key == '[') {
|
||||||
|
document.getElementById("qr_search_field").focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
{% block custom_scripts %}
|
{% block custom_scripts %}
|
||||||
{% endblock custom_scripts %}
|
{% endblock custom_scripts %}
|
||||||
|
|
||||||
|
@@ -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 %}
|
||||||
@@ -119,6 +124,19 @@
|
|||||||
<th scope="col"></th>
|
<th scope="col"></th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{% for param in package_parameters %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h6 {% if param.parameter_type.parameter_description %} class="accordion-header" data-bs-toggle="collapse" data-bs-target="#collapse-pkg-parameter-desc-{{forloop.counter}}"{% endif %}>
|
||||||
|
{{param.parameter_type.parameter_name}}
|
||||||
|
</h6>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{param.resolved_value_as_string}}
|
||||||
|
</td>
|
||||||
|
<td><span class="text-info">from Package</span></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
{% for param in parameters %}
|
{% for param in parameters %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -148,6 +166,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% for param in package_parameters %}
|
||||||
|
{% if param.parameter_type.parameter_description %}
|
||||||
|
<div class="collapse accordion-collapse" id="collapse-pkg-parameter-desc-{{forloop.counter}}" data-bs-parent="#accordion-param-desc">
|
||||||
|
{{param.parameter_type.parameter_description}}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
@@ -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 %}
|
||||||
|
@@ -47,6 +47,21 @@ 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>
|
||||||
|
@@ -26,6 +26,46 @@
|
|||||||
<input type="submit" class="btn btn-primary" value="Save" name="submit-pkg-edit">
|
<input type="submit" class="btn btn-primary" value="Save" name="submit-pkg-edit">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<h3>Parameters <button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#new-component-parameter-modal"><i class="bi bi-plus-circle"></i></button></h3>
|
||||||
|
<table class="table align-middle mb-3">
|
||||||
|
<thead>
|
||||||
|
<th scope="col">Parameter</th>
|
||||||
|
<th scope="col">Value</th>
|
||||||
|
<th scope="col"></th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for param in parameters %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h6 {% if param.parameter_type.parameter_description %} class="accordion-header" data-bs-toggle="collapse" data-bs-target="#collapse-parameter-desc-{{forloop.counter}}"{% endif %}>
|
||||||
|
{{param.parameter_type.parameter_name}}
|
||||||
|
</h6>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{param.resolved_value_as_string}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" value="{{param.id}}" name="param_num">
|
||||||
|
<button class="btn btn-danger" name="submit-delete-param">X</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="accordion" id="accordion-param-desc">
|
||||||
|
{% for param in parameters %}
|
||||||
|
{% if param.parameter_type.parameter_description %}
|
||||||
|
<div class="collapse accordion-collapse" id="collapse-parameter-desc-{{forloop.counter}}" data-bs-parent="#accordion-param-desc">
|
||||||
|
{{param.parameter_type.parameter_description}}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -80,7 +120,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'parts/modals/new-component-parameter-modal.html' with component_name=object.name form=new_param_form %}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
@@ -40,7 +56,9 @@
|
|||||||
<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 %}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Startup the db container
|
# Startup the db container
|
||||||
docker-compose start shimatta-kenkyusho-db
|
docker compose start shimatta-kenkyusho-db
|
||||||
|
|
||||||
# Override entrypoint to get interactive shell
|
# Override entrypoint to get interactive shell
|
||||||
docker-compose run --entrypoint="/bin/sh" -p 8000:8000 shimatta-kenkyusho-web
|
docker compose run --entrypoint="/bin/sh" -p 8000:8000 shimatta-kenkyusho-web
|
||||||
|
Reference in New Issue
Block a user