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 = [
|
||||
"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'
|
||||
LOGOUT_REDIRECT_URL = 'login'
|
||||
|
||||
@ -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)
|
||||
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)
|
||||
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',)
|
||||
@ -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")
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
Pillow
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user