from decimal import Decimal from django import forms from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.utils import timezone from .models import BusinessProfile, Transaction INPUT_CLASS = 'form-control form-control-lg momo-input' SELECT_CLASS = 'form-select form-select-lg momo-input' class SignUpForm(UserCreationForm): first_name = forms.CharField(max_length=150, required=True, widget=forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'First name'})) last_name = forms.CharField(max_length=150, required=True, widget=forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Last name'})) email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Email address'})) class Meta(UserCreationForm.Meta): model = User fields = ('first_name', 'last_name', 'username', 'email') widgets = { 'username': forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Username'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['username'].widget.attrs.update({'class': INPUT_CLASS, 'placeholder': 'Username'}) self.fields['password1'].widget.attrs.update({'class': INPUT_CLASS, 'placeholder': 'Password'}) self.fields['password2'].widget.attrs.update({'class': INPUT_CLASS, 'placeholder': 'Confirm password'}) def save(self, commit=True): user = super().save(commit=False) user.first_name = self.cleaned_data['first_name'] user.last_name = self.cleaned_data['last_name'] user.email = self.cleaned_data['email'] if commit: user.save() return user class LoginForm(AuthenticationForm): username = forms.CharField(widget=forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Username'})) password = forms.CharField(widget=forms.PasswordInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Password'})) class BusinessProfileForm(forms.ModelForm): class Meta: model = BusinessProfile fields = ['business_name', 'logo', 'opening_ecash', 'opening_physical_cash'] widgets = { 'business_name': forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'e.g. BrightPay MoMo Point'}), 'logo': forms.ClearableFileInput(attrs={'class': 'form-control momo-file', 'accept': 'image/*'}), 'opening_ecash': forms.NumberInput(attrs={'class': INPUT_CLASS, 'step': '0.01', 'min': '0'}), 'opening_physical_cash': forms.NumberInput(attrs={'class': INPUT_CLASS, 'step': '0.01', 'min': '0'}), } help_texts = { 'opening_ecash': 'Used as your starting e-cash wallet for the first setup.', 'opening_physical_cash': 'Used as your starting notes/coins balance for the first setup.', } def save(self, commit=True): profile = super().save(commit=False) should_sync = not profile.pk or not profile.transactions.exists() if should_sync: profile.sync_current_to_opening() if commit: profile.save() return profile class TransactionForm(forms.Form): client_name = forms.CharField(max_length=120, widget=forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Client name'})) amount = forms.DecimalField(max_digits=12, decimal_places=2, min_value=Decimal('0.01'), widget=forms.NumberInput(attrs={'class': INPUT_CLASS, 'step': '0.01', 'min': '0.01'})) transaction_type = forms.ChoiceField(choices=Transaction.TYPE_CHOICES, widget=forms.Select(attrs={'class': SELECT_CLASS})) notes = forms.CharField(required=False, widget=forms.Textarea(attrs={'class': 'form-control momo-input', 'rows': 3, 'placeholder': 'Optional note about the transaction'})) def __init__(self, *args, business=None, instance=None, **kwargs): self.business = business self.instance = instance super().__init__(*args, **kwargs) def clean(self): cleaned_data = super().clean() transaction_type = cleaned_data.get('transaction_type') amount = cleaned_data.get('amount') if not self.business or not transaction_type or amount is None: return cleaned_data if self.instance is not None: return cleaned_data ecash_delta, physical_delta, _ = Transaction.calculate_effect(transaction_type, amount) if self.business.current_ecash + ecash_delta < 0: self.add_error('amount', 'Not enough e-cash for this transaction.') if self.business.current_physical_cash + physical_delta < 0: self.add_error('amount', 'Not enough physical cash for this transaction.') return cleaned_data def save(self, user): if not self.business: raise ValidationError('Business profile is required.') if self.instance is not None: return self.instance.update_logged_transaction( client_name=self.cleaned_data['client_name'], amount=self.cleaned_data['amount'], transaction_type=self.cleaned_data['transaction_type'], notes=self.cleaned_data['notes'], ) return Transaction.create_logged_transaction( business=self.business, user=user, client_name=self.cleaned_data['client_name'], amount=self.cleaned_data['amount'], transaction_type=self.cleaned_data['transaction_type'], notes=self.cleaned_data['notes'], ) class ReportFilterForm(forms.Form): PERIOD_CHOICES = [ ('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('yearly', 'Yearly'), ('custom', 'Custom range'), ] period = forms.ChoiceField(choices=PERIOD_CHOICES, initial='daily', widget=forms.Select(attrs={'class': 'form-select momo-input'})) start_date = forms.DateField(required=False, widget=forms.DateInput(attrs={'class': 'form-control momo-input', 'type': 'date'})) end_date = forms.DateField(required=False, widget=forms.DateInput(attrs={'class': 'form-control momo-input', 'type': 'date'})) def clean(self): cleaned_data = super().clean() period = cleaned_data.get('period') start_date = cleaned_data.get('start_date') end_date = cleaned_data.get('end_date') if period == 'custom': if not start_date or not end_date: raise forms.ValidationError('Choose both a start and end date for a custom report.') if end_date < start_date: raise forms.ValidationError('End date cannot be before start date.') return cleaned_data def get_range(self): today = timezone.localdate() period = self.cleaned_data.get('period') or 'daily' if period == 'daily': return today, today if period == 'weekly': start = today - timezone.timedelta(days=today.weekday()) return start, start + timezone.timedelta(days=6) if period == 'monthly': start = today.replace(day=1) if start.month == 12: next_month = start.replace(year=start.year + 1, month=1, day=1) else: next_month = start.replace(month=start.month + 1, day=1) return start, next_month - timezone.timedelta(days=1) if period == 'yearly': start = today.replace(month=1, day=1) return start, today.replace(month=12, day=31) return self.cleaned_data['start_date'], self.cleaned_data['end_date']