diff --git a/lelcsc/core/forms.py b/lelcsc/core/forms.py new file mode 100644 index 0000000..939765b --- /dev/null +++ b/lelcsc/core/forms.py @@ -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) diff --git a/lelcsc/core/signals.py b/lelcsc/core/signals.py index 2764c3e..04e760a 100644 --- a/lelcsc/core/signals.py +++ b/lelcsc/core/signals.py @@ -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() + + +@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"), + }, + ] diff --git a/lelcsc/core/static/core/add_stock.js b/lelcsc/core/static/core/add_stock.js new file mode 100644 index 0000000..71e6de5 --- /dev/null +++ b/lelcsc/core/static/core/add_stock.js @@ -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); +} diff --git a/lelcsc/core/templates/base.html b/lelcsc/core/templates/base.html index fafe60a..e9a1879 100644 --- a/lelcsc/core/templates/base.html +++ b/lelcsc/core/templates/base.html @@ -28,5 +28,6 @@ e.classList.remove('show'); }); + {% block extra_js %}{% endblock %} diff --git a/lelcsc/core/templates/core/add_stock.html b/lelcsc/core/templates/core/add_stock.html new file mode 100644 index 0000000..9c26f70 --- /dev/null +++ b/lelcsc/core/templates/core/add_stock.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% load django_bootstrap5 %} +{% load i18n %} +{% load static %} + +{% block page_title %}{% translate "Add stock" %}{% endblock %} + +{% block content %} +

{% translate "Add stock" %}

+
+ {% csrf_token %} + {{ formset.management_form }} + {% bootstrap_formset_errors formset layout="floating" %} + {% for form in formset %} +

#{{ forloop.counter }}

+ {% 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 %} +
+
+ {% bootstrap_form formset.empty_form layout="floating" %} +
+{% endblock %} + +{% block extra_js %} +{{ properties|json_script:"properties" }} +{{ request.user.username|json_script:"currentUser" }} + +{% endblock %} diff --git a/lelcsc/core/urls.py b/lelcsc/core/urls.py index f0603f3..573b57b 100644 --- a/lelcsc/core/urls.py +++ b/lelcsc/core/urls.py @@ -4,6 +4,7 @@ from . import views urlpatterns = [ path("", views.index, name="index"), + path("add-stock", views.add_stock, name="add_stock"), path("unauthorized", views.unauthorized, name="unauthorized"), path("admin/login/", views.admin_login), ] diff --git a/lelcsc/core/views.py b/lelcsc/core/views.py index c2d8ca7..fe805a4 100644 --- a/lelcsc/core/views.py +++ b/lelcsc/core/views.py @@ -1,7 +1,14 @@ +from django.contrib import messages 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.shortcuts import redirect, render 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 @@ -13,6 +20,101 @@ def unauthorized(request): 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): if request.user.is_authenticated: return redirect("unauthorized")