Compare commits

...

11 commits

72 changed files with 1681 additions and 326 deletions

6
.gitignore vendored
View file

@ -3,7 +3,11 @@ __pycache__
.python_version
*.egg-info
media/
./static/
/static/
media.tar.gz
*.pg
override.env
.env

21
Dockerfile Normal file
View file

@ -0,0 +1,21 @@
FROM python:3.13-alpine
WORKDIR /usr/src/app
RUN pip install --no-cache-dir poetry
ENV INVENTORY_STATIC_FILES=/static INVENTORY_MEDIA_FILES=/media
RUN mkdir -p "$INVENTORY_MEDIA_FILES" && mkdir -p "$INVENTORY_STATIC_FILES"
COPY pyproject.toml /usr/src/app
COPY poetry.lock /usr/src/app
RUN poetry install --no-root --no-interaction --no-cache
COPY manage.py ./manage.py
COPY inventory ./inventory
COPY inventory_project ./inventory_project
COPY entrypoint.sh /entrypoint.sh
RUN poetry run python manage.py collectstatic
CMD [ "/bin/sh", "/entrypoint.sh" ]

115
Readme.md
View file

@ -9,17 +9,19 @@ what area of your workshop you have to search for to find the part you
currently need. It has been optimized to store information for electronics
parts and small other hardware like screws, nuts and bolts.
### Prerequisites
### Prerequisites for manual install or docker Standalone
As configured by default you will need the following:
- A postgres database named `inventory` with a postgres user `inventory` that
may connect without password or by default with the password `inventory`
### Installation (manual)
You will need:
- Python > 3.10
- Poetry to install requirements and create a virtualenv
### Installation
This is a standard Django 5.1 application, if you know how to deploy those the
following might sound familiar:
@ -27,16 +29,103 @@ following might sound familiar:
- Github `git clone https://github.com/dunkelstern/inventory.git`
- ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git`
2. Change to checkout: `cd inventory`
3. Install virtualenv and dependencies: `poetry install --no-root`
4. Migrate the Database: `poetry run python manage.py migrate`
5. Create an admin user: `poetry run python manage.py createsuperuser`
6. Run the server
3. Install virtualenv and dependencies:
```
poetry install --no-root
```
4. If you want to use the system in another language than the default english set it
up in the `inventory_project/settings.py`:
```python
LANGUAGE_CODE = 'en-us' # or something like 'de-de'
```
see the settings file for defined languages.
5. If you changed the language rebuild the translation files:
```
poetry run python manage.py compilemessages
```
6. Migrate the Database:
```
poetry run python manage.py migrate
```
7. Optionally create an admin user. If not done manually the application will prompt you on first run.
```
poetry run python manage.py createsuperuser
```
8. Run the server
- Development server (not for deployment!): `poetry run python manage.py runserver`
- Deployment via `gunicorn` on port 8000: `poetry run gunicorn inventory_project.wsgi -b 0.0.0.0:8000`
Then login on `http://localhost:8000/admin/` for the Django admin interface or
go to `http://localhost:8000` to enter the inventory management system directly
### Installation (Standalone Docker)
#### Building yourself
1. Checkout repository:
- Github `git clone https://github.com/dunkelstern/inventory.git`
- ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git`
2. Change to checkout: `cd inventory`
3. Build Docker image: `docker build -t 'dunkelstern/inventory:latest' .`
next steps below
#### Pulling from docker hub
1. Pull Docker image: `docker pull 'dunkelstern/inventory:latest'`
next steps below
#### Next steps
1. Install a PostgreSQL DB somewhere and create a user and DB.
2. Setup environment (put everything in a `.env` file):
```
INVENTORY_DB_HOST=
INVENTORY_DB_NAME=
INVENTORY_DB_USER=
INVENTORY_DB_PASSWORD=
INVENTORY_SECRET_KEY=
INVENTORY_EXTERNAL_URL=http://localhost:8000
INVENTORY_TRUSTED_ORIGINS=localhost:8000
INVENTORY_DEBUG=FALSE
INVENTORY_LANGUAGE=en-us
INVENTORY_TIMEZONE=UTC
INVENTORY_PAGE_SIZE=25
```
3. Create a media directory for uploaded files: `mkdir -p media`
4. Run the container:
```
docker run \
--name inventory \
-d \
--restart=always \
--env-file=.env \
-p 8000:8000 \
--volume ./media:/media \
dunkelstern/inventory:latest
```
5. The onboarding process will start on first call of the application and prompt to create an admin user.
### Installation (Docker compose)
1. Checkout repository:
- Github `git clone https://github.com/dunkelstern/inventory.git`
- ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git`
2. Change to checkout: `cd inventory`
3. Copy `default.env` to `override.env` and check settings. Use a long random string for `INVENTORY_SECRET_KEY`!
4. Build the stack: `docker-compose up --build -d`
5. You can reach the application on port 8000
6. The onboarding process will start on first call of the application and prompt to create an admin user.
The compose stack will create two volumes:
- `inventory_dbdata` which contains the PostgreSQL database directory
- `inventory_mediafiles` which will contain any uploaded file
### Additional information
1. The initial DB migration pre-populates the database with some useful defaults
@ -51,6 +140,18 @@ go to `http://localhost:8000` to enter the inventory management system directly
[the service file](inventory.service) in the root of this repository for an
example.
### Changelog
#### 1.1
- Part count can be configured to be available in settings
- Currency can be configured in settings
- Complete system is translateable (English and German are provided)
- Onboarding process so you do not have to create a superuser on the command line
- Docker and Docker-Compose files
![Settings](docs/settings.jpeg)
### Screenshots
#### Login

22
default.env Normal file
View file

@ -0,0 +1,22 @@
# override this with a long random string (used for CSRF protection)
INVENTORY_SECRET_KEY=""
# override with URL the service will be available under
INVENTORY_EXTERNAL_URL="https://inventory.example.com"
# override with URLs that are used to send POST requests here,
# the EXTERNAL_URL will be in there already, http and https
# will be added automatically, separate multiple origins with commas
INVENTORY_TRUSTED_ORIGINS="localhost"
# keep this to FALSE for deployments
INVENTORY_DEBUG="FALSE"
# if you want to run the service in another language, override this
INVENTORY_LANGUAGE="en-us"
# if you want the service to use local time then override this
INVENTORY_TIMEZONE="UTC"
# This is the default pagination size
INVENTORY_PAGE_SIZE="25"

46
docker-compose.yaml Normal file
View file

@ -0,0 +1,46 @@
name: inventory
services:
db:
image: postgres:17-alpine
restart: always
shm_size: 128mb
environment:
POSTGRES_PASSWORD: inventory
POSTGRES_USER: inventory
POSTGRES_DB: inventory
volumes:
- dbdata:/var/lib/postgresql/data
inventory:
image: dunkelstern/inventory
build: .
restart: always
depends_on:
- db
env_file:
- path: ./default.env
required: true
- path: ./override.env
required: false
environment:
INVENTORY_DB_HOST: db
INVENTORY_DB_NAME: inventory
INVENTORY_DB_USER: inventory
INVENTORY_DB_PASSWORD: inventory
ports:
- name: web
target: 8000
host_ip: 127.0.0.1
published: "8000"
protocol: tcp
app_protocol: http
mode: host
volumes:
- mediafiles:/media
links:
- db
volumes:
dbdata:
mediafiles:

BIN
docs/settings.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

8
entrypoint.sh Normal file
View file

@ -0,0 +1,8 @@
#!/bin/sh
cd /usr/src/app
# poetry run python manage.py collectstatic --noinput
poetry run python manage.py migrate --noinput
exec poetry run gunicorn inventory_project.wsgi -b 0.0.0.0:8000

View file

@ -3,7 +3,7 @@ from django.shortcuts import redirect
from django.contrib import admin
from django.conf import settings
from inventory.models import Item, Documentation
from inventory.models import Item, Documentation, Settings
class DocumentationAdmin(admin.ModelAdmin):
@ -19,6 +19,12 @@ class ItemAdmin(admin.ModelAdmin):
readonly_fields = ['created_at', 'changed_at']
filter_horizontal = ('tags', 'documentation')
def get_exclude(self, request, obj=None):
s = Settings.objects.first()
if (s.track_amount):
return self.exclude
return (self.exclude or tuple()) + ('count', 'low_count')
def view_on_site(self, obj):
url = reverse('item-detail', kwargs={'pk': obj.id})
return settings.SERVER_URL + url

Binary file not shown.

View file

@ -0,0 +1,629 @@
# Translation for Inventory management system
# Copyright (C) 2025 Johannes Schriewer
# This file is distributed under the same license as the Inventory package.
# Johannes Schriewer <hallo@dunkelstern.de>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-11 03:20+0100\n"
"PO-Revision-Date: 2025-01-07 23:00+0100\n"
"Last-Translator: Johannes Schriewer <hallo@dunkelstern.de>\n"
"Language: German\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\inventory\models\area.py:8 .\inventory\models\box.py:10
#: .\inventory\models\distributor.py:6 .\inventory\models\documentation.py:6
#: .\inventory\models\form_factor.py:6 .\inventory\models\item.py:8
#: .\inventory\models\layout.py:6 .\inventory\models\manufacturer.py:6
#: .\inventory\models\tag.py:6 .\inventory\models\workshop.py:8
#: .\inventory\templates\inventory\distributor_detail.html:26
#: .\inventory\templates\inventory\item_detail.html:27
#: .\inventory\templates\inventory\item_list.html:7
#: .\inventory\templates\inventory\manufacturer_detail.html:26
msgid "Name"
msgstr "Name"
#: .\inventory\models\area.py:9 .\inventory\models\box.py:11
#: .\inventory\models\distributor.py:7 .\inventory\models\form_factor.py:7
#: .\inventory\models\item.py:9 .\inventory\models\layout.py:7
#: .\inventory\models\manufacturer.py:7 .\inventory\models\tag.py:7
#: .\inventory\models\workshop.py:9
#: .\inventory\templates\inventory\distributor_detail.html:30
#: .\inventory\templates\inventory\item_detail.html:31
#: .\inventory\templates\inventory\item_list.html:9
#: .\inventory\templates\inventory\manufacturer_detail.html:30
msgid "Description"
msgstr "Beschreibung"
#: .\inventory\models\area.py:11
msgid "Show sub area"
msgstr "Unterbereiche anzeigen"
#: .\inventory\models\area.py:13
msgid "Allow sub-areas to be defined in this area"
msgstr "Erlaube Unterbereiche in diesem Bereich"
#: .\inventory\models\area.py:15 .\inventory\models\box.py:14
#: .\inventory\models\distributor.py:16 .\inventory\models\documentation.py:7
#: .\inventory\models\form_factor.py:13 .\inventory\models\item.py:76
#: .\inventory\models\layout.py:8 .\inventory\models\manufacturer.py:13
#: .\inventory\models\tag.py:8 .\inventory\models\workshop.py:17
#: .\inventory\templates\inventory\distributor_detail.html:89
#: .\inventory\templates\inventory\item_detail.html:128
#: .\inventory\templates\inventory\manufacturer_detail.html:57
msgid "Created at"
msgstr "Erstellt am"
#: .\inventory\models\area.py:16 .\inventory\models\box.py:15
#: .\inventory\models\distributor.py:17 .\inventory\models\documentation.py:8
#: .\inventory\models\form_factor.py:14 .\inventory\models\item.py:77
#: .\inventory\models\layout.py:9 .\inventory\models\manufacturer.py:14
#: .\inventory\models\tag.py:9 .\inventory\models\workshop.py:18
#: .\inventory\templates\inventory\distributor_detail.html:91
#: .\inventory\templates\inventory\item_detail.html:130
#: .\inventory\templates\inventory\manufacturer_detail.html:59
msgid "Changed at"
msgstr "Geändert am"
#: .\inventory\models\area.py:24
msgid "Area"
msgstr "Bereich"
#: .\inventory\models\area.py:25
#: .\inventory\templates\inventory\area_detail.html:25
#: .\inventory\templates\inventory\workshop_detail.html:23
msgid "Areas"
msgstr "Bereiche"
#: .\inventory\models\box.py:12 .\inventory\models\distributor.py:14
#: .\inventory\models\form_factor.py:11 .\inventory\models\item.py:58
#: .\inventory\models\manufacturer.py:11 .\inventory\models\tag.py:17
#: .\inventory\models\workshop.py:15 .\inventory\templates\base.html:58
#: .\inventory\templates\inventory\distributor_detail.html:77
#: .\inventory\templates\inventory\item_detail.html:54
#: .\inventory\templates\inventory\manufacturer_detail.html:45
#: .\inventory\templates\inventory\tag_list.html:9
msgid "Tags"
msgstr "Tags"
#: .\inventory\models\box.py:35
msgid "Box"
msgstr "Kisten"
#: .\inventory\models\box.py:36
#: .\inventory\templates\inventory\box-generic.html:7
#: .\inventory\templates\inventory\tag_detail.html:43
msgid "Boxes"
msgstr "Kisten"
#: .\inventory\models\container.py:9 .\inventory\models\layout.py:18
msgid "Layout"
msgstr "Layout"
#: .\inventory\models\container.py:35 .\inventory\models\container.py:45
#: .\inventory\models\container.py:51
#: .\inventory\templates\inventory\item_list.html:10
msgid "Container"
msgstr "Behälter"
#: .\inventory\models\container.py:46
#: .\inventory\templates\inventory\area_detail.html:51
#: .\inventory\templates\inventory\workshop_detail.html:49
msgid "Containers"
msgstr "Behälter"
#: .\inventory\models\container.py:56
msgid "Index of compartment in layout"
msgstr "Index des Abteils im Layout"
#: .\inventory\models\distributor.py:9 .\inventory\models\manufacturer.py:9
#: .\inventory\templates\inventory\distributor_detail.html:40
#: .\inventory\templates\inventory\manufacturer_detail.html:40
msgid "Web link"
msgstr "Web link"
#: .\inventory\models\distributor.py:10
#: .\inventory\templates\inventory\item_detail.html:121
msgid "Search link"
msgstr "Such-link"
#: .\inventory\models\distributor.py:10
msgid "Use {} for search placeholder"
msgstr "Benutze {} oder {0} als Platzhalter"
#: .\inventory\models\distributor.py:11
#: .\inventory\templates\inventory\distributor_detail.html:65
msgid "Phone"
msgstr "Telefon"
#: .\inventory\models\distributor.py:12
#: .\inventory\templates\inventory\distributor_detail.html:71
msgid "E-Mail"
msgstr "E-Mail"
#: .\inventory\models\distributor.py:13 .\inventory\models\form_factor.py:9
#: .\inventory\models\manufacturer.py:10
#: .\inventory\templates\inventory\distributor_detail.html:35
#: .\inventory\templates\inventory\distributor_list.html:15
#: .\inventory\templates\inventory\manufacturer_detail.html:35
#: .\inventory\templates\inventory\manufacturer_list.html:15
msgid "Icon"
msgstr "Symbol"
#: .\inventory\models\distributor.py:24 .\inventory\models\item.py:31
#: .\inventory\templates\inventory\distributor_detail.html:6
#: .\inventory\templates\inventory\distributor_detail.html:10
#: .\inventory\templates\inventory\distributor_list.html:16
#: .\inventory\templates\inventory\item_detail.html:96
#: .\inventory\templates\inventory\item_list.html:15
msgid "Distributor"
msgstr "Händler"
#: .\inventory\models\distributor.py:25 .\inventory\templates\base.html:55
#: .\inventory\templates\inventory\distributor_list.html:5
#: .\inventory\templates\inventory\distributor_list.html:8
#: .\inventory\templates\inventory\tag_detail.html:63
msgid "Distributors"
msgstr "Händler"
#: .\inventory\models\documentation.py:17
#: .\inventory\models\documentation.py:18 .\inventory\models\item.py:52
#: .\inventory\templates\inventory\item_detail.html:135
msgid "Documentation"
msgstr "Dokumentation"
#: .\inventory\models\form_factor.py:10
#: .\inventory\templates\inventory\cell.html:16
#: .\inventory\templates\inventory\item_list.html:25
#: .\inventory\templates\inventory\search_result_item.html:6
msgid "Datasheet"
msgstr "Datenblatt"
#: .\inventory\models\form_factor.py:23 .\inventory\models\item.py:17
#: .\inventory\templates\inventory\item_detail.html:68
msgid "Form factor"
msgstr "Formfaktor"
#: .\inventory\models\form_factor.py:24
#: .\inventory\templates\inventory\tag_detail.html:121
msgid "Form factors"
msgstr "Formfaktoren"
#: .\inventory\models\item.py:11
msgid "Size"
msgstr "Größe"
#: .\inventory\models\item.py:13
msgid "Number of sub-compartments this item takes up"
msgstr "Anzahl der Unterteilungen die dieses Teil benötigt"
#: .\inventory\models\item.py:24 .\inventory\models\manufacturer.py:21
#: .\inventory\templates\inventory\item_detail.html:85
#: .\inventory\templates\inventory\item_list.html:12
#: .\inventory\templates\inventory\manufacturer_detail.html:6
#: .\inventory\templates\inventory\manufacturer_detail.html:10
#: .\inventory\templates\inventory\manufacturer_list.html:16
msgid "Manufacturer"
msgstr "Hersteller"
#: .\inventory\models\item.py:37
msgid "Distributor item no."
msgstr "Händler Bestellnummer"
#: .\inventory\models\item.py:43
#: .\inventory\templates\inventory\item_detail.html:107
msgid "Price"
msgstr "Preis"
#: .\inventory\models\item.py:49
msgid "Last ordered on"
msgstr "Zuletzt bestellt am"
#: .\inventory\models\item.py:63
msgid "Count"
msgstr "Anzahl"
#: .\inventory\models\item.py:66
msgid "Number of parts available"
msgstr "Anzahl der verfügbaren Teile"
#: .\inventory\models\item.py:69
#: .\inventory\templates\inventory\item_detail.html:49
msgid "Low watermark"
msgstr "Bestellgrenze"
#: .\inventory\models\item.py:72
msgid "Low watermark on which to alert ordering more"
msgstr "Untere Grenze ab der das Teil nachbestellt werden soll"
#: .\inventory\models\item.py:75
msgid "Custom metadata, used by templates"
msgstr "Metadaten die in der Vorlage verwendet werden können"
#: .\inventory\models\item.py:98
msgid "Item"
msgstr "Teil"
#: .\inventory\models\item.py:99
#: .\inventory\templates\inventory\box-generic.html:16
#: .\inventory\templates\inventory\manufacturer_detail.html:64
#: .\inventory\templates\inventory\tag_detail.html:113
msgid "Items"
msgstr "Teile"
#: .\inventory\models\layout.py:10
msgid "Data"
msgstr "Konfiguration"
#: .\inventory\models\layout.py:11
msgid "Template name"
msgstr "Vorlage"
#: .\inventory\models\layout.py:19
msgid "Layouts"
msgstr "Layouts"
#: .\inventory\models\manufacturer.py:22 .\inventory\templates\base.html:52
#: .\inventory\templates\inventory\manufacturer_list.html:5
#: .\inventory\templates\inventory\manufacturer_list.html:8
#: .\inventory\templates\inventory\tag_detail.html:88
msgid "Manufacturers"
msgstr "Hersteller"
#: .\inventory\models\settings.py:13
msgid "Default container to display when calling the index page"
msgstr "Standardbehälter der beim Aufruf der Index-Seite gezeigt werden soll"
#: .\inventory\models\settings.py:17
msgid "Show item count in overview and warn on low watermarks"
msgstr "Aktiviere die Mengenangaben und Nachbestellgrenzen"
#: .\inventory\models\settings.py:19
msgid "Currency"
msgstr "Währung"
#: .\inventory\models\settings.py:19
msgid "Currency name"
msgstr "Name der Währung"
#: .\inventory\models\settings.py:20
msgid "Currency symbol"
msgstr "Währungssymbol"
#: .\inventory\models\settings.py:22
msgid "Currency symbol at end"
msgstr "Währungssymbol am Ende"
#: .\inventory\models\settings.py:24
msgid "Currency symbol after amount"
msgstr "Zeige das Währungssymbol nach der Zahl an"
#: .\inventory\models\settings.py:28 .\inventory\models\settings.py:31
#: .\inventory\models\settings.py:32
msgid "Settings"
msgstr "Einstellungen"
#: .\inventory\models\tag.py:16
#: .\inventory\templates\inventory\tag_detail.html:5
msgid "Tag"
msgstr "Tag"
#: .\inventory\models\workshop.py:11
msgid "Show boxes"
msgstr "Behälter anzeigen"
#: .\inventory\models\workshop.py:13
msgid "Allow boxes to be defined directly in this workshop"
msgstr "Erlaube Behälter in dieser Werkstatt"
#: .\inventory\models\workshop.py:26 .\inventory\templates\base.html:61
#: .\inventory\templates\inventory\workshop_detail.html:5
msgid "Workshop"
msgstr "Werkstatt"
#: .\inventory\models\workshop.py:27
#: .\inventory\templates\inventory\tag_detail.html:23
#: .\inventory\templates\inventory\workshop_list.html:5
#: .\inventory\templates\inventory\workshop_list.html:8
msgid "Workshops"
msgstr "Werkstätten"
#: .\inventory\templates\base.html:43 .\inventory\templates\base.html:46
#: .\inventory\templates\base.html:47
#: .\inventory\templates\inventory\distributor_detail.html:45
#: .\inventory\templates\inventory\distributor_detail.html:58
#: .\inventory\templates\inventory\search.html:5
#: .\inventory\templates\inventory\search.html:8
#: .\inventory\templates\inventory\search.html:13
#: .\inventory\templates\inventory\search.html:14
#: .\inventory\templates\inventory\search_result.html:6
#: .\inventory\templates\inventory\search_result.html:9
#: .\inventory\templates\inventory\search_result.html:14
#: .\inventory\templates\inventory\search_result.html:15
#: .\inventory\templates\inventory\tag_list.html:43
#: .\inventory\templates\inventory\tag_list.html:44
msgid "Search"
msgstr "Suchen"
#: .\inventory\templates\base.html:68
msgid "Logout"
msgstr "Ausloggen"
#: .\inventory\templates\inventory\area_detail.html:16
#: .\inventory\templates\inventory\area_detail.html:36
#: .\inventory\templates\inventory\area_detail.html:62
#: .\inventory\templates\inventory\box-detail.html:20
#: .\inventory\templates\inventory\cell.html:19
#: .\inventory\templates\inventory\distributor_detail.html:17
#: .\inventory\templates\inventory\distributor_list.html:33
#: .\inventory\templates\inventory\item_detail.html:18
#: .\inventory\templates\inventory\item_list.html:28
#: .\inventory\templates\inventory\manufacturer_detail.html:17
#: .\inventory\templates\inventory\manufacturer_list.html:33
#: .\inventory\templates\inventory\tag_detail.html:16
#: .\inventory\templates\inventory\tag_detail.html:33
#: .\inventory\templates\inventory\tag_detail.html:53
#: .\inventory\templates\inventory\tag_detail.html:78
#: .\inventory\templates\inventory\tag_detail.html:103
#: .\inventory\templates\inventory\workshop_detail.html:16
#: .\inventory\templates\inventory\workshop_detail.html:34
#: .\inventory\templates\inventory\workshop_detail.html:59
#: .\inventory\templates\inventory\workshop_list.html:21
msgid "Edit"
msgstr "Bearbeiten"
#: .\inventory\templates\inventory\area_detail.html:41
#: .\inventory\templates\inventory\workshop_detail.html:39
msgid "No areas defined"
msgstr "Keine Bereiche definiert"
#: .\inventory\templates\inventory\area_detail.html:47
#: .\inventory\templates\inventory\workshop_detail.html:45
msgid "Create new area..."
msgstr "Neuen Bereich anlegen..."
#: .\inventory\templates\inventory\area_detail.html:67
#: .\inventory\templates\inventory\workshop_detail.html:64
msgid "No containers defined"
msgstr "Keine Behälter definiert..."
#: .\inventory\templates\inventory\area_detail.html:73
#: .\inventory\templates\inventory\workshop_detail.html:70
msgid "Create new container..."
msgstr "Neuen Behälter anlegen..."
#: .\inventory\templates\inventory\box-detail.html:12
#: .\inventory\templates\inventory\distributor_detail.html:9
#: .\inventory\templates\inventory\item_detail.html:10
#: .\inventory\templates\inventory\manufacturer_detail.html:9
#: .\inventory\templates\inventory\tag_detail.html:8
#: .\inventory\templates\inventory\workshop_detail.html:8
msgid "Back"
msgstr "Zurück"
#: .\inventory\templates\inventory\cell.html:21
msgid "Details"
msgstr "Details"
#: .\inventory\templates\inventory\cell.html:26
#: .\inventory\templates\inventory\item_detail.html:42
msgid "Low stock"
msgstr "Wenig Vorrat"
#: .\inventory\templates\inventory\cell.html:43
msgid "New item..."
msgstr "Neues Teil..."
#: .\inventory\templates\inventory\distributor_detail.html:57
msgid "Search query"
msgstr "Suchanfrage"
#: .\inventory\templates\inventory\distributor_detail.html:83
#: .\inventory\templates\inventory\item_detail.html:60
#: .\inventory\templates\inventory\manufacturer_detail.html:51
#: .\inventory\templates\inventory\tag_list.html:50
msgid "No tags"
msgstr "Keine Tags"
#: .\inventory\templates\inventory\distributor_list.html:8
#: .\inventory\templates\inventory\tag_list.html:9
#: .\inventory\templates\inventory\workshop_list.html:8
#: .\inventory\templates\registration\login.html:5
msgid "Inventory management"
msgstr "Inventarverwaltung"
#: .\inventory\templates\inventory\distributor_list.html:43
msgid "Create new distributor..."
msgstr "Neuen Händler anlegen..."
#: .\inventory\templates\inventory\item_detail.html:36
#: .\inventory\templates\inventory\item_detail.html:45
msgid "Amount"
msgstr "Anzahl"
#: .\inventory\templates\inventory\item_detail.html:46
msgid "Update"
msgstr "Speichern"
#: .\inventory\templates\inventory\item_detail.html:108
msgid "Sum"
msgstr "Summe"
#: .\inventory\templates\inventory\item_detail.html:114
msgid "Last ordered"
msgstr "Zuletzt bestellt"
#: .\inventory\templates\inventory\manufacturer_list.html:8
msgid "Inventory Management"
msgstr "Inventarverwaltung"
#: .\inventory\templates\inventory\manufacturer_list.html:42
msgid "Create new manufacturer..."
msgstr "Neuen Hersteller anlegen..."
#: .\inventory\templates\inventory\onboarding.html:5
#: .\inventory\templates\inventory\onboarding.html:8
#: .\inventory\templates\inventory\onboarding_success.html:5
#: .\inventory\templates\inventory\onboarding_success.html:8
msgid "Inventory Setup"
msgstr "Inventarverwaltung Setup"
#: .\inventory\templates\inventory\onboarding.html:12
msgid "Welcome to the Inventory Management setup"
msgstr "Willkommen zur Einrichtung der Inventarverwaltung"
#: .\inventory\templates\inventory\onboarding.html:15
msgid ""
"\n"
" Currently no admin user is defined in the database.\n"
" To use the inventory management system you need at least one admin "
"user...\n"
" "
msgstr ""
"\n"
" Aktuell ist kein Admin Benutzer in der Datenbank angelegt.\n"
" Um die Inventarverwaltung nutzen zu können muss mindestens\n"
" ein Administrator angelegt werden...\n"
" "
#: .\inventory\templates\inventory\onboarding.html:22
msgid ""
"\n"
" Please verify that the following settings are correct and then fill "
"out the\n"
" form at the end and click\n"
" "
msgstr ""
" Bitte überprüfe die folgenden Einstellungen und klicke dann auf\n"
" "
#: .\inventory\templates\inventory\onboarding.html:26
#: .\inventory\templates\inventory\onboarding.html:38
#: .\inventory\templates\inventory\onboarding.html:51
msgid "Create user"
msgstr "Benutzer anlegen"
#: .\inventory\templates\inventory\onboarding.html:29
msgid "Current settings"
msgstr "Aktuelle Einstellungen"
#: .\inventory\templates\inventory\onboarding.html:42
msgid ""
"\n"
" Please correct the errors below.\n"
" "
msgstr ""
"\n"
" Bitte die unten angezeigten Fehler korrigieren.\n"
" "
#: .\inventory\templates\inventory\pagination.html:6
#: .\inventory\templates\inventory\search_result.html:33
msgid "First page"
msgstr "Erste Seite"
#: .\inventory\templates\inventory\pagination.html:7
#: .\inventory\templates\inventory\search_result.html:34
msgid "Previous page"
msgstr "Vorherige Seite"
#: .\inventory\templates\inventory\pagination.html:13
#: .\inventory\templates\inventory\search_result.html:38
msgid "Next page"
msgstr "Nächste Seite"
#: .\inventory\templates\inventory\pagination.html:14
#: .\inventory\templates\inventory\search_result.html:39
msgid "Last page"
msgstr "Letzte Seite"
#: .\inventory\templates\inventory\search_result.html:18
msgid "Search result for"
msgstr "Suchergebnisse für"
#: .\inventory\templates\inventory\search_result.html:27
msgid "Nothing found"
msgstr "Nichts gefunden"
#: .\inventory\templates\inventory\search_result_item.html:11
msgid "Contained in"
msgstr "Im Behälter"
#: .\inventory\templates\inventory\set_index.html:10
msgid "Currently set as index page"
msgstr "Momentan als Index-Seite definiert"
#: .\inventory\templates\inventory\set_index.html:12
msgid "Promote to index page"
msgstr "Als Index-Seite definieren"
#: .\inventory\templates\inventory\tag_list.html:55
msgid "Create new tag..."
msgstr "Neues Tag anlegen..."
#: .\inventory\templates\inventory\workshop_list.html:30
msgid "Create new workshop..."
msgstr "Neue Werkstatt anlegen..."
#: .\inventory\templates\registration\login.html:5
msgid "Login"
msgstr "Einloggen"
#: .\inventory\templates\registration\login.html:11
msgid "Your username and password didn't match. Please try again."
msgstr ""
"Benutzername und Passwort passen nicht zusammen, bitte versuche es noch "
"einmal."
#: .\inventory\templates\registration\login.html:17
msgid ""
"\n"
" Your account doesn't have access to this page. To proceed,\n"
" please login with an account that has access.\n"
" "
msgstr ""
"\n"
" Dein Zugang hat keinen Zugriff auf diese Seite.\n"
" Zum Fortfahren bitte mit einem Zugang einloggen der die\n"
" entsprechenden Rechte besitzt!\n"
" "
#: .\inventory\templates\registration\login.html:24
msgid ""
"\n"
" Please login to see this page.\n"
" "
msgstr ""
"\n"
" Bitte einloggen um auf diese Seite Zugriff zu bekommen.\n"
" "
#: .\inventory\templates\registration\login.html:49
msgid "Lost password?"
msgstr "Passwort vergessen?"
#: .\inventory\views\onboarding.py:14
msgid "Username"
msgstr "Benutzername"
#: .\inventory\views\onboarding.py:15
msgid "Email"
msgstr "E-Mail"
#: .\inventory\views\onboarding.py:16
msgid "Password"
msgstr "Passwort"
#: .\inventory_project\settings.py:130
msgid "German"
msgstr "Deutsch"
#: .\inventory_project\settings.py:131
msgid "English"
msgstr "Englisch"

View file

@ -0,0 +1,23 @@
# Generated by Django 5.1.4 on 2025-01-07 17:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_alter_sorting_collation'),
]
operations = [
migrations.AddField(
model_name='item',
name='count',
field=models.PositiveIntegerField(default=1, help_text='Number of parts available'),
),
migrations.AddField(
model_name='item',
name='low_count',
field=models.PositiveIntegerField(default=0, help_text='Low watermark on which to alert ordering more'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-01-07 17:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_item_count_item_low_count'),
]
operations = [
migrations.AddField(
model_name='settings',
name='track_amount',
field=models.BooleanField(default=False, help_text='Show item count in overview and warn on low watermarks'),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 5.1.4 on 2025-01-07 18:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0007_settings_track_amount'),
]
operations = [
migrations.AddField(
model_name='settings',
name='currency',
field=models.CharField(default='Euro', help_text='Currency name', max_length=30),
),
migrations.AddField(
model_name='settings',
name='currency_symbol',
field=models.CharField(default='&euro;', help_text='Currency symbol', max_length=20),
),
migrations.AddField(
model_name='settings',
name='currency_symbol_position',
field=models.BooleanField(default=True, help_text='Currency symbol after amount'),
),
]

View file

@ -1,14 +1,19 @@
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.db import models
from .container import Container, CanBeContained
class Area(CanBeContained, Container):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4096)
show_sub_area = models.BooleanField(default=True, help_text="Allow sub-areas to be defined in this area")
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(_("Description"), max_length=4096)
show_sub_area = models.BooleanField(
_("Show sub area"),
default=True,
help_text=_("Allow sub-areas to be defined in this area")
)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
@property
def url(self):
@ -16,3 +21,5 @@ class Area(CanBeContained, Container):
class Meta:
ordering = ("name", )
verbose_name = _("Area")
verbose_name_plural = _("Areas")

View file

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.utils.text import slugify
from django.template.loader import get_template, TemplateDoesNotExist
@ -6,15 +7,12 @@ from .container import CanBeContained, Container
class Box(CanBeContained, Container):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4096)
tags = models.ManyToManyField('inventory.Tag', blank=True)
name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(_("Description"), max_length=4096)
tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name_plural = 'Boxes'
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
@property
def template_name(self):
@ -34,3 +32,5 @@ class Box(CanBeContained, Container):
class Meta:
ordering = ("name", )
verbose_name = _("Box")
verbose_name_plural = _("Boxes")

View file

@ -1,9 +1,16 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.apps import apps
class Container(models.Model):
layout = models.ForeignKey('inventory.Layout', on_delete=models.PROTECT, null=True, blank=True)
layout = models.ForeignKey(
'inventory.Layout',
verbose_name=_("Layout"),
on_delete=models.PROTECT,
null=True,
blank=True
)
@property
def subclass(self):
@ -25,7 +32,7 @@ class Container(models.Model):
_, obj = self.subclass
if obj is not None:
return obj.name
return 'Container'
return _("Container")
@property
def url(self):
@ -34,15 +41,19 @@ class Container(models.Model):
return obj.url
return None
class Meta:
verbose_name = _("Container")
verbose_name_plural = _("Containers")
class CanBeContained(models.Model):
container = models.ForeignKey(
'inventory.Container',
verbose_name=_("Container"),
related_name="%(class)s_related",
null=True,
on_delete=models.CASCADE
)
index = models.PositiveIntegerField('Index of compartment in layout')
index = models.PositiveIntegerField(_("Index of compartment in layout"))
class Meta:
abstract = True

View file

@ -1,22 +1,25 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
class Distributor(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4096)
name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(_("Description"), max_length=4096)
web_link = models.URLField(null=True, blank=True)
search_link = models.URLField(help_text='Use {} for search placeholder', null=True, blank=True)
phone = models.CharField(max_length=128, null=True, blank=True)
email = models.EmailField(null=True, blank=True, default=None)
icon = models.ImageField(null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', blank=True)
web_link = models.URLField(_("Web link"), null=True, blank=True)
search_link = models.URLField(_("Search link"), help_text=_("Use {} for search placeholder"), null=True, blank=True)
phone = models.CharField(_("Phone"), max_length=128, null=True, blank=True)
email = models.EmailField(_("E-Mail"), null=True, blank=True, default=None)
icon = models.ImageField(_("Icon"), null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self):
return self.name
class Meta:
ordering = ("name", )
verbose_name = _("Distributor")
verbose_name_plural = _("Distributors")

View file

@ -1,10 +1,11 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
class Documentation(models.Model):
name = models.CharField(max_length=255, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
name = models.CharField(_("Name"), max_length=255, unique=True)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
file = models.FileField()
@ -13,3 +14,5 @@ class Documentation(models.Model):
class Meta:
ordering = ("name", )
verbose_name = _("Documentation")
verbose_name_plural = _("Documentation")

View file

@ -1,16 +1,17 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
class FormFactor(models.Model):
name = models.CharField(max_length=255, unique=True, db_collation="numeric")
description = models.CharField(max_length=4096, db_collation="numeric")
name = models.CharField(_("Name"), max_length=255, unique=True, db_collation="numeric")
description = models.CharField(_("Description"), max_length=4096, db_collation="numeric")
icon = models.ImageField(null=True, blank=True)
datasheet = models.FileField(null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', blank=True)
icon = models.ImageField(_("Icon"), null=True, blank=True)
datasheet = models.FileField(_("Datasheet"), null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self):
items = [self.name]
@ -19,3 +20,5 @@ class FormFactor(models.Model):
class Meta:
ordering = ("name", )
verbose_name = _("Form factor")
verbose_name_plural = _("Form factors")

View file

@ -1,24 +1,80 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from .container import CanBeContained
class Item(CanBeContained):
name = models.TextField(max_length=255, db_collation="numeric")
description = models.CharField(max_length=4096, db_collation="numeric")
size = models.PositiveIntegerField(default=1, help_text="Number of sub-compartments this item takes up")
form_factor = models.ForeignKey('inventory.FormFactor', null=True, blank=True, on_delete=models.PROTECT)
manufacturer = models.ForeignKey('inventory.Manufacturer', null=True, blank=True, on_delete=models.PROTECT)
distributor = models.ForeignKey('inventory.Distributor', null=True, blank=True, on_delete=models.PROTECT)
distributor_item_no = models.CharField(max_length=255, null=True, blank=True)
price = models.DecimalField(decimal_places=3, max_digits=7, null=True, blank=True)
last_ordered_on = models.DateField(null=True, blank=True)
documentation = models.ManyToManyField('inventory.Documentation', related_name='items', blank=True)
tags = models.ManyToManyField('inventory.Tag', blank=True)
name = models.TextField(_("Name"), max_length=255, db_collation="numeric")
description = models.CharField(_("Description"), max_length=4096, db_collation="numeric")
size = models.PositiveIntegerField(
_("Size"),
default=1,
help_text=_("Number of sub-compartments this item takes up")
)
form_factor = models.ForeignKey(
'inventory.FormFactor',
verbose_name=_("Form factor"),
null=True,
blank=True,
on_delete=models.PROTECT
)
manufacturer = models.ForeignKey(
'inventory.Manufacturer',
verbose_name=_("Manufacturer"),
null=True,
blank=True,
on_delete=models.PROTECT
)
distributor = models.ForeignKey(
'inventory.Distributor',
verbose_name=_("Distributor"),
null=True,
blank=True,
on_delete=models.PROTECT
)
distributor_item_no = models.CharField(
_("Distributor item no."),
max_length=255,
null=True,
blank=True
)
price = models.DecimalField(
_("Price"),
decimal_places=3,
max_digits=7,
null=True,
blank=True
)
last_ordered_on = models.DateField(_("Last ordered on"), null=True, blank=True)
documentation = models.ManyToManyField(
'inventory.Documentation',
verbose_name=_("Documentation"),
related_name='items',
blank=True
)
tags = models.ManyToManyField(
'inventory.Tag',
verbose_name=_("Tags"),
blank=True
)
metadata = models.JSONField('Custom metadata, used by templates', blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
count = models.PositiveIntegerField(
_("Count"),
default=1,
null=False,
help_text=_("Number of parts available")
)
low_count = models.PositiveIntegerField(
_("Low watermark"),
default=0,
null=False,
help_text=_("Low watermark on which to alert ordering more")
)
metadata = models.JSONField(_("Custom metadata, used by templates"), blank=True, null=True)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self):
items = [self.name, self.description]
@ -33,5 +89,11 @@ class Item(CanBeContained):
else:
return list(self.tags.all())
@property
def value(self):
return self.count * self.price
class Meta:
ordering = ("name", )
verbose_name = _("Item")
verbose_name_plural = _("Items")

View file

@ -1,16 +1,19 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
class Layout(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4096)
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
data = models.JSONField()
template_name = models.CharField(max_length=255, null=True, blank=True)
name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(_("Description"), max_length=4096)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
data = models.JSONField(_("Data"))
template_name = models.CharField(_("Template name"), max_length=255, null=True, blank=True)
def __str__(self):
return self.name
class Meta:
ordering = ("name", )
verbose_name = _("Layout")
verbose_name_plural = _("Layouts")

View file

@ -1,19 +1,22 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
class Manufacturer(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4096, blank=True)
name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(_("Description"), max_length=4096, blank=True)
web_link = models.URLField(null=True, blank=True)
icon = models.ImageField(null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', blank=True)
web_link = models.URLField(_("Web link"), null=True, blank=True)
icon = models.ImageField(_("Icon"), null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self):
return self.name
class Meta:
ordering = ("name", )
verbose_name = _("Manufacturer")
verbose_name_plural = _("Manufacturers")

View file

@ -1,3 +1,5 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from django.db import models
@ -8,11 +10,23 @@ class Settings(models.Model):
default=None,
null=True,
blank=True,
help_text='Default container to display when calling the index page'
help_text=_("Default container to display when calling the index page")
)
track_amount = models.BooleanField(
default=False,
help_text=_("Show item count in overview and warn on low watermarks")
)
currency = models.CharField(_("Currency"), max_length=30, help_text=_("Currency name"), default="Euro")
currency_symbol = models.CharField(_("Currency symbol"), max_length=20, default="&euro;")
currency_symbol_position = models.BooleanField(
_("Currency symbol at end"),
default=True,
help_text=_("Currency symbol after amount")
)
def __str__(self):
return 'Settings'
return gettext("Settings")
class Meta:
verbose_name_plural = 'Settings'
verbose_name = _("Settings")
verbose_name_plural = _("Settings")

View file

@ -1,14 +1,17 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=255, unique=True, db_collation="numeric")
description = models.CharField(max_length=4096, db_collation="numeric")
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
name = models.CharField(_("Name"), max_length=255, unique=True, db_collation="numeric")
description = models.CharField(_("Description"), max_length=4096, db_collation="numeric")
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self):
return self.name
class Meta:
ordering = ['name', 'pk']
verbose_name = _("Tag")
verbose_name_plural = _("Tags")

View file

@ -1,16 +1,21 @@
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.db import models
from .container import Container
class Workshop(Container):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4096)
show_boxes = models.BooleanField(default=True, help_text="Allow boxes to be defined directly in this workshop")
tags = models.ManyToManyField('inventory.Tag', blank=True)
name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(_("Description"), max_length=4096)
show_boxes = models.BooleanField(
_("Show boxes"),
default=True,
help_text=_("Allow boxes to be defined directly in this workshop")
)
tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
@property
def url(self):
@ -18,3 +23,5 @@ class Workshop(Container):
class Meta:
ordering = ("name", )
verbose_name = _("Workshop")
verbose_name_plural = _("Workshops")

View file

@ -89,6 +89,24 @@ td.disabled {
opacity: 0.75;
}
.cell .stock {
position: absolute;
top: 5px;
left: 5px;
display: inline;
font-size: xx-small;
color: #808080;
line-height: 10px;
}
.cell .stock .icon {
display: inline-block;
width: 10px;
height: 10px;
margin: 0 2px 0 0;
vertical-align: text-top;
}
.cell .form_factor {
position: absolute;
display: inline;
@ -154,6 +172,12 @@ td.disabled {
right: 7px;
}
.cell .stock {
font-size: 75%;
top: 3px;
left: 3px;
}
.cell .price {
font-size: 75%;
bottom: 3px;

View file

@ -26,6 +26,11 @@ table.attribute-list th {
border-style: solid;
}
table.attribute-list .small {
color: #808080;
font-size: xx-small;
}
table.box {
width: 100%;
border-collapse: collapse;
@ -158,8 +163,7 @@ img.icon {
margin-right: 10px;
width: 18px;
height: 18px;
position: relative;
top: 2px;
vertical-align: middle;
}
img.logo {
@ -277,9 +281,6 @@ table.list thead th {
padding: 10px;
}
table.list tbody tr {
}
table.list tbody td {
border-bottom: 1px solid black;
padding: 10px;
@ -322,3 +323,26 @@ table.list tbody td {
font-weight: normal;
color: #808080;
}
div.icon {
width: 18px;
height: 18px;
display: inline-block;
vertical-align: middle;
}
div.warning-icon {
background-color: #c00000;
-webkit-mask: url(/static/inventory/img/warn.svg) no-repeat center;
mask: url(/static/inventory/img/warn.svg) no-repeat center;
}
form label {
display: inline-block;
width: 100px;
}
dt {
font-weight: bold;
font-family: 'Courier New', Courier, monospace;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>

After

Width:  |  Height:  |  Size: 542 B

View file

@ -1,6 +1,8 @@
{% load static %}
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
@ -38,32 +40,32 @@
}
}
</script>
<a href="{% url 'search' %}" onclick="showSearch(event)"><img class="icon" title="Search" src="{% static "inventory/img/search.svg" %}"></a>
<a href="{% url 'search' %}" onclick="showSearch(event)"><img class="icon" title="{% translate 'Search' %}" src="{% static "inventory/img/search.svg" %}"></a>
<div id="search-container">
<form action="{% url 'search' %}" method="get">
<input name="q" id="search" type="text" placeholder="Search">
<button type="submit">Search</button>
<input name="q" id="search" type="text" placeholder="{% translate 'Search' %}">
<button type="submit">{% translate 'Search' %}</button>
</form>
</div>
</li>
<li>
<a href="{% url 'manufacturer-list' %}"><img class="icon" title="Manufacturers" src="{% static "inventory/img/manufacturer.svg" %}"></a>
<a href="{% url 'manufacturer-list' %}"><img class="icon" title="{% translate 'Manufacturers' %}" src="{% static "inventory/img/manufacturer.svg" %}"></a>
</li>
<li>
<a href="{% url 'distributor-list' %}"><img class="icon" title="Distributors" src="{% static "inventory/img/distributor.svg" %}"></a>
<a href="{% url 'distributor-list' %}"><img class="icon" title="{% translate 'Distributors' %}" src="{% static "inventory/img/distributor.svg" %}"></a>
</li>
<li>
<a href="{% url 'tag-list' %}"><img class="icon" title="Tags" src="{% static "inventory/img/tags.svg" %}"></a>
<a href="{% url 'tag-list' %}"><img class="icon" title="{% translate 'Tags' %}" src="{% static "inventory/img/tags.svg" %}"></a>
</li>
<li>
<a href="{% url 'index' %}"><img class="icon" title="Workshops" src="{% static "inventory/img/workshop.svg" %}"></a>
<a href="{% url 'index' %}"><img class="icon" title="{% translate 'Workshop' %}" src="{% static "inventory/img/workshop.svg" %}"></a>
</li>
</ul>
{% if user.is_authenticated %}
<form method="POST" action="{% url "logout" %}">
{% csrf_token %}
<button><img class="icon" src="{% static "inventory/img/logout.svg" %}"> Logout</button>
<button type="submit"><img class="icon" src="{% static "inventory/img/logout.svg" %}" title="{% translate 'Logout' %}"> {% translate 'Logout' %}</button>
</form>
{% endif %}
</nav>

View file

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{{ area.name }}{% endblock %}
@ -12,7 +13,7 @@
{% block header_icons %}
{% if user.is_staff %}
<li>
<a href="{% url "admin:inventory_area_change" object_id=area.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a href="{% url "admin:inventory_area_change" object_id=area.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
</li>
{% include 'inventory/set_index.html' with item=area is_index=is_index %}
{% endif %}
@ -21,7 +22,7 @@
{% block content %}
{% if area.show_sub_area %}
<h3>Areas</h3>
<h3>{% translate 'Areas' %}</h3>
<table class="list">
<tbody>
@ -32,22 +33,22 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_area_change" object_id=a.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_area_change" object_id=a.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td>No areas defined</td></tr>
<tr><td>{% translate 'No areas defined' %}</td></tr>
{% endfor %}
</tbody>
</table>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_area_add" %}?container={{ area.id }}&index=0">Create new area...</a></p>
<p><a href="{% url "admin:inventory_area_add" %}?container={{ area.id }}&index=0">{% translate 'Create new area...' %}</a></p>
{% endif %}
{% endif %}
<h3>Containers</h3>
<h3>{% translate 'Containers' %}</h3>
<table class="list">
<tbody>
@ -58,17 +59,17 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_box_change" object_id=box.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_box_change" object_id=box.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td>No containers defined</td></tr>
<tr><td>{% translate 'No containers defined' %}</td></tr>
{% endfor %}
</tbody>
</table>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_box_add" %}?container={{ area.id }}&index=0">Create new container...</a></p>
<p><a href="{% url "admin:inventory_box_add" %}?container={{ area.id }}&index=0">{% translate 'Create new container...' %}</a></p>
{% endif %}
{% endblock %}

View file

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block head %}
<link rel="stylesheet" type="text/css" href="{% static "inventory/css/cell.css" %}">
@ -8,7 +9,7 @@
{% block title %}{{ object.name }}{% endblock %}
{% block header_bar %}
<a href="{{ object.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
<a href="{{ object.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}" title="{% translate 'Back' %}"></a>
{{ object.name }}
<span class="small">{{ object.description}}</span></h2>
{% endblock %}
@ -16,7 +17,7 @@
{% block header_icons %}
{% if user.is_staff %}
<li>
<a href="{% url "admin:inventory_box_change" object_id=object.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a href="{% url "admin:inventory_box_change" object_id=object.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
</li>
{% include 'inventory/set_index.html' with item=object is_index=is_index %}
{% endif %}

View file

@ -1,9 +1,10 @@
{% extends "inventory/box-detail.html" %}
{% load i18n %}
{% block content %}
{% if box.box_related.exists %}
<h3>Boxes</h3>
<h3>{% translate 'Boxes' %}</h3>
<ul>
{% for b in box.box_related.all %}
<li><a href="{% url 'box-detail' b.id %}" title="{{ b.description }}">{{ b.name }} ({{ b.layout.name }})</a></li>
@ -12,7 +13,7 @@
{% endif %}
{% if box.item_related.exists %}
<h3>Items</h3>
<h3>{% translate 'Items' %}</h3>
<ul>
{% for item in box.item_related.all %}
<li {% if hilight == item.id %}class="hilighted"{% endif %}><a href="{% url 'item-detail' item.id %}" title="{{ item.description }}">{{ item.name }}</a></li>

View file

@ -9,7 +9,7 @@
<td rowspan="2">
<div class="compartments-vertical">
{% for compartment in layouted.0.0 %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -17,7 +17,7 @@
<td class="triple-height" colspan="3">
<div class="compartments">
{% for compartment in layouted.0.1 %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -27,7 +27,7 @@
<td class="triple-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -9,7 +9,7 @@
<td>
<div class="compartments">
{% for compartment in layouted.0 %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -10,7 +10,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -20,7 +20,7 @@
<td rowspan="2">
<div class="compartments-vertical">
{% for compartment in layouted.1.0 %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -29,7 +29,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -40,7 +40,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -11,7 +11,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -27,7 +27,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -42,7 +42,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -11,7 +11,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -27,7 +27,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -42,7 +42,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -11,7 +11,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -27,7 +27,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -42,7 +42,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -11,7 +11,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -10,7 +10,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -21,7 +21,7 @@
<td>
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -10,7 +10,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -21,7 +21,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -32,7 +32,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -45,7 +45,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -10,7 +10,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -21,7 +21,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -32,7 +32,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>
@ -45,7 +45,7 @@
<td class="double-height">
<div class="compartments">
{% for compartment in item %}
{% include "inventory/cell.html" with item=compartment hilight=hilight %}
{% include "inventory/cell.html" with item=compartment hilight=hilight settings=settings %}
{% endfor %}
</div>
</td>

View file

@ -1,5 +1,8 @@
{% load static %}
{% load i18n %}
{% load admin_urls %}
{% load currency %}
<div class="cell{% if hilight == item.id %} search-hilight{% endif %}">
{% if item.name %}
{% if item.metadata.package %}
@ -10,16 +13,23 @@
</a>
<div class="cell-buttons">
{% if item.documentation.all %}
<a class="datasheet" href="{{ item.documentation.all.0.file.url }}"><img class="icon" src="{% static "inventory/img/datasheet.svg" %}"></a>
<a class="datasheet" href="{{ item.documentation.all.0.file.url }}"><img class="icon" src="{% static "inventory/img/datasheet.svg" %}" title="{% translate 'Datasheet' %}"></a>
{% endif %}
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_item_change" object_id=item.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a class="edit" href="{% url "admin:inventory_item_change" object_id=item.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
{% endif %}
<a class="details" href="{% url "item-detail" pk=item.pk %}"><img class="icon" src="{% static "inventory/img/detail.svg" %}"></a>
<a class="details" href="{% url "item-detail" pk=item.pk %}"><img class="icon" src="{% static "inventory/img/detail.svg" %}" title="{% translate 'Details' %}"></a>
</div>
{% if settings.track_amount %}
{% if item.count <= item.low_count %}
<div class="stock"><div class="icon warning-icon" title="{% translate 'Low stock' %}"></div>{{ item.count }}</div>
{% else %}
<div class="stock">{{ item.count }}</div>
{% endif %}
{% endif %}
{% if item.price %}
<div class="price">{{ item.price | floatformat:2 }} &euro;</div>
<div class="price">{{ item.price | currency:"short" }}</div>
{% endif %}
{% if item.form_factor %}
<div class="form_factor">{{ item.form_factor.name }}</div>
@ -30,7 +40,7 @@
{% else %}
<div class="cell-buttons">
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_item_add" %}?index={{ item.index }}&container={{ item.container_id }}"><img class="icon" src="{% static "inventory/img/add.svg" %}"></a>
<a class="edit" href="{% url "admin:inventory_item_add" %}?index={{ item.index }}&container={{ item.container_id }}"><img class="icon" src="{% static "inventory/img/add.svg" %}" title="{% translate 'New item...' %}"></a>
{% endif %}
</div>
{% endif %}

View file

@ -1,19 +1,20 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load formatstring %}
{% block title %}Distributor: {{ distributor }}{% endblock %}
{% block title %}{% translate 'Distributor' %}: {{ distributor }}{% endblock %}
{% block header_bar %}
<a href="{% url 'distributor-list' %}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
Distributor: {{ distributor.name }}
<a href="{% url 'distributor-list' %}"><img class="icon" src="{% static "inventory/img/back.svg" %}" title="{% translate 'Back' %}"></a>
{% translate 'Distributor' %}: {{ distributor.name }}
<span class="small">{{ distributor.description }}</span></h2>
{% endblock %}
{% block header_icons %}
{% if user.is_staff %}
<li>
<a href="{% url "admin:inventory_distributor_change" object_id=distributor.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a href="{% url "admin:inventory_distributor_change" object_id=distributor.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
</li>
{% endif %}
{% endblock %}
@ -22,26 +23,26 @@
<table class="attribute-list">
<tbody>
<tr>
<th>Name</th>
<th>{% translate 'Name' %}</th>
<td>{{ distributor.name }}</td>
</tr>
<tr>
<th>Description</th>
<th>{% translate 'Description' %}</th>
<td>{{ distributor.description }}</td>
</tr>
{% if distributor.icon %}
<tr>
<th>Icon</th>
<th>{% translate 'Icon' %}</th>
<td><img src="{{ distributor.icon.url }}" title="{{ distributor.name }}" style="max-height: 20px;"></td>
</tr>
{% endif %}
<tr>
<th>Link</th>
<th>{% translate 'Web link' %}</th>
<td>{% if distributor.web_link %}<a href="{{ distributor.web_link }}" title="{{ distributor.name }}">{{ distributor.web_link }}{% else %}-{% endif %}</td>
</tr>
{% if distributor.search_link %}
<tr>
<th>Search</th>
<th>{% translate 'Search' %}</th>
<td>
<script>
function distro_search(e) {
@ -53,42 +54,41 @@
}
</script>
<form>
<label for="distributor_search">Search query:</label>
<input name="distributor_search" id="distributor_search" type="text">
<button onclick="distro_search(event)" type="submit">Search</button>
<input name="distributor_search" id="distributor_search" type="text" placeholder="{% translate 'Search query' %}">
<button onclick="distro_search(event)" type="submit">{% translate 'Search' %}</button>
</form>
</td>
</tr>
{% endif %}
{% if distributor.phone %}
<tr>
<th>Phone</th>
<th>{% translate 'Phone' %}</th>
<td>{{ distributor.phone }}</td>
</tr>
{% endif %}
{% if distributor.email %}
<tr>
<th>Email</th>
<th>{% translate 'E-Mail' %}</th>
<td><a href="mailto:{{ distributor.email }}">{{ distributor.email }}</a></td>
</tr>
{% endif %}
<tr>
<th>Tags</th>
<th>{% translate 'Tags' %}</th>
<td>
<ul class="tag-list">
{% for tag in distributor.tags.all %}
<li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
{% empty %}
No tags
{% translate 'No tags' %}
{% endfor %}
</ul>
</td>
</tr>
<tr><th>Created at</th><td>{{ distributor.created_at }}</td></tr>
<tr><th>{% translate 'Created at' %}</th><td>{{ distributor.created_at }}</td></tr>
{% if distributor.created_at != distributor.changed_at %}
<tr><th>Last change</th><td>{{ distributor.changed_at }}</td></tr>
<tr><th>{% translate 'Changed at' %}</th><td>{{ distributor.changed_at }}</td></tr>
{% endif %}
</tbody>
</table>

View file

@ -1,18 +1,19 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}Distributors{% endblock %}
{% block title %}{% translate 'Distributors' %}{% endblock %}
{% block header_bar %}
Inventory management - Distributors
{% translate 'Inventory management' %} - {% translate 'Distributors' %}
{% endblock %}
{% block content %}
<table class="list">
<thead>
<tr>
<th class="icon">Logo</th>
<th>Distributor</th>
<th class="icon">{% translate 'Icon' %}</th>
<th>{% translate 'Distributor' %}</th>
<th></th>
</tr>
</thead>
@ -29,7 +30,7 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_distributor_change" object_id=distributor.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_distributor_change" object_id=distributor.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
@ -39,7 +40,7 @@
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_distributor_add" %}?layout=1">Create new distributor...</a></p>
<p><a href="{% url "admin:inventory_distributor_add" %}?layout=1">{% translate 'Create new distributor...' %}</a></p>
{% endif %}

View file

@ -1,11 +1,13 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load formatstring %}
{% load currency %}
{% block title %}{{ item }}{% endblock %}
{% block header_bar %}
<a href="{{ item.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
<a href="{{ item.container_url }}"><img class="icon" src="{% static "inventory/img/back.svg" %}" title="{% translate 'Back' %}"></a>
{{ item.name }}
<span class="small">{{ item.description }}{% if item.form_factor %}, {{ item.form_factor }}{% endif %}</span></h2>
{% endblock %}
@ -13,7 +15,7 @@
{% block header_icons %}
{% if user.is_staff %}
<li>
<a href="{% url "admin:inventory_item_change" object_id=item.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a href="{% url "admin:inventory_item_change" object_id=item.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
</li>
{% endif %}
{% endblock %}
@ -22,21 +24,40 @@
<table class="attribute-list">
<tbody>
<tr>
<th>Name</th>
<th>{% translate 'Name' %}</th>
<td>{{ item.name }}</td>
</tr>
<tr>
<th>Description</th>
<th>{% translate 'Description' %}</th>
<td>{{ item.description }}</td>
</tr>
{% if settings.track_amount %}
<tr>
<th>Tags</th>
<th>{% translate 'Amount' %}</th>
<td>
<form method="post" action="{% url 'item-detail' item.pk %}">
{% csrf_token %}
<label for="amount">
{% if item.count <= item.low_count %}
<div class="icon warning-icon" title="{% translate 'Low stock' %}"></div>
{% endif %}
</label>
<input type="text" name="amount" id="amount" value="{{ item.count }}" placeholder="{% translate 'Amount' %}">
<button name="save" type="submit">{% translate 'Update' %}</button>
<button name="dec" type="submit">-1</button>
</form>
<span class="small right">{% translate 'Low watermark' %}: {{ item.low_count }}</span>
</td>
</tr>
{% endif %}
<tr>
<th>{% translate 'Tags' %}</th>
<td>
<ul class="tag-list">
{% for tag in item.all_tags %}
<li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
{% empty %}
No tags
{% translate 'No tags' %}
{% endfor %}
</ul>
</td>
@ -44,7 +65,7 @@
{% if item.form_factor %}
<tr>
<th>Form factor</th>
<th>{% translate 'Form factor' %}</th>
<td>
{% if item.form_factor.datasheet %}
<a href="{% url 'distributor-detail' item.distributor.id %}">
@ -61,7 +82,7 @@
{% if item.manufacturer %}
<tr>
<th>Manufacturer</th>
<th>{% translate 'Manufacturer' %}</th>
<td>
<a href="{% url 'manufacturer-detail' item.manufacturer.id %}">
{% if item.manufacturer.icon %}<img src="{{ item.manufacturer.icon.url }}" class="icon">{% endif %}{{ item.manufacturer.name }}
@ -72,7 +93,7 @@
{% if item.distributor %}
<tr>
<th>Distributor</th>
<th>{% translate 'Distributor' %}</th>
<td>
<a href="{% url 'distributor-detail' item.distributor.id %}">
{% if item.distributor.icon %}<img src="{{ item.distributor.icon.url }}" class="icon">{% endif %}{{ item.distributor.name }}
@ -83,35 +104,35 @@
{% if item.price %}
<tr>
<th>Price</th>
<td>{{ item.price }} Euro</td>
<th>{% translate 'Price' %}</th>
<td>{{ item.price | currency:"detail" }} <span class="small">{% if settings.track_amount %}({% translate 'Sum' %}: {{ item.value | currency:"long" }}){% endif %}</span></td>
</tr>
{% endif %}
{% if item.last_ordered_on %}
<tr>
<th>Last ordered</th>
<th>{% translate 'Last ordered' %}</th>
<td>{{ item.last_ordered_on }}</td>
</tr>
{% endif %}
{% if item.distributor_item_no %}
<tr>
<th>Link</th>
<th>{% translate 'Search link' %}</th>
<td>
<a href="{% formatstring item.distributor.search_link item.distributor_item_no %}">{% formatstring item.distributor.search_link item.distributor_item_no %}</a>
</td>
</tr>
{% endif %}
<tr><th>Created at</th><td>{{ item.created_at }}</td></tr>
<tr><th>{% translate 'Created at' %}</th><td>{{ item.created_at }}</td></tr>
{% if item.created_at != item.changed_at %}
<tr><th>Last change</th><td>{{ item.changed_at }}</td></tr>
<tr><th>{% translate 'Changed at' %}</th><td>{{ item.changed_at }}</td></tr>
{% endif %}
{% if item.documentation.exists %}
<tr>
<th>Datasheets</th>
<th>{% translate 'Documentation' %}</th>
<td>
<ul>
{% for doc in item.documentation.all %}

View file

@ -1,17 +1,18 @@
{% load i18n %}
{% load static %}
<table class="list">
<thead>
<tr>
<th>Name</th>
<th>{% translate 'Name' %}</th>
<th></th>
<th>Description</th>
<th>Container</th>
<th>{% translate 'Description' %}</th>
<th>{% translate 'Container' %}</th>
{% if show_manufacturer %}
<th>Manufacturer</th>
<th>{% translate 'Manufacturer' %}</th>
{% endif %}
{% if show_distributor %}
<th>Distributor</th>
<th>{% translate 'Distributor' %}</th>
{% endif %}
</tr>
</thead>
@ -21,10 +22,10 @@
<td><a href="{% url 'item-detail' item.id %}">{{ item.name }}</a></td>
<td class="right">
{% if item.documentation.all %}
<a class="datasheet" href="{{ item.documentation.all.0.file.url }}"><img class="icon" src="{% static "inventory/img/datasheet.svg" %}"></a>
<a class="datasheet" href="{{ item.documentation.all.0.file.url }}"><img class="icon" src="{% static "inventory/img/datasheet.svg" %}" title="{% translate 'Datasheet' %}"></a>
{% endif %}
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_item_change" object_id=item.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a class="edit" href="{% url "admin:inventory_item_change" object_id=item.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
<td>{{ item.description }}</td>

View file

@ -1,19 +1,20 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load formatstring %}
{% block title %}Manufacturer: {{ manufacturer }}{% endblock %}
{% block title %}{% translate 'Manufacturer' %}: {{ manufacturer }}{% endblock %}
{% block header_bar %}
<a href="{% url 'manufacturer-list' %}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
Manufacturer: {{ manufacturer.name }}
<a href="{% url 'manufacturer-list' %}"><img class="icon" src="{% static "inventory/img/back.svg" %}" title="{% translate 'Back' %}"></a>
{% translate 'Manufacturer' %}: {{ manufacturer.name }}
<span class="small">{{ manufacturer.description }}</span></h2>
{% endblock %}
{% block header_icons %}
{% if user.is_staff %}
<li>
<a href="{% url "admin:inventory_manufacturer_change" object_id=manufacturer.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a href="{% url "admin:inventory_manufacturer_change" object_id=manufacturer.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
</li>
{% endif %}
{% endblock %}
@ -22,45 +23,45 @@
<table class="attribute-list">
<tbody>
<tr>
<th>Name</th>
<th>{% translate 'Name' %}</th>
<td>{{ manufacturer.name }}</td>
</tr>
<tr>
<th>Description</th>
<th>{% translate 'Description' %}</th>
<td>{{ manufacturer.description }}</td>
</tr>
{% if manufacturer.icon %}
<tr>
<th>Icon</th>
<th>{% translate 'Icon' %}</th>
<td><img src="{{ manufacturer.icon.url }}" title="{{ manufacturer.name }}" style="max-height: 20px;"></td>
</tr>
{% endif %}
<tr>
<th>Link</th>
<th>{% translate 'Web link' %}</th>
<td>{% if manufacturer.web_link %}<a href="{{ manufacturer.web_link }}" title="{{ manufacturer.name }}">{{ manufacturer.web_link }}{% else %}-{% endif %}</td>
</tr>
<tr>
<th>Tags</th>
<th>{% translate 'Tags' %}</th>
<td>
<ul class="tag-list">
{% for tag in manufacturer.tags.all %}
<li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
{% empty %}
No tags
{% translate 'No tags' %}
{% endfor %}
</ul>
</td>
</tr>
<tr><th>Created at</th><td>{{ manufacturer.created_at }}</td></tr>
<tr><th>{% translate 'Created at' %}</th><td>{{ manufacturer.created_at }}</td></tr>
{% if manufacturer.created_at != manufacturer.changed_at %}
<tr><th>Last change</th><td>{{ manufacturer.changed_at }}</td></tr>
<tr><th>{% translate 'Changed at' %}</th><td>{{ manufacturer.changed_at }}</td></tr>
{% endif %}
</tbody>
</table>
<h2>Items</h2>
<h2>{% translate 'Items' %}</h2>
{% url 'manufacturer-detail' manufacturer.id as this_url %}
{% include "inventory/pagination.html" with url=this_url id="paginator_top" param="item" paginator=items %}

View file

@ -1,18 +1,19 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Manufacturers{% endblock %}
{% block title %}{% translate 'Manufacturers' %}{% endblock %}
{% block header_bar %}
Inventory management - Manufacturers
{% translate 'Inventory Management' %} - {% translate 'Manufacturers' %}
{% endblock %}
{% block content %}
<table class="list">
<thead>
<tr>
<th>Logo</th>
<th>Manufacturer</th>
<th>{% translate 'Icon' %}</th>
<th>{% translate 'Manufacturer' %}</th>
<th></th>
</tr>
</thead>
@ -29,7 +30,7 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_manufacturer_change" object_id=manufacturer.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_manufacturer_change" object_id=manufacturer.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
@ -38,7 +39,7 @@
</table>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_manufacturer_add" %}?layout=1">Create new manufacturer...</a></p>
<p><a href="{% url "admin:inventory_manufacturer_add" %}?layout=1">{% translate 'Create new manufacturer...' %}</a></p>
{% endif %}

View file

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% translate 'Inventory Setup' %}{% endblock %}
{% block header_bar %}
{% translate 'Inventory Setup' %}
{% endblock %}
{% block content %}
<h2>{% translate 'Welcome to the Inventory Management setup' %}</h2>
<p>
{% blocktranslate %}
Currently no admin user is defined in the database.
To use the inventory management system you need at least one admin user...
{% endblocktranslate %}
</p>
<p>
{% blocktranslate %}
Please verify that the following settings are correct and then fill out the
form at the end and click
{% endblocktranslate %}
<em>{% translate 'Create user' %}</em>
</p>
<h2>{% translate 'Current settings' %}</h2>
<dl>
{% for key, value in settings.items %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
<h2>{% translate 'Create user' %}</h2>
{% if form.errors %}
<p>
{% blocktranslate %}
Please correct the errors below.
{% endblocktranslate %}
</p>
{% endif %}
<form action="{% url 'onboarding' %}" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="{% translate 'Create user' %}">
</form>
{% endblock %}

View file

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% translate 'Inventory Setup' %}{% endblock %}
{% block header_bar %}
{% translate 'Inventory Setup' %}
{% endblock %}
{% block content %}
<h2>Onboarding completed!</h2>
<p>Congratulations, you have successfully setup the Inventory management system</p>
<p>You may now log in with the password you just set up.</p>
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
<td>{{ form.username }}</td>
</tr>
<tr>
<td>{{ form.password.label_tag }}</td>
<td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>
{% endblock %}

View file

@ -1,15 +1,16 @@
{% load static %}
{% load i18n %}
<div class="pagination" id="{{ id }}">
{% if paginator.has_previous %}
<a href="{{ url }}?{{ param }}_page=1#{{ id }}"><img src="{% static 'inventory/img/first.svg' %}" class="icon" title="First page"></a>
<a href="{{ url }}?{{ param }}_page={{ paginator.previous_page_number }}#{{ id }}"><img src="{% static 'inventory/img/previous.svg' %}" class="icon" title="Previous page"></a>
<a href="{{ url }}?{{ param }}_page=1#{{ id }}"><img src="{% static 'inventory/img/first.svg' %}" class="icon" title="{% translate 'First page' %}"></a>
<a href="{{ url }}?{{ param }}_page={{ paginator.previous_page_number }}#{{ id }}"><img src="{% static 'inventory/img/previous.svg' %}" class="icon" title="{% translate 'Previous page' %}"></a>
{% endif %}
{% if paginator.paginator.num_pages > 1 %}
{{ paginator.number }}/{{ paginator.paginator.num_pages }}
{% endif %}
{% if paginator.has_next %}
<a href="{{ url }}?{{ param }}_page={{ paginator.next_page_number }}#{{ id }}"><img src="{% static 'inventory/img/next.svg' %}" class="icon" title="Next page"></a>
<a href="{{ url }}?{{ param }}_page={{ paginator.paginator.num_pages}}#{{ id }}"><img src="{% static 'inventory/img/last.svg' %}" class="icon" title="Last page"></a>
<a href="{{ url }}?{{ param }}_page={{ paginator.next_page_number }}#{{ id }}"><img src="{% static 'inventory/img/next.svg' %}" class="icon" title="{% translate 'Next page' %}"></a>
<a href="{{ url }}?{{ param }}_page={{ paginator.paginator.num_pages}}#{{ id }}"><img src="{% static 'inventory/img/last.svg' %}" class="icon" title="{% translate 'Last page' %}"></a>
{% endif %}
</div>

View file

@ -1,15 +1,16 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Search{% endblock %}
{% block title %}{% translate 'Search' %}{% endblock %}
{% block header_bar %}
Search
{% translate 'Search' %}
{% endblock %}
{% block content %}
<form method="get" action="{% url 'search' %}">
<input type="text" id="search" name="q">
<button type="submit">Search</button>
<input type="text" id="search" name="q" placeholder="{% translate 'Search' %}">
<button type="submit">{% translate 'Search' %}</button>
</form>
{% endblock %}

View file

@ -1,20 +1,21 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load hilight %}
{% block title %}Search{% endblock %}
{% block title %}{% translate 'Search' %}{% endblock %}
{% block header_bar %}
Search
{% translate 'Search' %}
{% endblock %}
{% block content %}
<form method="get" action="{% url 'search' %}">
<input type="text" id="search" name="q" value="{{ q }}">
<button type="submit">Search</button>
<input type="text" id="search" name="q" value="{{ q }}" placeholder="{% translate 'Search' %}">
<button type="submit">{% translate 'Search' %}</button>
</form>
<h2>Search result for '{{ q }}'</h2>
<h2>{% translate 'Search result for' %} '{{ q }}'</h2>
{% for result in results %}
<div class="search-result">
@ -23,19 +24,19 @@
<hr>
</div>
{% empty %}
<p>Noting found</p>
<p>{% translate 'Nothing found' %}</p>
{% endfor %}
{% if pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="{% url 'search' %}?q={{ q }}&page=1"><img src="{% static 'inventory/img/first.svg' %}" class="icon" title="First page"></a>
<a href="{% url 'search' %}?q={{ q }}&page={{ page | add:'-1'}}"><img src="{% static 'inventory/img/previous.svg' %}" class="icon" title="Previous page"></a>
<a href="{% url 'search' %}?q={{ q }}&page=1"><img src="{% static 'inventory/img/first.svg' %}" class="icon" title="{% translate 'First page' %}"></a>
<a href="{% url 'search' %}?q={{ q }}&page={{ page | add:'-1'}}"><img src="{% static 'inventory/img/previous.svg' %}" class="icon" title="{% translate 'Previous page' %}"></a>
{% endif %}
{{ page }}/{{ pages }}
{% if page < pages %}
<a href="{% url 'search' %}?q={{ q }}&page={{ page | add:'1'}}"><img src="{% static 'inventory/img/next.svg' %}" class="icon" title="Next page"></a>
<a href="{% url 'search' %}?q={{ q }}&page={{ pages }}"><img src="{% static 'inventory/img/last.svg' %}" class="icon" title="Last page"></a>
<a href="{% url 'search' %}?q={{ q }}&page={{ page | add:'1'}}"><img src="{% static 'inventory/img/next.svg' %}" class="icon" title="{% translate 'Next page' %}"></a>
<a href="{% url 'search' %}?q={{ q }}&page={{ pages }}"><img src="{% static 'inventory/img/last.svg' %}" class="icon" title="{% translate 'Last page' %}"></a>
{% endif %}
</div>
{% endif %}

View file

@ -1,17 +1,16 @@
{% load static %}
{% load i18n %}
{% load hilight %}
<p>
{% if item.documentation.all %}
<a class="datasheet" href="{{ item.documentation.all.0.file.url }}"><img class="icon" src="{% static "inventory/img/datasheet.svg" %}"></a>
<a class="datasheet" href="{{ item.documentation.all.0.file.url }}"><img class="icon" src="{% static "inventory/img/datasheet.svg" %}" title="{% translate 'Datasheet' %}"></a>
{% endif %}
{{ item | hilight:tokens }}
<span class="small">{{item.form_factor.name}}</span>
</p>
<p>Contained in: <a href="{{ item.container_url }}?hilight={{ item.id }}">{{ item.container.display_name }}</a></p>
<p>{% translate 'Contained in' %}: <a href="{{ item.container_url }}?hilight={{ item.id }}">{{ item.container.display_name }}</a></p>
<ul class="tag-list">
{% for tag in item.all_tags %}
<li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
{% empty %}
No tags
{% endfor %}
</ul>

View file

@ -1,4 +1,5 @@
{% load static %}
{% load i18n %}
<li>
<form method="POST" action="{% url "index" %}">
@ -6,9 +7,9 @@
<input type="hidden" name="index_id" value='{{ item.id }}'>
<button class="icon-only-button">
{% if is_index %}
<img class="icon" src="{% static "inventory/img/star-filled.svg" %}">
<img class="icon" src="{% static "inventory/img/star-filled.svg" %}" title="{% translate 'Currently set as index page' %}">
{% else %}
<img class="icon" src="{% static "inventory/img/star-outline.svg" %}">
<img class="icon" src="{% static "inventory/img/star-outline.svg" %}" title="{% translate 'Promote to index page' %}">
{% endif %}
</button>
</form>

View file

@ -1,10 +1,11 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Tag {{ tag.name }}{% endblock %}
{% block title %}{% translate 'Tag' %} {{ tag.name }}{% endblock %}
{% block header_bar %}
<a href="{% url "tag-list" %}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
<a href="{% url "tag-list" %}"><img class="icon" src="{% static "inventory/img/back.svg" %}" title="{% translate 'Back' %}"></a>
{{ tag.name }}
<span class="small">{{ tag.description}}</span>
{% endblock %}
@ -12,14 +13,14 @@
{% block header_icons %}
{% if user.is_staff %}
<li>
<a href="{% url "admin:inventory_tag_change" object_id=tag.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a href="{% url "admin:inventory_tag_change" object_id=tag.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
</li>
{% endif %}
{% endblock %}
{% block content %}
{% if tag.workshop_set.count > 0 %}
<h3>Workshops</h3>
<h3>{% translate 'Workshops' %}</h3>
<table class="list">
<tbody>
{% for workshop in tag.workshop_set.all %}
@ -29,7 +30,7 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=workshop.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=workshop.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
@ -39,7 +40,7 @@
{% endif %}
{% if tag.box_set.count > 0 %}
<h3>Boxes</h3>
<h3>{% translate 'Boxes' %}</h3>
<table class="list">
<tbody>
{% for box in tag.box_set.all %}
@ -49,7 +50,7 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=box.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=box.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
@ -59,7 +60,7 @@
{% endif %}
{% if tag.distributor_set.count > 0 %}
<h3>Distributors</h3>
<h3>{% translate 'Distributors' %}</h3>
<table class="list">
<tbody>
{% for distributor in tag.distributor_set.all %}
@ -74,7 +75,7 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=distributor.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=distributor.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
@ -84,7 +85,7 @@
{% endif %}
{% if tag.manufacturer_set.count > 0 %}
<h3>Manufacturers</h3>
<h3>{% translate 'Manufacturers' %}</h3>
<table class="list">
<tbody>
{% for manufacturer in tag.manufacturer_set.all %}
@ -99,7 +100,7 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=manufacturer.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=manufacturer.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
@ -109,7 +110,7 @@
{% endif %}
{% if items %}
<h3>Items</h3>
<h3>{% translate 'Items' %}</h3>
{% url 'tag-detail' tag.id as this_url %}
{% include "inventory/pagination.html" with url=this_url id="items_paginator_top" param="item" paginator=items %}
{% include "inventory/item_list.html" with items=items show_manufacturer=1 show_distributor=1 %}
@ -117,7 +118,7 @@
{% endif %}
{% if tag.formfactor_set.count > 0 %}
<h3>Form factors</h3>
<h3>{% translate 'Form factors' %}</h3>
<ul class="compact-list">
{% for formfactor in tag.formfactor_set.all %}
<li>{{ formfactor.name }}</li>

View file

@ -1,11 +1,12 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load formatstring %}
{% block title %}Tags{% endblock %}
{% block header_bar %}
Inventory Management - Tags
{% translate 'Inventory management' %} - {% translate 'Tags' %}
{% endblock %}
{% block content %}
@ -39,19 +40,19 @@
}
</script>
<form>
<input type="text" id="tag-search" name="tag-search" placeholder="Search" oninput="updateSearch(event)">
<button onclick="updateSearch(event)">Search</button>
<input type="text" id="tag-search" name="tag-search" placeholder="{% translate 'Search' %}" oninput="updateSearch(event)">
<button onclick="updateSearch(event)">{% translate 'Search' %}</button>
</form>
<ul class="tag-list" id="tagcloud">
{% for tag in object_list %}
<li data-search="{{ tag.name | lower }} {{ tag.description | lower }}"><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
{% empty %}
No tags
{% translate 'No tags' %}
{% endfor %}
</ul>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_tag_add" %}?layout=1">Create new tag...</a></p>
<p><a href="{% url "admin:inventory_tag_add" %}?layout=1">{% translate 'Create new tag...' %}</a></p>
{% endif %}
{% endblock %}

View file

@ -1,10 +1,11 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Workshop {{ workshop.name }}{% endblock %}
{% block title %}{% translate 'Workshop' %} {{ workshop.name }}{% endblock %}
{% block header_bar %}
<a href="{% url "workshop-list" %}"><img class="icon" src="{% static "inventory/img/back.svg" %}"></a>
<a href="{% url "workshop-list" %}"><img class="icon" src="{% static "inventory/img/back.svg" %}" title="{% translate 'Back' %}"></a>
{{ workshop.name }}
<span class="small">{{ workshop.description}}</span>
{% endblock %}
@ -12,14 +13,14 @@
{% block header_icons %}
{% if user.is_staff %}
<li>
<a href="{% url "admin:inventory_workshop_change" object_id=workshop.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}"></a>
<a href="{% url "admin:inventory_workshop_change" object_id=workshop.pk %}"><img class="icon" src="{% static "inventory/img/edit.svg" %}" title="{% translate 'Edit' %}"></a>
</li>
{% include 'inventory/set_index.html' with item=workshop is_index=is_index %}
{% endif %}
{% endblock %}
{% block content %}
<h3>Areas</h3>
<h3>{% translate 'Areas' %}</h3>
<table class="list">
<tbody>
@ -30,22 +31,22 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_area_change" object_id=area.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_area_change" object_id=area.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td>No areas defined</td></tr>
<tr><td>{% translate 'No areas defined' %}</td></tr>
{% endfor %}
</tbody>
</table>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_area_add" %}?container={{ workshop.id }}&index=0">Create new area...</a></p>
<p><a href="{% url "admin:inventory_area_add" %}?container={{ workshop.id }}&index=0">{% translate 'Create new area...' %}</a></p>
{% endif %}
{% if workshop.show_boxes %}
<h3>Containers</h3>
<h3>{% translate 'Containers' %}</h3>
<table class="list">
<tbody>
{% for box in workshop.box_related.all %}
@ -55,18 +56,18 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_box_change" object_id=box.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_box_change" object_id=box.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td>No containers defined</td></tr>
<tr><td>{% translate 'No containers defined' %}</td></tr>
{% endfor %}
</tbody>
</table>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_box_add" %}?container={{ workshop.id }}&index=0">Create new container...</a></p>
<p><a href="{% url "admin:inventory_box_add" %}?container={{ workshop.id }}&index=0">{% translate 'Create new container...' %}</a></p>
{% endif %}
{% endif %}
{% endblock %}

View file

@ -1,10 +1,11 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Workshops{% endblock %}
{% block title %}{% translate 'Workshops' %}{% endblock %}
{% block header_bar %}
Inventory management - Workshops
{% translate 'Inventory management' %} - {% translate 'Workshops' %}
{% endblock %}
{% block content %}
@ -17,7 +18,7 @@
</td>
<td>
{% if user.is_staff %}
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=workshop.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}"></a>
<a class="edit" href="{% url "admin:inventory_workshop_change" object_id=workshop.pk %}"><img class="icon" src="{% static 'inventory/img/edit.svg' %}" title="{% translate 'Edit' %}"></a>
{% endif %}
</td>
</tr>
@ -26,7 +27,7 @@
</table>
{% if user.is_staff %}
<p><a href="{% url "admin:inventory_workshop_add" %}?layout=1">Create new workshop...</a></p>
<p><a href="{% url "admin:inventory_workshop_add" %}?layout=1">{% translate 'Create new workshop...' %}</a></p>
{% endif %}

View file

@ -1,21 +1,30 @@
{% extends "base.html" %}
{% load i18n%}
{% block header_bar %}
Inventory management - Login
{% translate 'Inventory management' %} - {% translate 'Login' %}
{% endblock %}
{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
<p>{% blocktranslate %}Your username and password didn't match. Please try again.{% endblocktranslate %}</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed,
please login with an account that has access.</p>
<p>
{% blocktranslate %}
Your account doesn't have access to this page. To proceed,
please login with an account that has access.
{% endblocktranslate %}
</p>
{% else %}
<p>Please login to see this page.</p>
<p>
{% blocktranslate %}
Please login to see this page.
{% endblocktranslate %}
</p>
{% endif %}
{% endif %}
@ -37,6 +46,6 @@
</form>
{# Assumes you setup the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>
<p><a href="{% url 'password_reset' %}">{% translate 'Lost password?' %}</a></p>
{% endblock %}

View file

@ -0,0 +1,32 @@
from django import template
from django.utils.safestring import mark_safe
from django.utils.formats import number_format
from inventory.models import Settings
register = template.Library()
s = None
@register.filter(name='currency')
def currency(value, format):
global s
if s is None:
s = Settings.objects.first()
value = float(value)
if format == 'detail':
value = number_format(round(value, 3), 3, use_l10n=True)
else:
value = number_format(round(value, 2), 2, use_l10n=True)
if format == 'long' or format == 'detail':
symbol = s.currency
else:
symbol = s.currency_symbol
if s.currency_symbol_position:
result = f"{value} {symbol}"
else:
result = f"{symbol} {value}"
return mark_safe(result)

View file

@ -34,7 +34,8 @@ from .views import (
ManufacturerView,
IndexView,
TagView,
SearchView
SearchView,
OnboardingView
)
urlpatterns = [
@ -54,5 +55,6 @@ urlpatterns = [
path('tags', TagListView.as_view(), name='tag-list'),
path('tag/<int:pk>', TagView.as_view(), name='tag-detail'),
path('search', SearchView.as_view(), name='search'),
path('onboarding', OnboardingView.as_view(), name='onboarding'),
path('', IndexView.as_view(), name='index')
]

View file

@ -7,6 +7,7 @@ from .workshop import WorkshopView, WorkshopListView
from .index import IndexView
from .tag import TagListView, TagView
from .search import SearchView
from .onboarding import OnboardingView
__all__ = [
AreaView, AreaListView,
@ -17,5 +18,6 @@ __all__ = [
WorkshopView, WorkshopListView,
IndexView,
TagView, TagListView,
SearchView
SearchView,
OnboardingView
]

View file

@ -4,7 +4,7 @@ from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView
from django.db.models import QuerySet
from inventory.models import Box
from inventory.models import Box, Settings
from .utils import CanBeIndexMixin
@ -54,6 +54,7 @@ class BoxView(CanBeIndexMixin, DetailView):
context = self.get_context_data(object=self.object)
context['layouted'], _ = self.layout(self.object.item_related.all().order_by('index'), self.object.layout.data)
context['hilight'] = hilighted
context['settings'] = Settings.objects.first()
return self.render_to_response(context)

View file

@ -3,14 +3,24 @@ from django.shortcuts import redirect
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import View
from django.contrib.auth import get_user_model
from django.contrib.auth.views import redirect_to_login
from inventory.models import Settings
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name='post')
class IndexView(View):
def get(self, request):
User = get_user_model()
if User.objects.all().count() == 0:
# redirect to onboarding
return redirect(reverse('onboarding'))
if not request.user.is_authenticated:
path = request.get_full_path()
return redirect_to_login(path, reverse('login'))
# check settings for correct starred index page
settings = Settings.objects.first()
if settings.default_container is not None:
return redirect(settings.default_container.url)

View file

@ -1,15 +1,30 @@
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView
from django.shortcuts import get_object_or_404
from inventory.models import Item
from inventory.models import Item, Settings
@method_decorator(login_required, name='dispatch')
class ItemView(DetailView):
context_object_name = 'item'
queryset = Item.objects.all().select_related('container', 'container__layout', 'manufacturer', 'distributor').prefetch_related('documentation')
def get_context_data(self, *args, object_list=None, **kwargs):
result = super().get_context_data(*args, object_list=object_list, **kwargs)
result['settings'] = Settings.objects.first()
return result
def post(self, request, pk):
item = get_object_or_404(Item.objects.filter(pk=pk))
if request.POST.get('dec', None) is not None:
if item.count > 0:
item.count -= 1
item.save()
if request.POST.get('save', None) is not None:
item.count = request.POST.get('amount', 0)
item.save()
return self.get(request)
@method_decorator(login_required, name='dispatch')
class ItemListView(ListView):

View file

@ -0,0 +1,60 @@
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.template.response import TemplateResponse
from django.shortcuts import redirect
from django.views.generic import View
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import AuthenticationForm
from django.conf import settings
class OnboardingForm(forms.Form):
username = forms.CharField(label=_("Username"), max_length=150, required=True)
email = forms.EmailField(label=_("Email"), max_length=254, required=True)
password = forms.CharField(label=_("Password"), max_length=1024, min_length=8, widget=forms.PasswordInput(), required=True)
class OnboardingView(View):
def get(self, request):
User = get_user_model()
if User.objects.all().count() != 0:
# redirect to index
return redirect(reverse('index'))
return TemplateResponse(request, "inventory/onboarding.html", {
"settings": {
"SERVER_URL": settings.SERVER_URL,
"DEBUG": settings.DEBUG,
"ALLOWED_HOSTS": settings.ALLOWED_HOSTS,
"DATABASE_HOST": settings.DATABASES['default']['HOST'],
"DATABASE_NAME": settings.DATABASES['default']['NAME'],
"DATABASE_USER": settings.DATABASES['default']['USER'],
"DATABASE_PASSWORD": settings.DATABASES['default']['PASSWORD'],
"LANGUAGE_CODE": settings.LANGUAGE_CODE,
"TIME_ZONE": settings.TIME_ZONE,
"STATIC_ROOT": settings.STATIC_ROOT,
"MEDIA_ROOT": settings.MEDIA_ROOT,
"PAGE_SIZE": settings.PAGE_SIZE,
},
"form": OnboardingForm()
})
def post(self, request):
# validate we have everything
form = OnboardingForm(request.POST)
if form.is_valid():
# create superuser
User = get_user_model()
User.objects.create_superuser(
form.cleaned_data['username'],
form.cleaned_data['email'],
form.cleaned_data['password']
)
# show success screen
login_form = AuthenticationForm(data={"username": form.cleaned_data['username'] })
return TemplateResponse(request, "inventory/onboarding_success.html", {"form": login_form, "next": reverse('index')})
return TemplateResponse(request, "inventory/onboarding.html", {"settings": settings, "form": form})

View file

@ -10,10 +10,13 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
from typing import List
import os
import sys
import asyncio
import socket
from urllib.parse import urlparse
from uuid import uuid4
from django.utils.translation import gettext_lazy as _
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
@ -22,19 +25,34 @@ if sys.platform == 'win32':
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Externally visible URL of the server
SERVER_URL = "http://127.0.0.1:8000"
SERVER_URL = os.environ.get('INVENTORY_EXTERNAL_URL', "http://127.0.0.1:8000")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'nqo*a(^g$8#0%&+*_7#b_7ybn-znk4#=45_(qy-lq-^v675pqk'
SECRET_KEY = os.environ.get('INVENTORY_SECRET_KEY', uuid4().hex)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = (os.environ.get('INVENTORY_DEBUG', "FALSE") == "TRUE")
ALLOWED_HOSTS: List[str] = []
parsed_url = urlparse(SERVER_URL)
ALLOWED_HOSTS: list[str] = [
'.localhost',
'127.0.0.1',
'[::1]',
parsed_url.hostname,
socket.gethostbyname('localhost')
]
CSRF_TRUSTED_ORIGINS: list[str] = [
*[
f'{prot}://{host}'
for host in os.environ.get("INVENTORY_TRUSTED_ORIGINS", "localhost").split(',')
for prot in ('http', 'https')
],
f'{SERVER_URL}'
]
# Application definition
@ -88,9 +106,10 @@ ASGI_APPLICATION = 'inventory_project.asgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'inventory',
'USER': 'inventory',
'PASSWORD': 'inventory'
'HOST': os.environ.get('INVENTORY_DB_HOST', 'localhost'),
'NAME': os.environ.get('INVENTORY_DB_NAME', 'inventory'),
'USER': os.environ.get('INVENTORY_DB_USER', 'inventory'),
'PASSWORD': os.environ.get('INVENTORY_DB_PASSWORD', 'inventory')
}
}
@ -116,10 +135,14 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGES = [
("de", _("German")),
("en", _("English")),
]
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = os.environ.get('INVENTORY_LANGUAGE', 'en-us')
TIME_ZONE = 'UTC'
TIME_ZONE = os.environ.get('INVENTORY_TIMEZONE', 'UTC')
USE_I18N = True
@ -132,13 +155,13 @@ USE_TZ = True
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_ROOT = os.environ.get('INVENTORY_STATIC_FILES', os.path.join(BASE_DIR, 'static'))
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
SERVE_MEDIA_FILES = DEBUG
MEDIA_ROOT = os.environ.get('INVENTORY_MEDIA_FILES', os.path.join(BASE_DIR, 'media'))
SERVE_MEDIA_FILES = True
# Default page size for paginated content
PAGE_SIZE = 25
PAGE_SIZE = int(os.environ.get('INVENTORY_PAGE_SIZE', "25"))

46
poetry.lock generated
View file

@ -126,22 +126,23 @@ pyflakes = ">=3.2.0,<3.3.0"
[[package]]
name = "gunicorn"
version = "20.1.0"
version = "23.0.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.7"
files = [
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
{file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
{file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
]
[package.dependencies]
setuptools = ">=3.0"
packaging = "*"
[package.extras]
eventlet = ["eventlet (>=0.24.1)"]
eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"]
[[package]]
@ -249,6 +250,17 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "pillow"
version = "10.4.0"
@ -521,26 +533,6 @@ files = [
{file = "python_monkey_business-1.1.0-py2.py3-none-any.whl", hash = "sha256:15b4f603c749ba9a7b4f1acd36af023a6c5ba0f7e591c945f8253f0ef44bf389"},
]
[[package]]
name = "setuptools"
version = "75.6.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
files = [
{file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"},
{file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"]
core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"]
[[package]]
name = "sqlparse"
version = "0.5.3"
@ -647,4 +639,4 @@ brotli = ["brotli"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "9fa0905357cc8444a092294c75ee8b373078c78723c6c345cf56a59091cf0ce7"
content-hash = "41ac47a86e2c341e2862f4309757b703ab5b6d4939a664ec1289611a0191228b"

View file

@ -7,11 +7,11 @@ authors = ["Johannes Schriewer <hallo@dunkelstern.de>"]
[tool.poetry.dependencies]
python = "^3.10"
django = "^5"
gunicorn = "^20"
gunicorn = "^23"
whitenoise = "^6.6"
commentjson = "^0.9"
psycopg = { version = "^3", extras = [ "binary" ] }
django-nested-admin = "^4.0.0"
django-nested-admin = "^4"
pillow = "^10"
[tool.poetry.group.dev.dependencies]