Files
company_site/company_site/financial/forms.py
T
westfarn 7dd5ec3be1
Unit Tests / test (push) Successful in 16s
Add Employee vs Client user type and filter time/reports by employee (#14) (#15)
## 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
2026-07-05 12:54:50 +00:00

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