39710-vm/core/forms.py
2026-04-17 04:26:07 +00:00

166 lines
7.6 KiB
Python

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']