Compare commits

..

11 commits

72 changed files with 1681 additions and 326 deletions

8
.gitignore vendored
View file

@ -3,7 +3,11 @@ __pycache__
.python_version .python_version
*.egg-info *.egg-info
media/ media/
./static/ /static/
media.tar.gz media.tar.gz
*.pg *.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 currently need. It has been optimized to store information for electronics
parts and small other hardware like screws, nuts and bolts. 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: As configured by default you will need the following:
- A postgres database named `inventory` with a postgres user `inventory` that - A postgres database named `inventory` with a postgres user `inventory` that
may connect without password or by default with the password `inventory` may connect without password or by default with the password `inventory`
### Installation (manual)
You will need:
- Python > 3.10 - Python > 3.10
- Poetry to install requirements and create a virtualenv - 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 This is a standard Django 5.1 application, if you know how to deploy those the
following might sound familiar: following might sound familiar:
@ -27,16 +29,103 @@ following might sound familiar:
- Github `git clone https://github.com/dunkelstern/inventory.git` - Github `git clone https://github.com/dunkelstern/inventory.git`
- ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git` - ForgeJo: `git clone https://git.dunkelstern.de/dunkelstern/inventory.git`
2. Change to checkout: `cd inventory` 2. Change to checkout: `cd inventory`
3. Install virtualenv and dependencies: `poetry install --no-root` 3. Install virtualenv and dependencies:
4. Migrate the Database: `poetry run python manage.py migrate` ```
5. Create an admin user: `poetry run python manage.py createsuperuser` poetry install --no-root
6. Run the server ```
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` - 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` - 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 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 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 ### Additional information
1. The initial DB migration pre-populates the database with some useful defaults 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 [the service file](inventory.service) in the root of this repository for an
example. 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 ### Screenshots
#### Login #### 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.contrib import admin
from django.conf import settings from django.conf import settings
from inventory.models import Item, Documentation from inventory.models import Item, Documentation, Settings
class DocumentationAdmin(admin.ModelAdmin): class DocumentationAdmin(admin.ModelAdmin):
@ -19,6 +19,12 @@ class ItemAdmin(admin.ModelAdmin):
readonly_fields = ['created_at', 'changed_at'] readonly_fields = ['created_at', 'changed_at']
filter_horizontal = ('tags', 'documentation') 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): def view_on_site(self, obj):
url = reverse('item-detail', kwargs={'pk': obj.id}) url = reverse('item-detail', kwargs={'pk': obj.id})
return settings.SERVER_URL + url 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.urls import reverse
from django.db import models from django.db import models
from .container import Container, CanBeContained from .container import Container, CanBeContained
class Area(CanBeContained, Container): class Area(CanBeContained, Container):
name = models.CharField(max_length=255, unique=True) name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(max_length=4096) description = models.CharField(_("Description"), max_length=4096)
show_sub_area = models.BooleanField(default=True, help_text="Allow sub-areas to be defined in this area") show_sub_area = models.BooleanField(
created_at = models.DateTimeField(auto_now_add=True) _("Show sub area"),
changed_at = models.DateTimeField(auto_now=True) 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 @property
def url(self): def url(self):
@ -16,3 +21,5 @@ class Area(CanBeContained, Container):
class Meta: class Meta:
ordering = ("name", ) 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.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.template.loader import get_template, TemplateDoesNotExist from django.template.loader import get_template, TemplateDoesNotExist
@ -6,15 +7,12 @@ from .container import CanBeContained, Container
class Box(CanBeContained, Container): class Box(CanBeContained, Container):
name = models.CharField(max_length=255, unique=True) name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(max_length=4096) description = models.CharField(_("Description"), max_length=4096)
tags = models.ManyToManyField('inventory.Tag', blank=True) tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True) changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
class Meta:
verbose_name_plural = 'Boxes'
@property @property
def template_name(self): def template_name(self):
@ -34,3 +32,5 @@ class Box(CanBeContained, Container):
class Meta: class Meta:
ordering = ("name", ) 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.db import models
from django.apps import apps from django.apps import apps
class Container(models.Model): 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 @property
def subclass(self): def subclass(self):
@ -25,7 +32,7 @@ class Container(models.Model):
_, obj = self.subclass _, obj = self.subclass
if obj is not None: if obj is not None:
return obj.name return obj.name
return 'Container' return _("Container")
@property @property
def url(self): def url(self):
@ -34,15 +41,19 @@ class Container(models.Model):
return obj.url return obj.url
return None return None
class Meta:
verbose_name = _("Container")
verbose_name_plural = _("Containers")
class CanBeContained(models.Model): class CanBeContained(models.Model):
container = models.ForeignKey( container = models.ForeignKey(
'inventory.Container', 'inventory.Container',
verbose_name=_("Container"),
related_name="%(class)s_related", related_name="%(class)s_related",
null=True, null=True,
on_delete=models.CASCADE on_delete=models.CASCADE
) )
index = models.PositiveIntegerField('Index of compartment in layout') index = models.PositiveIntegerField(_("Index of compartment in layout"))
class Meta: class Meta:
abstract = True abstract = True

View file

@ -1,22 +1,25 @@
from django.utils.translation import gettext_lazy as _
from django.db import models from django.db import models
class Distributor(models.Model): class Distributor(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(max_length=4096) description = models.CharField(_("Description"), max_length=4096)
web_link = models.URLField(null=True, blank=True) web_link = models.URLField(_("Web link"), null=True, blank=True)
search_link = models.URLField(help_text='Use {} for search placeholder', null=True, blank=True) search_link = models.URLField(_("Search link"), help_text=_("Use {} for search placeholder"), null=True, blank=True)
phone = models.CharField(max_length=128, null=True, blank=True) phone = models.CharField(_("Phone"), max_length=128, null=True, blank=True)
email = models.EmailField(null=True, blank=True, default=None) email = models.EmailField(_("E-Mail"), null=True, blank=True, default=None)
icon = models.ImageField(null=True, blank=True) icon = models.ImageField(_("Icon"), null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', blank=True) tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True) changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self): def __str__(self):
return self.name return self.name
class Meta: class Meta:
ordering = ("name", ) 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 from django.db import models
class Documentation(models.Model): class Documentation(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(_("Name"), max_length=255, unique=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True) changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
file = models.FileField() file = models.FileField()
@ -13,3 +14,5 @@ class Documentation(models.Model):
class Meta: class Meta:
ordering = ("name", ) 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 from django.db import models
class FormFactor(models.Model): class FormFactor(models.Model):
name = models.CharField(max_length=255, unique=True, db_collation="numeric") name = models.CharField(_("Name"), max_length=255, unique=True, db_collation="numeric")
description = models.CharField(max_length=4096, db_collation="numeric") description = models.CharField(_("Description"), max_length=4096, db_collation="numeric")
icon = models.ImageField(null=True, blank=True) icon = models.ImageField(_("Icon"), null=True, blank=True)
datasheet = models.FileField(null=True, blank=True) datasheet = models.FileField(_("Datasheet"), null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', blank=True) tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True) changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self): def __str__(self):
items = [self.name] items = [self.name]
@ -19,3 +20,5 @@ class FormFactor(models.Model):
class Meta: class Meta:
ordering = ("name", ) 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 django.db import models
from .container import CanBeContained from .container import CanBeContained
class Item(CanBeContained): class Item(CanBeContained):
name = models.TextField(max_length=255, db_collation="numeric") name = models.TextField(_("Name"), max_length=255, db_collation="numeric")
description = models.CharField(max_length=4096, db_collation="numeric") description = models.CharField(_("Description"), max_length=4096, db_collation="numeric")
size = models.PositiveIntegerField(default=1, help_text="Number of sub-compartments this item takes up") size = models.PositiveIntegerField(
form_factor = models.ForeignKey('inventory.FormFactor', null=True, blank=True, on_delete=models.PROTECT) _("Size"),
manufacturer = models.ForeignKey('inventory.Manufacturer', null=True, blank=True, on_delete=models.PROTECT) default=1,
distributor = models.ForeignKey('inventory.Distributor', null=True, blank=True, on_delete=models.PROTECT) help_text=_("Number of sub-compartments this item takes up")
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) form_factor = models.ForeignKey(
last_ordered_on = models.DateField(null=True, blank=True) 'inventory.FormFactor',
documentation = models.ManyToManyField('inventory.Documentation', related_name='items', blank=True) verbose_name=_("Form factor"),
tags = models.ManyToManyField('inventory.Tag', blank=True) 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) count = models.PositiveIntegerField(
created_at = models.DateTimeField(auto_now_add=True) _("Count"),
changed_at = models.DateTimeField(auto_now=True) 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): def __str__(self):
items = [self.name, self.description] items = [self.name, self.description]
@ -32,6 +88,12 @@ class Item(CanBeContained):
return list(self.tags.all()) + list(self.form_factor.tags.all()) return list(self.tags.all()) + list(self.form_factor.tags.all())
else: else:
return list(self.tags.all()) return list(self.tags.all())
@property
def value(self):
return self.count * self.price
class Meta: class Meta:
ordering = ("name", ) 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 from django.db import models
class Layout(models.Model): class Layout(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(max_length=4096) description = models.CharField(_("Description"), max_length=4096)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True) changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
data = models.JSONField() data = models.JSONField(_("Data"))
template_name = models.CharField(max_length=255, null=True, blank=True) template_name = models.CharField(_("Template name"), max_length=255, null=True, blank=True)
def __str__(self): def __str__(self):
return self.name return self.name
class Meta: class Meta:
ordering = ("name", ) 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 from django.db import models
class Manufacturer(models.Model): class Manufacturer(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(max_length=4096, blank=True) description = models.CharField(_("Description"), max_length=4096, blank=True)
web_link = models.URLField(null=True, blank=True) web_link = models.URLField(_("Web link"), null=True, blank=True)
icon = models.ImageField(null=True, blank=True) icon = models.ImageField(_("Icon"), null=True, blank=True)
tags = models.ManyToManyField('inventory.Tag', blank=True) tags = models.ManyToManyField('inventory.Tag', verbose_name=_("Tags"), blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True) changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self): def __str__(self):
return self.name return self.name
class Meta: class Meta:
ordering = ("name", ) 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 from django.db import models
@ -8,11 +10,23 @@ class Settings(models.Model):
default=None, default=None,
null=True, null=True,
blank=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): def __str__(self):
return 'Settings' return gettext("Settings")
class Meta: 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 from django.db import models
class Tag(models.Model): class Tag(models.Model):
name = models.CharField(max_length=255, unique=True, db_collation="numeric") name = models.CharField(_("Name"), max_length=255, unique=True, db_collation="numeric")
description = models.CharField(max_length=4096, db_collation="numeric") description = models.CharField(_("Description"), max_length=4096, db_collation="numeric")
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True) changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
def __str__(self): def __str__(self):
return self.name return self.name
class Meta: class Meta:
ordering = ['name', 'pk'] 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.urls import reverse
from django.db import models from django.db import models
from .container import Container from .container import Container
class Workshop(Container): class Workshop(Container):
name = models.CharField(max_length=255, unique=True) name = models.CharField(_("Name"), max_length=255, unique=True)
description = models.CharField(max_length=4096) description = models.CharField(_("Description"), max_length=4096)
show_boxes = models.BooleanField(default=True, help_text="Allow boxes to be defined directly in this workshop") show_boxes = models.BooleanField(
tags = models.ManyToManyField('inventory.Tag', blank=True) _("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) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
changed_at = models.DateTimeField(auto_now=True) changed_at = models.DateTimeField(_("Changed at"), auto_now=True)
@property @property
def url(self): def url(self):
@ -18,3 +23,5 @@ class Workshop(Container):
class Meta: class Meta:
ordering = ("name", ) ordering = ("name", )
verbose_name = _("Workshop")
verbose_name_plural = _("Workshops")

View file

@ -89,6 +89,24 @@ td.disabled {
opacity: 0.75; 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 { .cell .form_factor {
position: absolute; position: absolute;
display: inline; display: inline;
@ -153,7 +171,13 @@ td.disabled {
top: 3px; top: 3px;
right: 7px; right: 7px;
} }
.cell .stock {
font-size: 75%;
top: 3px;
left: 3px;
}
.cell .price { .cell .price {
font-size: 75%; font-size: 75%;
bottom: 3px; bottom: 3px;

View file

@ -26,6 +26,11 @@ table.attribute-list th {
border-style: solid; border-style: solid;
} }
table.attribute-list .small {
color: #808080;
font-size: xx-small;
}
table.box { table.box {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@ -158,8 +163,7 @@ img.icon {
margin-right: 10px; margin-right: 10px;
width: 18px; width: 18px;
height: 18px; height: 18px;
position: relative; vertical-align: middle;
top: 2px;
} }
img.logo { img.logo {
@ -277,9 +281,6 @@ table.list thead th {
padding: 10px; padding: 10px;
} }
table.list tbody tr {
}
table.list tbody td { table.list tbody td {
border-bottom: 1px solid black; border-bottom: 1px solid black;
padding: 10px; padding: 10px;
@ -322,3 +323,26 @@ table.list tbody td {
font-weight: normal; font-weight: normal;
color: #808080; 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 static %}
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="{{ LANGUAGE_CODE }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1"> <meta name="viewport" content="initial-scale=1, maximum-scale=1">
@ -38,32 +40,32 @@
} }
} }
</script> </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"> <div id="search-container">
<form action="{% url 'search' %}" method="get"> <form action="{% url 'search' %}" method="get">
<input name="q" id="search" type="text" placeholder="Search"> <input name="q" id="search" type="text" placeholder="{% translate 'Search' %}">
<button type="submit">Search</button> <button type="submit">{% translate 'Search' %}</button>
</form> </form>
</div> </div>
</li> </li>
<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>
<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>
<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>
<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> </li>
</ul> </ul>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<form method="POST" action="{% url "logout" %}"> <form method="POST" action="{% url "logout" %}">
{% csrf_token %} {% 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> </form>
{% endif %} {% endif %}
</nav> </nav>

View file

@ -1,5 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}{{ area.name }}{% endblock %} {% block title %}{{ area.name }}{% endblock %}
@ -12,7 +13,7 @@
{% block header_icons %} {% block header_icons %}
{% if user.is_staff %} {% if user.is_staff %}
<li> <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> </li>
{% include 'inventory/set_index.html' with item=area is_index=is_index %} {% include 'inventory/set_index.html' with item=area is_index=is_index %}
{% endif %} {% endif %}
@ -21,7 +22,7 @@
{% block content %} {% block content %}
{% if area.show_sub_area %} {% if area.show_sub_area %}
<h3>Areas</h3> <h3>{% translate 'Areas' %}</h3>
<table class="list"> <table class="list">
<tbody> <tbody>
@ -32,22 +33,22 @@
</td> </td>
<td> <td>
{% if user.is_staff %} {% 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 %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td>No areas defined</td></tr> <tr><td>{% translate 'No areas defined' %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if user.is_staff %} {% 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 %}
{% endif %} {% endif %}
<h3>Containers</h3> <h3>{% translate 'Containers' %}</h3>
<table class="list"> <table class="list">
<tbody> <tbody>
@ -58,17 +59,17 @@
</td> </td>
<td> <td>
{% if user.is_staff %} {% 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 %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td>No containers defined</td></tr> <tr><td>{% translate 'No containers defined' %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if user.is_staff %} {% 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 %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,5 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block head %} {% block head %}
<link rel="stylesheet" type="text/css" href="{% static "inventory/css/cell.css" %}"> <link rel="stylesheet" type="text/css" href="{% static "inventory/css/cell.css" %}">
@ -8,7 +9,7 @@
{% block title %}{{ object.name }}{% endblock %} {% block title %}{{ object.name }}{% endblock %}
{% block header_bar %} {% 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 }} {{ object.name }}
<span class="small">{{ object.description}}</span></h2> <span class="small">{{ object.description}}</span></h2>
{% endblock %} {% endblock %}
@ -16,7 +17,7 @@
{% block header_icons %} {% block header_icons %}
{% if user.is_staff %} {% if user.is_staff %}
<li> <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> </li>
{% include 'inventory/set_index.html' with item=object is_index=is_index %} {% include 'inventory/set_index.html' with item=object is_index=is_index %}
{% endif %} {% endif %}

View file

@ -1,9 +1,10 @@
{% extends "inventory/box-detail.html" %} {% extends "inventory/box-detail.html" %}
{% load i18n %}
{% block content %} {% block content %}
{% if box.box_related.exists %} {% if box.box_related.exists %}
<h3>Boxes</h3> <h3>{% translate 'Boxes' %}</h3>
<ul> <ul>
{% for b in box.box_related.all %} {% 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> <li><a href="{% url 'box-detail' b.id %}" title="{{ b.description }}">{{ b.name }} ({{ b.layout.name }})</a></li>
@ -12,7 +13,7 @@
{% endif %} {% endif %}
{% if box.item_related.exists %} {% if box.item_related.exists %}
<h3>Items</h3> <h3>{% translate 'Items' %}</h3>
<ul> <ul>
{% for item in box.item_related.all %} {% 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> <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"> <td rowspan="2">
<div class="compartments-vertical"> <div class="compartments-vertical">
{% for compartment in layouted.0.0 %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -17,7 +17,7 @@
<td class="triple-height" colspan="3"> <td class="triple-height" colspan="3">
<div class="compartments"> <div class="compartments">
{% for compartment in layouted.0.1 %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -27,7 +27,7 @@
<td class="triple-height"> <td class="triple-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -9,7 +9,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in layouted.0 %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -10,7 +10,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -20,7 +20,7 @@
<td rowspan="2"> <td rowspan="2">
<div class="compartments-vertical"> <div class="compartments-vertical">
{% for compartment in layouted.1.0 %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -29,7 +29,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -40,7 +40,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -11,7 +11,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -27,7 +27,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -42,7 +42,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -11,7 +11,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -27,7 +27,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -42,7 +42,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -11,7 +11,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -27,7 +27,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -42,7 +42,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -11,7 +11,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -10,7 +10,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -21,7 +21,7 @@
<td> <td>
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -10,7 +10,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -21,7 +21,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -32,7 +32,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -45,7 +45,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -10,7 +10,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -21,7 +21,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -32,7 +32,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>
@ -45,7 +45,7 @@
<td class="double-height"> <td class="double-height">
<div class="compartments"> <div class="compartments">
{% for compartment in item %} {% 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 %} {% endfor %}
</div> </div>
</td> </td>

View file

@ -1,5 +1,8 @@
{% load static %} {% load static %}
{% load i18n %}
{% load admin_urls %} {% load admin_urls %}
{% load currency %}
<div class="cell{% if hilight == item.id %} search-hilight{% endif %}"> <div class="cell{% if hilight == item.id %} search-hilight{% endif %}">
{% if item.name %} {% if item.name %}
{% if item.metadata.package %} {% if item.metadata.package %}
@ -10,16 +13,23 @@
</a> </a>
<div class="cell-buttons"> <div class="cell-buttons">
{% if item.documentation.all %} {% 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 %} {% endif %}
{% if user.is_staff %} {% 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 %} {% 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> </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 %} {% if item.price %}
<div class="price">{{ item.price | floatformat:2 }} &euro;</div> <div class="price">{{ item.price | currency:"short" }}</div>
{% endif %} {% endif %}
{% if item.form_factor %} {% if item.form_factor %}
<div class="form_factor">{{ item.form_factor.name }}</div> <div class="form_factor">{{ item.form_factor.name }}</div>
@ -30,7 +40,7 @@
{% else %} {% else %}
<div class="cell-buttons"> <div class="cell-buttons">
{% if user.is_staff %} {% 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 %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View file

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

View file

@ -1,18 +1,19 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %}
{% load static %} {% load static %}
{% block title %}Distributors{% endblock %} {% block title %}{% translate 'Distributors' %}{% endblock %}
{% block header_bar %} {% block header_bar %}
Inventory management - Distributors {% translate 'Inventory management' %} - {% translate 'Distributors' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<table class="list"> <table class="list">
<thead> <thead>
<tr> <tr>
<th class="icon">Logo</th> <th class="icon">{% translate 'Icon' %}</th>
<th>Distributor</th> <th>{% translate 'Distributor' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -29,7 +30,7 @@
</td> </td>
<td> <td>
{% if user.is_staff %} {% 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 %} {% endif %}
</td> </td>
</tr> </tr>
@ -39,7 +40,7 @@
{% if user.is_staff %} {% 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 %} {% endif %}

View file

@ -1,11 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% load formatstring %} {% load formatstring %}
{% load currency %}
{% block title %}{{ item }}{% endblock %} {% block title %}{{ item }}{% endblock %}
{% block header_bar %} {% 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 }} {{ item.name }}
<span class="small">{{ item.description }}{% if item.form_factor %}, {{ item.form_factor }}{% endif %}</span></h2> <span class="small">{{ item.description }}{% if item.form_factor %}, {{ item.form_factor }}{% endif %}</span></h2>
{% endblock %} {% endblock %}
@ -13,7 +15,7 @@
{% block header_icons %} {% block header_icons %}
{% if user.is_staff %} {% if user.is_staff %}
<li> <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> </li>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@ -22,21 +24,40 @@
<table class="attribute-list"> <table class="attribute-list">
<tbody> <tbody>
<tr> <tr>
<th>Name</th> <th>{% translate 'Name' %}</th>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
</tr> </tr>
<tr> <tr>
<th>Description</th> <th>{% translate 'Description' %}</th>
<td>{{ item.description }}</td> <td>{{ item.description }}</td>
</tr> </tr>
{% if settings.track_amount %}
<tr> <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> <td>
<ul class="tag-list"> <ul class="tag-list">
{% for tag in item.all_tags %} {% for tag in item.all_tags %}
<li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li> <li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
{% empty %} {% empty %}
No tags {% translate 'No tags' %}
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
@ -44,7 +65,7 @@
{% if item.form_factor %} {% if item.form_factor %}
<tr> <tr>
<th>Form factor</th> <th>{% translate 'Form factor' %}</th>
<td> <td>
{% if item.form_factor.datasheet %} {% if item.form_factor.datasheet %}
<a href="{% url 'distributor-detail' item.distributor.id %}"> <a href="{% url 'distributor-detail' item.distributor.id %}">
@ -61,7 +82,7 @@
{% if item.manufacturer %} {% if item.manufacturer %}
<tr> <tr>
<th>Manufacturer</th> <th>{% translate 'Manufacturer' %}</th>
<td> <td>
<a href="{% url 'manufacturer-detail' item.manufacturer.id %}"> <a href="{% url 'manufacturer-detail' item.manufacturer.id %}">
{% if item.manufacturer.icon %}<img src="{{ item.manufacturer.icon.url }}" class="icon">{% endif %}{{ item.manufacturer.name }} {% if item.manufacturer.icon %}<img src="{{ item.manufacturer.icon.url }}" class="icon">{% endif %}{{ item.manufacturer.name }}
@ -72,7 +93,7 @@
{% if item.distributor %} {% if item.distributor %}
<tr> <tr>
<th>Distributor</th> <th>{% translate 'Distributor' %}</th>
<td> <td>
<a href="{% url 'distributor-detail' item.distributor.id %}"> <a href="{% url 'distributor-detail' item.distributor.id %}">
{% if item.distributor.icon %}<img src="{{ item.distributor.icon.url }}" class="icon">{% endif %}{{ item.distributor.name }} {% if item.distributor.icon %}<img src="{{ item.distributor.icon.url }}" class="icon">{% endif %}{{ item.distributor.name }}
@ -83,35 +104,35 @@
{% if item.price %} {% if item.price %}
<tr> <tr>
<th>Price</th> <th>{% translate 'Price' %}</th>
<td>{{ item.price }} Euro</td> <td>{{ item.price | currency:"detail" }} <span class="small">{% if settings.track_amount %}({% translate 'Sum' %}: {{ item.value | currency:"long" }}){% endif %}</span></td>
</tr> </tr>
{% endif %} {% endif %}
{% if item.last_ordered_on %} {% if item.last_ordered_on %}
<tr> <tr>
<th>Last ordered</th> <th>{% translate 'Last ordered' %}</th>
<td>{{ item.last_ordered_on }}</td> <td>{{ item.last_ordered_on }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if item.distributor_item_no %} {% if item.distributor_item_no %}
<tr> <tr>
<th>Link</th> <th>{% translate 'Search link' %}</th>
<td> <td>
<a href="{% formatstring item.distributor.search_link item.distributor_item_no %}">{% formatstring item.distributor.search_link item.distributor_item_no %}</a> <a href="{% formatstring item.distributor.search_link item.distributor_item_no %}">{% formatstring item.distributor.search_link item.distributor_item_no %}</a>
</td> </td>
</tr> </tr>
{% endif %} {% 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 %} {% 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 %} {% endif %}
{% if item.documentation.exists %} {% if item.documentation.exists %}
<tr> <tr>
<th>Datasheets</th> <th>{% translate 'Documentation' %}</th>
<td> <td>
<ul> <ul>
{% for doc in item.documentation.all %} {% for doc in item.documentation.all %}

View file

@ -1,17 +1,18 @@
{% load i18n %}
{% load static %} {% load static %}
<table class="list"> <table class="list">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>{% translate 'Name' %}</th>
<th></th> <th></th>
<th>Description</th> <th>{% translate 'Description' %}</th>
<th>Container</th> <th>{% translate 'Container' %}</th>
{% if show_manufacturer %} {% if show_manufacturer %}
<th>Manufacturer</th> <th>{% translate 'Manufacturer' %}</th>
{% endif %} {% endif %}
{% if show_distributor %} {% if show_distributor %}
<th>Distributor</th> <th>{% translate 'Distributor' %}</th>
{% endif %} {% endif %}
</tr> </tr>
</thead> </thead>
@ -21,10 +22,10 @@
<td><a href="{% url 'item-detail' item.id %}">{{ item.name }}</a></td> <td><a href="{% url 'item-detail' item.id %}">{{ item.name }}</a></td>
<td class="right"> <td class="right">
{% if item.documentation.all %} {% 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 %} {% endif %}
{% if user.is_staff %} {% 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 %} {% endif %}
</td> </td>
<td>{{ item.description }}</td> <td>{{ item.description }}</td>

View file

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

View file

@ -1,18 +1,19 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}Manufacturers{% endblock %} {% block title %}{% translate 'Manufacturers' %}{% endblock %}
{% block header_bar %} {% block header_bar %}
Inventory management - Manufacturers {% translate 'Inventory Management' %} - {% translate 'Manufacturers' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<table class="list"> <table class="list">
<thead> <thead>
<tr> <tr>
<th>Logo</th> <th>{% translate 'Icon' %}</th>
<th>Manufacturer</th> <th>{% translate 'Manufacturer' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -29,7 +30,7 @@
</td> </td>
<td> <td>
{% if user.is_staff %} {% 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 %} {% endif %}
</td> </td>
</tr> </tr>
@ -38,7 +39,7 @@
</table> </table>
{% if user.is_staff %} {% 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 %} {% 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 static %}
{% load i18n %}
<div class="pagination" id="{{ id }}"> <div class="pagination" id="{{ id }}">
{% if paginator.has_previous %} {% 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=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="Previous 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 %} {% endif %}
{% if paginator.paginator.num_pages > 1 %} {% if paginator.paginator.num_pages > 1 %}
{{ paginator.number }}/{{ paginator.paginator.num_pages }} {{ paginator.number }}/{{ paginator.paginator.num_pages }}
{% endif %} {% endif %}
{% if paginator.has_next %} {% 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.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="Last 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 %} {% endif %}
</div> </div>

View file

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

View file

@ -1,20 +1,21 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% load hilight %} {% load hilight %}
{% block title %}Search{% endblock %} {% block title %}{% translate 'Search' %}{% endblock %}
{% block header_bar %} {% block header_bar %}
Search {% translate 'Search' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<form method="get" action="{% url 'search' %}"> <form method="get" action="{% url 'search' %}">
<input type="text" id="search" name="q" value="{{ q }}"> <input type="text" id="search" name="q" value="{{ q }}" placeholder="{% translate 'Search' %}">
<button type="submit">Search</button> <button type="submit">{% translate 'Search' %}</button>
</form> </form>
<h2>Search result for '{{ q }}'</h2> <h2>{% translate 'Search result for' %} '{{ q }}'</h2>
{% for result in results %} {% for result in results %}
<div class="search-result"> <div class="search-result">
@ -23,19 +24,19 @@
<hr> <hr>
</div> </div>
{% empty %} {% empty %}
<p>Noting found</p> <p>{% translate 'Nothing found' %}</p>
{% endfor %} {% endfor %}
{% if pages > 1 %} {% if pages > 1 %}
<div class="pagination"> <div class="pagination">
{% if page > 1 %} {% 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=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="Previous 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 %} {% endif %}
{{ page }}/{{ pages }} {{ page }}/{{ pages }}
{% if 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={{ 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="Last 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 %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View file

@ -1,17 +1,16 @@
{% load static %} {% load static %}
{% load i18n %}
{% load hilight %} {% load hilight %}
<p> <p>
{% if item.documentation.all %} {% 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 %} {% endif %}
{{ item | hilight:tokens }} {{ item | hilight:tokens }}
<span class="small">{{item.form_factor.name}}</span> <span class="small">{{item.form_factor.name}}</span>
</p> </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"> <ul class="tag-list">
{% for tag in item.all_tags %} {% for tag in item.all_tags %}
<li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li> <li><a href="{% url 'tag-detail' tag.id %}" title="{{ tag.name }}">{{ tag.name }}</a></li>
{% empty %}
No tags
{% endfor %} {% endfor %}
</ul> </ul>

View file

@ -1,4 +1,5 @@
{% load static %} {% load static %}
{% load i18n %}
<li> <li>
<form method="POST" action="{% url "index" %}"> <form method="POST" action="{% url "index" %}">
@ -6,9 +7,9 @@
<input type="hidden" name="index_id" value='{{ item.id }}'> <input type="hidden" name="index_id" value='{{ item.id }}'>
<button class="icon-only-button"> <button class="icon-only-button">
{% if is_index %} {% 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 %} {% 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 %} {% endif %}
</button> </button>
</form> </form>

View file

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

View file

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

View file

@ -1,10 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}Workshop {{ workshop.name }}{% endblock %} {% block title %}{% translate 'Workshop' %} {{ workshop.name }}{% endblock %}
{% block header_bar %} {% 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 }} {{ workshop.name }}
<span class="small">{{ workshop.description}}</span> <span class="small">{{ workshop.description}}</span>
{% endblock %} {% endblock %}
@ -12,14 +13,14 @@
{% block header_icons %} {% block header_icons %}
{% if user.is_staff %} {% if user.is_staff %}
<li> <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> </li>
{% include 'inventory/set_index.html' with item=workshop is_index=is_index %} {% include 'inventory/set_index.html' with item=workshop is_index=is_index %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h3>Areas</h3> <h3>{% translate 'Areas' %}</h3>
<table class="list"> <table class="list">
<tbody> <tbody>
@ -30,22 +31,22 @@
</td> </td>
<td> <td>
{% if user.is_staff %} {% 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 %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td>No areas defined</td></tr> <tr><td>{% translate 'No areas defined' %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if user.is_staff %} {% 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 %} {% endif %}
{% if workshop.show_boxes %} {% if workshop.show_boxes %}
<h3>Containers</h3> <h3>{% translate 'Containers' %}</h3>
<table class="list"> <table class="list">
<tbody> <tbody>
{% for box in workshop.box_related.all %} {% for box in workshop.box_related.all %}
@ -55,18 +56,18 @@
</td> </td>
<td> <td>
{% if user.is_staff %} {% 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 %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td>No containers defined</td></tr> <tr><td>{% translate 'No containers defined' %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if user.is_staff %} {% 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 %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,10 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}Workshops{% endblock %} {% block title %}{% translate 'Workshops' %}{% endblock %}
{% block header_bar %} {% block header_bar %}
Inventory management - Workshops {% translate 'Inventory management' %} - {% translate 'Workshops' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -17,7 +18,7 @@
</td> </td>
<td> <td>
{% if user.is_staff %} {% 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 %} {% endif %}
</td> </td>
</tr> </tr>
@ -26,7 +27,7 @@
</table> </table>
{% if user.is_staff %} {% 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 %} {% endif %}

View file

@ -1,21 +1,30 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n%}
{% block header_bar %} {% block header_bar %}
Inventory management - Login {% translate 'Inventory management' %} - {% translate 'Login' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if form.errors %} {% 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 %} {% endif %}
{% if next %} {% if next %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed, <p>
please login with an account that has access.</p> {% blocktranslate %}
Your account doesn't have access to this page. To proceed,
please login with an account that has access.
{% endblocktranslate %}
</p>
{% else %} {% else %}
<p>Please login to see this page.</p> <p>
{% blocktranslate %}
Please login to see this page.
{% endblocktranslate %}
</p>
{% endif %} {% endif %}
{% endif %} {% endif %}
@ -37,6 +46,6 @@
</form> </form>
{# Assumes you setup the password_reset view in your URLconf #} {# 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 %} {% 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, ManufacturerView,
IndexView, IndexView,
TagView, TagView,
SearchView SearchView,
OnboardingView
) )
urlpatterns = [ urlpatterns = [
@ -54,5 +55,6 @@ urlpatterns = [
path('tags', TagListView.as_view(), name='tag-list'), path('tags', TagListView.as_view(), name='tag-list'),
path('tag/<int:pk>', TagView.as_view(), name='tag-detail'), path('tag/<int:pk>', TagView.as_view(), name='tag-detail'),
path('search', SearchView.as_view(), name='search'), path('search', SearchView.as_view(), name='search'),
path('onboarding', OnboardingView.as_view(), name='onboarding'),
path('', IndexView.as_view(), name='index') path('', IndexView.as_view(), name='index')
] ]

View file

@ -7,6 +7,7 @@ from .workshop import WorkshopView, WorkshopListView
from .index import IndexView from .index import IndexView
from .tag import TagListView, TagView from .tag import TagListView, TagView
from .search import SearchView from .search import SearchView
from .onboarding import OnboardingView
__all__ = [ __all__ = [
AreaView, AreaListView, AreaView, AreaListView,
@ -17,5 +18,6 @@ __all__ = [
WorkshopView, WorkshopListView, WorkshopView, WorkshopListView,
IndexView, IndexView,
TagView, TagListView, 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.views.generic import ListView, DetailView
from django.db.models import QuerySet from django.db.models import QuerySet
from inventory.models import Box from inventory.models import Box, Settings
from .utils import CanBeIndexMixin from .utils import CanBeIndexMixin
@ -54,6 +54,7 @@ class BoxView(CanBeIndexMixin, DetailView):
context = self.get_context_data(object=self.object) 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['layouted'], _ = self.layout(self.object.item_related.all().order_by('index'), self.object.layout.data)
context['hilight'] = hilighted context['hilight'] = hilighted
context['settings'] = Settings.objects.first()
return self.render_to_response(context) 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.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import View 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 from inventory.models import Settings
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='post')
class IndexView(View): class IndexView(View):
def get(self, request): 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() settings = Settings.objects.first()
if settings.default_container is not None: if settings.default_container is not None:
return redirect(settings.default_container.url) return redirect(settings.default_container.url)

View file

@ -1,15 +1,30 @@
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView 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') @method_decorator(login_required, name='dispatch')
class ItemView(DetailView): class ItemView(DetailView):
context_object_name = 'item' context_object_name = 'item'
queryset = Item.objects.all().select_related('container', 'container__layout', 'manufacturer', 'distributor').prefetch_related('documentation') 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') @method_decorator(login_required, name='dispatch')
class ItemListView(ListView): 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/ https://docs.djangoproject.com/en/3.0/ref/settings/
""" """
from typing import List
import os import os
import sys import sys
import asyncio 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': if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 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__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Externally visible URL of the server # 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 # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # 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! # 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 # Application definition
@ -88,9 +106,10 @@ ASGI_APPLICATION = 'inventory_project.asgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'inventory', 'HOST': os.environ.get('INVENTORY_DB_HOST', 'localhost'),
'USER': 'inventory', 'NAME': os.environ.get('INVENTORY_DB_NAME', 'inventory'),
'PASSWORD': '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 # Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/ # 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 USE_I18N = True
@ -132,13 +155,13 @@ USE_TZ = True
# https://docs.djangoproject.com/en/3.0/howto/static-files/ # https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/' 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' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_ROOT = os.environ.get('INVENTORY_MEDIA_FILES', os.path.join(BASE_DIR, 'media'))
SERVE_MEDIA_FILES = DEBUG SERVE_MEDIA_FILES = True
# Default page size for paginated content # 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]] [[package]]
name = "gunicorn" name = "gunicorn"
version = "20.1.0" version = "23.0.0"
description = "WSGI HTTP Server for UNIX" description = "WSGI HTTP Server for UNIX"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.7"
files = [ files = [
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
] ]
[package.dependencies] [package.dependencies]
setuptools = ">=3.0" packaging = "*"
[package.extras] [package.extras]
eventlet = ["eventlet (>=0.24.1)"] eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
gevent = ["gevent (>=1.4.0)"] gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"] setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"] tornado = ["tornado (>=0.2)"]
[[package]] [[package]]
@ -249,6 +250,17 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, {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]] [[package]]
name = "pillow" name = "pillow"
version = "10.4.0" version = "10.4.0"
@ -521,26 +533,6 @@ files = [
{file = "python_monkey_business-1.1.0-py2.py3-none-any.whl", hash = "sha256:15b4f603c749ba9a7b4f1acd36af023a6c5ba0f7e591c945f8253f0ef44bf389"}, {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]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.3" version = "0.5.3"
@ -647,4 +639,4 @@ brotli = ["brotli"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" 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] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
django = "^5" django = "^5"
gunicorn = "^20" gunicorn = "^23"
whitenoise = "^6.6" whitenoise = "^6.6"
commentjson = "^0.9" commentjson = "^0.9"
psycopg = { version = "^3", extras = [ "binary" ] } psycopg = { version = "^3", extras = [ "binary" ] }
django-nested-admin = "^4.0.0" django-nested-admin = "^4"
pillow = "^10" pillow = "^10"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]