v1.0
This commit is contained in:
@@ -5,7 +5,6 @@ from core.forms.base import BaseForm
|
||||
|
||||
|
||||
class InventoryItemForm(BaseForm):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
||||
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
@@ -44,8 +45,8 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], verbose_name='Anzahl')),
|
||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Preis')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
@@ -60,7 +61,7 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('quantity', models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], 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')),
|
||||
@@ -77,7 +78,7 @@ class Migration(migrations.Migration):
|
||||
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')),
|
||||
('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], 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')),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
||||
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
+5
-4
@@ -3,6 +3,7 @@ from django.contrib.auth.models import User
|
||||
from projects.models import Project
|
||||
from finance.models import Category, Supplier
|
||||
from django.db.models import Sum
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
class InventoryCategory(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True, verbose_name="Name")
|
||||
@@ -31,7 +32,7 @@ class InventoryLocation(models.Model):
|
||||
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")
|
||||
quantity = models.PositiveIntegerField(default=0, verbose_name="Anzahl", validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")])
|
||||
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")
|
||||
@@ -55,7 +56,7 @@ class ProjectInventoryItem(models.Model):
|
||||
|
||||
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")
|
||||
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl", validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")])
|
||||
status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN, verbose_name="Status")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -72,9 +73,9 @@ 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")
|
||||
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl", validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")])
|
||||
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")
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Preis",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
+8
-1
@@ -32,7 +32,6 @@ class InventoryGlobalTable(tables.Table):
|
||||
model = InventoryItem
|
||||
template_name = "django_tables2/table_toggle.html"
|
||||
fields = (
|
||||
"expand",
|
||||
"name",
|
||||
"category",
|
||||
"available",
|
||||
@@ -85,6 +84,11 @@ class InventoryAddTable(tables.Table):
|
||||
}
|
||||
|
||||
class ShoppingItemTable(tables.Table):
|
||||
selection = tables.TemplateColumn(
|
||||
template_name="inventory/tables/shopping/selection.html",
|
||||
orderable=False,
|
||||
verbose_name="",
|
||||
)
|
||||
|
||||
name = tables.TemplateColumn(
|
||||
template_name="inventory/tables/shopping/name.html",
|
||||
@@ -125,6 +129,7 @@ class ShoppingItemTable(tables.Table):
|
||||
class Meta:
|
||||
model = ShoppingItem
|
||||
fields = (
|
||||
"selection",
|
||||
"name",
|
||||
"category",
|
||||
"quantity",
|
||||
@@ -137,6 +142,8 @@ class ShoppingItemTable(tables.Table):
|
||||
"class": "w-full text-sm"
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ProjectInventoryItemTable(tables.Table):
|
||||
|
||||
name = tables.Column(
|
||||
|
||||
@@ -18,7 +18,7 @@ Globale Inventarverwaltung
|
||||
<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>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Inventarartikel hinzufügen</p>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ Globale Inventarverwaltung
|
||||
<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>
|
||||
<h2 class="text-lg font-bold">Inventarliste</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Gelagerte und verfügbare Artikel</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Vorhandenen & benötigtes Material
|
||||
<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>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Artikel zur Packliste oder Einkaufsliste hinzufügen</p>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ Vorhandenen & benötigtes Material
|
||||
<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>
|
||||
<h2 class="text-lg font-bold">Packliste</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Vorhandene und benötigte Artikel</p>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ Vorhandenen & benötigtes Material
|
||||
<div class="flex flex-wrap justify-between items-center mb-6">
|
||||
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">
|
||||
<h2 class="text-lg font-bold">
|
||||
Einkaufsliste
|
||||
</h2>
|
||||
|
||||
@@ -64,9 +64,13 @@ Vorhandenen & benötigtes Material
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-xl">
|
||||
|
||||
<form id="bulk-shopping-form" method="post" action="{% url 'inventory:shopping_to_expense' project.id %}" class="rounded-xl">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-action mb-2">
|
||||
Zu Ausgabe konvertieren
|
||||
</button>
|
||||
</form>
|
||||
{% render_table shopping_table %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -10,20 +10,20 @@
|
||||
name="item_id"
|
||||
value="{{ record.id }}"
|
||||
form="save-form-{{ record.id }}">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
form="save-form-{{ record.id }}"
|
||||
class="btn btn-save">
|
||||
Speichern
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
form="save-form-{{ record.id }}"
|
||||
formaction="{% url 'inventory:delete' project.id record.id %}"
|
||||
class="btn btn-delete">
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -3,8 +3,12 @@
|
||||
{{ record.available_quantity }}
|
||||
</span>
|
||||
{% elif record.available_quantity == 0 %}
|
||||
<span class="text-orange-600 font-semibold">
|
||||
<span class="text-yellow-600 font-semibold">
|
||||
0
|
||||
</span>
|
||||
{% elif record.available_quantity < 0 %}
|
||||
<span class="text-orange-600 font-semibold">
|
||||
{{ record.available_quantity }}
|
||||
</span>
|
||||
{% endif %}
|
||||
/ {{ record.quantity }}
|
||||
@@ -1,7 +1,7 @@
|
||||
<form
|
||||
id="shopping-save-form-{{ record.id }}"
|
||||
method="post"
|
||||
action="{% url 'inventory:shopping_edit' table.project.id record.id %}">
|
||||
action="{% url 'inventory:shopping_edit' shopping_table.project.id record.id %}">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
form="shopping-save-form-{{ record.id }}"
|
||||
formaction="{% url 'inventory:shopping_delete' table.project.id record.id %}"
|
||||
formaction="{% url 'inventory:shopping_delete' shopping_table.project.id record.id %}"
|
||||
class="btn btn-delete">
|
||||
Löschen
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<option value="">---</option>
|
||||
|
||||
{% for c in table.categories %}
|
||||
{% for c in shopping_table.categories %}
|
||||
<option value="{{ c.id }}"
|
||||
{% if record.category_id == c.id %}selected{% endif %}>
|
||||
{{ c.name }}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
name="selection"
|
||||
value="{{ record.id }}"
|
||||
form="bulk-shopping-form">
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<option value="">---</option>
|
||||
|
||||
{% for s in table.suppliers %}
|
||||
{% for s in shopping_table.suppliers %}
|
||||
<option value="{{ s.id }}"
|
||||
{% if record.supplier_id == s.id %}selected{% endif %}>
|
||||
{{ s.name }}
|
||||
|
||||
@@ -13,4 +13,6 @@ urlpatterns = [
|
||||
|
||||
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"),
|
||||
|
||||
path("<int:project_id>/inventory/shopping-to-expense/", views.shopping_to_expense, name="shopping_to_expense"),
|
||||
]
|
||||
@@ -9,6 +9,9 @@ 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
|
||||
import json
|
||||
from collections import Counter
|
||||
from django.utils import timezone
|
||||
|
||||
@login_required
|
||||
@permission_required("projects_inventory_inventory_view")
|
||||
@@ -428,4 +431,94 @@ def inventory_global_edit(request, item_id):
|
||||
"locations": InventoryLocation.objects.all(),
|
||||
"users": User.objects.order_by("username"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("projects_inventory_shopping_to_expense")
|
||||
def shopping_to_expense(request, project_id):
|
||||
|
||||
ids = request.POST.getlist("selection")
|
||||
|
||||
items = ShoppingItem.objects.filter(pk__in=ids)
|
||||
|
||||
data = []
|
||||
total_amount = 0
|
||||
categories = []
|
||||
suppliers = []
|
||||
|
||||
for item in items:
|
||||
|
||||
total_amount += (item.quantity * item.price)
|
||||
|
||||
if item.category:
|
||||
categories.append(item.category.id)
|
||||
|
||||
if item.supplier:
|
||||
suppliers.append(item.supplier.id)
|
||||
|
||||
data.append({
|
||||
"name": item.name,
|
||||
"quantity": item.quantity,
|
||||
"price": str(item.price),
|
||||
"supplier": str(item.supplier) if item.supplier else None,
|
||||
"category": str(item.category) if item.category else None,
|
||||
"description": item.description,
|
||||
})
|
||||
|
||||
most_category = (
|
||||
Counter(categories).most_common(1)[0][0]
|
||||
if categories else None
|
||||
)
|
||||
|
||||
most_supplier = (
|
||||
Counter(suppliers).most_common(1)[0][0]
|
||||
if suppliers else None
|
||||
)
|
||||
|
||||
request.session["expense_initial"] = {
|
||||
"name": "Einkauf vom " + timezone.now().strftime("%d.%m.%Y"),
|
||||
"amount": str(total_amount),
|
||||
"date": timezone.now().strftime("%Y-%m-%dT%H:%M"),
|
||||
"category": most_category,
|
||||
"supplier": most_supplier,
|
||||
"note": json.dumps(
|
||||
data,
|
||||
ensure_ascii=False,
|
||||
indent=2
|
||||
)
|
||||
}
|
||||
|
||||
request.session["shopping_item_ids"] = list(
|
||||
items.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
return redirect(
|
||||
"finance:add_expense",
|
||||
project_id=project_id
|
||||
)
|
||||
Reference in New Issue
Block a user