diff --git a/core/admin.py b/core/admin.py index e82cf4b..f2861a0 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,28 +1,101 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import User +from django.db import models from .models import Worker, Project, Team, WorkLog, UserProfile -@admin.register(UserProfile) -class UserProfileAdmin(admin.ModelAdmin): - list_display = ('user', 'pin', 'is_admin') + +# Inline UserProfile on User admin +class UserProfileInline(admin.StackedInline): + model = UserProfile + can_delete = False + verbose_name_plural = 'Profile' + + +# Customise the User admin to show groups prominently +class UserAdmin(BaseUserAdmin): + inlines = [UserProfileInline] + list_display = ('username', 'first_name', 'last_name', 'is_staff', 'get_groups') + list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') + + # Reorder fieldsets so Groups appears near the top + fieldsets = ( + (None, {'fields': ('username', 'password')}), + ('Personal info', {'fields': ('first_name', 'last_name', 'email')}), + ('Role & Groups', { + 'description': 'Assign user to "Admin" or "Work Logger" group to set permissions automatically.', + 'fields': ('is_active', 'is_staff', 'groups'), + }), + ('Individual Permissions', { + 'classes': ('collapse',), + 'description': 'Fine-tune permissions beyond what the group provides. Usually not needed.', + 'fields': ('is_superuser', 'user_permissions'), + }), + ('Important dates', { + 'classes': ('collapse',), + 'fields': ('last_login', 'date_joined'), + }), + ) + + def get_groups(self, obj): + return ', '.join(g.name for g in obj.groups.all()) or '-' + get_groups.short_description = 'Groups' + + +# Re-register User with our custom admin +admin.site.unregister(User) +admin.site.register(User, UserAdmin) + @admin.register(Worker) class WorkerAdmin(admin.ModelAdmin): list_display = ('name', 'id_no', 'phone_no', 'monthly_salary', 'date_of_employment', 'projects_worked_on_count') search_fields = ('name', 'id_no') - readonly_fields = ('projects_worked_on_count',) # Calculated field should be readonly in edit form + readonly_fields = ('projects_worked_on_count',) + @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): - list_display = ('name', 'created_at') + list_display = ('name', 'get_supervisors', 'is_active', 'created_at') + list_filter = ('is_active',) filter_horizontal = ('supervisors',) + def get_supervisors(self, obj): + return ', '.join(u.username for u in obj.supervisors.all()) or '-' + get_supervisors.short_description = 'Supervisors' + + def formfield_for_manytomany(self, db_field, request, **kwargs): + if db_field.name == 'supervisors': + # Only show users who are staff or in Work Logger/Admin group + kwargs['queryset'] = User.objects.filter( + models.Q(is_staff=True) | + models.Q(groups__name__in=['Admin', 'Work Logger']) + ).distinct().order_by('username') + return super().formfield_for_manytomany(db_field, request, **kwargs) + + @admin.register(Team) class TeamAdmin(admin.ModelAdmin): - list_display = ('name', 'supervisor', 'created_at') + list_display = ('name', 'supervisor', 'worker_count', 'is_active', 'created_at') + list_filter = ('is_active', 'supervisor') filter_horizontal = ('workers',) + def worker_count(self, obj): + return obj.workers.count() + worker_count.short_description = 'Workers' + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == 'supervisor': + # Only show users who are staff or in Work Logger/Admin group + kwargs['queryset'] = User.objects.filter( + models.Q(is_staff=True) | + models.Q(groups__name__in=['Admin', 'Work Logger']) + ).distinct().order_by('username') + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + @admin.register(WorkLog) class WorkLogAdmin(admin.ModelAdmin): list_display = ('date', 'project', 'supervisor') list_filter = ('date', 'project', 'supervisor') - filter_horizontal = ('workers',) \ No newline at end of file + filter_horizontal = ('workers',) diff --git a/core/management/commands/setup_groups.py b/core/management/commands/setup_groups.py new file mode 100644 index 0000000..b2b3dae --- /dev/null +++ b/core/management/commands/setup_groups.py @@ -0,0 +1,69 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType + + +class Command(BaseCommand): + help = 'Creates Admin and Work Logger permission groups with pre-assigned permissions' + + def handle(self, *args, **options): + # --- Admin Group --- + # Full access to all core business models + user management + admin_group, created = Group.objects.get_or_create(name='Admin') + admin_perms = [] + + # All core model permissions + for model in ['project', 'worker', 'team', 'worklog', 'payrollrecord', + 'loan', 'payrolladjustment', 'expensereceipt', 'expenselineitem']: + ct = ContentType.objects.filter(app_label='core', model=model).first() + if ct: + admin_perms.extend(Permission.objects.filter(content_type=ct)) + + # User management permissions + user_ct = ContentType.objects.filter(app_label='auth', model='user').first() + if user_ct: + admin_perms.extend(Permission.objects.filter(content_type=user_ct)) + + group_ct = ContentType.objects.filter(app_label='auth', model='group').first() + if group_ct: + admin_perms.extend(Permission.objects.filter(content_type=group_ct)) + + admin_group.permissions.set(admin_perms) + status = 'Created' if created else 'Updated' + self.stdout.write(self.style.SUCCESS( + f'{status} "Admin" group with {admin_group.permissions.count()} permissions' + )) + + # --- Work Logger Group --- + # Can log work, view history, create receipts - restricted to their teams/projects + supervisor_group, created = Group.objects.get_or_create(name='Work Logger') + supervisor_codenames = [ + # Projects - view only + 'view_project', + # Workers - view only + 'view_worker', + # Teams - view only + 'view_team', + # Work logs - full access (log attendance, edit, view) + 'add_worklog', 'change_worklog', 'view_worklog', + # Expense receipts - create and view + 'add_expensereceipt', 'view_expensereceipt', + # Expense line items - create and view (needed for receipt creation) + 'add_expenselineitem', 'view_expenselineitem', + ] + + supervisor_perms = Permission.objects.filter( + content_type__app_label='core', + codename__in=supervisor_codenames + ) + supervisor_group.permissions.set(supervisor_perms) + status = 'Created' if created else 'Updated' + self.stdout.write(self.style.SUCCESS( + f'{status} "Work Logger" group with {supervisor_group.permissions.count()} permissions' + )) + + self.stdout.write('') + self.stdout.write('To assign a user to a group:') + self.stdout.write(' 1. Go to Admin Panel > Users > select user') + self.stdout.write(' 2. Under "Groups", add them to "Admin" or "Work Logger"') + self.stdout.write(' 3. For Work Loggers, also assign them to Projects/Teams') diff --git a/core/migrations/0011_remove_userprofile_pin_and_is_admin.py b/core/migrations/0011_remove_userprofile_pin_and_is_admin.py new file mode 100644 index 0000000..a4710f7 --- /dev/null +++ b/core/migrations/0011_remove_userprofile_pin_and_is_admin.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2026-02-08 18:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_alter_expenselineitem_options_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='is_admin', + ), + migrations.RemoveField( + model_name='userprofile', + name='pin', + ), + ] diff --git a/core/migrations/0012_add_team_to_worklog.py b/core/migrations/0012_add_team_to_worklog.py new file mode 100644 index 0000000..1f296c4 --- /dev/null +++ b/core/migrations/0012_add_team_to_worklog.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.7 on 2026-02-08 19:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0011_remove_userprofile_pin_and_is_admin'), + ] + + operations = [ + migrations.AddField( + model_name='worklog', + name='team', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='work_logs', to='core.team'), + ), + ] diff --git a/core/models.py b/core/models.py index 43ea234..b7d56a2 100644 --- a/core/models.py +++ b/core/models.py @@ -6,8 +6,6 @@ from django.utils import timezone class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') - pin = models.CharField(max_length=4, help_text="4-digit PIN for login") - is_admin = models.BooleanField(default=False) class Meta: verbose_name = "User Profile" @@ -78,6 +76,7 @@ class Team(models.Model): class WorkLog(models.Model): date = models.DateField() project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='logs') + team = models.ForeignKey(Team, on_delete=models.SET_NULL, null=True, blank=True, related_name='work_logs') workers = models.ManyToManyField(Worker, related_name='work_logs') supervisor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) notes = models.TextField(blank=True) diff --git a/core/templates/base.html b/core/templates/base.html index 1f614ac..e097e70 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -33,47 +33,31 @@