loko/streetup/stock/forms.py
2026-07-22 14:48:40 +02:00

519 lines
22 KiB
Python

from django import forms
from django.forms import inlineformset_factory
from django.db.models import Q
from .models import (
Inventory,
InventoryItem,
Product,
ProductCategory,
PurchaseOrder,
PurchaseOrderItem,
PreparationOrder,
PreparationOrderItem,
ProductionOrder,
ProductionOrderComponent,
Supplier,
Warehouse,
WarehouseLocation,
RequestForQuote,
RequestForQuoteItem,
)
class PreparationOrderForm(forms.ModelForm):
class Meta:
model = PreparationOrder
fields = ["order_number", "status", "requester", "priority", "is_loan", "estimated_return_date"]
widgets = {
"requester": forms.TextInput(attrs={
"class": "form-control",
"placeholder": "Nom, service ou équipe à l'origine de la demande",
}),
"estimated_return_date": forms.DateInput(attrs={
"type": "date",
"class": "form-control",
}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name in self.fields:
if field_name != "requester":
if isinstance(self.fields[field_name].widget, forms.Select):
self.fields[field_name].widget.attrs['class'] = 'form-select'
elif isinstance(self.fields[field_name].widget, forms.CheckboxInput):
self.fields[field_name].widget.attrs['class'] = 'form-check-input'
else:
self.fields[field_name].widget.attrs['class'] = 'form-control'
self.fields["requester"].required = False
# class PreparationOrderDesignForm(forms.Form):
# """Formulaire utilisé côté serveur pour valider une ligne conception."""
# design_id = forms.IntegerField()
# quantity = forms.IntegerField(min_value=1, initial=1)
class PreparationOrderItemForm(forms.ModelForm):
class Meta:
model = PreparationOrderItem
fields = ["product", "quantity"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk and self.instance.product:
self.fields["product"].queryset = Product.objects.filter(
Q(is_active=True) | Q(pk=self.instance.product.pk)
).order_by("name")
else:
self.fields["product"].queryset = Product.objects.filter(is_active=True).order_by("name")
class RequestForQuoteForm(forms.ModelForm):
class Meta:
model = RequestForQuote
fields = ["warehouse", "suppliers", "notes", "status"]
widgets = {
"warehouse": forms.Select(attrs={"class": "form-select"}),
"suppliers": forms.SelectMultiple(attrs={"class": "form-select", "size": "6"}),
"notes": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"status": forms.Select(attrs={"class": "form-select"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["suppliers"].required = False
self.fields["notes"].required = False
self.fields["suppliers"].queryset = Supplier.objects.filter(is_active=True).order_by("name")
class RequestForQuoteItemForm(forms.ModelForm):
class Meta:
model = RequestForQuoteItem
fields = ["product", "num_articles", "quantity_per_article", "quantity"]
widgets = {
"num_articles": forms.NumberInput(attrs={"class": "form-control", "min": "1", "placeholder": "ex: 10"}),
"quantity_per_article": forms.NumberInput(attrs={"class": "form-control", "min": "0.001", "step": "any", "placeholder": "ex: 10"}),
"quantity": forms.NumberInput(attrs={"class": "form-control", "min": "0"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["num_articles"].required = False
self.fields["quantity_per_article"].required = False
self.fields["quantity"].required = False
if not self.instance.pk:
self.fields["quantity_per_article"].initial = 1
if self.instance and self.instance.pk and self.instance.product:
self.fields["product"].queryset = Product.objects.filter(
Q(is_active=True) | Q(pk=self.instance.product.pk)
).order_by("name")
else:
self.fields["product"].queryset = Product.objects.filter(is_active=True).order_by("name")
def clean(self):
cleaned_data = super().clean()
num_articles = cleaned_data.get("num_articles")
quantity_per_article = cleaned_data.get("quantity_per_article")
quantity = cleaned_data.get("quantity")
if num_articles and quantity_per_article:
cleaned_data["quantity"] = int(num_articles * quantity_per_article)
elif not quantity:
raise forms.ValidationError(
"Indiquez soit une quantité totale, soit un nombre d'articles et une quantité par article."
)
return cleaned_data
RequestForQuoteItemFormSet = inlineformset_factory(
RequestForQuote,
RequestForQuoteItem,
form=RequestForQuoteItemForm,
extra=1,
can_delete=True,
)
class PurchaseOrderForm(forms.ModelForm):
class Meta:
model = PurchaseOrder
fields = ["supplier", "warehouse", "status", "request_for_quote"]
widgets = {
"request_for_quote": forms.HiddenInput(),
}
class PurchaseOrderItemForm(forms.ModelForm):
class Meta:
model = PurchaseOrderItem
fields = ["product", "location", "num_articles", "quantity_per_article", "quantity", "unit_price", "vat_rate"]
widgets = {
"num_articles": forms.NumberInput(attrs={"class": "form-control", "min": "1", "placeholder": "ex: 10"}),
"quantity_per_article": forms.NumberInput(attrs={"class": "form-control", "min": "0.001", "step": "any", "placeholder": "ex: 10"}),
"quantity": forms.NumberInput(attrs={"class": "form-control", "min": "0"}),
"unit_price": forms.NumberInput(attrs={"class": "form-control", "min": "0", "step": "0.01", "placeholder": "ex: 47.11"}),
"vat_rate": forms.NumberInput(attrs={"class": "form-control", "min": "0", "step": "0.01", "placeholder": "ex: 21.00"}),
}
def __init__(self, *args, **kwargs):
warehouse = kwargs.pop("warehouse", None)
super().__init__(*args, **kwargs)
if warehouse:
self.fields["location"].queryset = WarehouseLocation.objects.filter(
warehouse=warehouse
)
if not self.instance.pk:
default_loc = WarehouseLocation.objects.filter(
warehouse=warehouse, is_default=True
).first()
if default_loc:
self.initial.setdefault("location", default_loc.pk)
self.fields["num_articles"].required = False
self.fields["quantity_per_article"].required = False
self.fields["quantity"].required = False
self.fields["unit_price"].required = False
self.fields["vat_rate"].required = False
if not self.instance.pk:
self.fields["quantity_per_article"].initial = 1
self.fields["vat_rate"].initial = 21.00
if self.instance and self.instance.pk and self.instance.product:
self.fields["product"].queryset = Product.objects.filter(
Q(is_active=True) | Q(pk=self.instance.product.pk)
).order_by("name")
else:
self.fields["product"].queryset = Product.objects.filter(is_active=True).order_by("name")
def clean(self):
cleaned_data = super().clean()
num_articles = cleaned_data.get("num_articles")
quantity_per_article = cleaned_data.get("quantity_per_article")
quantity = cleaned_data.get("quantity")
if num_articles and quantity_per_article:
from decimal import Decimal
cleaned_data["quantity"] = int(num_articles * quantity_per_article)
elif not quantity:
raise forms.ValidationError(
"Indiquez soit une quantité totale, soit un nombre d'articles et une quantité par article."
)
# Validation des emplacements autorisés pour ce produit
product = cleaned_data.get("product")
location = cleaned_data.get("location")
if product and location:
if product.authorized_locations.exists() and location not in product.authorized_locations.all():
from django.utils.translation import gettext_lazy as _
self.add_error('location', _("L'emplacement '%(loc)s' n'est pas autorisé pour le produit '%(prod)s'.") % {
'loc': location.name,
'prod': product.name
})
return cleaned_data
PurchaseOrderItemFormSet = inlineformset_factory(
PurchaseOrder,
PurchaseOrderItem,
form=PurchaseOrderItemForm,
extra=1,
can_delete=True,
)
class InventoryForm(forms.ModelForm):
class Meta:
model = Inventory
fields = ["warehouse_location", "performed_by", "status"]
def __init__(self, *args, **kwargs):
warehouse = kwargs.pop("warehouse", None)
super().__init__(*args, **kwargs)
if warehouse:
self.fields["warehouse_location"].queryset = WarehouseLocation.objects.filter(
warehouse=warehouse
).order_by("name")
if not self.instance.pk:
default_loc = WarehouseLocation.objects.filter(
warehouse=warehouse, is_default=True
).first()
if default_loc:
self.initial.setdefault("warehouse_location", default_loc.pk)
class InventoryItemForm(forms.ModelForm):
class Meta:
model = InventoryItem
fields = ["product", "expected_quantity", "counted_quantity", "comment"]
widgets = {
"comment": forms.Textarea(attrs={"rows": 1, "placeholder": "Commentaire optionnel…"}),
}
def __init__(self, *args, **kwargs):
kwargs.pop("warehouse", None)
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk and self.instance.product:
self.fields["product"].queryset = Product.objects.filter(
Q(is_active=True) | Q(pk=self.instance.product.pk)
).order_by("name")
else:
self.fields["product"].queryset = Product.objects.filter(is_active=True).order_by("name")
InventoryItemFormSet = inlineformset_factory(
Inventory,
InventoryItem,
form=InventoryItemForm,
extra=1,
can_delete=True,
)
class InventoryItemEditForm(forms.ModelForm):
"""Formulaire pour l'édition d'un article d'inventaire existant.
Le champ produit est rendu en HiddenInput pour les articles existants,
évitant de charger un <select> de tous les produits pour chaque ligne
(ce qui causerait un DOM très volumineux et un temps de chargement élevé).
"""
class Meta:
model = InventoryItem
fields = ["product", "expected_quantity", "counted_quantity", "comment"]
widgets = {
"product": forms.HiddenInput(),
"comment": forms.Textarea(attrs={"rows": 1, "placeholder": "Commentaire optionnel…"}),
}
def __init__(self, *args, **kwargs):
kwargs.pop("warehouse", None)
super().__init__(*args, **kwargs)
class SupplierForm(forms.ModelForm):
class Meta:
model = Supplier
fields = ["name", "enterprise_number", "contact_email", "phone_number", "address", "is_active"]
widgets = {
"name": forms.TextInput(attrs={"class": "form-control"}),
"enterprise_number": forms.TextInput(attrs={"class": "form-control"}),
"contact_email": forms.EmailInput(attrs={"class": "form-control"}),
"phone_number": forms.TextInput(attrs={"class": "form-control"}),
"address": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
# class ProductDesignForm(forms.ModelForm):
# class Meta:
# model = ProductDesign
# fields = ["code", "name", "description", "image", "status", "category", "warehouse", "default_location", "output_product"]
# widgets = {
# "code": forms.TextInput(attrs={"class": "form-control"}),
# "name": forms.TextInput(attrs={"class": "form-control"}),
# "description": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
# "image": forms.ClearableFileInput(attrs={"class": "form-control"}),
# "status": forms.Select(attrs={"class": "form-select"}),
# "category": forms.Select(attrs={"class": "form-select"}),
# "warehouse": forms.Select(attrs={"class": "form-select", "id": "id_warehouse"}),
# "default_location": forms.Select(attrs={"class": "form-select", "id": "id_default_location"}),
# "output_product": forms.Select(attrs={"class": "form-select"}),
# }
#
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# if self.instance and self.instance.pk and self.instance.warehouse_id:
# self.fields['default_location'].queryset = WarehouseLocation.objects.filter(
# warehouse=self.instance.warehouse
# ).order_by('name')
# if not self.instance.default_location_id:
# default_loc = WarehouseLocation.objects.filter(
# warehouse=self.instance.warehouse, is_default=True
# ).first()
# if default_loc:
# self.initial.setdefault('default_location', default_loc.pk)
# else:
# self.fields['default_location'].queryset = WarehouseLocation.objects.none()
# self.fields['output_product'].required = False
# self.fields['output_product'].queryset = Product.objects.all().order_by('name')
# self.fields['output_product'].empty_label = "— Aucun produit par défaut —"
# class ProductDesignComponentForm(forms.ModelForm):
# class Meta:
# model = ProductDesignComponent
# fields = ["product", "quantity", "notes"]
# widgets = {
# "product": forms.Select(attrs={"class": "form-select product-select"}),
# "quantity": forms.NumberInput(attrs={"class": "form-control", "min": 1}),
# "notes": forms.TextInput(attrs={"class": "form-control"}),
# }
# ProductDesignComponentFormSet = inlineformset_factory(
# ProductDesign,
# ProductDesignComponent,
# form=ProductDesignComponentForm,
# extra=1,
# can_delete=True,
# )
class ProductionOrderComponentForm(forms.ModelForm):
class Meta:
model = ProductionOrderComponent
fields = ["product", "quantity", "notes"]
widgets = {
"product": forms.Select(attrs={"class": "form-select product-select"}),
"quantity": forms.NumberInput(attrs={"class": "form-control", "min": 1}),
"notes": forms.TextInput(attrs={"class": "form-control"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk and self.instance.product:
self.fields["product"].queryset = Product.objects.filter(
Q(is_active=True) | Q(pk=self.instance.product.pk)
).order_by("name")
else:
self.fields["product"].queryset = Product.objects.filter(is_active=True).order_by("name")
ProductionOrderComponentFormSet = inlineformset_factory(
ProductionOrder,
ProductionOrderComponent,
form=ProductionOrderComponentForm,
extra=1,
can_delete=True,
)
class ProductionOrderForm(forms.ModelForm):
class Meta:
model = ProductionOrder
fields = [
"warehouse", "quantity", "status", "destination",
"preparation_order", "notes",
"output_product", "output_product_name", "output_location",
]
widgets = {
"warehouse": forms.Select(attrs={"class": "form-select"}),
"quantity": forms.NumberInput(attrs={"class": "form-control", "min": 1}),
"status": forms.Select(attrs={"class": "form-select"}),
"destination": forms.Select(attrs={"class": "form-select"}),
"preparation_order": forms.Select(attrs={"class": "form-select"}),
"notes": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"output_product": forms.Select(attrs={"class": "form-select"}),
"output_product_name": forms.TextInput(attrs={"class": "form-control"}),
"output_location": forms.Select(attrs={"class": "form-select"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["preparation_order"].required = False
self.fields["preparation_order"].queryset = PreparationOrder.objects.filter(
status__in=["to_process", "pending"]
)
self.fields["output_product"].required = False
# Pour Select2 AJAX : on ne charge que le produit actuellement sélectionné (0-1 option)
# La validation est gérée par clean_output_product
current_pk = None
if self.instance and self.instance.pk:
current_pk = self.instance.output_product_id
elif self.is_bound:
current_pk = self.data.get("output_product") or None
elif self.initial.get("output_product"):
current_pk = self.initial.get("output_product")
if current_pk:
self.fields["output_product"].queryset = Product.objects.filter(pk=current_pk)
else:
self.fields["output_product"].queryset = Product.objects.none()
self.fields["output_product"].empty_label = "— Sélectionner un produit existant —"
self.fields["output_product_name"].required = False
self.fields["output_location"].required = False
self.fields["output_location"].queryset = WarehouseLocation.objects.all().select_related("warehouse").order_by("warehouse__name", "name")
self.fields["output_location"].empty_label = "— Emplacement par défaut de la conception —"
def clean_output_product(self):
"""Valide l'ID soumis par Select2 AJAX contre tous les produits réels."""
product_id = self.data.get("output_product")
if not product_id:
return None
try:
return Product.objects.get(pk=product_id)
except (Product.DoesNotExist, ValueError, TypeError):
raise forms.ValidationError("Produit invalide.")
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ["code", "name", "category", "sku", "description", "price", "unit", "threshold", "is_serialized", "authorized_locations"]
widgets = {
"code": forms.TextInput(attrs={"class": "form-control"}),
"name": forms.TextInput(attrs={"class": "form-control"}),
"category": forms.Select(attrs={"class": "form-select"}),
"sku": forms.TextInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"price": forms.NumberInput(attrs={"class": "form-control", "min": "0", "step": "0.01"}),
"unit": forms.Select(attrs={"class": "form-select"}),
"threshold": forms.NumberInput(attrs={"class": "form-control", "min": "0"}),
"is_serialized": forms.CheckboxInput(attrs={"class": "form-check-input"}),
"authorized_locations": forms.SelectMultiple(attrs={"class": "form-select", "size": "6"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["category"].queryset = ProductCategory.objects.all().order_by("name_fr")
self.fields["category"].empty_label = "— Sélectionner une catégorie —"
self.fields["category"].required = False
self.fields["description"].required = False
self.fields["price"].required = False
# Code and SKU are auto-generated and locked
self.fields["code"].widget.attrs["readonly"] = True
self.fields["sku"].widget.attrs["readonly"] = True
from django.utils.translation import gettext_lazy as _
self.fields["authorized_locations"].queryset = WarehouseLocation.objects.select_related("warehouse").order_by("warehouse__name", "name")
self.fields["authorized_locations"].required = False
self.fields["authorized_locations"].help_text = _("Si vide, le produit est autorisé dans tous les emplacements.")
class ProductAtLocationForm(ProductForm):
quantity = forms.IntegerField(
min_value=0,
initial=0,
label=forms.CharField().label, # just fallback
widget=forms.NumberInput(attrs={"class": "form-control"}),
)
class Meta(ProductForm.Meta):
fields = ProductForm.Meta.fields + ["quantity"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from django.utils.translation import gettext_lazy as _
self.fields["quantity"].label = _("Quantité initiale")
class WarehouseLocationForm(forms.ModelForm):
class Meta:
model = WarehouseLocation
fields = ["warehouse", "code", "name", "description", "is_default"]
widgets = {
"warehouse": forms.Select(attrs={"class": "form-select"}),
"code": forms.TextInput(attrs={"class": "form-control"}),
"name": forms.TextInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"is_default": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
def __init__(self, *args, warehouse=None, **kwargs):
super().__init__(*args, **kwargs)
self.fields["description"].required = False
if warehouse is not None:
self.fields["warehouse"].initial = warehouse
self.fields["warehouse"].queryset = Warehouse.objects.filter(pk=warehouse.pk)
self.fields["warehouse"].widget = forms.HiddenInput()
else:
self.fields["warehouse"].queryset = Warehouse.objects.all().order_by("name")