171 lines
8.1 KiB
Python
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}.")
|