2026-09-15 10:47:19 +05:30

171 lines
8.1 KiB
Python

import base64
import logging
import time
# pyrefly: ignore [missing-import]
from odoo import fields, _
# pyrefly: ignore [missing-import]
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class ReconciliationService:
@classmethod
def reconcile_transaction(cls, env, transaction, razorpay_payment_id=None, raw_gateway_response=None):
"""
Execute automatic reconciliation inside a single DB transaction savepoint.
"""
if transaction.status == 'success':
_logger.info("Transaction %s is already reconciled. Skipping duplicate execution.", transaction.name)
return transaction
with env.cr.savepoint():
student = transaction.student_id
now = fields.Datetime.now()
today_str = fields.Date.today().strftime('%Y%m%d')
receipt_num = f"REC-ONL-{today_str}-{transaction.id}"
# 1. Create education.student.payment
payment = env['education.student.payment'].sudo().create({
'student_id': student.id,
'amount': transaction.amount,
'payment_method': 'online',
'receipt_number': receipt_num,
'reference_number': razorpay_payment_id or transaction.razorpay_order_id or transaction.name,
'payment_date': fields.Date.today(),
'status': 'confirmed',
'remarks': f"Online Fee Payment via Gateway ({transaction.provider.title()}). TXN Ref: {transaction.name}",
'company_id': transaction.company_id.id,
})
# 2. Mark fee assignments as paid
if transaction.fee_assignment_ids:
transaction.fee_assignment_ids.sudo().write({
'payment_status': 'paid',
})
# 3. Post Credit Entry in education.student.ledger
ledger_entry = env['education.student.ledger'].sudo().create({
'student_id': student.id,
'date': fields.Date.today(),
'description': f"Online Payment Received ({receipt_num})",
'debit': 0.0,
'credit': transaction.amount,
'status': 'paid',
'company_id': transaction.company_id.id,
})
# 4. Generate PDF Receipt
pdf_content, _ = env['ir.actions.report'].sudo()._render_qweb_pdf(
'education_finance.action_report_receipt_pdf', [payment.id]
)
# Create Attachment
attachment = env['ir.attachment'].sudo().create({
'name': f"Receipt-{receipt_num}.pdf",
'type': 'binary',
'datas': base64.b64encode(pdf_content),
'res_model': 'education.student.payment',
'res_id': payment.id,
'mimetype': 'application/pdf',
})
# 5. Send Email Confirmation if enabled
ICP = env['ir.config_parameter'].sudo()
auto_email = ICP.get_param('education_portal.auto_send_payment_receipt_email', 'True')
if auto_email and student.email:
cls._send_email_receipt(env, student, payment, transaction, attachment)
# 6. Update Payment Transaction Record
transaction.sudo().write({
'status': 'success',
'verified_on': now,
'receipt_number': receipt_num,
'payment_id': payment.id,
'razorpay_payment_id': razorpay_payment_id or transaction.razorpay_payment_id,
'gateway_response': raw_gateway_response or transaction.gateway_response,
})
transaction.log_audit(
f"Reconciliation Complete. Payment Record: #{payment.id} ({receipt_num}), "
f"Ledger Entry: #{ledger_entry.id}, Dues Reconciled: {len(transaction.fee_assignment_ids)} items."
)
_logger.info("Successfully reconciled transaction %s for student %s", transaction.name, student.display_name)
return transaction
@classmethod
def _send_email_receipt(cls, env, student, payment, transaction, attachment):
"""Send Email Receipt with attached PDF to student"""
try:
subject = f"Payment Receipt Confirmation - {payment.receipt_number}"
body_html = f"""
<div style="font-family: Arial, sans-serif; color: #2d3748; padding: 20px; max-width: 600px; border: 1px solid #e2e8f0; border-radius: 12px;">
<h2 style="color: #1f8f8f; margin-top: 0;">Payment Received Successfully</h2>
<p>Dear <strong>{student.display_name}</strong>,</p>
<p>Thank you for your payment. Your fee payment has been successfully processed and verified.</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="border-bottom: 1px solid #e2e8f0;">
<td style="padding: 8px; font-weight: bold;">Receipt Number:</td>
<td style="padding: 8px;">{payment.receipt_number}</td>
</tr>
<tr style="border-bottom: 1px solid #e2e8f0;">
<td style="padding: 8px; font-weight: bold;">Transaction Reference:</td>
<td style="padding: 8px;">{transaction.name}</td>
</tr>
<tr style="border-bottom: 1px solid #e2e8f0;">
<td style="padding: 8px; font-weight: bold;">Amount Paid:</td>
<td style="padding: 8px; font-size: 1.1rem; color: #2b6cb0; font-weight: bold;">₹{payment.amount:,.2f}</td>
</tr>
<tr style="border-bottom: 1px solid #e2e8f0;">
<td style="padding: 8px; font-weight: bold;">Payment Date:</td>
<td style="padding: 8px;">{payment.payment_date}</td>
</tr>
<tr>
<td style="padding: 8px; font-weight: bold;">Remaining Outstanding:</td>
<td style="padding: 8px; font-weight: bold; color: #e53e3e;">₹{student.outstanding_amount:,.2f}</td>
</tr>
</table>
<p>Please find your official payment receipt PDF attached to this email.</p>
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;"/>
<p style="font-size: 0.85rem; color: #718096;">This is an automated notification from your Education ERP Portal.</p>
</div>
"""
mail_values = {
'subject': subject,
'body_html': body_html,
'email_to': student.email,
'attachment_ids': [(6, 0, [attachment.id])],
}
env['mail.mail'].sudo().create(mail_values).send()
except Exception as e:
_logger.warning("Failed to send payment receipt email to %s: %s", student.email, str(e))
@classmethod
def reverse_reconciliation(cls, env, transaction):
"""Process refund / reversal of reconciled payment"""
with env.cr.savepoint():
student = transaction.student_id
# Create Debit Reversal line on Ledger
env['education.student.ledger'].sudo().create({
'student_id': student.id,
'date': fields.Date.today(),
'description': f"Refund Reversal ({transaction.name})",
'debit': transaction.amount,
'credit': 0.0,
'status': 'outstanding',
'company_id': transaction.company_id.id,
})
# Re-open linked fee assignments
if transaction.fee_assignment_ids:
transaction.fee_assignment_ids.sudo().write({'payment_status': 'unpaid'})
# Mark transaction refunded
transaction.sudo().write({
'status': 'refunded',
})
transaction.log_audit(f"Transaction REFUNDED. Reversal ledger line posted for ₹{transaction.amount:,.2f}.")