Source code for odoo.addons.g2p_programs.models.managers.cycle_manager

# Part of OpenG2P. See LICENSE file for full copyright and licensing details.
import logging
from datetime import datetime, timedelta

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError

from odoo.addons.queue_job.delay import group

from .. import constants

_logger = logging.getLogger(__name__)


class CycleManager(models.Model):
    _name = "g2p.cycle.manager"
    _description = "Cycle 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.cycle.manager.default", "Default")
        if new_manager not in selection:
            selection.append(new_manager)
        return selection


[docs]class BaseCycleManager(models.AbstractModel): _name = "g2p.base.cycle.manager" _description = "Base Cycle Manager" 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) auto_approve_entitlements = fields.Boolean( string="Auto-approve Entitlements", default=False )
[docs] def check_eligibility(self, cycle, beneficiaries=None): """ Validate the eligibility of each beneficiary for the cycle """ raise NotImplementedError()
[docs] def prepare_entitlements(self, cycle): """ Prepare the entitlements for the cycle """ raise NotImplementedError()
[docs] def issue_payments(self, cycle): """ Issue the payments based on entitlements for the cycle """ raise NotImplementedError()
[docs] def validate_entitlements(self, cycle, cycle_memberships): """ Validate the entitlements for the cycle """ raise NotImplementedError()
[docs] def new_cycle(self, name, new_start_date, sequence): """ Create a new cycle for the program """ raise NotImplementedError()
[docs] def mark_distributed(self, cycle): """ Mark the cycle as distributed """ raise NotImplementedError()
[docs] def mark_ended(self, cycle): """ Mark the cycle as ended """ raise NotImplementedError()
[docs] def mark_cancelled(self, cycle): """ Mark the cycle as cancelled """ raise NotImplementedError()
[docs] def add_beneficiaries(self, cycle, beneficiaries, state="draft"): """ Add beneficiaries to the cycle """ raise NotImplementedError()
[docs] def on_start_date_change(self, cycle): """ Hook for when the start date change """ raise NotImplementedError()
[docs] def approve_cycle(self, cycle, auto_approve=False, entitlement_manager=None): """ :param cycle: :param auto_approve: :param entitlement_manager: :return: """ # Check if user is allowed to approve the cycle if cycle.state == "to_approve": cycle.update({"state": "approved"}) # Running on_state_change because it is not triggered automatically with rec.update above self.on_state_change(cycle) else: message = _("Only 'to approve' cycles can be approved.") kind = "danger" return { "type": "ir.actions.client", "tag": "display_notification", "params": { "title": _("Cycle"), "message": message, "sticky": True, "type": kind, "next": { "type": "ir.actions.act_window_close", }, }, } # Approve entitlements if auto_approve: if entitlement_manager.IS_CASH_ENTITLEMENT: entitlement_mdl = "g2p.entitlement" else: entitlement_mdl = "g2p.entitlement.inkind" entitlements = cycle.get_entitlements( ["draft", "pending_validation"], entitlement_model=entitlement_mdl ) if entitlements: return entitlement_manager.validate_entitlements(cycle) else: message = _( "Auto-approve entitlements is set but there are no entitlements to process." ) kind = "warning" return { "type": "ir.actions.client", "tag": "display_notification", "params": { "title": _("Entitlements"), "message": message, "sticky": True, "type": kind, "next": { "type": "ir.actions.act_window_close", }, }, }
[docs] def on_state_change(self, cycle): """ :param cycle: :return: """ if cycle.state == cycle.STATE_APPROVED: if not self.approver_group_id: raise ValidationError(_("The cycle approver group is not specified!")) else: if ( self.env.user.id != 1 and self.env.user.id not in self.approver_group_id.users.ids ): raise ValidationError( _("You are not allowed to approve this cycle!") )
def _ensure_can_edit_cycle(self, cycle): """Base :meth:'_ensure_can_edit_cycle` Check if the cycle can be editted :param cycle: A recordset of cycle :return: """ if cycle.state not in [cycle.STATE_DRAFT]: raise ValidationError(_("The Cycle is not in draft mode"))
[docs] def mark_import_as_done(self, cycle, msg): """Complete the import of beneficiaries. Base :meth:`mark_import_as_done`. This is executed when all the jobs are completed. 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) # Update Statistics cycle._compute_members_count()
[docs] def mark_prepare_entitlement_as_done(self, cycle, msg): """Complete the preparation of entitlements. Base :meth:`mark_prepare_entitlement_as_done`. This is executed when all the jobs are completed. 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) # Update Statistics cycle._compute_entitlements_count()
[docs]class DefaultCycleManager(models.Model): _name = "g2p.cycle.manager.default" _inherit = [ "g2p.base.cycle.manager", "g2p.cycle.recurrence.mixin", "g2p.manager.source.mixin", ] _description = "Default Cycle Manager" cycle_duration = fields.Integer(default=1, required=True, string="Recurrence") approver_group_id = fields.Many2one( comodel_name="res.groups", string="Approver Group", copy=True, )
[docs] def check_eligibility(self, cycle, beneficiaries=None): """ Default Cycle Manager eligibility checker :param cycle: The cycle that is being verified :type cycle: :class:`g2p_programs.models.cycle.G2PCycle` :param beneficiaries: the beneficiaries that need to be verified. By Default the one with the state ``draft`` or ``enrolled`` are verified. :type beneficiaries: list or None :return: The list of eligible beneficiaries :rtype: list Validate the eligibility of each beneficiary for the cycle using the configured manager(s) :class:`g2p_programs.models.managers.eligibility_manager.BaseEligibilityManager`. If there is multiple managers for eligibility, each of them are run using the filtered list of eligible beneficiaries from the previous one. The ``state`` of beneficiaries is updated to either ``enrolled`` if they match the enrollment criteria or ``not_eligible`` in case they do not match them. """ for rec in self: rec._ensure_can_edit_cycle(cycle) # Get all the draft and enrolled beneficiaries if beneficiaries is None: beneficiaries = cycle.get_beneficiaries(["draft", "enrolled"]) eligibility_managers = rec.program_id.get_managers( constants.MANAGER_ELIGIBILITY ) filtered_beneficiaries = beneficiaries for manager in eligibility_managers: filtered_beneficiaries = manager.verify_cycle_eligibility( cycle, filtered_beneficiaries ) filtered_beneficiaries.write({"state": "enrolled"}) beneficiaries_ids = beneficiaries.ids filtered_beneficiaries_ids = filtered_beneficiaries.ids ids_to_remove = list( set(beneficiaries_ids) - set(filtered_beneficiaries_ids) ) # Mark the beneficiaries as not eligible memberships_to_remove = self.env["g2p.cycle.membership"].browse( ids_to_remove ) memberships_to_remove.write({"state": "not_eligible"}) # Update the members_count field cycle._compute_members_count() # TODO: Move this to the entitlement manager # Disable the entitlements of the beneficiaries entitlements = self.env["g2p.entitlement"].search( [ ("cycle_id", "=", cycle.id), ("partner_id", "in", memberships_to_remove.mapped("partner_id.id")), ] ) entitlements.write({"state": "cancelled"}) return filtered_beneficiaries
[docs] def prepare_entitlements(self, cycle): for rec in self: rec._ensure_can_edit_cycle(cycle) # Get all the enrolled beneficiaries beneficiaries_count = cycle.get_beneficiaries(["enrolled"], count=True) rec.program_id.get_manager(constants.MANAGER_ENTITLEMENT) if beneficiaries_count < self.MIN_ROW_JOB_QUEUE: self._prepare_entitlements(cycle, do_count=True) else: self._prepare_entitlements_async(cycle, beneficiaries_count)
def _prepare_entitlements_async(self, cycle, beneficiaries_count): _logger.debug("Prepare entitlement asynchronously") cycle.message_post( body=_( "Prepare entitlement for %s beneficiaries started.", beneficiaries_count ) ) cycle.write( { "locked": True, "locked_reason": _("Prepare entitlement for beneficiaries."), } ) jobs = [] for i in range(0, beneficiaries_count, self.MAX_ROW_JOB_QUEUE): jobs.append( self.delayable()._prepare_entitlements(cycle, i, self.MAX_ROW_JOB_QUEUE) ) main_job = group(*jobs) main_job.on_done( self.delayable().mark_prepare_entitlement_as_done( cycle, _("Entitlement Ready.") ) ) main_job.delay() def _prepare_entitlements(self, cycle, offset=0, limit=None, do_count=False): """Prepare Entitlements Get the beneficiaries and generate their entitlements. :param cycle: The cycle :param offset: Optional integer value for the ORM search offset :param limit: Optional integer value for the ORM search limit :param do_count: Boolean - set to False to not run compute function :return: """ beneficiaries = cycle.get_beneficiaries( ["enrolled"], offset=offset, limit=limit, order="id" ) entitlement_manager = self.program_id.get_manager(constants.MANAGER_ENTITLEMENT) entitlement_manager.prepare_entitlements(cycle, beneficiaries) if do_count: # Update Statistics cycle._compute_entitlements_count()
[docs] def mark_distributed(self, cycle): cycle.update({"state": constants.STATE_DISTRIBUTED})
[docs] def mark_ended(self, cycle): cycle.update({"state": constants.STATE_ENDED})
[docs] def mark_cancelled(self, cycle): cycle.update({"state": constants.STATE_CANCELLED})
[docs] def new_cycle(self, name, new_start_date, sequence): _logger.debug("Creating new cycle for program %s", self.program_id.name) _logger.debug("New start date: %s", new_start_date) # convert date to datetime new_start_date = datetime.combine(new_start_date, datetime.min.time()) # get start date and end date # Note: # second argument is irrelevant but make sure it is in timedelta class # and do not exceed to 24 hours occurences = self._get_ranges(new_start_date, timedelta(seconds=1)) prev_occurence = next(occurences) current_occurence = next(occurences) start_date = None end_date = None # This prevents getting an end date that is less than the start date while True: # get the date of occurences start_date = prev_occurence[0] end_date = current_occurence[0] - timedelta(days=1) # To handle DST (Daylight Saving Time) changes start_date = start_date + timedelta(hours=11) end_date = end_date + timedelta(hours=11) if start_date >= new_start_date: break # move current occurence to previous then get a new current occurence prev_occurence = current_occurence current_occurence = next(occurences) for rec in self: cycle = self.env["g2p.cycle"].create( { "program_id": rec.program_id.id, "name": name, "state": "draft", "sequence": sequence, "start_date": start_date, "end_date": end_date, "auto_approve_entitlements": rec.auto_approve_entitlements, } ) _logger.debug("New cycle created: %s", cycle.name) return cycle
[docs] def copy_beneficiaries_from_program(self, cycle, state="enrolled"): self._ensure_can_edit_cycle(cycle) self.ensure_one() for rec in self: beneficiary_ids = rec.program_id.get_beneficiaries(["enrolled"]).mapped( "partner_id.id" ) return rec.add_beneficiaries(cycle, beneficiary_ids, state)
[docs] def add_beneficiaries(self, cycle, beneficiaries, state="draft"): """ Add beneficiaries to the cycle """ self.ensure_one() self._ensure_can_edit_cycle(cycle) _logger.debug("Adding beneficiaries to the cycle %s", cycle.name) _logger.debug("Beneficiaries: %s", beneficiaries) # Only add beneficiaries not added yet existing_ids = cycle.cycle_membership_ids.mapped("partner_id.id") beneficiaries = list(set(beneficiaries) - set(existing_ids)) if len(beneficiaries) == 0: message = _("No beneficiaries to import.") kind = "warning" elif len(beneficiaries) < self.MIN_ROW_JOB_QUEUE: self._add_beneficiaries(cycle, beneficiaries, state, do_count=True) message = _("%s beneficiaries imported.", len(beneficiaries)) kind = "success" else: self._add_beneficiaries_async(cycle, beneficiaries, state) message = _("Import of %s beneficiaries started.", len(beneficiaries)) kind = "warning" return { "type": "ir.actions.client", "tag": "display_notification", "params": { "title": _("Enrollment"), "message": message, "sticky": True, "type": kind, "next": { "type": "ir.actions.act_window_close", }, }, }
def _add_beneficiaries_async(self, cycle, beneficiaries, state): _logger.debug("Adding beneficiaries asynchronously") cycle.message_post( body="Import of %s beneficiaries started." % len(beneficiaries) ) cycle.write({"locked": True, "locked_reason": _("Importing beneficiaries.")}) beneficiaries_count = len(beneficiaries) jobs = [] for i in range(0, beneficiaries_count, self.MAX_ROW_JOB_QUEUE): jobs.append( self.delayable()._add_beneficiaries( cycle, beneficiaries[i : i + self.MAX_ROW_JOB_QUEUE], state, ) ) main_job = group(*jobs) main_job.on_done( self.delayable().mark_import_as_done( cycle, _("Beneficiary import finished.") ) ) main_job.delay() def _add_beneficiaries(self, cycle, beneficiaries, state="draft", do_count=False): """Add Beneficiaries :param cycle: Recordset of cycle :param beneficiaries: Recordset of beneficiaries :param state: String state to be set to beneficiary :param do_count: Boolean - set to False to not run compute functions :return: Integer - count of not enrolled members """ new_beneficiaries = [] for r in beneficiaries: new_beneficiaries.append( [ 0, 0, { "partner_id": r, "enrollment_date": fields.Date.today(), "state": state, }, ] ) cycle.update({"cycle_membership_ids": new_beneficiaries}) if do_count: # Update Statistics cycle._compute_members_count() @api.depends("cycle_duration") def _compute_interval(self): for rec in self: rec.interval = rec.cycle_duration