Ver 10.3 fixing Spark
This commit is contained in:
parent
8feedf5963
commit
60ef14cac4
Binary file not shown.
Binary file not shown.
@ -23,14 +23,16 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
|||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
"localhost",
|
"localhost",
|
||||||
|
"foxlog.flatlogic.app",
|
||||||
os.getenv("HOST_FQDN", ""),
|
os.getenv("HOST_FQDN", ""),
|
||||||
]
|
]
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
origin for origin in [
|
"https://foxlog.flatlogic.app",
|
||||||
|
*[origin for origin in [
|
||||||
os.getenv("HOST_FQDN", ""),
|
os.getenv("HOST_FQDN", ""),
|
||||||
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
||||||
] if origin
|
] if origin]
|
||||||
]
|
]
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
f"https://{host}" if not host.startswith(("http://", "https://")) else host
|
f"https://{host}" if not host.startswith(("http://", "https://")) else host
|
||||||
@ -156,6 +158,10 @@ STATICFILES_DIRS = [
|
|||||||
BASE_DIR / 'node_modules',
|
BASE_DIR / 'node_modules',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Media files (Uploads)
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAIL_BACKEND = os.getenv(
|
EMAIL_BACKEND = os.getenv(
|
||||||
"EMAIL_BACKEND",
|
"EMAIL_BACKEND",
|
||||||
|
|||||||
@ -28,3 +28,4 @@ urlpatterns = [
|
|||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -7,8 +7,9 @@ class UserProfileAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Worker)
|
@admin.register(Worker)
|
||||||
class WorkerAdmin(admin.ModelAdmin):
|
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')
|
search_fields = ('name', 'id_no')
|
||||||
|
readonly_fields = ('projects_worked_on_count',) # Calculated field should be readonly in edit form
|
||||||
|
|
||||||
@admin.register(Project)
|
@admin.register(Project)
|
||||||
class ProjectAdmin(admin.ModelAdmin):
|
class ProjectAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@ -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/'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -27,6 +27,13 @@ class Worker(models.Model):
|
|||||||
id_no = models.CharField(max_length=50, unique=True, verbose_name="ID Number")
|
id_no = models.CharField(max_length=50, unique=True, verbose_name="ID Number")
|
||||||
phone_no = models.CharField(max_length=20, verbose_name="Phone 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'))])
|
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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
@ -34,6 +41,11 @@ class Worker(models.Model):
|
|||||||
def day_rate(self):
|
def day_rate(self):
|
||||||
return self.monthly_salary / Decimal('20.0')
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|||||||
70
core/templates/core/email/payslip_email.html
Normal file
70
core/templates/core/email/payslip_email.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; border: 1px solid #ddd; padding: 20px; }
|
||||||
|
.header { text-align: center; border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px; }
|
||||||
|
.title { font-size: 24px; font-weight: bold; text-transform: uppercase; color: #000; }
|
||||||
|
.subtitle { font-size: 14px; color: #666; margin-top: 5px; }
|
||||||
|
.meta { margin-bottom: 20px; background-color: #f8f9fa; padding: 10px; border-radius: 4px; }
|
||||||
|
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
||||||
|
.items-table th, .items-table td { border-bottom: 1px solid #eee; padding: 8px; text-align: left; }
|
||||||
|
.items-table th { background-color: #f8f9fa; }
|
||||||
|
.totals { text-align: right; margin-top: 20px; border-top: 2px solid #333; padding-top: 10px; }
|
||||||
|
.total-row { font-size: 20px; font-weight: bold; color: #000; }
|
||||||
|
.footer { margin-top: 30px; font-size: 12px; color: #777; text-align: center; border-top: 1px solid #eee; padding-top: 10px; }
|
||||||
|
.positive { color: green; }
|
||||||
|
.negative { color: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Payslip</div>
|
||||||
|
<div class="subtitle">Reference: #{{ record.id }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<strong>Worker:</strong> {{ record.worker.name }}<br>
|
||||||
|
<strong>ID Number:</strong> {{ record.worker.id_no }}<br>
|
||||||
|
<strong>Date:</strong> {{ record.date }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="items-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th style="text-align: right;">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Base Pay -->
|
||||||
|
<tr>
|
||||||
|
<td>Base Pay ({{ logs_count }} days worked)</td>
|
||||||
|
<td style="text-align: right;">R {{ logs_amount }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Adjustments -->
|
||||||
|
{% for adj in adjustments %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ adj.get_type_display }}: {{ adj.description }}</td>
|
||||||
|
<td style="text-align: right;" class="{% if adj.amount < 0 %}negative{% else %}positive{% endif %}">
|
||||||
|
R {{ adj.amount }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="totals">
|
||||||
|
<p class="total-row">Net Pay: R {{ record.amount }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Generated by Fox Fitt App for {{ record.worker.name }}</p>
|
||||||
|
<p>Date Generated: {% now "Y-m-d H:i" %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -590,21 +590,27 @@ def process_payment(request, worker_id):
|
|||||||
|
|
||||||
# Email Notification
|
# Email Notification
|
||||||
subject = f"Payslip for {worker.name} - {payroll_record.date}"
|
subject = f"Payslip for {worker.name} - {payroll_record.date}"
|
||||||
message = (
|
|
||||||
f"Payslip Generated\n\n"
|
# Prepare HTML content
|
||||||
f"Record ID: #{payroll_record.id}\n"
|
context = {
|
||||||
f"Worker: {worker.name}\n"
|
'record': payroll_record,
|
||||||
f"Date: {payroll_record.date}\n"
|
'logs_count': log_count,
|
||||||
f"Total Paid: R {payroll_record.amount}\n\n"
|
'logs_amount': logs_amount,
|
||||||
f"Breakdown:\n"
|
'adjustments': payroll_record.adjustments.all(),
|
||||||
f"Base Pay ({log_count} days): R {logs_amount}\n"
|
}
|
||||||
f"Adjustments: R {adj_amount}\n\n"
|
html_message = render_to_string('core/email/payslip_email.html', context)
|
||||||
f"This is an automated notification."
|
plain_message = strip_tags(html_message)
|
||||||
)
|
|
||||||
recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com']
|
recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com']
|
||||||
|
|
||||||
try:
|
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}")
|
messages.success(request, f"Payment processed for {worker.name}. Net Pay: R {payroll_record.amount}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messages.warning(request, f"Payment processed, but email delivery failed: {str(e)}")
|
messages.warning(request, f"Payment processed, but email delivery failed: {str(e)}")
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
Django==5.2.7
|
Django==5.2.7
|
||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
|
Pillow
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user