Initial commit

This commit is contained in:
Felix Baumann
2026-06-27 17:07:34 +00:00
commit fb47c53d52
256 changed files with 21270 additions and 0 deletions
View File
+83
View File
@@ -0,0 +1,83 @@
from django.contrib import admin
from .models import (
InventoryCategory,
InventoryLocation,
InventoryItem,
ProjectInventoryItem,
ShoppingItem,
)
@admin.register(InventoryCategory)
class InventoryCategoryAdmin(admin.ModelAdmin):
list_display = ("name",)
search_fields = ("name",)
@admin.register(InventoryLocation)
class InventoryLocationAdmin(admin.ModelAdmin):
list_display = ("name",)
search_fields = ("name",)
@admin.register(InventoryItem)
class InventoryItemAdmin(admin.ModelAdmin):
list_display = (
"name",
"quantity",
"category",
"location",
"responsible",
"article_number",
)
list_filter = (
"category",
"location",
)
search_fields = (
"name",
"article_number",
"description",
)
@admin.register(ProjectInventoryItem)
class ProjectInventoryItemAdmin(admin.ModelAdmin):
list_display = (
"project",
"item",
"quantity",
"status",
"created_at",
)
list_filter = (
"status",
"project",
)
search_fields = (
"project__name",
"item__name",
)
@admin.register(ShoppingItem)
class ShoppingItemAdmin(admin.ModelAdmin):
list_display = (
"name",
"project",
"quantity",
"price",
"category",
"supplier",
"created_at",
)
list_filter = (
"project",
"category",
"supplier",
)
search_fields = (
"name",
"description",
"project__name",
)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class InventoryConfig(AppConfig):
name = 'inventory'
+43
View File
@@ -0,0 +1,43 @@
from django import forms
from .models import InventoryItem, ShoppingItem
from core.forms.base import BaseForm
class InventoryItemForm(BaseForm):
class Meta:
model = InventoryItem
fields = [
"name",
"description",
"quantity",
"location",
"article_number",
"responsible",
"category",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
class ShoppingItemForm(BaseForm):
class Meta:
model = ShoppingItem
fields = [
"name",
"description",
"quantity",
"supplier",
"price",
"category",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
+90
View File
@@ -0,0 +1,90 @@
# Generated by Django 6.0.6 on 2026-06-23 22:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='InventoryCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
],
options={
'verbose_name': 'Inventarkategorie',
'verbose_name_plural': 'Inventarkategorien',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='InventoryLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
],
options={
'verbose_name': 'Inventarstandort',
'verbose_name_plural': 'Inventarstandorte',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='ShoppingItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
('quantity', models.PositiveIntegerField(default=1, verbose_name='Anzahl')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Preis')),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Einkaufslistenartikel',
'verbose_name_plural': 'Einkaufslistenartikel',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='InventoryItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
('quantity', models.PositiveIntegerField(default=0, verbose_name='Anzahl')),
('article_number', models.CharField(blank=True, max_length=255, verbose_name='Artikelnummer')),
('created_at', models.DateTimeField(auto_now_add=True)),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.inventorycategory', verbose_name='Kategorie')),
('responsible', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Verantwortlicher')),
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.inventorylocation', verbose_name='Standort')),
],
options={
'verbose_name': 'Inventarartikel',
'verbose_name_plural': 'Inventarartikel',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='ProjectInventoryItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1, verbose_name='Anzahl')),
('status', models.CharField(choices=[('open', 'Offen'), ('packed', 'Eingepackt'), ('on_site', 'Vor Ort')], default='open', max_length=20, verbose_name='Status')),
('created_at', models.DateTimeField(auto_now_add=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.inventoryitem', verbose_name='Artikel')),
],
options={
'verbose_name': 'Projektartikel',
'verbose_name_plural': 'Projektartikel',
},
),
]
+42
View File
@@ -0,0 +1,42 @@
# Generated by Django 6.0.6 on 2026-06-23 22:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('finance', '0002_initial'),
('inventory', '0001_initial'),
('projects', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='projectinventoryitem',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='projects.project', verbose_name='Projekt'),
),
migrations.AddField(
model_name='shoppingitem',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.category', verbose_name='Kategorie'),
),
migrations.AddField(
model_name='shoppingitem',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shopping_items', to='projects.project', verbose_name='Projekt'),
),
migrations.AddField(
model_name='shoppingitem',
name='supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.supplier', verbose_name='Lieferant'),
),
migrations.AlterUniqueTogether(
name='projectinventoryitem',
unique_together={('project', 'item')},
),
]
View File
+87
View File
@@ -0,0 +1,87 @@
from django.db import models
from django.contrib.auth.models import User
from projects.models import Project
from finance.models import Category, Supplier
from django.db.models import Sum
class InventoryCategory(models.Model):
name = models.CharField(max_length=255, unique=True, verbose_name="Name")
class Meta:
verbose_name = "Inventarkategorie"
verbose_name_plural = "Inventarkategorien"
ordering = ["name"]
def __str__(self):
return self.name
class InventoryLocation(models.Model):
name = models.CharField(max_length=255, unique=True, verbose_name="Name")
class Meta:
verbose_name = "Inventarstandort"
verbose_name_plural = "Inventarstandorte"
ordering = ["name"]
def __str__(self):
return self.name
class InventoryItem(models.Model):
name = models.CharField(max_length=255, verbose_name="Name")
description = models.TextField(blank=True, verbose_name="Beschreibung")
quantity = models.PositiveIntegerField(default=0, verbose_name="Anzahl")
location = models.ForeignKey(InventoryLocation, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Standort")
article_number = models.CharField(max_length=255, blank=True, verbose_name="Artikelnummer")
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Verantwortlicher")
category = models.ForeignKey(InventoryCategory, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Inventarartikel"
verbose_name_plural = "Inventarartikel"
ordering = ["name"]
def __str__(self):
return self.name
class ProjectInventoryItem(models.Model):
class Status(models.TextChoices):
OPEN = "open", "Offen"
PACKED = "packed", "Eingepackt"
ON_SITE = "on_site", "Vor Ort"
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="inventory_items", verbose_name="Projekt")
item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, verbose_name="Artikel")
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl")
status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN, verbose_name="Status")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Projektartikel"
verbose_name_plural = "Projektartikel"
unique_together = ("project", "item")
def __str__(self):
return f"{self.project} - {self.item}"
class ShoppingItem(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="shopping_items", verbose_name="Projekt")
name = models.CharField(max_length=255, verbose_name="Name")
description = models.TextField(blank=True, verbose_name="Beschreibung")
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl")
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Preis")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Einkaufslistenartikel"
verbose_name_plural = "Einkaufslistenartikel"
ordering = ["name"]
def __str__(self):
return self.name
+19
View File
@@ -0,0 +1,19 @@
from masterdata.registry import register_setting
from .models import (
InventoryCategory,
InventoryLocation,
)
register_setting(
group="Inventar",
name="Kategorien",
model=InventoryCategory,
)
register_setting(
group="Inventar",
name="Standorte",
model=InventoryLocation,
)
+197
View File
@@ -0,0 +1,197 @@
import django_tables2 as tables
from .models import ProjectInventoryItem, ShoppingItem, InventoryItem
import django_tables2 as tables
class InventoryGlobalTable(tables.Table):
toggle_prefix = "inv"
detail_template = "inventory/tables/inventory_global/edit_form.html"
name = tables.Column(
verbose_name="Name"
)
category = tables.Column(
verbose_name="Kategorie"
)
available = tables.TemplateColumn(
template_name="inventory/tables/inventory_global/available.html",
verbose_name="Anzahl",
orderable=False
)
location = tables.Column(
verbose_name="Standort"
)
article_number = tables.Column(
verbose_name="Artikelnummer"
)
class Meta:
model = InventoryItem
template_name = "django_tables2/table_toggle.html"
fields = (
"expand",
"name",
"category",
"available",
"location",
"article_number",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class InventoryAddTable(tables.Table):
name = tables.Column(
accessor="name",
verbose_name="Name"
)
category = tables.Column(
accessor="category",
verbose_name="Kategorie"
)
available = tables.Column(
accessor="available_quantity",
verbose_name="Verfügbar"
)
article_number = tables.Column(
accessor="article_number",
verbose_name="Artikelnummer"
)
quantity = tables.TemplateColumn(
template_name="inventory/tables/inventory_add/quantity.html",
verbose_name="Anzahl hinzufügen",
orderable=False
)
class Meta:
model = InventoryItem
fields = (
"name",
"category",
"available",
"article_number",
"quantity",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class ShoppingItemTable(tables.Table):
name = tables.TemplateColumn(
template_name="inventory/tables/shopping/name.html",
verbose_name="Name"
)
category = tables.TemplateColumn(
template_name="inventory/tables/shopping/category.html",
verbose_name="Kategorie"
)
quantity = tables.TemplateColumn(
template_name="inventory/tables/shopping/quantity.html",
verbose_name="Anzahl"
)
supplier = tables.TemplateColumn(
template_name="inventory/tables/shopping/supplier.html",
verbose_name="Lieferant"
)
price = tables.TemplateColumn(
template_name="inventory/tables/shopping/price.html",
verbose_name="Preis"
)
description = tables.TemplateColumn(
template_name="inventory/tables/shopping/description.html",
verbose_name="Beschreibung"
)
actions = tables.TemplateColumn(
template_name="inventory/tables/shopping/actions.html",
verbose_name="Aktionen",
orderable=False
)
class Meta:
model = ShoppingItem
fields = (
"name",
"category",
"quantity",
"supplier",
"price",
"description",
"actions",
)
attrs = {
"class": "w-full text-sm"
}
class ProjectInventoryItemTable(tables.Table):
name = tables.Column(
accessor="item.name",
verbose_name="Name"
)
category = tables.Column(
accessor="item.category",
verbose_name="Kategorie"
)
quantity = tables.TemplateColumn(
template_name="inventory/tables/quantity.html",
verbose_name="Anzahl"
)
location = tables.Column(
accessor="item.location",
verbose_name="Standort"
)
article_number = tables.Column(
accessor="item.article_number",
verbose_name="Artikelnummer"
)
responsible = tables.Column(
accessor="item.responsible",
verbose_name="Verantwortlicher"
)
status = tables.TemplateColumn(
template_name="inventory/tables/status.html",
verbose_name="Status"
)
actions = tables.TemplateColumn(
template_name="inventory/tables/actions.html",
verbose_name="Aktionen",
orderable=False
)
class Meta:
model = ProjectInventoryItem
fields = (
"name",
"category",
"quantity",
"location",
"article_number",
"responsible",
"status",
"actions",
)
attrs = {
"class": "w-full text-sm"
}
+33
View File
@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Inventar
{% endblock %}
{% block header_title %}
Packartikel hinzufügen
{% endblock %}
{% block header_description %}
Artikel zur Packliste hinzufügen
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post">
{% csrf_token %}
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
<button
type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</form>
</div>
{% endblock %}
@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}
Inventar
{% endblock %}
{% block header_title %}
Inventarartikel hinzufügen
{% endblock %}
{% block header_description %}
Neuen Artikel zum globalen Inventar hinzufügen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" class="space-y-4">
{% csrf_token %}
<div class="grid grid-cols-1 gap-4">
{{ form.as_p }}
</div>
<button
type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</form>
</div>
{% endblock %}
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}
Inventar
{% endblock %}
{% block header_title %}
Inventar
{% endblock %}
{% block header_description %}
Globale Inventarverwaltung
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">Inventarartikel hinzufügen</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'inventory_global:add' %}" class="btn btn-add">+ Artikel</a>
</div>
</div>
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="mb-6">
<h2 class="text-xl font-bold">Inventarliste</h2>
<p class="text-slate-400 text-sm mt-1">Gelagerte und verfügbare Artikel</p>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,73 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Inventar
{% endblock %}
{% block header_title %}
Pack-/Einkaufsliste
{% endblock %}
{% block header_description %}
Vorhandenen & benötigtes Material
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">Artikel zur Packliste oder Einkaufsliste hinzufügen</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'inventory:add' project.id %}" class="btn btn-add">+ Packliste</a>
<a href="{% url 'inventory:shopping_add' project.id %}" class="btn btn-add">+ Einkaufsliste</a>
</div>
</div>
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="mb-6">
<h2 class="text-xl font-bold">Packliste</h2>
<p class="text-slate-400 text-sm mt-1">Vorhandene und benötigte Artikel</p>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mt-8">
<div class="flex flex-wrap justify-between items-center mb-6">
<div>
<h2 class="text-xl font-bold">
Einkaufsliste
</h2>
<p class="text-slate-400 text-sm mt-1">
Nicht-Vorhandene und benötigte Artikel
</p>
</div>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table shopping_table %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Inventar
{% endblock %}
{% block header_title %}
Einkaufsartikel hinzufügen
{% endblock %}
{% block header_description %}
Artikel zur Einkaufsliste hinzufügen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" class="space-y-4">
{% csrf_token %}
<div class="space-y-4">
{{ form.as_p }}
</div>
<button
type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</form>
</div>
{% endblock %}
@@ -0,0 +1,29 @@
<form
id="save-form-{{ record.id }}"
method="post"
action="{% url 'inventory:edit' project.id record.id %}">
{% csrf_token %}
<input
type="hidden"
name="item_id"
value="{{ record.id }}"
form="save-form-{{ record.id }}">
<button
type="submit"
form="save-form-{{ record.id }}"
class="btn btn-save">
Speichern
</button>
<button
type="submit"
form="save-form-{{ record.id }}"
formaction="{% url 'inventory:delete' project.id record.id %}"
class="btn btn-delete">
Löschen
</button>
</form>
@@ -0,0 +1,7 @@
<input
type="number"
min="0"
max="{{ record.available }}"
value="0"
name="item_{{ record.id }}"
class="border rounded px-2 py-1 w-24">
@@ -0,0 +1,10 @@
{% if record.available_quantity > 0 %}
<span class="text-green-600 font-semibold">
{{ record.available_quantity }}
</span>
{% elif record.available_quantity == 0 %}
<span class="text-orange-600 font-semibold">
0
</span>
{% endif %}
/ {{ record.quantity }}
@@ -0,0 +1,155 @@
<form method="post" action="{% url 'inventory_global:edit' item.id %}">
{% csrf_token %}
<input type="hidden" name="item_id" value="{{ item.id }}">
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-4">
<div>
<label class="block text-sm mb-1">
Name
</label>
<input
type="text"
name="name"
value="{{ item.name }}"
class="border rounded px-3 py-2 w-full">
</div>
<div>
<label class="block text-sm mb-1">
Beschreibung
</label>
<input
type="text"
name="description"
value="{{ item.description }}"
class="border rounded px-3 py-2 w-full">
</div>
<div>
<label class="block text-sm mb-1">
Anzahl
</label>
<input
type="number"
name="quantity"
value="{{ item.quantity }}"
class="border rounded px-3 py-2 w-full">
</div>
<div>
<label class="block text-sm mb-1">
Kategorie
</label>
<select
name="category"
class="border rounded px-3 py-2 w-full">
<option value="">
---
</option>
{% for c in categories %}
<option
value="{{ c.id }}"
{% if item.category_id == c.id %}selected{% endif %}>
{{ c.name }}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm mb-1">
Standort
</label>
<select
name="location"
class="border rounded px-3 py-2 w-full">
<option value="">
---
</option>
{% for l in locations %}
<option
value="{{ l.id }}"
{% if item.location_id == l.id %}selected{% endif %}>
{{ l.name }}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm mb-1">
Artikelnummer
</label>
<input
type="text"
name="article_number"
value="{{ item.article_number }}"
class="border rounded px-3 py-2 w-full">
</div>
<div>
<label class="block text-sm mb-1">
Verantwortlicher
</label>
<select
name="responsible"
class="border rounded px-3 py-2 w-full">
<option value="">
---
</option>
{% for u in users %}
<option
value="{{ u.id }}"
{% if item.responsible_id == u.id %}selected{% endif %}>
{{ u.get_full_name|default:u.username }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="mt-5 flex gap-2">
<button
type="submit"
class="btn btn-save">
Speichern
</button>
<button
type="submit"
formaction="{% url 'inventory_global:delete' item.id %}"
class="btn btn-delete">
Löschen
</button>
</div>
</form>
@@ -0,0 +1,6 @@
<button
type="button"
onclick="toggleItem('{{ record.id }}')"
class="font-bold">
▼
</button>
@@ -0,0 +1,7 @@
<input
type="number"
name="quantity"
min="0"
value="{{ record.quantity }}"
form="save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-24">
@@ -0,0 +1,32 @@
<form
id="shopping-save-form-{{ record.id }}"
method="post"
action="{% url 'inventory:shopping_edit' table.project.id record.id %}">
{% csrf_token %}
<input
type="hidden"
name="shopping_item_id"
value="{{ record.id }}">
<div class="flex gap-2">
<button
type="submit"
form="shopping-save-form-{{ record.id }}"
class="btn btn-save">
Speichern
</button>
<button
type="submit"
form="shopping-save-form-{{ record.id }}"
formaction="{% url 'inventory:shopping_delete' table.project.id record.id %}"
class="btn btn-delete">
Löschen
</button>
</div>
</form>
@@ -0,0 +1,15 @@
<select
name="category"
form="shopping-save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-full">
<option value="">---</option>
{% for c in table.categories %}
<option value="{{ c.id }}"
{% if record.category_id == c.id %}selected{% endif %}>
{{ c.name }}
</option>
{% endfor %}
</select>
@@ -0,0 +1,6 @@
<input
type="text"
name="description"
value="{{ record.description }}"
form="shopping-save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-full">
@@ -0,0 +1,6 @@
<input
type="text"
name="name"
value="{{ record.name }}"
form="shopping-save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-full">
@@ -0,0 +1,7 @@
<input
type="number"
step="0.01"
name="price"
value="{{ record.price }}"
form="shopping-save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-28">
@@ -0,0 +1,6 @@
<input
type="number"
name="quantity"
value="{{ record.quantity }}"
form="shopping-save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-24">
@@ -0,0 +1,15 @@
<select
name="supplier"
form="shopping-save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-full">
<option value="">---</option>
{% for s in table.suppliers %}
<option value="{{ s.id }}"
{% if record.supplier_id == s.id %}selected{% endif %}>
{{ s.name }}
</option>
{% endfor %}
</select>
@@ -0,0 +1,21 @@
<select
name="status"
form="save-form-{{ record.id }}"
class="border rounded px-2 py-1">
<option value="open"
{% if record.status == "open" %}selected{% endif %}>
Offen
</option>
<option value="packed"
{% if record.status == "packed" %}selected{% endif %}>
Eingepackt
</option>
<option value="on_site"
{% if record.status == "on_site" %}selected{% endif %}>
Vor Ort
</option>
</select>
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+16
View File
@@ -0,0 +1,16 @@
from django.urls import path
from . import views
app_name = "inventory"
urlpatterns = [
path("<int:project_id>/inventory/", views.inventory_overview, name="overview"),
path("<int:project_id>/inventory/add/", views.inventory_add, name="add"),
path("<int:project_id>/inventory/shopping-add/", views.shopping_add, name="shopping_add"),
path("<int:project_id>/inventory/edit/<int:item_id>/", views.inventory_edit, name="edit"),
path("<int:project_id>/inventory/shopping-edit/<int:item_id>/", views.shopping_edit, name="shopping_edit"),
path("<int:project_id>/inventory/delete/<int:item_id>/", views.inventory_delete, name="delete"),
path("<int:project_id>/inventory/shopping-delete/<int:item_id>/", views.shopping_delete, name="shopping_delete"),
]
+11
View File
@@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = "inventory_global"
urlpatterns = [
path("", views.inventory_global_overview, name="overview"),
path("add/", views.inventory_global_add, name="add"),
path("edit/<int:item_id>/", views.inventory_global_edit, name="edit"),
path("delete/<int:item_id>/", views.inventory_global_delete, name="delete"),
]
+431
View File
@@ -0,0 +1,431 @@
from django.contrib.auth.decorators import login_required
from accounts.decorators import permission_required
from django.shortcuts import render, get_object_or_404, redirect
from projects.models import ProjectMembership
from .models import InventoryItem, InventoryCategory, InventoryLocation, ProjectInventoryItem, ShoppingItem, Category, Supplier
from django.contrib.auth.models import User
from .forms import InventoryItemForm, ShoppingItemForm
from django.db.models import Sum, Value, F
from django_tables2 import RequestConfig
from .tables import ProjectInventoryItemTable, ShoppingItemTable, InventoryAddTable, InventoryGlobalTable
from django.db.models.functions import Coalesce
@login_required
@permission_required("projects_inventory_inventory_view")
def inventory_overview(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
items = (
ProjectInventoryItem.objects
.select_related(
"item",
"item__category",
"item__location",
"item__responsible",
)
.filter(project=project)
.order_by("item__name")
)
table = ProjectInventoryItemTable(items)
# Zusatzdaten für TemplateColumns
table.project = project
RequestConfig(
request,
paginate=False
).configure(table)
shopping_items = (
ShoppingItem.objects
.select_related(
"category",
"supplier"
)
.filter(project=project)
.order_by("name")
)
shopping_table = ShoppingItemTable(shopping_items)
shopping_table.project = project
shopping_table.categories = Category.objects.order_by("name")
shopping_table.suppliers = Supplier.objects.order_by("name")
return render(request, "inventory/overview.html", {
"project": project,
"table": table,
"shopping_table": shopping_table,
})
@login_required
@permission_required("projects_inventory_shopping_add")
def shopping_add(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
if request.method == "POST":
form = ShoppingItemForm(request.POST)
if form.is_valid():
item = form.save(commit=False)
item.project = project
item.save()
return redirect(
"inventory:overview",
project_id=project.id
)
else:
form = ShoppingItemForm()
return render(
request,
"inventory/shopping_add.html",
{
"project": project,
"form": form,
}
)
@login_required
@permission_required("projects_inventory_inventory_add")
def inventory_add(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
items = (
InventoryItem.objects
.select_related(
"category",
"location"
)
.annotate(
used_quantity=Coalesce(
Sum("projectinventoryitem__quantity"),
Value(0)
)
)
.annotate(
available_quantity=F("quantity") - F("used_quantity")
)
.filter(
available_quantity__gt=0
)
.order_by("name")
)
if request.method == "POST":
for item in items:
quantity = request.POST.get(
f"item_{item.id}"
)
if not quantity:
continue
quantity = int(quantity)
if quantity <= 0:
continue
if quantity > item.available_quantity:
continue
project_item, created = ProjectInventoryItem.objects.get_or_create(
project=project,
item=item,
defaults={
"quantity": quantity,
"status": ProjectInventoryItem.Status.OPEN,
}
)
if not created:
project_item.quantity += quantity
project_item.save()
return redirect(
"inventory:overview",
project_id=project.id
)
table = InventoryAddTable(items)
RequestConfig(
request,
paginate=False
).configure(table)
return render(
request,
"inventory/add.html",
{
"project": project,
"table": table,
}
)
@login_required
@permission_required("inventory_view")
def inventory_global_overview(request):
items = (
InventoryItem.objects
.select_related(
"category",
"location",
"responsible"
)
.annotate(
used_quantity=Coalesce(
Sum("projectinventoryitem__quantity"),
Value(0)
)
)
.annotate(
available_quantity=F("quantity") - F("used_quantity")
)
.order_by("name")
)
for item in items:
used_quantity = (
ProjectInventoryItem.objects
.filter(item=item)
.aggregate(total=Sum("quantity"))
.get("total") or 0
)
item.available_quantity = max(
0,
item.quantity - used_quantity
)
table = InventoryGlobalTable(items)
table.categories = InventoryCategory.objects.all()
table.locations = InventoryLocation.objects.all()
table.users = User.objects.order_by("username")
RequestConfig(
request,
paginate=False
).configure(table)
return render(
request,
"inventory/global/overview.html",
{
"table": table,
}
)
@login_required
@permission_required("inventory_add")
def inventory_global_add(request):
if request.method == "POST":
form = InventoryItemForm(request.POST)
if form.is_valid():
form.save()
return redirect("inventory_global:overview")
else:
form = InventoryItemForm()
return render(request, "inventory/global/add.html", {"form": form,})
@login_required
@permission_required("projects_inventory_inventory_delete")
def inventory_delete(request, project_id, item_id):
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
item = get_object_or_404(ProjectInventoryItem, id=item_id, project=membership.project)
if request.method == "POST":
item.delete()
return redirect("inventory:overview", project_id=project_id)
@login_required
@permission_required("projects_inventory_shopping_delete")
def shopping_delete(request, project_id, item_id):
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
item = get_object_or_404(ShoppingItem, id=item_id, project=membership.project)
if request.method == "POST":
item.delete()
return redirect("inventory:overview", project_id=project_id)
@login_required
@permission_required("inventory_delete")
def inventory_global_delete(request, item_id):
item = get_object_or_404(
InventoryItem,
id=item_id
)
if request.method == "POST":
item.delete()
return redirect("inventory_global:overview")
@login_required
@permission_required("projects_inventory_inventory_edit")
def inventory_edit(request, project_id, item_id):
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
item = get_object_or_404(ProjectInventoryItem, id=item_id, project=membership.project)
if request.method == "POST":
item.quantity = request.POST.get("quantity") or 0
item.status = request.POST.get("status")
item.save()
return redirect("inventory:overview", project_id=project_id)
return render(
request,
"inventory/edit.html",
{
"project": membership.project,
"item": item,
}
)
@login_required
@permission_required("projects_inventory_shopping_edit")
def shopping_edit(request, project_id, item_id):
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
item = get_object_or_404(ShoppingItem, id=item_id, project=membership.project)
if request.method == "POST":
item.name = request.POST.get("name")
item.description = request.POST.get("description", "")
item.quantity = request.POST.get("quantity") or 1
item.price = request.POST.get("price") or None
item.category_id = request.POST.get("category") or None
item.supplier_id = request.POST.get("supplier") or None
item.save()
return redirect("inventory:overview", project_id=project_id)
return render(
request,
"inventory/shopping_edit.html",
{
"project": membership.project,
"item": item,
"categories": Category.objects.all(),
"suppliers": Supplier.objects.all(),
}
)
@login_required
@permission_required("inventory_edit")
def inventory_global_edit(request, item_id):
item = get_object_or_404(
InventoryItem,
id=item_id
)
if request.method == "POST":
item.name = request.POST.get("name")
item.quantity = request.POST.get("quantity")
item.article_number = request.POST.get("article_number")
item.description = request.POST.get("description", "")
item.category_id = request.POST.get("category") or None
item.location_id = request.POST.get("location") or None
item.responsible_id = request.POST.get("responsible") or None
item.save()
return redirect("inventory_global:overview")
return render(
request,
"inventory/global/edit.html",
{
"item": item,
"categories": InventoryCategory.objects.all(),
"locations": InventoryLocation.objects.all(),
"users": User.objects.order_by("username"),
}
)