Ver 10.4 fix mail to spark
This commit is contained in:
parent
60ef14cac4
commit
0702dcda68
BIN
core/__pycache__/utils.cpython-311.pyc
Normal file
BIN
core/__pycache__/utils.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
79
core/templates/core/pdf/payslip_pdf.html
Normal file
79
core/templates/core/pdf/payslip_pdf.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: a4 portrait;
|
||||||
|
margin: 2cm;
|
||||||
|
@frame footer_frame { /* Another static Frame */
|
||||||
|
-pdf-frame-content: footerContent;
|
||||||
|
bottom: 1cm;
|
||||||
|
margin-left: 1cm;
|
||||||
|
margin-right: 1cm;
|
||||||
|
height: 1cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body { font-family: Helvetica, sans-serif; font-size: 12pt; line-height: 1.5; color: #333; }
|
||||||
|
.header { text-align: center; border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px; }
|
||||||
|
.title { font-size: 24pt; font-weight: bold; text-transform: uppercase; color: #000; }
|
||||||
|
.subtitle { font-size: 14pt; color: #666; margin-top: 5px; }
|
||||||
|
.meta { margin-bottom: 20px; padding: 10px; border: 1px solid #ddd; background-color: #f9f9f9; }
|
||||||
|
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
||||||
|
.items-table th { border-bottom: 1px solid #000; padding: 8px; text-align: left; background-color: #eee; font-weight: bold; }
|
||||||
|
.items-table td { border-bottom: 1px solid #eee; padding: 8px; text-align: left; }
|
||||||
|
.totals { text-align: right; margin-top: 20px; border-top: 2px solid #333; padding-top: 10px; }
|
||||||
|
.total-row { font-size: 16pt; font-weight: bold; color: #000; }
|
||||||
|
.footer { text-align: center; font-size: 10pt; color: #777; }
|
||||||
|
.positive { color: green; }
|
||||||
|
.negative { color: red; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<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 class="text-right">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Base Pay -->
|
||||||
|
<tr>
|
||||||
|
<td>Base Pay ({{ logs_count }} days worked)</td>
|
||||||
|
<td class="text-right">R {{ logs_amount|floatformat:2 }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Adjustments -->
|
||||||
|
{% for adj in adjustments %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ adj.get_type_display }}: {{ adj.description }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
R {{ adj.amount|floatformat:2 }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="totals">
|
||||||
|
<p class="total-row">Net Pay: R {{ record.amount|floatformat:2 }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="footerContent" class="footer">
|
||||||
|
Generated by Fox Fitt App for {{ record.worker.name }} | {% now "Y-m-d H:i" %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
70
core/templates/core/pdf/receipt_pdf.html
Normal file
70
core/templates/core/pdf/receipt_pdf.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: a4 portrait;
|
||||||
|
margin: 2cm;
|
||||||
|
@frame footer_frame {
|
||||||
|
-pdf-frame-content: footerContent;
|
||||||
|
bottom: 1cm;
|
||||||
|
margin-left: 1cm;
|
||||||
|
margin-right: 1cm;
|
||||||
|
height: 1cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body { font-family: Helvetica, sans-serif; font-size: 12pt; line-height: 1.5; color: #333; }
|
||||||
|
.header { text-align: center; border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px; }
|
||||||
|
.vendor-name { font-size: 24pt; font-weight: bold; text-transform: uppercase; color: #000; }
|
||||||
|
.sub-header { font-size: 12pt; color: #666; }
|
||||||
|
.meta { margin-bottom: 20px; padding: 10px; border: 1px solid #ddd; background-color: #f9f9f9; }
|
||||||
|
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
||||||
|
.items-table th { border-bottom: 1px solid #000; padding: 8px; text-align: left; background-color: #eee; font-weight: bold; }
|
||||||
|
.items-table td { border-bottom: 1px solid #eee; padding: 8px; text-align: left; }
|
||||||
|
.totals { text-align: right; margin-top: 20px; }
|
||||||
|
.total-row { font-size: 16pt; font-weight: bold; border-top: 1px solid #000; padding-top: 5px; margin-top: 5px; }
|
||||||
|
.footer { text-align: center; font-size: 10pt; color: #777; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="sub-header">RECEIPT FROM</div>
|
||||||
|
<div class="vendor-name">{{ receipt.vendor }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<strong>Date:</strong> {{ receipt.date }}<br>
|
||||||
|
<strong>Payment Method:</strong> {{ receipt.get_payment_method_display }}<br>
|
||||||
|
<strong>Description:</strong> {{ receipt.description|default:"-" }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="items-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th class="text-right">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.product }}</td>
|
||||||
|
<td class="text-right">R {{ item.amount|floatformat:2 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="totals">
|
||||||
|
<p>Subtotal: R {{ receipt.subtotal|floatformat:2 }}</p>
|
||||||
|
<p>VAT ({{ receipt.get_vat_type_display }}): R {{ receipt.vat_amount|floatformat:2 }}</p>
|
||||||
|
<p class="total-row">Total: R {{ receipt.total_amount|floatformat:2 }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="footerContent" class="footer">
|
||||||
|
Generated by {{ receipt.user.get_full_name|default:receipt.user.username }} via Fox Fitt App
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
core/utils.py
Normal file
20
core/utils.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from xhtml2pdf import pisa
|
||||||
|
from django.conf import settings
|
||||||
|
import os
|
||||||
|
|
||||||
|
def render_to_pdf(template_src, context_dict={}):
|
||||||
|
template = get_template(template_src)
|
||||||
|
html = template.render(context_dict)
|
||||||
|
result = BytesIO()
|
||||||
|
|
||||||
|
# Enable logging for debugging
|
||||||
|
# pisa.showLogging()
|
||||||
|
|
||||||
|
# Create the PDF
|
||||||
|
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
|
||||||
|
|
||||||
|
if not pdf.err:
|
||||||
|
return result.getvalue()
|
||||||
|
return None
|
||||||
@ -8,7 +8,7 @@ from django.shortcuts import render, redirect, get_object_or_404
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.db.models import Sum, Q, Prefetch
|
from django.db.models import Sum, Q, Prefetch
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail, EmailMultiAlternatives
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import JsonResponse, HttpResponse
|
from django.http import JsonResponse, HttpResponse
|
||||||
@ -18,6 +18,7 @@ from .models import Worker, Project, Team, WorkLog, PayrollRecord, Loan, Payroll
|
|||||||
from .forms import WorkLogForm, ExpenseReceiptForm, ExpenseLineItemFormSet
|
from .forms import WorkLogForm, ExpenseReceiptForm, ExpenseLineItemFormSet
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from core.utils import render_to_pdf
|
||||||
|
|
||||||
def is_staff_or_supervisor(user):
|
def is_staff_or_supervisor(user):
|
||||||
"""Check if user is staff or manages at least one team/project."""
|
"""Check if user is staff or manages at least one team/project."""
|
||||||
@ -591,26 +592,37 @@ 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}"
|
||||||
|
|
||||||
# Prepare HTML content
|
# Prepare Context
|
||||||
context = {
|
context = {
|
||||||
'record': payroll_record,
|
'record': payroll_record,
|
||||||
'logs_count': log_count,
|
'logs_count': log_count,
|
||||||
'logs_amount': logs_amount,
|
'logs_amount': logs_amount,
|
||||||
'adjustments': payroll_record.adjustments.all(),
|
'adjustments': payroll_record.adjustments.all(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 1. Render HTML Body
|
||||||
html_message = render_to_string('core/email/payslip_email.html', context)
|
html_message = render_to_string('core/email/payslip_email.html', context)
|
||||||
plain_message = strip_tags(html_message)
|
plain_message = strip_tags(html_message)
|
||||||
|
|
||||||
|
# 2. Render PDF Attachment
|
||||||
|
pdf_content = render_to_pdf('core/pdf/payslip_pdf.html', context)
|
||||||
|
|
||||||
recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com']
|
recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
send_mail(
|
# Construct Email with Attachment
|
||||||
subject,
|
email = EmailMultiAlternatives(
|
||||||
plain_message,
|
subject,
|
||||||
settings.DEFAULT_FROM_EMAIL,
|
plain_message,
|
||||||
recipient_list,
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
html_message=html_message
|
recipient_list,
|
||||||
)
|
)
|
||||||
|
email.attach_alternative(html_message, "text/html")
|
||||||
|
|
||||||
|
if pdf_content:
|
||||||
|
email.attach(f"Payslip_{worker.id}_{payroll_record.date}.pdf", pdf_content, 'application/pdf')
|
||||||
|
|
||||||
|
email.send()
|
||||||
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)}")
|
||||||
@ -780,21 +792,33 @@ def create_receipt(request):
|
|||||||
subject = f"Receipt from {receipt.vendor} - {receipt.date}"
|
subject = f"Receipt from {receipt.vendor} - {receipt.date}"
|
||||||
recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com']
|
recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com']
|
||||||
|
|
||||||
# Prepare HTML content
|
# Prepare Context
|
||||||
html_message = render_to_string('core/email/receipt_email.html', {
|
context = {
|
||||||
'receipt': receipt,
|
'receipt': receipt,
|
||||||
'items': line_items,
|
'items': line_items,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
# 1. Render HTML Body
|
||||||
|
html_message = render_to_string('core/email/receipt_email.html', context)
|
||||||
plain_message = strip_tags(html_message)
|
plain_message = strip_tags(html_message)
|
||||||
|
|
||||||
|
# 2. Render PDF Attachment
|
||||||
|
pdf_content = render_to_pdf('core/pdf/receipt_pdf.html', context)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
send_mail(
|
# Construct Email with Attachment
|
||||||
subject,
|
email = EmailMultiAlternatives(
|
||||||
plain_message,
|
subject,
|
||||||
settings.DEFAULT_FROM_EMAIL,
|
plain_message,
|
||||||
recipient_list,
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
html_message=html_message
|
recipient_list,
|
||||||
)
|
)
|
||||||
|
email.attach_alternative(html_message, "text/html")
|
||||||
|
|
||||||
|
if pdf_content:
|
||||||
|
email.attach(f"Receipt_{receipt.id}.pdf", pdf_content, 'application/pdf')
|
||||||
|
|
||||||
|
email.send()
|
||||||
messages.success(request, "Receipt created and sent to SparkReceipt.")
|
messages.success(request, "Receipt created and sent to SparkReceipt.")
|
||||||
return redirect('create_receipt')
|
return redirect('create_receipt')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -807,4 +831,4 @@ def create_receipt(request):
|
|||||||
return render(request, 'core/create_receipt.html', {
|
return render(request, 'core/create_receipt.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
'items': items
|
'items': items
|
||||||
})
|
})
|
||||||
@ -2,3 +2,4 @@ Django==5.2.7
|
|||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
Pillow
|
Pillow
|
||||||
|
xhtml2pdf
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user