feat: add form for adding stock

This commit is contained in:
Luca 2024-11-02 21:02:13 +01:00
parent 715b65e9d7
commit 9bf0516d89
7 changed files with 243 additions and 1 deletions

17
lelcsc/core/forms.py Normal file
View File

@ -0,0 +1,17 @@
from django import forms
from django.utils.translation import gettext_lazy as _
class AddStockForm(forms.Form):
part_number = forms.CharField(label=_("Component"))
properties = forms.JSONField(initial={}, label=_("Properties"), required=False)
quantity = forms.IntegerField(min_value=1, label=_("Quantity"))
original_quantity = forms.IntegerField(min_value=1, label=_("Original quantity"))
total_value = forms.DecimalField(
min_value=0, max_digits=8, decimal_places=2, label=_("Total value")
)
location = forms.CharField(label=_("Location"), widget=forms.Textarea)
owner = forms.CharField(label=_("Owner"))
AddStockFormSet = forms.formset_factory(AddStockForm, extra=0)

View File

@ -1,3 +1,21 @@
from django.dispatch import Signal from django.dispatch import Signal, receiver
from django.urls import reverse
from django.utils.translation import gettext as _
populate_nav = Signal() populate_nav = Signal()
@receiver(populate_nav, dispatch_uid="populate_nav_core")
def populate_nav_core(sender, **kwargs):
request = sender
if not request.user.is_authenticated:
return []
return [
{
"is_active": request.resolver_match.url_name == "add_stock",
"link": reverse("add_stock"),
"text": _("Add stock"),
},
]

View File

@ -0,0 +1,71 @@
let totalForms = Object.defineProperty({}, 'value', {
get() {
return +document.getElementById('id_form-TOTAL_FORMS').value;
},
set(value) {
document.getElementById('id_form-TOTAL_FORMS').value = value;
},
enumerable: false,
configurable: false,
});
const updateNames = (elem, i) => {
const replaceIndex = s => s.replaceAll('__prefix__', `${i}`);
if (elem.hasAttribute('for')) {
elem.htmlFor = replaceIndex(elem.htmlFor);
}
if (elem.hasAttribute('id')) {
elem.id = replaceIndex(elem.id);
}
if (elem.hasAttribute('name')) {
elem.name = replaceIndex(elem.name);
}
for (const child of elem.children) {
updateNames(child, i);
}
};
const autofillQuantity = i => {
const originalQuantity = document.getElementById(`id_form-${i}-original_quantity`);
const quantity = document.getElementById(`id_form-${i}-quantity`);
const autofillOnInput = (self, other) => () => {
delete self.dataset.autofilled;
if (!other.value || other.dataset.autofilled) {
other.value = self.value;
other.dataset.autofilled = true;
}
};
originalQuantity.addEventListener('input', autofillOnInput(originalQuantity, quantity));
quantity.addEventListener('input', autofillOnInput(quantity, originalQuantity));
};
const addFormButton = document.getElementById('addForm');
const addForm = () => {
const form = document.getElementById('formTemplate').cloneNode(true);
const i = totalForms.value++;
updateNames(form, i);
const heading = document.createElement('h3');
heading.innerText = `#${i+1}`;
addFormButton.before(heading, ...form.children);
autofillQuantity(i);
document.getElementById(`id_form-${i}-owner`).value = JSON.parse(document.getElementById('currentUser').textContent);
};
addFormButton.addEventListener('click', addForm);
for (let i = 0; i < totalForms.value; ++i) {
autofillQuantity(i);
}

View File

@ -28,5 +28,6 @@
e.classList.remove('show'); e.classList.remove('show');
}); });
</script> </script>
{% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% load i18n %}
{% load static %}
{% block page_title %}{% translate "Add stock" %}{% endblock %}
{% block content %}
<h1>{% translate "Add stock" %}</h1>
<form class="mb-3" method="post">
{% csrf_token %}
{{ formset.management_form }}
{% bootstrap_formset_errors formset layout="floating" %}
{% for form in formset %}
<h3>#{{ forloop.counter }}</h3>
{% bootstrap_form form layout="floating" %}
{% endfor %}
{% bootstrap_button "+" button_class="btn-outline-secondary" button_type="button" id="addForm" %}
{% translate "Add" as add_button_text %}
{% bootstrap_button button_type="submit" content=add_button_text %}
</form>
<div class="d-none" id="formTemplate">
{% bootstrap_form formset.empty_form layout="floating" %}
</div>
{% endblock %}
{% block extra_js %}
{{ properties|json_script:"properties" }}
{{ request.user.username|json_script:"currentUser" }}
<script src="{% static 'core/add_stock.js' %}"></script>
{% endblock %}

View File

@ -4,6 +4,7 @@ from . import views
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
path("add-stock", views.add_stock, name="add_stock"),
path("unauthorized", views.unauthorized, name="unauthorized"), path("unauthorized", views.unauthorized, name="unauthorized"),
path("admin/login/", views.admin_login), path("admin/login/", views.admin_login),
] ]

View File

@ -1,7 +1,14 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.forms.models import model_to_dict
from django.http import QueryDict from django.http import QueryDict
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ngettext
from .forms import AddStockFormSet
from .models import Component, Property, Stock, User, Value
@login_required @login_required
@ -13,6 +20,101 @@ def unauthorized(request):
return render(request, "core/unauthorized.html", {}) return render(request, "core/unauthorized.html", {})
@login_required
def add_stock(request):
property_models = {p.name: p for p in Property.objects.all()}
if request.method == "POST":
formset = AddStockFormSet(request.POST, initial=[{"owner": request.user}])
if not formset.is_valid():
return render(request, "core/add_stock.html", {"formset": formset})
try:
with transaction.atomic():
for form in formset:
part_number = form.cleaned_data["part_number"]
component, created = Component.objects.get_or_create(
part_number__iexact=part_number,
defaults={"part_number": part_number},
)
if created:
properties = form.cleaned_data["properties"]
for name in properties:
if name not in property_models:
property_kwargs = {
k: v
for k, v in properties[name].items()
if k
in (
"filterable",
"type",
"searchable",
"unit",
"scale",
)
}
property_models[name] = Property.objects.create(
name=name, **property_kwargs
)
property = property_models[name]
value_kwargs = {
k: v
for k, v in properties[name].items()
if k in ("integer", "text")
}
value_kwargs.setdefault("integer", 0)
value_kwargs.setdefault("text", "")
Value.objects.create(
property=property, component=component, **value_kwargs
)
owner_name = form.cleaned_data["owner"]
owner = User.objects.filter(username=owner_name).first()
Stock.objects.create(
component=component,
quantity=form.cleaned_data["quantity"],
original_quantity=form.cleaned_data["original_quantity"],
total_value=form.cleaned_data["total_value"],
location=form.cleaned_data["location"],
owner=owner,
owner_name=owner_name,
)
except Exception as e:
messages.add_message(request, messages.ERROR, str(e))
return render(
request,
"core/add_stock.html",
{
"formset": formset,
"properties": list(map(model_to_dict, property_models.values())),
},
)
messages.add_message(
request,
messages.SUCCESS,
ngettext(
formset.total_form_count,
"Added %(count)d stock entry",
"Added %(count)d stock entries",
)
% {"count": formset.total_form_count},
)
return render(
request,
"core/add_stock.html",
{
"formset": AddStockFormSet(initial=[{"owner": request.user}]),
"properties": list(map(model_to_dict, property_models.values())),
},
)
def admin_login(request): def admin_login(request):
if request.user.is_authenticated: if request.user.is_authenticated:
return redirect("unauthorized") return redirect("unauthorized")