diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 881731c..e7b86fc 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 42d995d..ac4b4d6 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..18cf908 100644 --- a/config/settings.py +++ b/config/settings.py @@ -135,7 +135,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Africa/Johannesburg' USE_I18N = True @@ -151,10 +151,12 @@ STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [ BASE_DIR / 'static', - BASE_DIR / 'assets', BASE_DIR / 'node_modules', ] +MEDIA_URL = 'media/' +MEDIA_ROOT = BASE_DIR / 'media' + # Email EMAIL_BACKEND = os.getenv( "EMAIL_BACKEND", diff --git a/config/urls.py b/config/urls.py index bcfc074..1eee8cd 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,19 +1,3 @@ -""" -URL configuration for config project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin from django.urls import include, path from django.conf import settings @@ -25,5 +9,5 @@ urlpatterns = [ ] if settings.DEBUG: - urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 2964e11..4d4addd 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..2b6866d Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 18a063c..bd60181 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ebb8c6e..399aecc 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 8d204fa..96f277a 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..1fd949f 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,73 @@ from django.contrib import admin +from .models import ( + UserProfile, Project, Worker, Team, WorkLog, + PayrollRecord, Loan, PayrollAdjustment, + ExpenseReceipt, ExpenseLineItem +) -# Register your models here. +@admin.register(UserProfile) +class UserProfileAdmin(admin.ModelAdmin): + list_display = ('user',) + search_fields = ('user__username', 'user__first_name', 'user__last_name') + +@admin.register(Project) +class ProjectAdmin(admin.ModelAdmin): + list_display = ('name', 'active') + list_filter = ('active',) + search_fields = ('name', 'description') + filter_horizontal = ('supervisors',) + +@admin.register(Worker) +class WorkerAdmin(admin.ModelAdmin): + list_display = ('name', 'id_number', 'monthly_salary', 'active') + list_filter = ('active',) + search_fields = ('name', 'id_number', 'phone_number') + +@admin.register(Team) +class TeamAdmin(admin.ModelAdmin): + list_display = ('name', 'supervisor', 'active') + list_filter = ('active', 'supervisor') + search_fields = ('name',) + filter_horizontal = ('workers',) + +@admin.register(WorkLog) +class WorkLogAdmin(admin.ModelAdmin): + list_display = ('date', 'project', 'supervisor', 'overtime_amount') + list_filter = ('date', 'project', 'supervisor') + search_fields = ('project__name', 'notes') + filter_horizontal = ('workers', 'priced_workers') + +@admin.register(PayrollRecord) +class PayrollRecordAdmin(admin.ModelAdmin): + list_display = ('worker', 'date', 'amount_paid') + list_filter = ('date', 'worker') + search_fields = ('worker__name',) + filter_horizontal = ('work_logs',) + +@admin.register(Loan) +class LoanAdmin(admin.ModelAdmin): + list_display = ('worker', 'principal_amount', 'remaining_balance', 'date', 'active') + list_filter = ('active', 'date', 'worker') + search_fields = ('worker__name', 'reason') + +@admin.register(PayrollAdjustment) +class PayrollAdjustmentAdmin(admin.ModelAdmin): + list_display = ('worker', 'type', 'amount', 'date') + list_filter = ('type', 'date', 'worker') + search_fields = ('worker__name', 'description') + +class ExpenseLineItemInline(admin.TabularInline): + model = ExpenseLineItem + extra = 1 + +@admin.register(ExpenseReceipt) +class ExpenseReceiptAdmin(admin.ModelAdmin): + list_display = ('vendor_name', 'date', 'total_amount', 'user') + list_filter = ('date', 'payment_method', 'vat_type') + search_fields = ('vendor_name', 'description') + inlines = [ExpenseLineItemInline] + +@admin.register(ExpenseLineItem) +class ExpenseLineItemAdmin(admin.ModelAdmin): + list_display = ('product_name', 'amount', 'receipt') + search_fields = ('product_name', 'receipt__vendor_name') \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..8e31894 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,22 @@ +from django import forms +from .models import WorkLog, Project, Team, Worker + +class AttendanceLogForm(forms.ModelForm): + class Meta: + model = WorkLog + fields = ['date', 'project', 'team', 'workers', 'supervisor', 'overtime_amount', 'notes'] + widgets = { + 'date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + 'project': forms.Select(attrs={'class': 'form-select'}), + 'team': forms.Select(attrs={'class': 'form-select'}), + 'workers': forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}), + 'supervisor': forms.Select(attrs={'class': 'form-select'}), + 'overtime_amount': forms.Select(attrs={'class': 'form-select'}), + 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['workers'].queryset = Worker.objects.filter(active=True) + self.fields['project'].queryset = Project.objects.filter(active=True) + self.fields['team'].queryset = Team.objects.filter(active=True) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..72592a0 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,136 @@ +# Generated by Django 5.2.7 on 2026-02-22 12:17 + +import django.db.models.deletion +import django.utils.timezone +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Worker', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('id_number', models.CharField(max_length=50, unique=True)), + ('phone_number', models.CharField(blank=True, max_length=20)), + ('monthly_salary', models.DecimalField(decimal_places=2, max_digits=10)), + ('photo', models.ImageField(blank=True, null=True, upload_to='workers/photos/')), + ('id_document', models.FileField(blank=True, null=True, upload_to='workers/documents/')), + ('employment_date', models.DateField(default=django.utils.timezone.now)), + ('notes', models.TextField(blank=True)), + ('active', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='ExpenseReceipt', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(default=django.utils.timezone.now)), + ('vendor_name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('payment_method', models.CharField(choices=[('Cash', 'Cash'), ('Card', 'Card'), ('EFT', 'EFT'), ('Other', 'Other')], max_length=20)), + ('vat_type', models.CharField(choices=[('Included', 'Included'), ('Excluded', 'Excluded'), ('None', 'None')], max_length=20)), + ('subtotal', models.DecimalField(decimal_places=2, max_digits=12)), + ('vat_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expense_receipts', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ExpenseLineItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_name', models.CharField(max_length=200)), + ('amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('receipt', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='line_items', to='core.expensereceipt')), + ], + ), + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('active', models.BooleanField(default=True)), + ('supervisors', models.ManyToManyField(related_name='assigned_projects', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('active', models.BooleanField(default=True)), + ('supervisor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='supervised_teams', to=settings.AUTH_USER_MODEL)), + ('workers', models.ManyToManyField(related_name='teams', to='core.worker')), + ], + ), + migrations.CreateModel( + name='Loan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('principal_amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('remaining_balance', models.DecimalField(decimal_places=2, max_digits=10)), + ('date', models.DateField(default=django.utils.timezone.now)), + ('reason', models.TextField(blank=True)), + ('active', models.BooleanField(default=True)), + ('worker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loans', to='core.worker')), + ], + ), + migrations.CreateModel( + name='WorkLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(default=django.utils.timezone.now)), + ('notes', models.TextField(blank=True)), + ('overtime_amount', models.DecimalField(choices=[(Decimal('0.00'), 'None'), (Decimal('0.25'), '1/4 Day'), (Decimal('0.50'), '1/2 Day'), (Decimal('0.75'), '3/4 Day'), (Decimal('1.00'), 'Full Day')], decimal_places=2, default=Decimal('0.00'), max_digits=3)), + ('priced_workers', models.ManyToManyField(blank=True, related_name='priced_overtime_logs', to='core.worker')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='work_logs', to='core.project')), + ('supervisor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='work_logs_created', to=settings.AUTH_USER_MODEL)), + ('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='work_logs', to='core.team')), + ('workers', models.ManyToManyField(related_name='work_logs', to='core.worker')), + ], + ), + migrations.CreateModel( + name='PayrollRecord', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(default=django.utils.timezone.now)), + ('amount_paid', models.DecimalField(decimal_places=2, max_digits=10)), + ('worker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payroll_records', to='core.worker')), + ('work_logs', models.ManyToManyField(related_name='payroll_records', to='core.worklog')), + ], + ), + migrations.CreateModel( + name='PayrollAdjustment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('date', models.DateField(default=django.utils.timezone.now)), + ('description', models.TextField(blank=True)), + ('type', models.CharField(choices=[('Bonus', 'Bonus'), ('Overtime', 'Overtime'), ('Deduction', 'Deduction'), ('Loan Repayment', 'Loan Repayment'), ('New Loan', 'New Loan'), ('Advance Payment', 'Advance Payment')], max_length=50)), + ('loan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='repayments', to='core.loan')), + ('payroll_record', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments', to='core.payrollrecord')), + ('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments_by_project', to='core.project')), + ('worker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adjustments', to='core.worker')), + ('work_log', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments_by_work_log', to='core.worklog')), + ], + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..47d6fc4 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..e38921a 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,163 @@ from django.db import models +from django.contrib.auth.models import User +from django.utils import timezone +from decimal import Decimal +from django.db.models.signals import post_save +from django.dispatch import receiver -# Create your models here. +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + # Add any extra profile fields if needed in the future + + def __str__(self): + return self.user.username + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + UserProfile.objects.get_or_create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + if hasattr(instance, 'profile'): + instance.profile.save() + +class Project(models.Model): + name = models.CharField(max_length=200) + description = models.TextField(blank=True) + supervisors = models.ManyToManyField(User, related_name='assigned_projects') + active = models.BooleanField(default=True) + + def __str__(self): + return self.name + +class Worker(models.Model): + name = models.CharField(max_length=200) + id_number = models.CharField(max_length=50, unique=True) + phone_number = models.CharField(max_length=20, blank=True) + monthly_salary = models.DecimalField(max_digits=10, decimal_places=2) + photo = models.ImageField(upload_to='workers/photos/', blank=True, null=True) + id_document = models.FileField(upload_to='workers/documents/', blank=True, null=True) + employment_date = models.DateField(default=timezone.now) + notes = models.TextField(blank=True) + active = models.BooleanField(default=True) + + @property + def daily_rate(self): + # monthly salary divided by 20 working days + return (self.monthly_salary / Decimal('20.00')).quantize(Decimal('0.01')) + + def __str__(self): + return self.name + +class Team(models.Model): + name = models.CharField(max_length=200) + workers = models.ManyToManyField(Worker, related_name='teams') + supervisor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='supervised_teams') + active = models.BooleanField(default=True) + + def __str__(self): + return self.name + +class WorkLog(models.Model): + OVERTIME_CHOICES = [ + (Decimal('0.00'), 'None'), + (Decimal('0.25'), '1/4 Day'), + (Decimal('0.50'), '1/2 Day'), + (Decimal('0.75'), '3/4 Day'), + (Decimal('1.00'), 'Full Day'), + ] + + date = models.DateField(default=timezone.now) + project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='work_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, related_name='work_logs_created') + notes = models.TextField(blank=True) + overtime_amount = models.DecimalField(max_digits=3, decimal_places=2, choices=OVERTIME_CHOICES, default=Decimal('0.00')) + priced_workers = models.ManyToManyField(Worker, related_name='priced_overtime_logs', blank=True) + + def __str__(self): + return f"{self.date} - {self.project.name}" + +class PayrollRecord(models.Model): + worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='payroll_records') + date = models.DateField(default=timezone.now) + amount_paid = models.DecimalField(max_digits=10, decimal_places=2) + work_logs = models.ManyToManyField(WorkLog, related_name='payroll_records') + + def __str__(self): + return f"{self.worker.name} - {self.date}" + +class Loan(models.Model): + worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='loans') + principal_amount = models.DecimalField(max_digits=10, decimal_places=2) + remaining_balance = models.DecimalField(max_digits=10, decimal_places=2) + date = models.DateField(default=timezone.now) + reason = models.TextField(blank=True) + active = models.BooleanField(default=True) + + def save(self, *args, **kwargs): + if not self.pk: + self.remaining_balance = self.principal_amount + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.worker.name} - Loan - {self.date}" + +class PayrollAdjustment(models.Model): + TYPE_CHOICES = [ + ('Bonus', 'Bonus'), + ('Overtime', 'Overtime'), + ('Deduction', 'Deduction'), + ('Loan Repayment', 'Loan Repayment'), + ('New Loan', 'New Loan'), + ('Advance Payment', 'Advance Payment'), + ] + + worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='adjustments') + payroll_record = models.ForeignKey(PayrollRecord, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments') + loan = models.ForeignKey(Loan, on_delete=models.SET_NULL, null=True, blank=True, related_name='repayments') + work_log = models.ForeignKey(WorkLog, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments_by_work_log') + project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments_by_project') + amount = models.DecimalField(max_digits=10, decimal_places=2) + date = models.DateField(default=timezone.now) + description = models.TextField(blank=True) + type = models.CharField(max_length=50, choices=TYPE_CHOICES) + + def __str__(self): + return f"{self.worker.name} - {self.type} - {self.amount}" + +class ExpenseReceipt(models.Model): + METHOD_CHOICES = [ + ('Cash', 'Cash'), + ('Card', 'Card'), + ('EFT', 'EFT'), + ('Other', 'Other'), + ] + VAT_CHOICES = [ + ('Included', 'Included'), + ('Excluded', 'Excluded'), + ('None', 'None'), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense_receipts') + date = models.DateField(default=timezone.now) + vendor_name = models.CharField(max_length=200) + description = models.TextField(blank=True) + payment_method = models.CharField(max_length=20, choices=METHOD_CHOICES) + vat_type = models.CharField(max_length=20, choices=VAT_CHOICES) + subtotal = models.DecimalField(max_digits=12, decimal_places=2) + vat_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) + total_amount = models.DecimalField(max_digits=12, decimal_places=2) + + def __str__(self): + return f"{self.vendor_name} - {self.date}" + +class ExpenseLineItem(models.Model): + receipt = models.ForeignKey(ExpenseReceipt, on_delete=models.CASCADE, related_name='line_items') + product_name = models.CharField(max_length=200) + amount = models.DecimalField(max_digits=12, decimal_places=2) + + def __str__(self): + return self.product_name diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..45e4df7 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,68 @@ +{% load static %} -
- -