Unit Tests / test (push) Successful in 16s
## Summary - Adds `UserProfile` model with mutually exclusive **Employee** / **Client** types - Replaces auto-Employee signal with auto-Client profile on user creation - Data migration: users with time log entries → Employee; others → Client (orphan Employee rows removed) - Admin UI at `/financial/manage_users` to set any user's type; profile page shows current type - **Employees** can log time; **Clients** get read-only access to reports and time logs - Time logs, reports, and dashboard filter to employees only - 14 new tests covering signals, type switching, access control, and filtering ## Design decisions (from issue Q&A) 1. Client login = read-only financial access (reports + time logs, no edit/log time) 2. Employee and Client are strictly mutually exclusive 3. Admins (superusers) can change type via Manage Users 4. Bulk migration applied for existing users ## Test plan - [x] `python manage.py test financial.tests` (14 tests pass) - [x] `python manage.py test public.tests` (21 tests pass) - [ ] Run migration on staging: `python manage.py migrate` - [ ] Verify admin can set user types at `/financial/manage_users` - [ ] Verify employee can log time at `/financial/timekeeping` - [ ] Verify client sees reports/time logs read-only, cannot log time - [ ] Verify employee filter dropdown excludes clients Closes #14 Reviewed-on: #15
102 lines
4.3 KiB
Python
102 lines
4.3 KiB
Python
import datetime
|
|
from django import forms
|
|
from django.contrib.auth.models import User
|
|
from django.forms import ModelForm
|
|
from .models import Employee, Contract, ChargeNumber, TimeCardCell, AddressModel, UserProfile, set_user_type
|
|
|
|
class NewEmployeeForm(ModelForm):
|
|
first_name = forms.CharField(max_length=30, required=False, label="First Name")
|
|
last_name = forms.CharField(max_length=30, required=False, label="Last Name")
|
|
address_1 = forms.CharField(max_length=128, label="Address Line 1")
|
|
address_2 = forms.CharField(max_length=128, required=False, label="Address Line 2")
|
|
city = forms.CharField(max_length=64, label="City")
|
|
state = forms.CharField(max_length=2, label="State (e.g. NY)")
|
|
zip_code = forms.CharField(max_length=5, label="ZIP Code")
|
|
|
|
class Meta:
|
|
model = Employee
|
|
fields = ["user", "manager", "phoneNumber", "slary"]
|
|
|
|
def save(self, commit=True):
|
|
employee = super().save(commit=False)
|
|
|
|
if self.cleaned_data.get('first_name') or self.cleaned_data.get('last_name'):
|
|
if self.cleaned_data.get('first_name'):
|
|
employee.user.first_name = self.cleaned_data.get('first_name')
|
|
if self.cleaned_data.get('last_name'):
|
|
employee.user.last_name = self.cleaned_data.get('last_name')
|
|
employee.user.save()
|
|
|
|
address = AddressModel.objects.create(
|
|
address_1=self.cleaned_data['address_1'],
|
|
address_2=self.cleaned_data['address_2'],
|
|
city=self.cleaned_data['city'],
|
|
state=self.cleaned_data['state'],
|
|
zip_code=self.cleaned_data['zip_code'],
|
|
)
|
|
employee.primaryAddress = address
|
|
employee.workAddress = address
|
|
if commit:
|
|
employee.save()
|
|
set_user_type(employee.user, UserProfile.UserType.EMPLOYEE)
|
|
return employee
|
|
|
|
class EmployeeForm(ModelForm):
|
|
class Meta:
|
|
model = Employee
|
|
fields = ["user", "manager", "primaryAddress", "workAddress", "phoneNumber", "slary"]
|
|
|
|
class UserProfileForm(ModelForm):
|
|
class Meta:
|
|
model = UserProfile
|
|
fields = ["user_type"]
|
|
|
|
class AdminUserTypeForm(forms.Form):
|
|
user = forms.ModelChoiceField(queryset=User.objects.order_by("username"))
|
|
user_type = forms.ChoiceField(choices=UserProfile.UserType.choices)
|
|
|
|
class ContractForm(ModelForm):
|
|
class Meta:
|
|
model = Contract
|
|
fields = ["contract_type","name","proposed_amount","baseline_amount","funded_amount","budget_hours","baseline_start","baseline_end"]
|
|
|
|
class ChargeNumberForm(ModelForm):
|
|
class Meta:
|
|
model = ChargeNumber
|
|
fields = ["charge_number_type","amount", "budget_hours", "percent_complete", "start_date","end_date"]
|
|
|
|
class TimeLogForm(ModelForm):
|
|
start_time = forms.TimeField(required=False, widget=forms.TimeInput(attrs={'type': 'time'}))
|
|
end_time = forms.TimeField(required=False, widget=forms.TimeInput(attrs={'type': 'time'}))
|
|
hour = forms.FloatField(required=False, label="Duration (hours)")
|
|
date = forms.DateField(initial=datetime.date.today, widget=forms.DateInput(attrs={'type': 'date'}))
|
|
|
|
class Meta:
|
|
model = TimeCardCell
|
|
fields = ["charge_number", "date", "start_time", "end_time", "hour"]
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
start = cleaned_data.get('start_time')
|
|
end = cleaned_data.get('end_time')
|
|
duration = cleaned_data.get('hour')
|
|
|
|
if start and end:
|
|
dt_start = datetime.datetime.combine(datetime.date.today(), start)
|
|
dt_end = datetime.datetime.combine(datetime.date.today(), end)
|
|
diff = (dt_end - dt_start).total_seconds() / 3600.0
|
|
if diff < 0:
|
|
diff += 24.0
|
|
cleaned_data['hour'] = round(diff, 2)
|
|
elif start and duration:
|
|
dt_start = datetime.datetime.combine(datetime.date.today(), start)
|
|
dt_end = dt_start + datetime.timedelta(hours=duration)
|
|
cleaned_data['end_time'] = (dt_end).time()
|
|
elif not duration and not (start and end):
|
|
raise forms.ValidationError("You must provide either (Start Time and End Time) OR (Start Time and Duration) OR (Duration).")
|
|
|
|
if not cleaned_data.get('hour') and duration:
|
|
cleaned_data['hour'] = duration
|
|
|
|
return cleaned_data
|