155 lines
7.1 KiB
Python
155 lines
7.1 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, **kwargs):
|
|
self.business = business
|
|
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
|
|
|
|
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.')
|
|
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']
|