[IMP] odex_takaful: sponsor replacement
This commit is contained in:
parent
f529354b99
commit
7bc6abd163
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -125,6 +125,13 @@
|
|||
class="btn-primary"
|
||||
attrs="{'invisible': ['|', '&',('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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue