| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- from django import forms
- from django.core.exceptions import ValidationError
- from django.contrib.auth import get_user_model
- from decimal import Decimal
- from django.utils import timezone
- from django.contrib.contenttypes.models import ContentType
- from .models import (
- MaterialCategory,
- Material,
- MaterialImage,
- ProvidedService,
- Customer,
- CustomerSite,
- MaterialEmissionFactor,
- PickupItem,
- )
- from markdownfield.widgets import MDEWidget
- from orgs.models import UserProfile
- class MaterialCategoryForm(forms.ModelForm):
- class Meta:
- model = MaterialCategory
- fields = ["organization", "name"]
- class MultiFileInput(forms.ClearableFileInput):
- allow_multiple_selected = True
- class MultiImageField(forms.Field):
- widget = MultiFileInput
- def __init__(self, *args, **kwargs):
- kwargs.setdefault("required", False)
- super().__init__(*args, **kwargs)
- def to_python(self, data):
- return data
- def validate(self, value):
- # Basic required check; skip per-file validation here
- if self.required and not value:
- raise ValidationError("This field is required.")
- class MaterialForm(forms.ModelForm):
- images = MultiImageField(help_text="Upload one or more sample images (optional)")
- class Meta:
- model = Material
- fields = ["organization", "category", "name", "code", "default_unit", "images"]
- def save(self, commit=True):
- instance = super().save(commit=commit)
- files = self.files.getlist("images") if hasattr(self, "files") else []
- if commit and files:
- # Instance has a PK; we can create images now
- order_start = instance.images.count()
- for i, f in enumerate(files):
- MaterialImage.objects.create(material=instance, image=f, display_order=order_start + i)
- else:
- # Defer image saving until caller completes save
- self._pending_images = files
- return instance
- def save_images(self, instance: Material | None = None):
- """Persist any pending images after the Material has been saved."""
- if not hasattr(self, "_pending_images"):
- return
- target = instance or getattr(self, "instance", None)
- if not target or not getattr(target, "pk", None):
- return
- order_start = target.images.count()
- for i, f in enumerate(self._pending_images or []):
- MaterialImage.objects.create(material=target, image=f, display_order=order_start + i)
- # Clear pending list
- self._pending_images = []
- class CustomerForm(forms.ModelForm):
- class Meta:
- model = Customer
- fields = ["organization", "name", "email", "phone", "billing_address", "price_list"]
- class CustomerSiteForm(forms.ModelForm):
- class Meta:
- model = CustomerSite
- fields = ["customer", "name", "address", "contact_name", "contact_phone", "contact_email"]
- class ProvidedServiceForm(forms.ModelForm):
- class Meta:
- model = ProvidedService
- fields = ["title", "description", "body", "display_order", "is_enabled"]
- widgets = {
- "description": forms.Textarea(attrs={"rows": 3}),
- "body": MDEWidget(),
- }
- # Operational forms ----------------------------------------------------------
- User = get_user_model()
- class PickupAssignForm(forms.Form):
- driver = forms.ModelChoiceField(queryset=User.objects.all(), required=True, label="Assign Driver")
- class PickupStatusForm(forms.Form):
- status = forms.ChoiceField(choices=(), required=True, label="Set Status")
- def __init__(self, *args, **kwargs):
- from .models import PickupOrder
- super().__init__(*args, **kwargs)
- self.fields["status"].choices = PickupOrder.STATUS_CHOICES
- class PaymentForm(forms.Form):
- amount = forms.DecimalField(max_digits=14, decimal_places=2)
- received_at = forms.DateTimeField(required=False, initial=timezone.now)
- reference = forms.CharField(max_length=128, required=False)
- class DocumentForm(forms.Form):
- organization = forms.ModelChoiceField(queryset=None)
- file = forms.FileField()
- kind = forms.CharField(max_length=64, required=False)
- content_type = forms.ModelChoiceField(queryset=ContentType.objects.all(), required=True, label="Attach To Model")
- object_id = forms.IntegerField(required=True, label="Object ID")
- def __init__(self, *args, **kwargs):
- from orgs.models import Organization as Org
- super().__init__(*args, **kwargs)
- self.fields["organization"].queryset = Org.objects.all()
- # User management ------------------------------------------------------------
- class UserCreateForm(forms.Form):
- username = forms.CharField(max_length=150)
- email = forms.EmailField(required=False)
- first_name = forms.CharField(max_length=150, required=False)
- last_name = forms.CharField(max_length=150, required=False)
- role = forms.ChoiceField(choices=UserProfile.ROLE_CHOICES)
- password1 = forms.CharField(widget=forms.PasswordInput)
- password2 = forms.CharField(widget=forms.PasswordInput)
- def clean_username(self):
- User = get_user_model()
- username = self.cleaned_data["username"].strip()
- if User.objects.filter(username__iexact=username).exists():
- raise forms.ValidationError("Username already taken")
- return username
- def clean(self):
- cleaned = super().clean()
- p1 = cleaned.get("password1")
- p2 = cleaned.get("password2")
- if p1 and p2 and p1 != p2:
- self.add_error("password2", "Passwords do not match")
- return cleaned
- class UserEditForm(forms.Form):
- email = forms.EmailField(required=False)
- first_name = forms.CharField(max_length=150, required=False)
- last_name = forms.CharField(max_length=150, required=False)
- role = forms.ChoiceField(choices=UserProfile.ROLE_CHOICES)
- is_active = forms.BooleanField(required=False, label="Active")
- password1 = forms.CharField(widget=forms.PasswordInput, required=False)
- password2 = forms.CharField(widget=forms.PasswordInput, required=False)
- def clean(self):
- cleaned = super().clean()
- p1 = cleaned.get("password1")
- p2 = cleaned.get("password2")
- if (p1 or p2) and p1 != p2:
- self.add_error("password2", "Passwords do not match")
- return cleaned
- class UserProfilePhotoForm(forms.ModelForm):
- remove_photo = forms.BooleanField(label="Remove current photo", required=False)
- class Meta:
- model = UserProfile
- fields = ["my_photo", "remove_photo"]
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["my_photo"].widget = forms.ClearableFileInput(attrs={
- "accept": "image/*",
- "class": "hidden",
- "id": "id_my_photo",
- })
- def clean_my_photo(self):
- f = self.cleaned_data.get("my_photo")
- if not f:
- return f
- content_type = getattr(f, "content_type", "")
- if content_type and not content_type.startswith("image/"):
- raise ValidationError("Please upload an image file.")
- max_bytes = 5 * 1024 * 1024
- if getattr(f, "size", 0) and f.size > max_bytes:
- raise ValidationError("Image too large (max 5MB).")
- return f
- def save(self, commit: bool = True):
- instance: UserProfile = super().save(commit=False)
- remove = self.cleaned_data.get("remove_photo", False)
- new_file = self.cleaned_data.get("my_photo")
- if remove and not new_file:
- try:
- if instance.my_photo:
- instance.my_photo.delete(save=False)
- except Exception:
- pass
- instance.my_photo = None
- if commit:
- instance.save()
- return instance
- # Weighing -------------------------------------------------------------------
- class WeighTicketForm(forms.Form):
- ticket_number = forms.CharField(max_length=64, required=False)
- gross_weight = forms.DecimalField(max_digits=12, decimal_places=3)
- tare_weight = forms.DecimalField(max_digits=12, decimal_places=3)
- net_weight = forms.DecimalField(max_digits=12, decimal_places=3, required=False, help_text="Auto-calculated if left blank")
- unit = forms.ChoiceField(choices=(), required=True)
- def __init__(self, *args, **kwargs):
- from .models import Material
- super().__init__(*args, **kwargs)
- self.fields["unit"].choices = Material.UNIT_CHOICES
- def clean(self):
- cleaned = super().clean()
- gross = cleaned.get("gross_weight")
- tare = cleaned.get("tare_weight")
- net = cleaned.get("net_weight")
- if gross is not None and tare is not None and net is None:
- cleaned["net_weight"] = (Decimal(gross) - Decimal(tare)).quantize(Decimal("0.001"))
- return cleaned
- class WeighLineForm(forms.Form):
- material = forms.ModelChoiceField(queryset=Material.objects.none())
- quantity = forms.DecimalField(max_digits=12, decimal_places=3)
- unit = forms.ChoiceField(choices=(), required=True)
- def __init__(self, *args, **kwargs):
- org = kwargs.pop("organization", None)
- super().__init__(*args, **kwargs)
- from .models import Material as Mat
- self.fields["unit"].choices = Mat.UNIT_CHOICES
- # Scope materials by organization if provided
- if org is not None:
- self.fields["material"].queryset = Material.objects.filter(organization=org).order_by("name")
- else:
- self.fields["material"].queryset = Material.objects.all().order_by("name")
- class MaterialEmissionFactorForm(forms.ModelForm):
- class Meta:
- model = MaterialEmissionFactor
- fields = [
- "organization",
- "material",
- "unit",
- "kgco2e_per_unit",
- "source",
- "effective_from",
- "effective_to",
- ]
- class PickupItemForm(forms.Form):
- material = forms.ModelChoiceField(queryset=Material.objects.none())
- estimated_qty = forms.DecimalField(max_digits=12, decimal_places=3, required=False)
- unit = forms.ChoiceField(choices=(), required=True)
- def __init__(self, *args, **kwargs):
- org = kwargs.pop("organization", None)
- super().__init__(*args, **kwargs)
- from .models import Material as Mat
- self.fields["unit"].choices = Mat.UNIT_CHOICES
- if org is not None:
- self.fields["material"].queryset = Material.objects.filter(organization=org).order_by("name")
- else:
- self.fields["material"].queryset = Material.objects.all().order_by("name")
- class WeighTicketWeightsForm(forms.Form):
- gross_weight = forms.DecimalField(max_digits=12, decimal_places=3)
- tare_weight = forms.DecimalField(max_digits=12, decimal_places=3)
- unit = forms.ChoiceField(choices=(), required=True)
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- from .models import Material as Mat
- self.fields["unit"].choices = Mat.UNIT_CHOICES
- def clean(self):
- cleaned = super().clean()
- g = cleaned.get("gross_weight")
- t = cleaned.get("tare_weight")
- if g is not None and t is not None and g < t:
- self.add_error("gross_weight", "Gross must be greater than or equal to tare")
- return cleaned
|