519 lines
22 KiB
Python
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")
|