Merge pull request #6094 from expsa/14.0-feat-odex_takaful-auto-20260109_203453

[IMP] odex_takaful: enhance data models and user interface
This commit is contained in:
Mohamed Eltayar 2026-01-09 20:35:17 +03:00 committed by GitHub
commit 49d662ea03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 343 additions and 33 deletions

View File

@ -62,6 +62,7 @@
'views/takaful_notification.xml',
'reports/month_payment_template.xml',
'reports/sponsorship_receipt_report.xml',
'reports/extension_receipt_report.xml',
'reports/orphan_mother_report.xml',
'reports/orphan_report.xml',
'wizards/takaful_reports_wizards.xml',

View File

@ -4,6 +4,7 @@ from odoo import models, fields, api, _
class DonationExtensionHistory(models.Model):
_name = 'donation.extension.history'
_inherit = ['mail.thread']
_description = 'Donation Extension History'
_order = 'extension_date desc'
_rec_name = 'extension_ref'
@ -34,9 +35,7 @@ class DonationExtensionHistory(models.Model):
sponsorship_id = fields.Many2one(
'takaful.sponsorship',
string='Sponsorship',
related='donation_detail_id.sponsorship_id',
store=True,
readonly=True
index=True
)
product_template_id = fields.Many2one(
@ -104,8 +103,43 @@ class DonationExtensionHistory(models.Model):
string='New Direct Debit',
readonly=True
)
state = fields.Selection([ ('active', 'Active'),('cancel', 'Canceled')], string='State', default='active')
state = fields.Selection([
('active', 'Active'),
('paid', 'Paid'),
('cancel', 'Canceled')
], string='State', default='active', tracking=True)
canceled = fields.Boolean()
# SMS related fields
sponsor_phone = fields.Char(
string='Sponsor Phone',
related='sponsorship_id.sponsor_phone',
readonly=True
)
receipt_url = fields.Char(
string='Receipt URL',
compute='_compute_receipt_url',
store=False
)
def _sms_get_number_fields(self):
"""Return fields to use for SMS phone number"""
return ['sponsor_phone']
def _compute_receipt_url(self):
"""Compute the extension receipt URL for SMS template"""
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', '')
for rec in self:
if rec.id:
rec.receipt_url = "%s/report/pdf/odex_takaful.report_extension_receipt_document/%s" % (
base_url, rec.id
)
else:
rec.receipt_url = False
@api.depends('donation_detail_id', 'extension_date')
def _compute_extension_ref(self):
for rec in self:
@ -116,4 +150,3 @@ class DonationExtensionHistory(models.Model):
)
else:
rec.extension_ref = _('Extension')

View File

@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Extension Receipt Report - based on sponsorship_receipt_report layout -->
<template id="report_extension_receipt_document">
<!-- Standard report container to provide <main> for PDF engine -->
<t t-call="web.html_container">
<!-- Get all extension history records for the same invoice -->
<t t-set="invoice_id" t-value="docs[0].invoice_id if docs else False"/>
<t t-set="all_extensions" t-value="docs.env['donation.extension.history'].search([('invoice_id', '=', invoice_id.id)]) if invoice_id else docs"/>
<t t-set="sponsorship" t-value="all_extensions[0].sponsorship_id if all_extensions else False"/>
<t t-if="sponsorship">
<!-- Reuse sale order header layout -->
<t t-call="sale_order_report.sale_order_report_header">
<div class="page font-noto" style="position: relative; min-height: 800px; overflow: visible;" dir="rtl">
<div style="position: relative; z-index: 1;">
<div class="oe_structure"/>
<!-- Greeting / intro -->
<div style="text-align: right; margin-bottom: 20px; padding-top: 250px;">
<div style="font-size: 1.5rem; color: #6B6B6A; font-weight: bold;">
مرحباً
<span style="color: #387F75;" t-esc="sponsorship.sponsor_name or sponsorship.sponsor_id.name"/>
</div>
<div style="font-size: 1.5rem; margin-top: 10px; color: #6B6B6A;">
بكل امتنان نشكر لك تجديد كفالتك، ونسأل الله أن يجعلك ممن يرافقون النبي <span style="white-space: nowrap;">
<img src="/odex_takaful/static/src/img/U+FDFA.svg"
style="height: 1.5em; vertical-align: middle; margin-bottom: 2px;"
alt="ﷺ"/>
</span><br/> في الجنة.
</div>
<div style="font-size: 1.5rem; color: #6B6B6A;">
ونتشرف دائما باستقبال عطائك المستمر لدعم أبنائكم الأيتام.
</div>
</div>
<!-- Extension info -->
<div style="text-align: right; margin-bottom: 20px;">
<div style="font-size: 1.5rem; color: #8F9090;">
<span style="font-weight: bold;">رقم الكفالة: </span>
<t t-esc="sponsorship.code"/>
</div>
<div style="font-size: 1.5rem; color: #8F9090;">
<span style="font-weight: bold;">تاريخ التجديد: </span>
<bdi style="display: inline-block; direction: rtl; unicode-bidi: embed;">
<span t-field="all_extensions[0].extension_date"
t-options='{"format": "dd MMMM yyyy - HH:mm"}'/>
</bdi>
</div>
</div>
<!-- Extension Lines table -->
<table class="table table-bordered" style="text-align: right;">
<thead>
<tr style="background-color: #f8f9fa;">
<th style="text-align: right; font-size: 1.4rem; padding: 8px; color: #6B6B6A; font-weight: bold;">
نوع التبرع
</th>
<th style="text-align: center; font-size: 1.4rem; padding: 8px; color: #6B6B6A; font-weight: bold;">
التفاصيل
</th>
<th style="text-align: center; font-size: 1.4rem; padding: 8px; color: #6B6B6A; font-weight: bold;">
الفترة الجديدة
</th>
<th style="text-align: center; font-size: 1.4rem; padding: 8px; color: #6B6B6A; font-weight: bold;">
المبلغ
</th>
</tr>
</thead>
<tbody>
<t t-foreach="all_extensions" t-as="ext">
<tr>
<!-- نوع التبرع -->
<td style="text-align: right; padding: 8px; font-size: 1.3rem; color: #387F75;">
<span t-esc="ext.product_template_id.with_context(lang='ar_001').name or ext.product_template_id.name"/>
</td>
<!-- التفاصيل -->
<td style="text-align: center; padding: 8px; font-size: 1.3rem; color: #387F75;">
<t t-if="ext.donation_detail_id.benefit_id">
<span t-esc="(ext.donation_detail_id.benefit_id.name or '').split()[0]"/> - <span t-esc="ext.donation_detail_id.benefit_family_code or '-'"/>
</t>
<t t-else="">
<span t-esc="ext.donation_detail_id.family_id.code or '-'"/>
</t>
<br/>
<small style="color: #8F9090;"><span t-esc="ext.extension_months"/> شهور التمديد:</small>
</td>
<!-- الفترة الجديدة -->
<td style="text-align: center; padding: 8px; font-size: 1.2rem; color: #387F75;">
<span t-field="ext.old_end_date" t-options='{"format": "dd/MM/yyyy"}'/>
<br/>إلى<br/>
<span t-field="ext.new_end_date" t-options='{"format": "dd/MM/yyyy"}'/>
</td>
<!-- المبلغ -->
<td style="text-align: center; padding: 8px; font-size: 1.3rem; color: #387F75;">
<t t-set="amt" t-value="ext.extension_amount"/>
<t t-if="amt == int(amt)">
<span t-esc="'{:,.0f}'.format(amt)"/> <span t-esc="sponsorship.currency_id.symbol"/>
</t>
<t t-else="">
<span t-esc="'{:,.2f}'.format(amt)"/> <span t-esc="sponsorship.currency_id.symbol"/>
</t>
</td>
</tr>
</t>
<!-- Summary Row: المجموع -->
<tr style="background-color: #f8f9fa;">
<td style="text-align: right; padding: 8px; font-size: 1.4rem; color: #6B6B6A; font-weight: bold;">
<strong>المجموع:</strong>
</td>
<td style="text-align: center; padding: 8px; font-size: 1.4rem; color: #387F75; font-weight: bold;">
<strong>-</strong>
</td>
<td style="text-align: center; padding: 8px; font-size: 1.4rem; color: #387F75; font-weight: bold;">
<strong>-</strong>
</td>
<td style="text-align: center; padding: 8px; font-size: 1.4rem; color: #387F75; font-weight: bold;">
<strong>
<t t-set="total_amt" t-value="sum(ext.extension_amount for ext in all_extensions)"/>
<t t-if="total_amt == int(total_amt)">
<span t-esc="'{:,.0f}'.format(total_amt)"/> <span t-esc="sponsorship.currency_id.symbol"/>
</t>
<t t-else="">
<span t-esc="'{:,.2f}'.format(total_amt)"/> <span t-esc="sponsorship.currency_id.symbol"/>
</t>
</strong>
</td>
</tr>
</tbody>
</table>
<!-- Payments table: وسيلة الدفع والمبلغ -->
<table class="table table-bordered" style="text-align: right; margin-top: 12px;">
<thead>
<tr style="background-color: #f8f9fa;">
<th style="text-align: right; width: 75%; font-size: 1.4rem; padding: 8px; color: #6B6B6A; font-weight: bold;">
وسيلة الدفع
</th>
<th style="text-align: center; width: 25%; font-size: 1.4rem; padding: 8px; color: #6B6B6A; font-weight: bold;">
المبلغ
</th>
</tr>
</thead>
<tbody>
<t t-if="sponsorship and sponsorship.payment_ids">
<t t-foreach="sponsorship.payment_ids.filtered(lambda p: invoice_id.id in p.reconciled_invoice_ids.ids)" t-as="pay">
<tr>
<td style="text-align: right; padding: 8px; font-size: 1.3rem; color: #387F75;">
<span t-esc="pay.with_context(lang='ar_001').payment_method_line_id.name or pay.with_context(lang='ar_001').payment_method_id.name"/>
</td>
<td style="text-align: center; padding: 8px; font-size: 1.3rem; color: #387F75;">
<t t-set="pay_amt" t-value="0"/>
<t t-foreach="invoice_id.line_ids.filtered(lambda l: l.account_id.internal_type in ('receivable', 'payable'))" t-as="inv_line">
<t t-foreach="inv_line.matched_debit_ids + inv_line.matched_credit_ids" t-as="partial">
<t t-if="partial.credit_move_id.payment_id == pay or partial.debit_move_id.payment_id == pay">
<t t-set="pay_amt" t-value="pay_amt + partial.amount"/>
</t>
</t>
</t>
<t t-if="pay_amt == int(pay_amt)">
<span t-esc="'{:,.0f}'.format(pay_amt)"/> <span t-esc="pay.currency_id.symbol"/>
</t>
<t t-else="">
<span t-esc="'{:,.2f}'.format(pay_amt)"/> <span t-esc="pay.currency_id.symbol"/>
</t>
</td>
</tr>
</t>
<tr style="background-color: #f8f9fa;">
<td style="text-align: right; padding: 8px; font-size: 1.4rem; color: #6B6B6A; font-weight: bold;">
<strong>
<span style="margin-left: 10px;">الإجمالي:</span>
</strong>
</td>
<td style="text-align: center; padding: 8px; font-size: 1.4rem; color: #387F75; font-weight: bold;">
<t t-set="final_amt" t-value="sum(ext.extension_amount for ext in all_extensions)"/>
<t t-if="final_amt == int(final_amt)">
<span t-esc="'{:,.0f}'.format(final_amt)"/> <span t-esc="sponsorship.currency_id.symbol"/>
</t>
<t t-else="">
<span t-esc="'{:,.2f}'.format(final_amt)"/> <span t-esc="sponsorship.currency_id.symbol"/>
</t>
</td>
</tr>
</t>
<t t-else="">
<tr>
<td colspan="2" style="text-align: center; padding: 8px; font-size: 1.3rem; color: #6B6B6A;">
<span>لا توجد دفعات</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@ -103,6 +103,19 @@
<t t-set="lines_to_report" t-value="doc.donations_details_lines + doc.donations_details_lines_mechanism_ids"/>
<t t-foreach="lines_to_report" t-as="line">
<t t-if="not line.display_type">
<!-- Calculate Original Values -->
<t t-set="ext_history" t-value="doc.env['donation.extension.history'].search([('donation_detail_id', '=', line.id)])"/>
<t t-set="ext_amt_sum" t-value="sum(h.extension_amount for h in ext_history)"/>
<t t-set="original_amt" t-value="line.total_donation_amount - ext_amt_sum"/>
<!-- Original Months -->
<t t-set="ext_months_sum" t-value="sum(h.extension_months for h in ext_history)"/>
<t t-set="original_months" t-value="line.payment_month_count - ext_months_sum"/>
<!-- Original End Date -->
<t t-set="sorted_ext" t-value="ext_history.sorted(key=lambda r: r.create_date)"/>
<t t-set="original_end_date" t-value="sorted_ext[0].old_end_date if sorted_ext else line.end_date"/>
<tr>
<!-- نوع التبرع -->
<td style="text-align: right; padding: 8px; font-size: 1.3rem; color: #387F75;">
@ -123,17 +136,17 @@
<t t-elif="doc.record_type == 'sponsorship'">
<span t-esc="(line.benefit_id.name or '').split()[0] if line.benefit_id else '-'"/> - <span t-esc="line.benefit_family_code or '-'"/>
<br/>
<small style="color: #8F9090;"><span t-esc="line.payment_month_count"/> شهر</small>
<small style="color: #8F9090;"><span t-esc="original_months"/> شهر</small>
</t>
</td>
<!-- المدة - only for sponsorships -->
<t t-if="doc.record_type == 'sponsorship'">
<td style="text-align: center; padding: 8px; font-size: 1.2rem; color: #387F75;">
<t t-if="line.start_date and line.end_date">
<t t-if="line.start_date and original_end_date">
<span t-field="line.start_date" t-options='{"format": "dd/MM/yyyy"}'/>
<br/>إلى<br/>
<span t-field="line.end_date" t-options='{"format": "dd/MM/yyyy"}'/>
<span t-esc="original_end_date.strftime('%d/%m/%Y')"/>
</t>
<t t-else="">-</t>
</td>
@ -141,12 +154,11 @@
<!-- المبلغ -->
<td style="text-align: center; padding: 8px; font-size: 1.3rem; color: #387F75;">
<t t-set="amt" t-value="line.total_donation_amount"/>
<t t-if="amt == int(amt)">
<span t-esc="'{:,.0f}'.format(amt)"/> <span t-esc="line.currency_id.symbol"/>
<t t-if="original_amt == int(original_amt)">
<span t-esc="'{:,.0f}'.format(original_amt)"/> <span t-esc="line.currency_id.symbol"/>
</t>
<t t-else="">
<span t-esc="'{:,.2f}'.format(amt)"/> <span t-esc="line.currency_id.symbol"/>
<span t-esc="'{:,.2f}'.format(original_amt)"/> <span t-esc="line.currency_id.symbol"/>
</t>
</td>
</tr>
@ -197,21 +209,37 @@
</thead>
<tbody>
<t t-if="doc.payment_ids">
<t t-set="extension_invoice_ids" t-value="doc.env['donation.extension.history'].search([('sponsorship_id', '=', doc.id)]).mapped('invoice_id').ids"/>
<t t-foreach="doc.payment_ids" t-as="pay">
<tr>
<td style="text-align: right; padding: 8px; font-size: 1.3rem; color: #387F75;">
<span t-esc="pay.with_context(lang='ar').payment_method_line_id.name or pay.with_context(lang='ar').payment_method_id.name"/>
</td>
<td style="text-align: center; padding: 8px; font-size: 1.3rem; color: #387F75;">
<t t-set="pay_amt" t-value="pay.amount"/>
<t t-if="pay_amt == int(pay_amt)">
<span t-esc="'{:,.0f}'.format(pay_amt)"/> <span t-esc="pay.currency_id.symbol"/>
<!-- Calculate share for NON-extension invoices -->
<t t-set="pay_amt" t-value="0"/>
<t t-foreach="pay.reconciled_invoice_ids" t-as="inv">
<t t-if="inv.id not in extension_invoice_ids">
<t t-foreach="inv.line_ids.filtered(lambda l: l.account_id.internal_type in ('receivable', 'payable'))" t-as="inv_line">
<t t-foreach="inv_line.matched_debit_ids + inv_line.matched_credit_ids" t-as="partial">
<t t-if="partial.credit_move_id.payment_id == pay or partial.debit_move_id.payment_id == pay">
<t t-set="pay_amt" t-value="pay_amt + partial.amount"/>
</t>
</t>
</t>
<t t-else="">
<span t-esc="'{:,.2f}'.format(pay_amt)"/> <span t-esc="pay.currency_id.symbol"/>
</t>
</td>
</tr>
</t>
</t>
<t t-if="pay_amt > 0">
<tr>
<td style="text-align: right; padding: 8px; font-size: 1.3rem; color: #387F75;">
<span t-esc="pay.with_context(lang='ar_001').payment_method_line_id.name or pay.with_context(lang='ar_001').payment_method_id.name"/>
</td>
<td style="text-align: center; padding: 8px; font-size: 1.3rem; color: #387F75;">
<t t-if="pay_amt == int(pay_amt)">
<span t-esc="'{:,.0f}'.format(pay_amt)"/> <span t-esc="pay.currency_id.symbol"/>
</t>
<t t-else="">
<span t-esc="'{:,.2f}'.format(pay_amt)"/> <span t-esc="pay.currency_id.symbol"/>
</t>
</td>
</tr>
</t>
</t>
<tr style="background-color: #f8f9fa;">
<td style="text-align: right; padding: 8px; font-size: 1.4rem; color: #6B6B6A; font-weight: bold;">

View File

@ -55,6 +55,18 @@
<field name="groups_id" eval="[(6, 0, [ref('odex_takaful.group_print_receipt')])]"/>
</record>
<!-- Extension Receipt Report on donation.extension.history -->
<record id="extension_receipt_report" model="ir.actions.report">
<field name="name">Extension Receipt</field>
<field name="model">donation.extension.history</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">odex_takaful.report_extension_receipt_document</field>
<field name="report_file">odex_takaful.report_extension_receipt_document</field>
<field name="print_report_name">
(object.extension_ref and ('Extension Receipt - %s' % (object.extension_ref,))) or 'Extension Receipt'
</field>
</record>
</data>
</odoo>

View File

@ -275,12 +275,21 @@ class AccountRegisterPayment(models.TransientModel):
@api.model
def _get_batch_available_partner_banks(self, batch_result, journal):
# First try to get partner from context (most reliable)
partner_id = self.env.context.get('force_sponsorship_line_partner_id')
if partner_id:
partner = self.env['res.partner'].browse(partner_id)
if partner.exists() and partner.bank_ids:
return partner.bank_ids
# Fallback to sponsorship_line_ids context
sponsorship_line_ids = self.env.context.get('sponsorship_line_ids')
sponsorship_lines = self.env['donations.details.lines'].browse(sponsorship_line_ids).filtered(
lambda r: r.display_type == False)
if sponsorship_lines:
return sponsorship_lines.sponsor_id.bank_ids
if sponsorship_line_ids:
sponsorship_lines = self.env['donations.details.lines'].browse(sponsorship_line_ids).filtered(
lambda r: r.display_type == False)
if sponsorship_lines and sponsorship_lines.sponsor_id:
return sponsorship_lines.sponsor_id.bank_ids
return super(AccountRegisterPayment, self)._get_batch_available_partner_banks(batch_result, journal)
def action_create_payments(self):
@ -346,6 +355,16 @@ class AccountRegisterPayment(models.TransientModel):
},
}
else:
# Full payment completed - update extension history state to trigger SMS
wiz = self.env['donation.extension.wizard'].browse(self.env.context.get('wiz_id'))
for line in wiz.line_ids:
if line.extension_invoice_id:
history = self.env['donation.extension.history'].search([
('invoice_id', '=', line.extension_invoice_id.id)
], limit=1)
if history:
history.sudo().write({'state': 'paid'})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',

View File

@ -99,11 +99,17 @@
<xpath expr="//field[@name='partner_bank_id']" position="after">
<field name="sponsorship_payment" invisible="1"/>
<field name="takaful_payment_method" invisible="1"/>
<field name="available_partner_bank_ids" invisible="1"/>
<!-- <field name="payment_method" attrs="{'invisible': [('sponsorship_payment', '=', False)]}"/>-->
<field name="payment_method" invisible="1"/>
<field name="check_number" attrs="{'invisible': ['|', ('sponsorship_payment', '=', False), ('takaful_payment_method','!=','check')]}"/>
<field name="check_due_date" attrs="{'invisible': ['|', ('sponsorship_payment', '=', False), ('takaful_payment_method','!=','check')]}"/>
<field name="partner_bank_id" string="Donor Bank Account" options="{'skip_disable_quick_create': True, 'no_quick_create': True}" context="{'form_view_ref': 'odex_takaful.res_partner_bank_view_form_quick_create', 'default_partner_id': context.get('force_sponsorship_line_partner_id')}" attrs="{'invisible': ['|',('sponsorship_payment', '=', False), ('takaful_payment_method','not in',['bank','check'])]}" create="1" edit="1"/>
<field name="partner_bank_id" string="Donor Bank Account"
domain="[('id', 'in', available_partner_bank_ids)]"
options="{'skip_disable_quick_create': True, 'no_quick_create': True}"
context="{'form_view_ref': 'odex_takaful.res_partner_bank_view_form_quick_create', 'default_partner_id': context.get('force_sponsorship_line_partner_id')}"
attrs="{'invisible': ['|',('sponsorship_payment', '=', False), ('takaful_payment_method','not in',['bank','check'])]}"
create="1" edit="1"/>
<field name="show_last_digits" invisible="1"/>
<field name="last_digits"

View File

@ -115,7 +115,10 @@ class DonationExtensionWizard(models.TransientModel):
donation_line_ids += result[1]
if invoice_ids:
sponsorship_id = self.line_ids.mapped('donation_line_id.sponsorship_mechanism_id.id')
# Get unique sponsorship IDs and take the first one (they should be same in normal use)
sponsorship_ids = list(set(self.line_ids.mapped('donation_line_id.sponsorship_mechanism_id.id')))
sponsorship_id = sponsorship_ids[0] if sponsorship_ids else False
return {
'name': _('Register Payment'),
'res_model': 'account.payment.register',
@ -130,7 +133,8 @@ class DonationExtensionWizard(models.TransientModel):
'sponsorship_payment': True,
'default_sponsorship_payment': True,
'wiz_id': self.id,
'sponsorship_id': sponsorship_id,
'sponsorship_id': sponsorship_id,
'default_takaful_sponsorship_id': sponsorship_id,
'force_sponsorship_line_partner_id': self.line_ids.mapped('partner_id')[:1].id,
},
'target': 'new',