Merge pull request #4941 from expsa/abdulrahman_dev_odex25_ensan

[IMP] odex_takaful: schedule payment & bill grouping
This commit is contained in:
abdurrahman-saber 2025-10-16 13:24:43 +03:00 committed by GitHub
commit 99a226c712
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 122 additions and 41 deletions

View File

@ -48,5 +48,21 @@
<field name="priority" eval="1" />
<field name="active" eval="True"/>
</record>
<!-- Scheduler for Processing Scheduled Payments Daily -->
<record id="scheduler_process_scheduled_payments_action" forcecreate='True' model="ir.cron">
<field name="name">Process Scheduled Payments Daily</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 03:00:00')" />
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model_id" ref="model_sponsorship_scheduling_line"/>
<field name="state">code</field>
<field name="code">model.with_context(from_cron=True).cron_process_scheduled_payments()</field>
<field name="priority" eval="3" />
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View File

@ -182,7 +182,7 @@ class AccountRegisterPayment(models.TransientModel):
def _create_payments(self):
sponsorship_line_ids = self.env.context.get('sponsorship_line_ids')
sponsorship_lines = []
if sponsorship_line_ids:
if sponsorship_line_ids and not self.env.context.get('from_cron',False):
sponsorship_lines = self.env['donations.details.lines'].browse(sponsorship_line_ids)
for line in sponsorship_lines:
if line.record_type == 'sponsorship':
@ -200,13 +200,15 @@ class AccountRegisterPayment(models.TransientModel):
sponsorship = sponsorship_lines.sponsorship_id or sponsorship_lines.sponsorship_mechanism_id
all_lines = sponsorship.donations_details_lines_mechanism_ids + sponsorship.donations_details_lines
if not self.env.context.get('schedule_line_payment'):
sponsorship_lines.write({'is_paid': True})
sponsorship_lines.filtered(lambda l: l.record_type == 'sponsorship' and (l.benefit_id + l.benefit_ids)).write({'state': 'active'})
sponsorship_lines.filtered(lambda l: l.record_type == 'sponsorship' and not (l.benefit_id + l.benefit_ids)).write({'state': 'waiting'})
sponsorship_lines.filtered(lambda l: l.record_type == 'donation').write({'state': 'paid'})
if self.env.context.get('schedule_line_payment'):
schedule_line = self.env['sponsorship.scheduling.line'].browse(self.env.context.get('schedule_line_id'))
schedule_line.sudo().write({'status': 'paid'})
all_schedule_lines_paid = schedule_line.donation_detail_linked_id.sponsorship_scheduling_line_ids.filtered(lambda l: l.status == 'paid')
if len(all_schedule_lines_paid) == len(schedule_line.donation_detail_linked_id.sponsorship_scheduling_line_ids):
schedule_line.donation_detail_linked_id.state = 'paid'
for line in sponsorship_lines:
if line.record_type != 'sponsorship':
continue
@ -216,7 +218,8 @@ class AccountRegisterPayment(models.TransientModel):
line.benefit_id.write({'sponsor_related_id': line.sponsorship_mechanism_id.sponsor_id.id, 'sponsorship_id': line.sponsorship_mechanism_id.id})
line.sponsorship_mechanism_id.sponsor_id.sudo().is_sponsor_portal = True
if 'draft' in all_lines.mapped('state'):
states = all_lines.mapped('state')
if any(state in ['draft', 'waiting', 'active'] for state in states):
sponsorship.state = 'wait_pay'
else:
sponsorship.state = 'paid'
@ -224,24 +227,26 @@ class AccountRegisterPayment(models.TransientModel):
payments.write({'takaful_sponsorship_id': sponsorship.id})
vendor_bill_vals = []
benefit_journal_id = self.env.company.kafala_benefit_journal_id.id
for line in sponsorship_lines:
for benefit in (line.benefit_id + line.benefit_ids):
bill_values = {
'takaful_sponsorship_id': line.sponsorship_id.id if line.sponsorship_id else line.sponsorship_mechanism_id.id,
'name': self.env['ir.sequence'].next_by_code('account.move.accrsp'),
'move_type': 'in_invoice',
'journal_id': benefit_journal_id,
'date': fields.Date.today(),
'partner_id': benefit.partner_id.id,
'invoice_line_ids': [(0, 0, {
'product_id': line.product_id.id,
'price_unit': line.donation_amount,
'quantity': 1,
'name': _("Benefit Number %s") % line.sequence_no,
'analytic_account_id': line.sponsorship_id.branch_custom_id.branch.analytic_account_id.id,
})]
}
vendor_bill_vals.append(bill_values)
benefit_ids = sponsorship_lines.benefit_id + sponsorship_lines.benefit_ids
for benefit in benefit_ids:
benefit_lines = sponsorship_lines.filtered(lambda l: l.benefit_id == benefit or benefit in l.benefit_ids)
sponsorship_id = (benefit_lines.sponsorship_id + benefit_lines.sponsorship_mechanism_id)[0]
bill_values = {
'takaful_sponsorship_id': sponsorship_id.id,
'name': self.env['ir.sequence'].next_by_code('account.move.accrsp'),
'move_type': 'in_invoice',
'journal_id': benefit_journal_id,
'date': fields.Date.today(),
'partner_id': benefit.partner_id.id,
'invoice_line_ids': [(0, 0, {
'product_id': line.product_id.id,
'price_unit': line.donation_amount,
'quantity': 1,
'name': _("Benefit Number %s") % line.sequence_no,
'analytic_account_id': sponsorship_id.branch_custom_id.branch.analytic_account_id.id,
}) for line in benefit_lines]
}
vendor_bill_vals.append(bill_values)
bill_id = self.env['account.move'].sudo().create(vendor_bill_vals)
for line in bill_id.invoice_line_ids:
line.account_id = line._get_computed_account()
@ -251,11 +256,6 @@ class AccountRegisterPayment(models.TransientModel):
line.tax_ids = taxes
line.product_uom_id = line._get_computed_uom()
if self.env.context.get('schedule_line_id'):
schedule_line = self.env['sponsorship.scheduling.line'].browse(self.env.context.get('schedule_line_id'))
schedule_line.sudo().write({'status': 'paid'})
return payments
def action_create_payments(self):

View File

@ -512,25 +512,18 @@ class DonationsDetailsLines(models.Model):
# Format the amount to 2 decimal places for better representation
amount = round(amount, 2)
# Calculate the month and year
month_year = (rec.start_date + relativedelta(months=index)).strftime("%m/%Y")
scheduled_date = (rec.start_date + relativedelta(months=index))
month_year = scheduled_date.strftime("%m/%Y")
# Create the Sponsorship Scheduling Link
SponsorshipSchedulingLink.create({
'sponsorship_id': sponsorship_id.id,
'donation_detail_linked_id': rec.id,
'beneficiary_id': rec.benefit_id.id,
'month_year': month_year,
'amount': amount
'amount': amount,
'scheduled_date': scheduled_date
})
# else:
# month_year = (sponsorship_id.sponsorship_creation_date + relativedelta(months=1)).strftime("%m/%Y")
# # Create the Sponsorship Scheduling Link
# SponsorshipSchedulingLink.create({
# 'sponsorship_id': sponsorship_id.id,
# 'donation_detail_linked_id': rec.id,
# 'beneficiary_id': rec.benefit_id.id,
# 'month_year': month_year,
# 'amount': rec.donation_amount
# })
rec.sponsorships_computed = True

View File

@ -13,6 +13,7 @@ from math import floor
import re
import requests
from odoo.osv import expression
import logging
@ -1745,6 +1746,7 @@ class SponsorshipSchedulingLine(models.Model):
sequence_no = fields.Char(default='/', readonly=True, copy=False)
beneficiary_id = fields.Many2one('family.member')
month_year = fields.Char()
scheduled_date = fields.Date(string='Scheduled Date', help='Date when the payment should be automatically processed')
amount = fields.Float()
refunded_amount = fields.Float(string='Refunded Amount')
status = fields.Selection([
@ -1839,6 +1841,70 @@ class SponsorshipSchedulingLine(models.Model):
vals['sequence_no'] = self.env['ir.sequence'].next_by_code('sponsorship.scheduling.line.sequence') or '/'
return super(SponsorshipSchedulingLine, self).create(vals)
@api.model
def cron_process_scheduled_payments(self):
today = fields.Date.today()
_logger = logging.getLogger(__name__)
scheduled_lines = self.search([
('scheduled_date', '<=', today),
('status', '=', 'unpaid'),
('sponsorship_state', 'in', ['confirmed', 'wait_pay'])
])
_logger.info(f"Found {len(scheduled_lines)} scheduling lines scheduled for {today}")
for line in scheduled_lines:
try:
if line.payment_method == 'direct_debit':
invoice_id = self.env['account.move'].sudo().search([
('takaful_sponsorship_id', '=', line.sponsorship_id.id),
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('payment_state', 'in', ['not_paid', 'partial'])
], limit=1)
if not invoice_id:
_logger.warning(f"No unpaid invoice found for sponsorship {line.sponsorship_id.code}, skipping line {line.sequence_no}")
continue
journal_id = line.donation_detail_linked_id.payment_method_id.journal_id
if not journal_id:
_logger.warning(f"No journal found for payment method of line {line.sequence_no}")
continue
payment_register_vals = {
'payment_date': fields.Date.today(),
'amount': line.amount,
'journal_id': journal_id.id,
'communication': f"{line.sponsorship_id.code} - {line.sequence_no}",
}
if line.donation_detail_linked_id.payment_method_id.bank_id:
payment_register_vals['partner_bank_id'] = line.donation_detail_linked_id.payment_method_id.bank_id.id
payment_register = self.env['account.payment.register'].sudo().with_context(
active_model='account.move',
active_ids=invoice_id.ids,
schedule_line_payment=True,
dont_redirect_to_payments=True,
schedule_line_id=line.id,
sponsorship_line_ids=line.donation_detail_linked_id.ids,
from_cron=True,
).create(payment_register_vals)
payment_register.action_create_payments()
_logger.info(f"Successfully processed scheduled payment for line {line.sequence_no}")
else:
_logger.warning(f"Scheduling line {line.sequence_no} payment method is not direct debit, skipping auto-payment")
except Exception as e:
_logger.error(f"Error processing scheduled payment for line {line.sequence_no}: {str(e)}")
continue
return True
def action_refund(self):
refund_amount = self.amount - self.refunded_amount

View File

@ -15,6 +15,7 @@
<field name="donation_detail_linked_id"/>
<field name="beneficiary_id"/>
<field name="month_year"/>
<field name="scheduled_date"/>
<field name="amount" sum="Total Amount"/>
<field name="refunded_amount" sum="Total Refunded"/>
<field name="payment_method"/>
@ -38,8 +39,12 @@
<field name="sponsorship_id"/>
<field name="beneficiary_id"/>
<field name="month_year"/>
<field name="scheduled_date"/>
<field name="donation_detail_linked_id"/>
<separator/>
<filter string="Scheduled Today" name="scheduled_today" domain="[('scheduled_date', '=', context_today().strftime('%Y-%m-%d'))]"/>
<filter string="Scheduled This Week" name="scheduled_this_week" domain="[('scheduled_date', '&gt;=', (context_today() - datetime.timedelta(days=context_today().weekday())).strftime('%Y-%m-%d')), ('scheduled_date', '&lt;=', (context_today() + datetime.timedelta(days=6-context_today().weekday())).strftime('%Y-%m-%d'))]"/>
<separator/>
<filter string="Unpaid" name="unpaid" domain="[('status', '=', 'unpaid')]"/>
<filter string="Paid" name="paid" domain="[('status', '=', 'paid')]"/>
<filter string="Approve Refund" name="approve_refund" domain="[('status', '=', 'approve_refund')]"/>
@ -58,6 +63,7 @@
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
<filter string="Payment Method" name="group_payment_method" context="{'group_by': 'payment_method'}"/>
<filter string="Month/Year" name="group_month_year" context="{'group_by': 'month_year'}"/>
<filter string="Scheduled Date" name="group_scheduled_date" context="{'group_by': 'scheduled_date'}"/>
<filter string="Sponsorship State" name="group_sponsorship_state" context="{'group_by': 'sponsorship_state'}"/>
</group>
</search>