# Part of OpenG2P. See LICENSE file for full copyright and licensing details.
import logging
from odoo import _, api, fields, models
from odoo.addons.queue_job.delay import group
_logger = logging.getLogger(__name__)
class EntitlementManager(models.Model):
_name = "g2p.program.entitlement.manager"
_description = "Entitlement Manager"
_inherit = "g2p.manager.mixin"
program_id = fields.Many2one("g2p.program", "Program")
@api.model
def _selection_manager_ref_id(self):
selection = super()._selection_manager_ref_id()
new_manager = ("g2p.program.entitlement.manager.default", "Default")
if new_manager not in selection:
selection.append(new_manager)
return selection
[docs]class BaseEntitlementManager(models.AbstractModel):
_name = "g2p.base.program.entitlement.manager"
_inherit = "base.programs.manager"
_description = "Base Entitlement Manager"
IS_CASH_ENTITLEMENT = True
MIN_ROW_JOB_QUEUE = 200
MAX_ROW_JOB_QUEUE = 2000
name = fields.Char("Manager Name", required=True)
program_id = fields.Many2one("g2p.program", string="Program", required=True)
[docs] def prepare_entitlements(self, cycle, beneficiaries):
"""
This method is used to prepare the entitlement list of the beneficiaries.
:param cycle: The cycle.
:param beneficiaries: The beneficiaries.
:return:
"""
raise NotImplementedError()
[docs] def set_pending_validation_entitlements(self, cycle):
"""Base Entitlement Manager :meth:`set_pending_validate_entitlements`
Set entitlements to pending_validation in a cycle
Override in entitlement manager
:param cycle: A recordset of cycle
:return:
"""
raise NotImplementedError()
def _set_pending_validation_entitlements_async(self, cycle, entitlements):
"""Set Entitlements to Pending Validation
Base Entitlement Manager :meth:`_set_pending_validation_entitlements_async`
Asynchronous setting of entitlements to pending_validation in a cycle using `job_queue`
:param cycle: A recordset of cycle
:param entitlements: A recordset of entitlements to process
:return:
"""
entitlements_count = len(entitlements)
_logger.debug("Set entitlements to pending validation asynchronously")
cycle.message_post(
body=_(
"Setting %s entitlements to pending validation has started.",
entitlements_count,
)
)
cycle.write(
{
"locked": True,
"locked_reason": _("Set entitlements to pending validation for cycle."),
}
)
jobs = []
for i in range(0, entitlements_count, self.MAX_ROW_JOB_QUEUE):
jobs.append(
self.delayable()._set_pending_validation_entitlements(
entitlements[i : i + self.MAX_ROW_JOB_QUEUE]
)
)
main_job = group(*jobs)
main_job.on_done(
self.delayable().mark_job_as_done(
cycle, _("Entitlements Set to Pending Validation.")
)
)
main_job.delay()
def _set_pending_validation_entitlements(self, entitlements):
"""
Base Entitlement Manager :meth:`_set_pending_validation_entitlements`
Synchronous setting of entitlements to pending_validation in a cycle
Override in entitlement manager
:param entitlements: A recordset of entitlements
:return:
"""
raise NotImplementedError()
[docs] def validate_entitlements(self, cycle):
"""Base Entitlement Manager :meth:`validate_entitlements`
Validate entitlements for a cycle
Override in entitlement manager
:param cycle: A recordset of cycle
:return:
"""
raise NotImplementedError()
def _validate_entitlements_async(self, cycle, entitlements, entitlements_count):
"""Validate Entitlements
Base Entitlement Manager :meth:`_validate_entitlements_async`
Asynchronous validation of entitlements in a cycle using `job_queue`
:param cycle: A recordset of cycle
:param entitlements: A recordset of entitlements to validate
:param entitlements_count: Integer count of entitlements to validate
:return:
"""
_logger.debug("Validate entitlements asynchronously")
cycle.message_post(
body=_("Validate %s entitlements started.", entitlements_count)
)
cycle.write(
{
"locked": True,
"locked_reason": _("Validate and approve entitlements for cycle."),
}
)
jobs = []
for i in range(0, entitlements_count, self.MAX_ROW_JOB_QUEUE):
jobs.append(
self.delayable()._validate_entitlements(
entitlements[i : i + self.MAX_ROW_JOB_QUEUE]
)
)
main_job = group(*jobs)
main_job.on_done(
self.delayable().mark_job_as_done(
cycle, _("Entitlements Validated and Approved.")
)
)
main_job.delay()
def _validate_entitlements(self, entitlements):
"""
Base Entitlement Manager :meth:`_validate_entitlements`
Synchronous validation of entitlements in a cycle
Override in entitlement manager
:param entitlements: A recordset of entitlements to validate
:return:
"""
# Call the program's entitlement manager and validate the entitlements
# TODO: Use a Job attached to the cycle
# TODO: Implement validation workflow
raise NotImplementedError()
[docs] def approve_entitlements(self, entitlements):
"""Base Entitlement Manager :meth:`_approve_entitlements`
Approve selected entitlements
Override in entitlement manager
:param entitlements: Selected entitlements to approve.
:return:
"""
raise NotImplementedError()
[docs] def cancel_entitlements(self, cycle):
"""Base Entitlement Manager :meth:`cancel_entitlements`
Cancel entitlements in a cycle
Override in entitlement manager
:param cycle: A recordset of cycle
:return:
"""
raise NotImplementedError()
def _cancel_entitlements_async(self, cycle, entitlements, entitlements_count):
"""Cancel Entitlements
Base Entitlement Manager :meth:`_cancel_entitlements_async`
Asynchronous cancellation of entitlements in a cycle using `job_queue`
:param cycle: A recordset of cycle
:param entitlements: A recordset of entitlements to cancel
:param entitlements_count: Integer value of total entitlements to process
:return:
"""
_logger.debug("Cancel entitlements asynchronously")
cycle.message_post(
body=_("Cancel %s entitlements started.", entitlements_count)
)
cycle.write(
{
"locked": True,
"locked_reason": _("Cancel entitlements for cycle."),
}
)
jobs = []
for i in range(0, entitlements_count, self.MAX_ROW_JOB_QUEUE):
jobs.append(
self.delayable()._cancel_entitlements(
entitlements[i : i + self.MAX_ROW_JOB_QUEUE]
)
)
main_job = group(*jobs)
main_job.on_done(
self.delayable().mark_job_as_done(cycle, _("Entitlements Cancelled."))
)
main_job.delay()
def _cancel_entitlements(self, entitlements):
"""
Base Entitlement Manager :meth:`_cancel_entitlements`
Synchronous cancellation of entitlements in a cycle
Override in entitlement manager
:param entitlements: A recordset of entitlements to cancel
:return:
"""
raise NotImplementedError()
[docs] def mark_job_as_done(self, cycle, msg):
"""
Base :meth:`mark_job_as_done`
Post a message in the chatter
:param cycle: A recordset of cycle
:param msg: A string to be posted in the chatter
:return:
"""
self.ensure_one()
cycle.locked = False
cycle.locked_reason = None
cycle.message_post(body=msg)
def check_fund_balance(self, program_id):
company_id = self.env.user.company_id and self.env.user.company_id.id or None
retval = 0.0
if company_id:
params = (
company_id,
program_id,
)
# Get the current fund balance
fund_bal = 0.0
sql = """
select sum(amount) as total_fund
from g2p_program_fund
where company_id = %s
AND program_id = %s
AND state = 'posted'
"""
self._cr.execute(sql, params)
program_funds = self._cr.dictfetchall()
fund_bal = program_funds[0]["total_fund"] or 0.0
# Get the current entitlement totals
total_entitlements = 0.0
sql = """
select sum(a.initial_amount) as total_entitlement
from g2p_entitlement a
left join g2p_cycle b on b.id = a.cycle_id
where a.company_id = %s
AND b.program_id = %s
AND a.state = 'approved'
"""
self._cr.execute(sql, params)
entitlements = self._cr.dictfetchall()
total_entitlements = entitlements[0]["total_entitlement"] or 0.0
retval = fund_bal - total_entitlements
return retval
[docs]class DefaultCashEntitlementManager(models.Model):
_name = "g2p.program.entitlement.manager.default"
_inherit = ["g2p.base.program.entitlement.manager", "g2p.manager.source.mixin"]
_description = "Default Entitlement Manager"
# Set to True so that the UI will display the payment management components
IS_CASH_ENTITLEMENT = True
amount_per_cycle = fields.Monetary(
currency_field="currency_id",
group_operator="sum",
default=0.0,
)
amount_per_individual_in_group = fields.Monetary(
currency_field="currency_id",
group_operator="sum",
default=0.0,
)
max_individual_in_group = fields.Integer(
default=0,
string="Maximum number of individual in group",
help="0 means no limit",
)
currency_id = fields.Many2one(
"res.currency", related="program_id.journal_id.currency_id", readonly=True
)
# Transfer Fees
transfer_fee_pct = fields.Float(
"Transfer Fee(%)",
digits=(5, 2),
default=0.0,
help="Transfer fee will be a percentage of amount",
)
transfer_fee_amt = fields.Monetary(
"Transfer Fee Amount",
default=0.0,
currency_field="currency_id",
help="Set fixed transfer fee amount",
)
# Group able to validate the payment
# Todo: Create a record rule for payment_validation_group
entitlement_validation_group_id = fields.Many2one(
"res.groups", string="Entitlement Validation Group"
)
[docs] @api.onchange("transfer_fee_pct")
def on_transfer_fee_pct_change(self):
if self.transfer_fee_pct > 0.0:
self.transfer_fee_amt = 0.0
[docs] @api.onchange("transfer_fee_amt")
def on_transfer_fee_amt_change(self):
if self.transfer_fee_amt > 0.0:
self.transfer_fee_pct = 0.0
[docs] def prepare_entitlements(self, cycle, beneficiaries):
"""Prepare entitlements.
This method is used to prepare the entitlement list of the beneficiaries.
:param cycle: The cycle.
:param beneficiaries: The beneficiaries.
:return:
"""
benecifiaries_ids = beneficiaries.mapped("partner_id.id")
benecifiaries_with_entitlements = (
self.env["g2p.entitlement"]
.search(
[("cycle_id", "=", cycle.id), ("partner_id", "in", benecifiaries_ids)]
)
.mapped("partner_id.id")
)
entitlements_to_create = [
benecifiaries_id
for benecifiaries_id in benecifiaries_ids
if benecifiaries_id not in benecifiaries_with_entitlements
]
entitlement_start_validity = cycle.start_date
entitlement_end_validity = cycle.end_date
entitlement_currency = self.currency_id.id
beneficiaries_with_entitlements_to_create = self.env["res.partner"].browse(
entitlements_to_create
)
individual_count = beneficiaries_with_entitlements_to_create.count_individuals()
individual_count_map = dict(individual_count)
entitlements = []
for beneficiary_id in beneficiaries_with_entitlements_to_create:
amount = self._calculate_amount(
beneficiary_id, individual_count_map.get(beneficiary_id.id, 0)
)
transfer_fee = 0.0
if self.transfer_fee_pct > 0.0:
transfer_fee = amount * (self.transfer_fee_pct / 100.0)
elif self.transfer_fee_amt > 0.0:
transfer_fee = self.transfer_fee_amt
entitlements.append(
{
"cycle_id": cycle.id,
"partner_id": beneficiary_id.id,
"initial_amount": amount,
"transfer_fee": transfer_fee,
"currency_id": entitlement_currency,
"state": "draft",
"is_cash_entitlement": True,
"valid_from": entitlement_start_validity,
"valid_until": entitlement_end_validity,
}
)
if entitlements:
self.env["g2p.entitlement"].create(entitlements)
[docs] def set_pending_validation_entitlements(self, cycle):
"""Set entitlements to pending validation.
Default Entitlement Manager :meth:`set_pending_validation_entitlements`
Set entitlements to pending_validation in a cycle
:param cycle: A recordset of cycle
:return:
"""
# Get the number of entitlements in cycle
entitlements = cycle.get_entitlements(
["draft"],
entitlement_model="g2p.entitlement",
)
entitlements_count = len(entitlements)
if entitlements_count < self.MIN_ROW_JOB_QUEUE:
self._set_pending_validation_entitlements(entitlements)
else:
self._set_pending_validation_entitlements_async(cycle, entitlements)
def _set_pending_validation_entitlements(self, entitlements):
"""Set entitlements to pending validation.
Default Entitlement Manager :meth:`_set_pending_validation_entitlements`
Synchronous setting of entitlements to pending_validation in a cycle
:param entitlements: A recordset of entitlements
:return:
"""
entitlements.update({"state": "pending_validation"})
[docs] def validate_entitlements(self, cycle):
"""Validate entitlements.
Default Entitlement Manager :meth:`validate_entitlements`
Validate entitlements in a cycle
:param cycle: A recordset of cycle
:return:
"""
# Get the number of entitlements in cycle
entitlements = cycle.get_entitlements(
["draft", "pending_validation"],
entitlement_model="g2p.entitlement",
)
entitlements_count = len(entitlements)
if entitlements_count < self.MIN_ROW_JOB_QUEUE:
err, message = self._validate_entitlements(entitlements)
if err > 0:
kind = "danger"
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Entitlement"),
"message": message,
"sticky": True,
"type": kind,
"next": {
"type": "ir.actions.act_window_close",
},
},
}
else:
kind = "success"
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Entitlement"),
"message": _("Entitlements are validated and approved."),
"sticky": True,
"type": kind,
"next": {
"type": "ir.actions.act_window_close",
},
},
}
else:
self._validate_entitlements_async(cycle, entitlements, entitlements_count)
def _validate_entitlements(self, entitlements):
"""Validate entitlements.
Default Entitlement Manager :meth:`_validate_entitlements`
Synchronous validation of entitlements in a cycle
:param entitlements: A recordset of entitlements to validate
:return err: Integer number of errors
:return message: String description of the error
"""
err, message = self.approve_entitlements(entitlements)
return err, message
[docs] def cancel_entitlements(self, cycle):
"""
Default Entitlement Manager :meth:`cancel_entitlements`
Cancel entitlements in a cycle
:param cycle: A recordset of cycle
:return:
"""
# Get the number of entitlements in cycle
entitlements = cycle.get_entitlements(
["draft", "pending_validation", "approved"],
entitlement_model="g2p.entitlement",
)
entitlements_count = len(entitlements)
if entitlements_count < self.MIN_ROW_JOB_QUEUE:
self._cancel_entitlements(entitlements)
else:
self._cancel_entitlements_async(cycle, entitlements, entitlements_count)
def _cancel_entitlements(self, entitlements):
"""
Default Entitlement Manager :meth:`_cancel_entitlements`
Synchronous cancellation of entitlements in a cycle
:param entitlements: A recordset of entitlements to cancel
:return:
"""
entitlements.update({"state": "cancelled"})
def _calculate_amount(self, beneficiary, num_individuals):
total = self.amount_per_cycle
if beneficiary.is_group:
if num_individuals:
if self.max_individual_in_group:
num_individuals = min(num_individuals, self.max_individual_in_group)
total += self.amount_per_individual_in_group * float(num_individuals)
return total
[docs] def approve_entitlements(self, entitlements):
"""Approve entitlements.
Default Entitlement Manager :meth:`approve_entitlements`
Approve selected entitlements
:param entitlements: Selected entitlements to approve
:return state_err: Integer number of errors
:return message: String description of the errors
"""
amt = 0.0
state_err = 0
message = ""
sw = 0
for rec in entitlements:
if rec.state in ("draft", "pending_validation"):
fund_balance = self.check_fund_balance(rec.cycle_id.program_id.id) - amt
if fund_balance >= rec.initial_amount:
amt += rec.initial_amount
# Prepare journal entry (account.move) via account.payment
amount = rec.initial_amount
new_service_fee = None
if rec.transfer_fee > 0.0:
amount -= rec.transfer_fee
# Incurred Fees (transfer fees)
payment = {
"partner_id": rec.partner_id.id,
"payment_type": "outbound",
"amount": rec.transfer_fee,
"currency_id": rec.journal_id.currency_id.id,
"journal_id": rec.journal_id.id,
"partner_type": "supplier",
"ref": "Service Fee: Code: %s" % rec.code,
}
new_service_fee = self.env["account.payment"].create(payment)
# Fund Disbursed (amount - transfer fees)
payment = {
"partner_id": rec.partner_id.id,
"payment_type": "outbound",
"amount": amount,
"currency_id": rec.journal_id.currency_id.id,
"journal_id": rec.journal_id.id,
"partner_type": "supplier",
"ref": "Fund disbursed to beneficiary: Code: %s" % rec.code,
}
new_payment = self.env["account.payment"].create(payment)
rec.update(
{
"disbursement_id": new_payment.id,
"service_fee_disbursement_id": new_service_fee
and new_service_fee.id
or None,
"state": "approved",
"date_approved": fields.Date.today(),
}
)
else:
message = _(
"The fund for the program: %(program)s [%(fund).2f] "
+ "is insufficient for the entitlement: %(entitlement)s"
) % {
"program": rec.cycle_id.program_id.name,
"fund": fund_balance,
"entitlement": rec.code,
}
# Stop the process and return an error
return (1, message)
else:
state_err += 1
if sw == 0:
sw = 1
message = _(
"Entitlement State Error! Entitlements not in 'pending validation' state:\n"
)
message += _("Program: %(prg)s, Beneficiary: %(partner)s.\n") % {
"prg": rec.cycle_id.program_id.name,
"partner": rec.partner_id.name,
}
return (state_err, message)