[IMP] odex_takaful: sponsor replacement

This commit is contained in:
Abdurrahman Saber 2025-10-21 09:29:19 +03:00
parent f529354b99
commit 7bc6abd163
14 changed files with 506 additions and 301 deletions

View File

@ -58,6 +58,7 @@
'wizards/refund_payment_wizard.xml',
'wizards/add_details_wizard.xml',
'wizards/donation_extension_wizard.xml',
'wizards/replace_sponsor_wizard.xml',
'views/donations_details_lines.xml',
'views/donation_extension_history_views.xml',
'views/sponsorship_scheduling_line.xml',

View File

@ -26,4 +26,5 @@ from . import res_config_settings
from . import product
from . import donation_details_lines
from . import donation_extension_history
from . import family_member
from . import family_member
from . import sponsorship_scheduling_line

View File

@ -147,7 +147,7 @@ class AccountRegisterPayment(models.TransientModel):
line.sponsorship_mechanism_id.sponsor_id.sudo().is_sponsor_portal = True
states = all_lines.mapped('state')
if any(state in ['draft', 'waiting', 'active'] for state in states):
if 'draft' in states:
sponsorship.state = 'wait_pay'
else:
sponsorship.state = 'paid'
@ -184,20 +184,31 @@ class AccountRegisterPayment(models.TransientModel):
line.tax_ids = taxes
line.product_uom_id = line._get_computed_uom()
return payments
def action_create_payments(self):
# Call the original method to create payments
payments = self._create_payments()
# Update `move_id` values to include `takaful_sponsorship_id`
# TODO: Not sure if this is needed
if self.is_refund_sponsorship:
for payment in payments:
if payment.move_id: # Ensure the payment has a move_id
payment.move_id.takaful_sponsorship_id = self.takaful_sponsorship_id.id
ctx = self.env.context.copy()
if self._context.get('dont_redirect_to_payments'):
return payments
@api.model
def _get_line_batch_key(self, line):
res = super(AccountRegisterPayment, self)._get_line_batch_key(line)
if self.env.context.get('force_sponsorship_line_partner_id'):
res['partner_id'] = self.env.context.get('force_sponsorship_line_partner_id')
return res
def _create_payment_vals_from_wizard(self):
res = super(AccountRegisterPayment, self)._create_payment_vals_from_wizard()
if self.env.context.get('force_sponsorship_line_partner_id'):
res['partner_id'] = self.env.context.get('force_sponsorship_line_partner_id')
return res
def action_create_payments(self):
res = super(AccountRegisterPayment, self).action_create_payments()
if self.env.context.get('dont_redirect_to_payments'):
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
@ -210,26 +221,4 @@ class AccountRegisterPayment(models.TransientModel):
},
}
# You can update it as needed
ctx.update({
'create': False,
})
# Prepare the action for redirection
action = {
'name': _('Payments'),
'type': 'ir.actions.act_window',
'res_model': 'account.payment',
'context': ctx,
}
if len(payments) == 1:
action.update({
'view_mode': 'form',
'res_id': payments.id,
})
else:
action.update({
'view_mode': 'tree,form',
'domain': [('id', 'in', payments.ids)],
})
return action
return res

View File

@ -52,9 +52,9 @@ class DonationsDetailsLines(models.Model):
benefit_id_number = fields.Char("Benefit ID Number")
benefit_family_code = fields.Char("Benefit Family Code")
editable = fields.Boolean(string="Editable", default=True) # To differentiate
sponsor_id = fields.Many2one('takaful.sponsor', related='sponsorship_id.sponsor_id')
sponsor_phone = fields.Char(related='sponsorship_id.sponsor_id.mobile')
branch_custom_id = fields.Many2one('branch.settings', related='sponsorship_id.branch_custom_id')
sponsor_id = fields.Many2one('takaful.sponsor', compute='_compute_sponsor_id', store=True, readonly=True)
sponsor_phone = fields.Char(related='sponsor_id.mobile')
branch_custom_id = fields.Many2one('branch.settings', compute='_compute_branch_custom_id')
record_type = fields.Selection([('sponsorship', 'Sponsorship'),('donation', 'Donation')], compute='_compute_record_type', store=True, readonly=True)
state = fields.Selection([('draft', 'To Pay'),('waiting', 'Waiting'), ('active', 'Active'),('closed', 'Closed'),('extended', 'Extended'), ('paid', 'Paid')], string='State', default='draft')
sponsorship_scheduling_line_ids = fields.One2many('sponsorship.scheduling.line', 'donation_detail_linked_id')
@ -69,6 +69,15 @@ class DonationsDetailsLines(models.Model):
def _compute_extension_count(self):
for rec in self:
rec.extension_count = len(rec.extension_history_ids)
@api.depends('sponsorship_id', 'sponsorship_mechanism_id')
def _compute_sponsor_id(self):
for rec in self:
rec.sponsor_id = rec.sponsorship_id.sponsor_id or rec.sponsorship_mechanism_id.sponsor_id
def _compute_branch_custom_id(self):
for rec in self:
rec.branch_custom_id = rec.sponsorship_id.branch_custom_id or rec.sponsorship_mechanism_id.branch_custom_id
@api.depends('payment_method', 'end_date')
def _compute_show_extend_button(self):
@ -599,6 +608,7 @@ class DonationsDetailsLines(models.Model):
'beneficiary_id': rec.benefit_id.id,
'month_year': month_year,
'amount': amount,
'sponsor_id': rec.sponsor_id.id,
'scheduled_date': scheduled_date
})
@ -786,6 +796,7 @@ class DonationsDetailsLines(models.Model):
'sponsorship_line_ids': self.ids,
'default_journal_id': self.payment_method_id.journal_id.id,
'default_amount': self.total_donation_amount,
'force_sponsorship_line_partner_id': self.sponsor_id.partner_id.id,
# 'default_payment_method_line_id': payment_method_line.id,
},
'target': 'new',
@ -907,3 +918,16 @@ class DonationsDetailsLines(models.Model):
'create': False,
},
}
def replace_sponsor_wizard(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Replace Sponsor'),
'res_model': 'replace.sponsor.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_donation_detail_id': self.id,
},
}

View File

@ -0,0 +1,264 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
import logging
_logger = logging.getLogger(__name__)
class SchedulingLine(models.Model):
_name = 'sponsorship.scheduling.line'
sponsorship_id = fields.Many2one('takaful.sponsorship')
sponsorship_state = fields.Selection(related='sponsorship_id.state')
donation_detail_linked_id = fields.Many2one('donations.details.lines')
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([
('unpaid', 'Unpaid'),
('paid', 'Paid'),
('approve_refund', 'Approve Refund'),
('under_refund', 'Under Refund'),
('partial_refund', 'Partial Refund'),
('fully_refund', 'Fully Refund'),
], copy=False, default='unpaid')
sponsor_id = fields.Many2one('takaful.sponsor')
payment_method = fields.Selection(related="donation_detail_linked_id.payment_method_id.payment_method")
cancel_refund = fields.Boolean(string='Cancel Refund Time', compute="compute_days_after_payment")
def compute_days_after_payment(self):
"""Check if the number of hours passed after payment is within the configured limit"""
for record in self:
days_passed = 0
config_param = record.env['ir.config_parameter'].sudo()
max_hours = int(config_param.get_param('odex_takaful.cancel_refund', default=0))
payment_record = record.env['payment.details.lines'].search([
'|',
('sponsorship_scheduling_line_id', '=', record.id),
('sponsorship_scheduling_line_ids', 'in', record.id),
('sponsorship_id', '=', record.sponsorship_id.id)
], order="payment_date desc", limit=1)
if payment_record and payment_record.payment_date:
now = fields.Datetime.now()
payment_datetime = fields.Datetime.to_datetime(payment_record.payment_date)
hours_passed = (now - payment_datetime).total_seconds() / 3600 # Convert seconds to hours
record.cancel_refund = hours_passed >= max_hours
else:
record.cancel_refund = False
def action_register_payment(self):
invoice_id = self.env['account.move'].sudo().search([
('takaful_sponsorship_id', '=', self.sponsorship_id.id),
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('payment_state', 'in', ['not_paid', 'partial'])], limit=1, order='id')
if not invoice_id:
raise ValidationError(_("No invoice found for this sponsorship."))
# payment_method_line = self.env['account.payment.method.line'].search([
# ('journal_id', '=', self.payment_method_id.journal_id.id),
# ('payment_type', '=', 'inbound')
# ], limit=1)
return {
'name': _('Register Payment'),
'res_model': 'account.payment.register',
'view_mode': 'form',
'context': {
'active_model': 'account.move',
'active_ids': invoice_id.ids,
'dont_redirect_to_payments': True,
'sponsorship_line_ids': self.donation_detail_linked_id.ids,
'default_amount': self.amount,
'default_journal_id': self.donation_detail_linked_id.payment_method_id.journal_id.id,
'schedule_line_payment': True,
'schedule_line_id': self.id,
'force_sponsorship_line_partner_id': self.donation_detail_linked_id.sponsor_id.partner_id.id,
},
'target': 'new',
'type': 'ir.actions.act_window',
}
def pay_sponsorship_scheduling(self):
"""
Opens the payment details wizard and passes default values.
"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Payment Details',
'res_model': 'payment.details.lines',
'view_mode': 'form',
'target': 'new',
'context': {
'default_donation_amount': self.amount,
# 'default_remaining_amount': self.amount,
'default_sponsorship_id': self.sponsorship_id.id,
'default_sponsorship_scheduling_line_id': self.id,
'default_payment_method': self.payment_method,
'default_payment_method_id': self.donation_detail_linked_id.payment_method_id.id,
# 'default_sponsorship_scheduling_line_ids':self.ids,
},
}
@api.model
def create(self, vals):
# Check if sequence_no is not already set
if vals.get('sequence_no', '/') == '/':
# Assign the next sequence number
vals['sequence_no'] = self.env['ir.sequence'].next_by_code('sponsorship.scheduling.line.sequence') or '/'
return super(SchedulingLine, self).create(vals)
@api.model
def cron_process_scheduled_payments(self):
today = fields.Date.today()
scheduled_lines = self.search([
('scheduled_date', '<=', today),
('status', '=', 'unpaid'),
])
_logger.info(f"Found {len(scheduled_lines)} scheduling lines scheduled for {today}")
for line in scheduled_lines:
donation_line = line.donation_detail_linked_id
sponsorship = donation_line.sponsorship_id or donation_line.sponsorship_mechanism_id
try:
if line.payment_method == 'direct_debit':
invoice_id = self.env['account.move'].sudo().search([
('takaful_sponsorship_id', '=', sponsorship.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,
force_sponsorship_line_partner_id=line.donation_detail_linked_id.sponsor_id.partner_id.id,
).create(payment_register_vals)
payment_register.action_create_payments()
line.sudo().write({'status': 'paid'})
_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
return {
'type': 'ir.actions.act_window',
'name': 'Refund Wizard',
'res_model': 'refund.wiz',
'view_mode': 'form',
'target': 'new',
'context': {
'default_payment_method': self.payment_method,
'default_payment_method_id': self.donation_detail_linked_id.payment_method_id.id,
'default_refund_amount': refund_amount,
}, }
def approve_refund(self):
self.final_refund()
self.sponsorship_id.state = 'under_refund'
def final_refund(self):
invoice_lines = []
unpaid_invoice_exists = self.env['account.move'].search(
[('takaful_sponsorship_id', '=', self.sponsorship_id.id), ('move_type', '=', 'out_refund'),
('payment_state', '!=', 'paid')])
refund_amount = self.sponsorship_id.refund_details_lines_ids.filtered(
lambda r: r.sponsorship_scheduling_line.id == self.id or self.id in r.sponsorship_scheduling_line_ids.ids and r.invoiced == False).mapped('refund_amount')[0] if self.sponsorship_id.refund_details_lines_ids else 0.0
sponsor_id = self.sponsorship_id.sponsor_id
payment_details_line = self.sponsorship_id.refund_details_lines_ids.filtered(
lambda r: r.sponsorship_scheduling_line.id == self.id or self.id in r.sponsorship_scheduling_line_ids.ids).mapped('payment_details_line')
if unpaid_invoice_exists:
raise ValidationError(_("You have Refund invoice not paid"))
# Check if product exists from donation item
product_template = self.donation_detail_linked_id.product_template_id
product = self.donation_detail_linked_id.product_id # Directly linked product
if not product:
# If no product_id exists in product_template, check if a product with the same name exists
product = self.env['product.product'].search([('name', '=', product_template.name)], limit=1)
if not product:
# Create new product if it doesn't exist
product = self.env['product.product'].create({
'name': product_template.name,
'type': 'service' # Setting type to service
})
invoice_lines.append((0, 0, {
'product_id': product.id,
'account_id': self.donation_detail_linked_id.product_template_id.property_account_income_id.id,
# The same account for all lines
'quantity': 1, # Qty is 1
# The same analytic account
'takaful_sponsorship_id': self.sponsorship_id.id,
'price_unit': refund_amount,
'analytic_account_id': self.sponsorship_id.branch_custom_id.branch.analytic_account_id.id
}))
Partner = self.env['res.partner'].sudo()
partner_id = Partner.search([('id', '=', sponsor_id.partner_id.id)], limit=1) or \
Partner.search([('name', '=', 'Cash Customer')], limit=1)
if not partner_id:
partner_id = Partner.create({
'company_type': 'person',
'name': 'Cash Customer'
})
invoice = self.env['account.move'].create({
'move_type': 'out_refund', # Refund Invoice
'partner_id': partner_id.id,
'invoice_date': fields.Date.today(),
'invoice_line_ids': invoice_lines,
'takaful_sponsorship_id': self.sponsorship_id.id,
'is_refund_sponsorship': True,
'sponsorship_scheduling_line': self.id,
'payment_details_line': payment_details_line,
})
invoice.action_post()
self.sponsorship_id.refund_details_lines_ids.write({'invoiced': True})
return invoice

View File

@ -10,7 +10,7 @@ class TakafulPaymentMethod(models.Model):
payment_method = fields.Selection([
("cash", "Cash"),
("bank", "Bank Transfer"),
("direct_debit", "Direct Debit")
("direct_debit", "Direct Debit"),
("check", "Check"),
("network", "Network"),
], required=True)

View File

@ -16,8 +16,6 @@ from odoo.osv import expression
import logging
def trunc_datetime(someDate):
# Compare two dates based on Month and Year only
return someDate.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
@ -1862,264 +1860,6 @@ class AnotherSponsors(models.Model):
receive_messages = fields.Boolean(string='Receive messages?')
note = fields.Char(string='Note')
class SponsorshipSchedulingLine(models.Model):
_name = 'sponsorship.scheduling.line'
sponsorship_id = fields.Many2one('takaful.sponsorship')
sponsorship_state = fields.Selection(related='sponsorship_id.state')
donation_detail_linked_id = fields.Many2one('donations.details.lines')
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([
('unpaid', 'Unpaid'),
('paid', 'Paid'),
('approve_refund', 'Approve Refund'),
('under_refund', 'Under Refund'),
('partial_refund', 'Partial Refund'),
('fully_refund', 'Fully Refund'),
], copy=False, default='unpaid')
payment_method = fields.Selection(related="donation_detail_linked_id.payment_method_id.payment_method")
cancel_refund = fields.Boolean(string='Cancel Refund Time', compute="compute_days_after_payment")
def compute_days_after_payment(self):
"""Check if the number of hours passed after payment is within the configured limit"""
for record in self:
days_passed = 0
config_param = record.env['ir.config_parameter'].sudo()
max_hours = int(config_param.get_param('odex_takaful.cancel_refund', default=0))
payment_record = record.env['payment.details.lines'].search([
'|',
('sponsorship_scheduling_line_id', '=', record.id),
('sponsorship_scheduling_line_ids', 'in', record.id),
('sponsorship_id', '=', record.sponsorship_id.id)
], order="payment_date desc", limit=1)
if payment_record and payment_record.payment_date:
now = fields.Datetime.now()
payment_datetime = fields.Datetime.to_datetime(payment_record.payment_date)
hours_passed = (now - payment_datetime).total_seconds() / 3600 # Convert seconds to hours
record.cancel_refund = hours_passed >= max_hours
else:
record.cancel_refund = False
def action_register_payment(self):
invoice_id = self.env['account.move'].sudo().search([
('takaful_sponsorship_id', '=', self.sponsorship_id.id),
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('payment_state', 'in', ['not_paid', 'partial'])], limit=1, order='id')
if not invoice_id:
raise ValidationError(_("No invoice found for this sponsorship."))
# payment_method_line = self.env['account.payment.method.line'].search([
# ('journal_id', '=', self.payment_method_id.journal_id.id),
# ('payment_type', '=', 'inbound')
# ], limit=1)
return {
'name': _('Register Payment'),
'res_model': 'account.payment.register',
'view_mode': 'form',
'context': {
'active_model': 'account.move',
'active_ids': invoice_id.ids,
'dont_redirect_to_payments': True,
'sponsorship_line_ids': self.donation_detail_linked_id.ids,
'default_amount': self.amount,
'default_journal_id': self.donation_detail_linked_id.payment_method_id.journal_id.id,
'schedule_line_payment': True,
'schedule_line_id': self.id,
},
'target': 'new',
'type': 'ir.actions.act_window',
}
def pay_sponsorship_scheduling(self):
"""
Opens the payment details wizard and passes default values.
"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Payment Details',
'res_model': 'payment.details.lines',
'view_mode': 'form',
'target': 'new',
'context': {
'default_donation_amount': self.amount,
# 'default_remaining_amount': self.amount,
'default_sponsorship_id': self.sponsorship_id.id,
'default_sponsorship_scheduling_line_id': self.id,
'default_payment_method': self.payment_method,
'default_payment_method_id': self.donation_detail_linked_id.payment_method_id.id,
# 'default_sponsorship_scheduling_line_ids':self.ids,
},
}
@api.model
def create(self, vals):
# Check if sequence_no is not already set
if vals.get('sequence_no', '/') == '/':
# Assign the next sequence number
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'),
])
_logger.info(f"Found {len(scheduled_lines)} scheduling lines scheduled for {today}")
for line in scheduled_lines:
donation_line = line.donation_detail_linked_id
sponsorship = donation_line.sponsorship_id or donation_line.sponsorship_mechanism_id
try:
if line.payment_method == 'direct_debit':
invoice_id = self.env['account.move'].sudo().search([
('takaful_sponsorship_id', '=', sponsorship.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()
line.sudo().write({'status': 'paid'})
_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
return {
'type': 'ir.actions.act_window',
'name': 'Refund Wizard',
'res_model': 'refund.wiz',
'view_mode': 'form',
'target': 'new',
'context': {
'default_payment_method': self.payment_method,
'default_payment_method_id': self.donation_detail_linked_id.payment_method_id.id,
'default_refund_amount': refund_amount,
}, }
def approve_refund(self):
self.final_refund()
self.sponsorship_id.state = 'under_refund'
def final_refund(self):
invoice_lines = []
unpaid_invoice_exists = self.env['account.move'].search(
[('takaful_sponsorship_id', '=', self.sponsorship_id.id), ('move_type', '=', 'out_refund'),
('payment_state', '!=', 'paid')])
refund_amount = self.sponsorship_id.refund_details_lines_ids.filtered(
lambda r: r.sponsorship_scheduling_line.id == self.id or self.id in r.sponsorship_scheduling_line_ids.ids and r.invoiced == False).mapped('refund_amount')[0] if self.sponsorship_id.refund_details_lines_ids else 0.0
sponsor_id = self.sponsorship_id.sponsor_id
payment_details_line = self.sponsorship_id.refund_details_lines_ids.filtered(
lambda r: r.sponsorship_scheduling_line.id == self.id or self.id in r.sponsorship_scheduling_line_ids.ids).mapped('payment_details_line')
if unpaid_invoice_exists:
raise ValidationError(_("You have Refund invoice not paid"))
# Check if product exists from donation item
product_template = self.donation_detail_linked_id.product_template_id
product = self.donation_detail_linked_id.product_id # Directly linked product
if not product:
# If no product_id exists in product_template, check if a product with the same name exists
product = self.env['product.product'].search([('name', '=', product_template.name)], limit=1)
if not product:
# Create new product if it doesn't exist
product = self.env['product.product'].create({
'name': product_template.name,
'type': 'service' # Setting type to service
})
invoice_lines.append((0, 0, {
'product_id': product.id,
'account_id': self.donation_detail_linked_id.product_template_id.property_account_income_id.id,
# The same account for all lines
'quantity': 1, # Qty is 1
# The same analytic account
'takaful_sponsorship_id': self.sponsorship_id.id,
'price_unit': refund_amount,
'analytic_account_id': self.sponsorship_id.branch_custom_id.branch.analytic_account_id.id
}))
Partner = self.env['res.partner'].sudo()
partner_id = Partner.search([('id', '=', sponsor_id.partner_id.id)], limit=1) or \
Partner.search([('name', '=', 'Cash Customer')], limit=1)
if not partner_id:
partner_id = Partner.create({
'company_type': 'person',
'name': 'Cash Customer'
})
invoice = self.env['account.move'].create({
'move_type': 'out_refund', # Refund Invoice
'partner_id': partner_id.id,
'invoice_date': fields.Date.today(),
'invoice_line_ids': invoice_lines,
'takaful_sponsorship_id': self.sponsorship_id.id,
'is_refund_sponsorship': True,
'sponsorship_scheduling_line': self.id,
'payment_details_line': payment_details_line,
})
invoice.action_post()
self.sponsorship_id.refund_details_lines_ids.write({'invoiced': True})
return invoice
class RefundDetailsLines(models.TransientModel):
_name = 'refund.details.lines'
_description = "Refund Destails Lines"

View File

@ -4,7 +4,7 @@ from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError, Warning
from dateutil.parser import parse
import re
from odoo.osv import expression
SAUDI_MOBILE_PATTERN = "(^(05|5)(5|0|3|6|4|9|1|8|7)([0-9]{7})$)"
@ -26,6 +26,7 @@ class TakafulSponsor(models.Model):
args = []
# Extend the domain filter with custom search conditions
domain = ['|', '|', ('name', operator, name),('id_number', operator, name),('mobile', operator, name)]
domain = expression.AND([domain, args])
# Combine domain filter with any existing args (domain filter in Many2one)
partners = self._search(domain, limit=limit, access_rights_uid=name_get_uid)

View File

@ -45,3 +45,5 @@ access_product_product_donation_officer,product_product_donation_officer,product
access_group_kufula_user_account_payment,access_group_kufula_user_account_payment,account.model_account_payment,odex_takaful.group_kufula_user,1,1,1,0
access_donation_extension_wizard,donation.extension.wizard.access,model_donation_extension_wizard,base.group_user,1,1,1,1
access_donation_extension_history,donation.extension.history.access,model_donation_extension_history,odex_takaful.group_kufula_user,1,1,1,0
access_replace_sponsor_wizard,replace.sponsor.wizard.access,model_replace_sponsor_wizard,odex_takaful.group_replace_sponsor,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
45
46
47
48
49

View File

@ -62,6 +62,12 @@
<field name="comment">Orphan Replacement</field>
</record>
<record id="group_replace_sponsor" model="res.groups">
<field name="name">Replace Sponsor</field>
<field name="category_id" ref="module_category_kufula"/>
<field name="comment">Can Replace Sponsor on Donation Lines</field>
</record>
<record id="group_show_donation_item_product" model="res.groups">
<field name="name">Show Donation Item Product</field>
<field name="category_id" ref="module_category_kufula"/>

View File

@ -125,6 +125,13 @@
class="btn-primary"
attrs="{'invisible': ['|', '&amp;',('state', 'not in', ['active', 'paid']), ('record_type', '=', 'donation'), ('donation_mechanism', '=', 'without_conditions')]}"
groups="odex_takaful.group_orphan_replacement" />
<button string="Replace Sponsor"
name="replace_sponsor_wizard"
type="object"
class="btn-primary"
icon="fa-exchange"
attrs="{'invisible': [('sponsor_id', '=', False)]}"
groups="odex_takaful.group_replace_sponsor" />
<field name="state" widget="statusbar"
statusbar_visible="draft,waiting,active,closed,extended" />
</header>
@ -157,8 +164,8 @@
<field name="total_donation_amount" />
</group>
<group string="Sponsorship Information">
<field name="sponsorship_id" />
<field name="sponsorship_mechanism_id" invisible="1" />
<field name="sponsorship_id" readonly="1" attrs="{'invisible': [('sponsorship_id', '=', False)]}" />
<field name="sponsorship_mechanism_id" readonly="1" attrs="{'invisible': [('sponsorship_mechanism_id', '=', False)]}" />
<field name="sponsor_id" />
<field name="sponsor_phone" />
<field name="branch_custom_id" />

View File

@ -7,4 +7,5 @@ from . import add_details_wizard
from . import orphan_replacement_wizard
from . import transfer_deduction_wizard
from . import donation_extension_wizard
from . import replace_sponsor_wizard

View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
import logging
_logger = logging.getLogger(__name__)
class ReplaceSponsorWizard(models.TransientModel):
_name = 'replace.sponsor.wizard'
_description = "Replace Sponsor Wizard"
donation_detail_id = fields.Many2one(
'donations.details.lines',
string='Donation Detail',
readonly=True
)
old_sponsor_id = fields.Many2one(
'takaful.sponsor',
string='Current Sponsor',
readonly=True,
compute='_compute_old_sponsor_id'
)
new_sponsor_id = fields.Many2one(
'takaful.sponsor',
domain="[('id', '!=', old_sponsor_id)]",
string='New Sponsor',
required=True,
)
reason = fields.Text(
string='Reason for Replacement',
required=True,
)
@api.depends('donation_detail_id')
def _compute_old_sponsor_id(self):
for rec in self:
if rec.donation_detail_id:
rec.old_sponsor_id = rec.donation_detail_id.sponsor_id
else:
rec.old_sponsor_id = False
@api.model
def default_get(self, fields_list):
res = super(ReplaceSponsorWizard, self).default_get(fields_list)
# Get the active donation detail line from context
active_id = self.env.context.get('active_id')
active_model = self.env.context.get('active_model')
if active_model == 'donations.details.lines' and active_id:
donation_line = self.env['donations.details.lines'].browse(active_id)
res['donation_detail_id'] = donation_line.id
return res
def action_replace_sponsor(self):
"""
Replace the sponsor on the donation detail line and post a message to the chatter
"""
self.ensure_one()
if not self.donation_detail_id:
raise ValidationError(_("No donation detail line found."))
if not self.new_sponsor_id:
raise ValidationError(_("Please select a new sponsor."))
if not self.reason:
raise ValidationError(_("Please provide a reason for the replacement."))
donation_line = self.donation_detail_id
old_sponsor_name = donation_line.sponsor_id.name if donation_line.sponsor_id else _('None')
new_sponsor_name = self.new_sponsor_id.name
# Get the sponsorship record (could be sponsorship_id or sponsorship_mechanism_id)
sponsorship = donation_line.sponsorship_id or donation_line.sponsorship_mechanism_id
if not sponsorship:
raise ValidationError(_("No sponsorship found for this donation detail line."))
# Update the sponsor on the sponsorship
donation_line.write({
'sponsor_id': self.new_sponsor_id.id
})
# Post message to the donation detail line chatter
message_body = _(
"<b>Sponsor Replacement</b><br/>"
"Previous Sponsor: <b>%s</b><br/>"
"New Sponsor: <b>%s</b><br/>"
"Reason: %s"
) % (old_sponsor_name, new_sponsor_name, self.reason)
donation_line.message_post(
body=message_body,
subject=_("Sponsor Replaced"),
message_type='notification',
)
# Also post to the sponsorship chatter
message_body += _("<br/>Line Number: <b>%s</b>") % donation_line.sequence_no
sponsorship.message_post(
body=message_body,
subject=_("Sponsor Replaced"),
message_type='notification',
)
_logger.info(
"Sponsor replaced on donation line %s from %s to %s. Reason: %s",
donation_line.sequence_no,
old_sponsor_name,
new_sponsor_name,
self.reason
)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
'message': _('Sponsor has been successfully replaced from %s to %s') % (
old_sponsor_name,
new_sponsor_name
),
'type': 'success',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'}
}
}
def action_cancel(self):
"""
Cancel the wizard
"""
return {'type': 'ir.actions.act_window_close'}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="replace_sponsor_wizard_form" model="ir.ui.view">
<field name="name">replace.sponsor.wizard.form</field>
<field name="model">replace.sponsor.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="donation_detail_id" invisible="1"/>
<group>
<group string="Sponsor Information">
<field name="old_sponsor_id" readonly="1"/>
<field name="new_sponsor_id"/>
</group>
</group>
<group>
<field name="reason" placeholder="Please provide a reason for replacing the sponsor..." nolabel="1"/>
</group>
</sheet>
<footer>
<button name="action_replace_sponsor" type="object" string="Replace Sponsor" class="oe_highlight"/>
or
<button name="action_cancel" type="object" string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>