diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 99b0063..80f84c6 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 ecbd4a1..52c9fc5 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 714f84e..ad444c4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -23,14 +23,16 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true" ALLOWED_HOSTS = [ "127.0.0.1", "localhost", + "foxlog.flatlogic.app", os.getenv("HOST_FQDN", ""), ] CSRF_TRUSTED_ORIGINS = [ - origin for origin in [ + "https://foxlog.flatlogic.app", + *[origin for origin in [ os.getenv("HOST_FQDN", ""), os.getenv("CSRF_TRUSTED_ORIGIN", "") - ] if origin + ] if origin] ] CSRF_TRUSTED_ORIGINS = [ f"https://{host}" if not host.startswith(("http://", "https://")) else host @@ -156,6 +158,10 @@ STATICFILES_DIRS = [ BASE_DIR / 'node_modules', ] +# Media files (Uploads) +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + # Email EMAIL_BACKEND = os.getenv( "EMAIL_BACKEND", @@ -185,4 +191,4 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Authentication LOGIN_URL = 'login' LOGIN_REDIRECT_URL = 'home' -LOGOUT_REDIRECT_URL = 'login' \ No newline at end of file +LOGOUT_REDIRECT_URL = 'login' diff --git a/config/urls.py b/config/urls.py index 2cb6937..7934289 100644 --- a/config/urls.py +++ b/config/urls.py @@ -27,4 +27,5 @@ urlpatterns = [ if settings.DEBUG: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index fe8bccf..0d1900a 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 1255e6c..d899904 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index bed590a..e3d2735 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 deee1d0..e82cf4b 100644 --- a/core/admin.py +++ b/core/admin.py @@ -7,8 +7,9 @@ class UserProfileAdmin(admin.ModelAdmin): @admin.register(Worker) class WorkerAdmin(admin.ModelAdmin): - list_display = ('name', 'id_no', 'phone_no', 'monthly_salary') + 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 @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): @@ -24,4 +25,4 @@ class TeamAdmin(admin.ModelAdmin): class WorkLogAdmin(admin.ModelAdmin): list_display = ('date', 'project', 'supervisor') list_filter = ('date', 'project', 'supervisor') - filter_horizontal = ('workers',) + filter_horizontal = ('workers',) \ No newline at end of file diff --git a/core/migrations/0009_worker_date_of_employment_worker_id_photo_and_more.py b/core/migrations/0009_worker_date_of_employment_worker_id_photo_and_more.py new file mode 100644 index 0000000..055d561 --- /dev/null +++ b/core/migrations/0009_worker_date_of_employment_worker_id_photo_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.7 on 2026-02-04 14:11 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_alter_expensereceipt_payment_method_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='worker', + name='date_of_employment', + field=models.DateField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='worker', + name='id_photo', + field=models.ImageField(blank=True, null=True, upload_to='workers/ids/', verbose_name='ID Document'), + ), + migrations.AddField( + model_name='worker', + name='notes', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='worker', + name='photo', + field=models.ImageField(blank=True, null=True, upload_to='workers/photos/'), + ), + ] diff --git a/core/migrations/__pycache__/0009_worker_date_of_employment_worker_id_photo_and_more.cpython-311.pyc b/core/migrations/__pycache__/0009_worker_date_of_employment_worker_id_photo_and_more.cpython-311.pyc new file mode 100644 index 0000000..1608208 Binary files /dev/null and b/core/migrations/__pycache__/0009_worker_date_of_employment_worker_id_photo_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 69779cf..c43ef4b 100644 --- a/core/models.py +++ b/core/models.py @@ -27,6 +27,13 @@ class Worker(models.Model): id_no = models.CharField(max_length=50, unique=True, verbose_name="ID Number") phone_no = models.CharField(max_length=20, verbose_name="Phone Number") monthly_salary = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))]) + + # New fields + photo = models.ImageField(upload_to='workers/photos/', blank=True, null=True) + id_photo = models.ImageField(upload_to='workers/ids/', blank=True, null=True, verbose_name="ID Document") + date_of_employment = models.DateField(default=timezone.now) + notes = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) is_active = models.BooleanField(default=True) @@ -34,6 +41,11 @@ class Worker(models.Model): def day_rate(self): return self.monthly_salary / Decimal('20.0') + @property + def projects_worked_on_count(self): + """Returns the number of distinct projects this worker has worked on.""" + return self.work_logs.values('project').distinct().count() + def __str__(self): return self.name diff --git a/core/templates/core/email/payslip_email.html b/core/templates/core/email/payslip_email.html new file mode 100644 index 0000000..48c203f --- /dev/null +++ b/core/templates/core/email/payslip_email.html @@ -0,0 +1,70 @@ + + + + + + +
+
+
Payslip
+
Reference: #{{ record.id }}
+
+ +
+ Worker: {{ record.worker.name }}
+ ID Number: {{ record.worker.id_no }}
+ Date: {{ record.date }} +
+ + + + + + + + + + + + + + + + + {% for adj in adjustments %} + + + + + {% endfor %} + +
DescriptionAmount
Base Pay ({{ logs_count }} days worked)R {{ logs_amount }}
{{ adj.get_type_display }}: {{ adj.description }} + R {{ adj.amount }} +
+ +
+

Net Pay: R {{ record.amount }}

+
+ + +
+ + diff --git a/core/views.py b/core/views.py index 1e77768..90f1af0 100644 --- a/core/views.py +++ b/core/views.py @@ -590,21 +590,27 @@ def process_payment(request, worker_id): # Email Notification subject = f"Payslip for {worker.name} - {payroll_record.date}" - message = ( - f"Payslip Generated\n\n" - f"Record ID: #{payroll_record.id}\n" - f"Worker: {worker.name}\n" - f"Date: {payroll_record.date}\n" - f"Total Paid: R {payroll_record.amount}\n\n" - f"Breakdown:\n" - f"Base Pay ({log_count} days): R {logs_amount}\n" - f"Adjustments: R {adj_amount}\n\n" - f"This is an automated notification." - ) + + # Prepare HTML content + context = { + 'record': payroll_record, + 'logs_count': log_count, + 'logs_amount': logs_amount, + 'adjustments': payroll_record.adjustments.all(), + } + html_message = render_to_string('core/email/payslip_email.html', context) + plain_message = strip_tags(html_message) + recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com'] try: - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list) + send_mail( + subject, + plain_message, + settings.DEFAULT_FROM_EMAIL, + recipient_list, + html_message=html_message + ) messages.success(request, f"Payment processed for {worker.name}. Net Pay: R {payroll_record.amount}") except Exception as e: messages.warning(request, f"Payment processed, but email delivery failed: {str(e)}") @@ -801,4 +807,4 @@ def create_receipt(request): return render(request, 'core/create_receipt.html', { 'form': form, 'items': items - }) \ No newline at end of file + }) diff --git a/requirements.txt b/requirements.txt index e22994c..65b2871 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +Pillow