From 0702dcda68ad7e3b63abd46af7db3dc85d993b9a Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Feb 2026 17:36:56 +0000 Subject: [PATCH] Ver 10.4 fix mail to spark --- core/__pycache__/utils.cpython-311.pyc | Bin 0 -> 1039 bytes core/__pycache__/views.cpython-311.pyc | Bin 37639 -> 38533 bytes core/templates/core/pdf/payslip_pdf.html | 79 +++++++++++++++++++++++ core/templates/core/pdf/receipt_pdf.html | 70 ++++++++++++++++++++ core/utils.py | 20 ++++++ core/views.py | 60 +++++++++++------ requirements.txt | 1 + 7 files changed, 212 insertions(+), 18 deletions(-) create mode 100644 core/__pycache__/utils.cpython-311.pyc create mode 100644 core/templates/core/pdf/payslip_pdf.html create mode 100644 core/templates/core/pdf/receipt_pdf.html create mode 100644 core/utils.py diff --git a/core/__pycache__/utils.cpython-311.pyc b/core/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30fc6a9c62c26c88167b18d0526922b89565b8f8 GIT binary patch literal 1039 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 delta 6449 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 diff --git a/core/templates/core/pdf/payslip_pdf.html b/core/templates/core/pdf/payslip_pdf.html new file mode 100644 index 0000000..2b3bb02 --- /dev/null +++ b/core/templates/core/pdf/payslip_pdf.html @@ -0,0 +1,79 @@ + + + + + + + +
+
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|floatformat:2 }}
{{ adj.get_type_display }}: {{ adj.description }} + R {{ adj.amount|floatformat:2 }} +
+ +
+

Net Pay: R {{ record.amount|floatformat:2 }}

+
+ + + + diff --git a/core/templates/core/pdf/receipt_pdf.html b/core/templates/core/pdf/receipt_pdf.html new file mode 100644 index 0000000..c81d724 --- /dev/null +++ b/core/templates/core/pdf/receipt_pdf.html @@ -0,0 +1,70 @@ + + + + + + + +
+
RECEIPT FROM
+
{{ receipt.vendor }}
+
+ +
+ Date: {{ receipt.date }}
+ Payment Method: {{ receipt.get_payment_method_display }}
+ Description: {{ receipt.description|default:"-" }} +
+ + + + + + + + + + {% for item in items %} + + + + + {% endfor %} + +
ItemAmount
{{ item.product }}R {{ item.amount|floatformat:2 }}
+ +
+

Subtotal: R {{ receipt.subtotal|floatformat:2 }}

+

VAT ({{ receipt.get_vat_type_display }}): R {{ receipt.vat_amount|floatformat:2 }}

+

Total: R {{ receipt.total_amount|floatformat:2 }}

+
+ + + + diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 0000000..e84415b --- /dev/null +++ b/core/utils.py @@ -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 diff --git a/core/views.py b/core/views.py index 90f1af0..db1a3f8 100644 --- a/core/views.py +++ b/core/views.py @@ -8,7 +8,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone from django.contrib.auth.decorators import login_required, user_passes_test 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.contrib import messages 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 datetime import timedelta from decimal import Decimal +from core.utils import render_to_pdf def is_staff_or_supervisor(user): """Check if user is staff or manages at least one team/project.""" @@ -591,26 +592,37 @@ def process_payment(request, worker_id): # Email Notification subject = f"Payslip for {worker.name} - {payroll_record.date}" - # Prepare HTML content + # Prepare Context context = { 'record': payroll_record, 'logs_count': log_count, 'logs_amount': logs_amount, 'adjustments': payroll_record.adjustments.all(), } + + # 1. Render HTML Body html_message = render_to_string('core/email/payslip_email.html', context) 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'] try: - send_mail( - subject, - plain_message, - settings.DEFAULT_FROM_EMAIL, - recipient_list, - html_message=html_message + # Construct Email with Attachment + email = EmailMultiAlternatives( + subject, + plain_message, + settings.DEFAULT_FROM_EMAIL, + 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}") except Exception as 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}" recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com'] - # Prepare HTML content - html_message = render_to_string('core/email/receipt_email.html', { + # Prepare Context + context = { 'receipt': receipt, 'items': line_items, - }) + } + + # 1. Render HTML Body + html_message = render_to_string('core/email/receipt_email.html', context) plain_message = strip_tags(html_message) + # 2. Render PDF Attachment + pdf_content = render_to_pdf('core/pdf/receipt_pdf.html', context) + try: - send_mail( - subject, - plain_message, - settings.DEFAULT_FROM_EMAIL, - recipient_list, - html_message=html_message + # Construct Email with Attachment + email = EmailMultiAlternatives( + subject, + plain_message, + settings.DEFAULT_FROM_EMAIL, + 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.") return redirect('create_receipt') except Exception as e: @@ -807,4 +831,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 65b2871..19a9652 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 Pillow +xhtml2pdf