Ejemplo n.º 1
0
class SpecialAudit(Engagement):
    objects = models.Manager()

    class Meta:
        ordering = ('id', )
        verbose_name = _('Special Audit')
        verbose_name_plural = _('Special Audits')

    def save(self, *args, **kwargs):
        self.engagement_type = Engagement.TYPES.sa
        return super(SpecialAudit, self).save(*args, **kwargs)

    @transition('status',
                source=Engagement.STATUSES.partner_contacted,
                target=Engagement.STATUSES.report_submitted,
                conditions=[
                    EngagementSubmitReportRequiredFieldsCheck.as_condition(),
                    SpecialAuditSubmitRelatedModelsCheck.as_condition(),
                    EngagementHasReportAttachmentsCheck.as_condition(),
                ],
                permission=has_action_permission(action='submit'))
    def submit(self, *args, **kwargs):
        return super(SpecialAudit, self).submit(*args, **kwargs)

    @transition('status',
                source=Engagement.STATUSES.report_submitted,
                target=Engagement.STATUSES.final,
                permission=has_action_permission(action='finalize'))
    def finalize(self, *args, **kwargs):
        self.partner.audits_completed(update_one=True)
        return super(SpecialAudit, self).finalize(*args, **kwargs)

    def get_object_url(self, **kwargs):
        return build_frontend_url('ap', 'special-audits', self.id, 'overview',
                                  **kwargs)
Ejemplo n.º 2
0
class SpotCheck(Engagement):
    total_amount_tested = models.DecimalField(
        verbose_name=_('Total Amount Tested'),
        null=True,
        blank=True,
        decimal_places=2,
        max_digits=20)
    total_amount_of_ineligible_expenditure = models.DecimalField(
        verbose_name=_('Total Amount of Ineligible Expenditure'),
        null=True,
        blank=True,
        decimal_places=2,
        max_digits=20,
    )

    internal_controls = models.TextField(verbose_name=_('Internal Controls'),
                                         blank=True)

    objects = models.Manager()

    class Meta:
        ordering = ('id', )
        verbose_name = _('Spot Check')
        verbose_name_plural = _('Spot Checks')

    @property
    def pending_unsupported_amount(self):
        try:
            return self.total_amount_of_ineligible_expenditure - self.additional_supporting_documentation_provided \
                - self.justification_provided_and_accepted - self.write_off_required
        except TypeError:
            return None

    def save(self, *args, **kwargs):
        self.engagement_type = Engagement.TYPES.sc
        return super(SpotCheck, self).save(*args, **kwargs)

    @transition('status',
                source=Engagement.STATUSES.partner_contacted,
                target=Engagement.STATUSES.report_submitted,
                conditions=[
                    SPSubmitReportRequiredFieldsCheck.as_condition(),
                    EngagementHasReportAttachmentsCheck.as_condition(),
                ],
                permission=has_action_permission(action='submit'))
    def submit(self, *args, **kwargs):
        return super(SpotCheck, self).submit(*args, **kwargs)

    @transition('status',
                source=Engagement.STATUSES.report_submitted,
                target=Engagement.STATUSES.final,
                permission=has_action_permission(action='finalize'))
    def finalize(self, *args, **kwargs):
        self.partner.spot_checks(
            update_one=True, event_date=self.date_of_draft_report_to_unicef)
        return super(SpotCheck, self).finalize(*args, **kwargs)

    def get_object_url(self, **kwargs):
        return build_frontend_url('ap', 'spot-checks', self.id, 'overview',
                                  **kwargs)
Ejemplo n.º 3
0
class EngagementActionPoint(ActionPoint):
    """
    This proxy class is for more easy permissions assigning.
    """
    objects = EngagementActionPointManager()

    class Meta(ActionPoint.Meta):
        verbose_name = _('Engagement Action Point')
        verbose_name_plural = _('Engagement Action Points')
        proxy = True

    @transition('status',
                source=ActionPoint.STATUSES.open,
                target=ActionPoint.STATUSES.completed,
                permission=has_action_permission(action='complete'),
                conditions=[])
    def complete(self):
        self._do_complete()

    def get_mail_context(self, user=None, include_token=False):
        context = super(EngagementActionPoint,
                        self).get_mail_context(user=user,
                                               include_token=include_token)
        if self.engagement:
            context['engagement'] = self.engagement_subclass.get_mail_context(
                user=user, include_token=include_token)
        return context
Ejemplo n.º 4
0
class MicroAssessment(Engagement):
    objects = models.Manager()

    class Meta:
        ordering = ('id', )
        verbose_name = _('Micro Assessment')
        verbose_name_plural = _('Micro Assessments')

    def save(self, *args, **kwargs):
        self.engagement_type = Engagement.TYPES.ma
        return super(MicroAssessment, self).save(*args, **kwargs)

    @transition('status',
                source=Engagement.STATUSES.partner_contacted,
                target=Engagement.STATUSES.report_submitted,
                conditions=[
                    EngagementSubmitReportRequiredFieldsCheck.as_condition(),
                    ValidateMARiskCategories.as_condition(),
                    ValidateMARiskExtra.as_condition(),
                    EngagementHasReportAttachmentsCheck.as_condition(),
                ],
                permission=has_action_permission(action='submit'))
    def submit(self, *args, **kwargs):
        return super(MicroAssessment, self).submit(*args, **kwargs)

    def __str__(self):
        return 'MicroAssessment ({}: {}, {})'.format(
            self.engagement_type, self.agreement.order_number,
            self.partner.name)

    def get_object_url(self, **kwargs):
        return build_frontend_url('ap', 'micro-assessments', self.id,
                                  'overview', **kwargs)
Ejemplo n.º 5
0
class TPMVisit(SoftDeleteMixin, TimeStampedModel, models.Model):

    DRAFT = 'draft'
    ASSIGNED = 'assigned'
    CANCELLED = 'cancelled'
    ACCEPTED = 'tpm_accepted'
    REJECTED = 'tpm_rejected'
    REPORTED = 'tpm_reported'
    REPORT_REJECTED = 'tpm_report_rejected'
    UNICEF_APPROVED = 'unicef_approved'

    STATUSES = Choices(
        (DRAFT, _('Draft')),
        (ASSIGNED, _('Assigned')),
        (CANCELLED, _('Cancelled')),
        (ACCEPTED, _('TPM Accepted')),
        (REJECTED, _('TPM Rejected')),
        (REPORTED, _('TPM Reported')),
        (REPORT_REJECTED, _('Sent Back to TPM')),
        (UNICEF_APPROVED, _('UNICEF Approved')),
    )

    STATUSES_DATES = {
        STATUSES.draft: 'date_created',
        STATUSES.assigned: 'date_of_assigned',
        STATUSES.cancelled: 'date_of_cancelled',
        STATUSES.tpm_accepted: 'date_of_tpm_accepted',
        STATUSES.tpm_rejected: 'date_of_tpm_rejected',
        STATUSES.tpm_reported: 'date_of_tpm_reported',
        STATUSES.tpm_report_rejected: 'date_of_tpm_report_rejected',
        STATUSES.unicef_approved: 'date_of_unicef_approved',
    }

    author = models.ForeignKey(settings.AUTH_USER_MODEL,
                               on_delete=models.SET_NULL,
                               blank=True,
                               null=True)

    tpm_partner = models.ForeignKey(
        TPMPartner,
        verbose_name=_('TPM Vendor'),
        null=True,
        on_delete=models.CASCADE,
    )

    status = FSMField(verbose_name=_('Status'),
                      max_length=20,
                      choices=STATUSES,
                      default=STATUSES.draft,
                      protected=True)

    # UNICEF cancelled visit
    cancel_comment = models.TextField(verbose_name=_('Cancel Comment'),
                                      blank=True)
    # TPM rejected visit
    reject_comment = models.TextField(verbose_name=_('Reason for Rejection'),
                                      blank=True)
    approval_comment = models.TextField(verbose_name=_('Approval Comments'),
                                        blank=True)

    report_attachments = GenericRelation(Attachment,
                                         verbose_name=_('Visit Report'),
                                         blank=True)

    visit_information = models.TextField(verbose_name=_('Visit Information'),
                                         blank=True)

    date_of_assigned = models.DateField(blank=True,
                                        null=True,
                                        verbose_name=_('Date of Assigned'))
    date_of_cancelled = models.DateField(blank=True,
                                         null=True,
                                         verbose_name=_('Date of Cancelled'))
    date_of_tpm_accepted = models.DateField(
        blank=True, null=True, verbose_name=_('Date of TPM Accepted'))
    date_of_tpm_rejected = models.DateField(
        blank=True, null=True, verbose_name=_('Date of TPM Rejected'))
    date_of_tpm_reported = models.DateField(
        blank=True, null=True, verbose_name=_('Date of TPM Reported'))
    date_of_tpm_report_rejected = models.DateField(
        blank=True, null=True, verbose_name=_('Date of Sent Back to TPM'))
    date_of_unicef_approved = models.DateField(
        blank=True, null=True, verbose_name=_('Date of UNICEF Approved'))

    tpm_partner_focal_points = models.ManyToManyField(
        TPMPartnerStaffMember,
        verbose_name=_('TPM Focal Points'),
        related_name='tpm_visits',
        blank=True)

    tpm_partner_tracker = FieldTracker(fields=[
        'tpm_partner',
    ])

    class Meta:
        ordering = ('id', )
        verbose_name = _('TPM Visit')
        verbose_name_plural = _('TPM Visits')

    @property
    def date_created(self):
        return self.created.date()

    @property
    def status_date(self):
        return getattr(self, self.STATUSES_DATES[self.status])

    @property
    def reference_number(self):
        return '{}/{}/{}/TPM'.format(
            connection.tenant.country_short_code or '',
            self.created.year,
            self.id,
        )

    @property
    def start_date(self):
        # TODO: Rewrite to reduce number of SQL queries.
        return self.tpm_activities.aggregate(models.Min('date'))['date__min']

    @property
    def end_date(self):
        # TODO: Rewrite to reduce number of SQL queries.
        return self.tpm_activities.aggregate(models.Max('date'))['date__max']

    @property
    def unicef_focal_points(self):
        return set(
            itertools.chain(*map(lambda a: a.unicef_focal_points.all(),
                                 self.tpm_activities.all())))

    @property
    def unicef_focal_points_with_emails(self):
        return list(
            filter(lambda u: u.email and u.is_active,
                   self.unicef_focal_points))

    @property
    def unicef_focal_points_and_pme(self):
        users = self.unicef_focal_points_with_emails
        if self.author and self.author.is_active and self.author.email:
            users += [self.author]

        return users

    def __str__(self):
        return 'Visit ({} to {} at {} - {})'.format(
            self.tpm_partner, ', '.join(
                filter(
                    lambda x: x,
                    self.tpm_activities.values_list('partner__name',
                                                    flat=True))),
            self.start_date, self.end_date)

    def get_mail_context(self,
                         user=None,
                         include_token=False,
                         include_activities=True):
        object_url = self.get_object_url(user=user,
                                         include_token=include_token)

        activities = self.tpm_activities.all()
        interventions = set(a.intervention.title for a in activities
                            if a.intervention)
        partner_names = set(a.partner.name for a in activities)
        context = {
            'reference_number': self.reference_number,
            'tpm_partner': self.tpm_partner.name if self.tpm_partner else '-',
            'multiple_tpm_activities': activities.count() > 1,
            'object_url': object_url,
            'partners': ', '.join(partner_names),
            'interventions': ', '.join(interventions),
        }

        if include_activities:
            context['tpm_activities'] = [
                a.get_mail_context(user=user, include_visit=False)
                for a in activities
            ]

        return context

    def _send_email(self,
                    recipients,
                    template_name,
                    context=None,
                    user=None,
                    include_token=False,
                    **kwargs):
        context = context or {}

        base_context = {
            'visit': self.get_mail_context(user=user,
                                           include_token=include_token),
            'environment': get_environment(),
        }
        base_context.update(context)
        context = base_context

        if isinstance(recipients, str):
            recipients = [
                recipients,
            ]
        else:
            recipients = list(recipients)

        # assert recipients
        if recipients:
            send_notification_with_template(
                recipients=recipients,
                template_name=template_name,
                context=context,
            )

    def _get_unicef_focal_points_as_email_recipients(self):
        return list(
            map(lambda u: u.email, self.unicef_focal_points_with_emails))

    def _get_unicef_focal_points_and_pme_as_email_recipients(self):
        return list(map(lambda u: u.email, self.unicef_focal_points_and_pme))

    def _get_tpm_focal_points_as_email_recipients(self):
        return list(
            self.tpm_partner_focal_points.filter(
                user__email__isnull=False,
                user__is_active=True).values_list('user__email', flat=True))

    @transition(
        status,
        source=[STATUSES.draft, STATUSES.tpm_rejected],
        target=STATUSES.assigned,
        conditions=[
            TPMVisitAssignRequiredFieldsCheck.as_condition(),
            ValidateTPMVisitActivities.as_condition(),
        ],
        permission=has_action_permission(action='assign'),
        custom={
            'name':
            lambda obj: _('Re-assign')
            if obj.status == TPMVisit.STATUSES.tpm_rejected else _('Assign')
        })
    def assign(self):
        self.date_of_assigned = timezone.now()

        if self.tpm_partner.email:
            self._send_email(
                self.tpm_partner.email,
                'tpm/visit/assign',
                cc=self._get_unicef_focal_points_and_pme_as_email_recipients())

        for staff_member in self.tpm_partner_focal_points.filter(
                user__email__isnull=False, user__is_active=True):
            self._send_email(
                staff_member.user.email,
                'tpm/visit/assign_staff_member',
                context={'recipient': staff_member.user.get_full_name()},
                user=staff_member.user,
                include_token=True)

    @transition(status,
                source=[
                    STATUSES.draft,
                    STATUSES.assigned,
                    STATUSES.tpm_accepted,
                    STATUSES.tpm_rejected,
                    STATUSES.tpm_reported,
                    STATUSES.tpm_report_rejected,
                ],
                target=STATUSES.cancelled,
                permission=has_action_permission(action='cancel'),
                custom={
                    'serializer': TPMVisitCancelSerializer,
                    'name': _('Cancel Visit')
                })
    def cancel(self, cancel_comment):
        self.cancel_comment = cancel_comment
        self.date_of_cancelled = timezone.now()

    @transition(status,
                source=[STATUSES.assigned],
                target=STATUSES.tpm_rejected,
                permission=has_action_permission(action='reject'),
                custom={'serializer': TPMVisitRejectSerializer})
    def reject(self, reject_comment):
        self.date_of_tpm_rejected = timezone.now()
        self.reject_comment = reject_comment

        for recipient in self.unicef_focal_points_and_pme:
            self._send_email(
                recipient.email,
                'tpm/visit/reject',
                cc=self._get_tpm_focal_points_as_email_recipients(),
                context={'recipient': recipient.get_full_name()},
                user=recipient,
            )

    @transition(status,
                source=[STATUSES.assigned],
                target=STATUSES.tpm_accepted,
                permission=has_action_permission(action='accept'))
    def accept(self):
        self.date_of_tpm_accepted = timezone.now()

    @transition(status,
                source=[STATUSES.tpm_accepted, STATUSES.tpm_report_rejected],
                target=STATUSES.tpm_reported,
                conditions=[
                    TPMVisitReportValidations.as_condition(),
                ],
                permission=has_action_permission(action='send_report'),
                custom={'name': _('Submit Report')})
    def send_report(self):
        self.date_of_tpm_reported = timezone.now()

        for recipient in self.unicef_focal_points_and_pme:
            self._send_email(
                recipient.email,
                'tpm/visit/report',
                cc=self._get_tpm_focal_points_as_email_recipients(),
                context={'recipient': recipient.get_full_name()},
                user=recipient,
            )

    @transition(status,
                source=[STATUSES.tpm_reported],
                target=STATUSES.tpm_report_rejected,
                permission=has_action_permission(action='reject_report'),
                custom={
                    'serializer': TPMVisitRejectSerializer,
                    'name': _('Send back to TPM')
                })
    def reject_report(self, reject_comment):
        self.date_of_tpm_report_rejected = timezone.now()
        TPMVisitReportRejectComment.objects.create(
            reject_reason=reject_comment, tpm_visit=self)

        for staff_user in self.tpm_partner_focal_points.filter(
                user__email__isnull=False, user__is_active=True):
            self._send_email(
                [staff_user.user.email],
                'tpm/visit/report_rejected',
                context={'recipient': staff_user.user.get_full_name()},
                user=staff_user.user)

    @transition(status,
                source=[STATUSES.tpm_reported],
                target=STATUSES.unicef_approved,
                custom={'serializer': TPMVisitApproveSerializer},
                permission=has_action_permission(action='approve'))
    def approve(self,
                mark_as_programmatic_visit=None,
                approval_comment=None,
                notify_focal_point=True,
                notify_tpm_partner=True):
        mark_as_programmatic_visit = mark_as_programmatic_visit or []

        self.tpm_activities.filter(id__in=mark_as_programmatic_visit).update(
            is_pv=True)

        self.date_of_unicef_approved = timezone.now()
        if notify_focal_point:
            for recipient in self.unicef_focal_points_with_emails:
                self._send_email(
                    recipient.email,
                    'tpm/visit/approve_report',
                    context={'recipient': recipient.get_full_name()},
                    user=recipient)

        if notify_tpm_partner:
            # TODO: Generate report as PDF attachment.
            for staff_user in self.tpm_partner_focal_points.filter(
                    user__email__isnull=False, user__is_active=True):
                self._send_email(
                    [
                        staff_user.user.email,
                    ],
                    'tpm/visit/approve_report_tpm',
                    context={'recipient': staff_user.user.get_full_name()},
                    user=staff_user.user)

        if approval_comment:
            self.approval_comment = approval_comment

    def get_object_url(self, **kwargs):
        return build_frontend_url('tpm', 'visits', self.id, 'details',
                                  **kwargs)
Ejemplo n.º 6
0
class Audit(Engagement):

    OPTION_UNQUALIFIED = "unqualified"
    OPTION_QUALIFIED = "qualified"
    OPTION_DENIAL = "disclaimer_opinion"
    OPTION_ADVERSE = "adverse_opinion"

    OPTIONS = Choices(
        (OPTION_UNQUALIFIED, _("Unqualified")),
        (OPTION_QUALIFIED, _("Qualified")),
        (OPTION_DENIAL, _("Disclaimer opinion")),
        (OPTION_ADVERSE, _("Adverse opinion")),
    )

    audited_expenditure = models.DecimalField(
        verbose_name=_('Audited Expenditure $'),
        null=True,
        blank=True,
        decimal_places=2,
        max_digits=20)
    financial_findings = models.DecimalField(
        verbose_name=_('Financial Findings $'),
        null=True,
        blank=True,
        decimal_places=2,
        max_digits=20)
    audit_opinion = models.CharField(
        verbose_name=_('Audit Opinion'),
        max_length=20,
        choices=OPTIONS,
        default='',
        blank=True,
    )

    objects = models.Manager()

    class Meta:
        ordering = ('id', )
        verbose_name = _('Audit')
        verbose_name_plural = _('Audits')

    def save(self, *args, **kwargs):
        self.engagement_type = Engagement.TYPES.audit
        return super(Audit, self).save(*args, **kwargs)

    @property
    def pending_unsupported_amount(self):
        try:
            return self.financial_findings - self.amount_refunded \
                - self.additional_supporting_documentation_provided \
                - self.justification_provided_and_accepted - self.write_off_required
        except TypeError:
            return None

    @property
    def percent_of_audited_expenditure(self):
        try:
            return 100 * self.financial_findings / self.audited_expenditure
        except (TypeError, DivisionByZero, InvalidOperation):
            return 0

    @transition('status',
                source=Engagement.STATUSES.partner_contacted,
                target=Engagement.STATUSES.report_submitted,
                conditions=[
                    AuditSubmitReportRequiredFieldsCheck.as_condition(),
                    EngagementHasReportAttachmentsCheck.as_condition(),
                ],
                permission=has_action_permission(action='submit'))
    def submit(self, *args, **kwargs):
        return super(Audit, self).submit(*args, **kwargs)

    @transition('status',
                source=Engagement.STATUSES.report_submitted,
                target=Engagement.STATUSES.final,
                permission=has_action_permission(action='finalize'))
    def finalize(self, *args, **kwargs):
        self.partner.audits_completed(update_one=True)
        return super(Audit, self).finalize(*args, **kwargs)

    def __str__(self):
        return 'Audit ({}: {}, {})'.format(self.engagement_type,
                                           self.agreement.order_number,
                                           self.partner.name)

    def get_object_url(self, **kwargs):
        return build_frontend_url('ap', 'audits', self.id, 'overview',
                                  **kwargs)
Ejemplo n.º 7
0
class Engagement(InheritedModelMixin, TimeStampedModel, models.Model):
    TYPE_AUDIT = 'audit'
    TYPE_MICRO_ASSESSMENT = 'ma'
    TYPE_SPOT_CHECK = 'sc'
    TYPE_SPECIAL_AUDIT = 'sa'

    TYPES = Choices(
        (TYPE_AUDIT, _('Audit')),
        (TYPE_MICRO_ASSESSMENT, _('Micro Assessment')),
        (TYPE_SPOT_CHECK, _('Spot Check')),
        (TYPE_SPECIAL_AUDIT, _('Special Audit')),
    )

    PARTNER_CONTACTED = 'partner_contacted'
    REPORT_SUBMITTED = 'report_submitted'
    FINAL = 'final'
    CANCELLED = 'cancelled'

    STATUSES = Choices(
        (PARTNER_CONTACTED, _('IP Contacted')),
        (REPORT_SUBMITTED, _('Report Submitted')),
        (FINAL, _('Final Report')),
        (CANCELLED, _('Cancelled')),
    )

    DISPLAY_STATUSES = Choices(
        ('partner_contacted', _('IP Contacted')),
        ('field_visit', _('Field Visit')),
        ('draft_issued_to_partner', _('Draft Report Issued to IP')),
        ('comments_received_by_partner', _('Comments Received from IP')),
        ('draft_issued_to_unicef', _('Draft Report Issued to UNICEF')),
        ('comments_received_by_unicef', _('Comments Received from UNICEF')),
        ('report_submitted', _('Report Submitted')),
        ('final', _('Final Report')),
        ('cancelled', _('Cancelled')),
    )
    DISPLAY_STATUSES_DATES = {
        DISPLAY_STATUSES.partner_contacted: 'partner_contacted_at',
        DISPLAY_STATUSES.field_visit: 'date_of_field_visit',
        DISPLAY_STATUSES.draft_issued_to_partner: 'date_of_draft_report_to_ip',
        DISPLAY_STATUSES.comments_received_by_partner:
        'date_of_comments_by_ip',
        DISPLAY_STATUSES.draft_issued_to_unicef:
        'date_of_draft_report_to_unicef',
        DISPLAY_STATUSES.comments_received_by_unicef:
        'date_of_comments_by_unicef',
        DISPLAY_STATUSES.report_submitted: 'date_of_report_submit',
        DISPLAY_STATUSES.final: 'date_of_final_report',
        DISPLAY_STATUSES.cancelled: 'date_of_cancel'
    }

    status = FSMField(verbose_name=_('Status'),
                      max_length=30,
                      choices=STATUSES,
                      default=STATUSES.partner_contacted,
                      protected=True)

    # auditor - partner organization from agreement
    agreement = models.ForeignKey(
        PurchaseOrder,
        verbose_name=_('Purchase Order'),
        on_delete=models.CASCADE,
    )
    po_item = models.ForeignKey(
        PurchaseOrderItem,
        verbose_name=_('PO Item Number'),
        null=True,
        on_delete=models.CASCADE,
    )

    partner = models.ForeignKey(
        'partners.PartnerOrganization',
        verbose_name=_('Partner'),
        on_delete=models.CASCADE,
    )
    partner_contacted_at = models.DateField(
        verbose_name=_('Date IP was contacted'), blank=True, null=True)
    engagement_type = models.CharField(verbose_name=_('Engagement Type'),
                                       max_length=10,
                                       choices=TYPES)
    start_date = models.DateField(verbose_name=_('Period Start Date'),
                                  blank=True,
                                  null=True)
    end_date = models.DateField(verbose_name=_('Period End Date'),
                                blank=True,
                                null=True)
    total_value = models.DecimalField(
        verbose_name=_('Total value of selected FACE form(s)'),
        blank=True,
        null=True,
        decimal_places=2,
        max_digits=20)
    exchange_rate = models.DecimalField(verbose_name=_('Exchange Rate'),
                                        blank=True,
                                        null=True,
                                        decimal_places=2,
                                        max_digits=20)

    engagement_attachments = CodedGenericRelation(
        Attachment,
        verbose_name=_('Related Documents'),
        code='audit_engagement',
        blank=True)
    report_attachments = CodedGenericRelation(
        Attachment,
        verbose_name=_('Report Attachments'),
        code='audit_report',
        blank=True)

    date_of_field_visit = models.DateField(
        verbose_name=_('Date of Field Visit'), null=True, blank=True)
    date_of_draft_report_to_ip = models.DateField(
        verbose_name=_('Date Draft Report Issued to IP'),
        null=True,
        blank=True)
    date_of_comments_by_ip = models.DateField(
        verbose_name=_('Date Comments Received from IP'),
        null=True,
        blank=True)
    date_of_draft_report_to_unicef = models.DateField(
        verbose_name=_('Date Draft Report Issued to UNICEF'),
        null=True,
        blank=True)
    date_of_comments_by_unicef = models.DateField(
        verbose_name=_('Date Comments Received from UNICEF'),
        null=True,
        blank=True)

    date_of_report_submit = models.DateField(
        verbose_name=_('Date Report Submitted'), null=True, blank=True)
    date_of_final_report = models.DateField(
        verbose_name=_('Date Report Finalized'), null=True, blank=True)
    date_of_cancel = models.DateField(verbose_name=_('Date Report Cancelled'),
                                      null=True,
                                      blank=True)

    amount_refunded = models.DecimalField(verbose_name=_('Amount Refunded'),
                                          null=True,
                                          blank=True,
                                          decimal_places=2,
                                          max_digits=20)
    additional_supporting_documentation_provided = models.DecimalField(
        verbose_name=_('Additional Supporting Documentation Provided'),
        null=True,
        blank=True,
        decimal_places=2,
        max_digits=20)
    justification_provided_and_accepted = models.DecimalField(
        verbose_name=_('Justification Provided and Accepted'),
        null=True,
        blank=True,
        decimal_places=2,
        max_digits=20)
    write_off_required = models.DecimalField(verbose_name=_('Impairment'),
                                             null=True,
                                             blank=True,
                                             decimal_places=2,
                                             max_digits=20)
    explanation_for_additional_information = models.TextField(verbose_name=_(
        'Provide explanation for additional information received from the IP or add attachments'
    ),
                                                              blank=True)

    joint_audit = models.BooleanField(verbose_name=_('Joint Audit'),
                                      default=False,
                                      blank=True)
    shared_ip_with = ArrayField(models.CharField(
        max_length=20, choices=PartnerOrganization.AGENCY_CHOICES),
                                blank=True,
                                default=[],
                                verbose_name=_('Shared Audit with'))

    staff_members = models.ManyToManyField(AuditorStaffMember,
                                           verbose_name=_('Staff Members'))

    cancel_comment = models.TextField(blank=True,
                                      verbose_name=_('Cancel Comment'))

    active_pd = models.ManyToManyField('partners.Intervention',
                                       verbose_name=_('Active PDs'))

    authorized_officers = models.ManyToManyField(
        PartnerStaffMember,
        verbose_name=_('Authorized Officers'),
        blank=True,
        related_name="engagement_authorizations")

    objects = InheritanceManager()

    class Meta:
        ordering = ('id', )
        verbose_name = _('Engagement')
        verbose_name_plural = _('Engagements')

    def __str__(self):
        return '{}: {}, {}'.format(self.engagement_type,
                                   self.agreement.order_number,
                                   self.partner.name)

    @property
    def displayed_status(self):
        if self.status != self.STATUSES.partner_contacted:
            return self.status

        if self.date_of_comments_by_unicef:
            return self.DISPLAY_STATUSES.comments_received_by_unicef
        elif self.date_of_draft_report_to_unicef:
            return self.DISPLAY_STATUSES.draft_issued_to_unicef
        elif self.date_of_comments_by_ip:
            return self.DISPLAY_STATUSES.comments_received_by_partner
        elif self.date_of_draft_report_to_ip:
            return self.DISPLAY_STATUSES.draft_issued_to_partner
        elif self.date_of_field_visit:
            return self.DISPLAY_STATUSES.field_visit

        return self.status

    @property
    def displayed_status_date(self):
        return getattr(self,
                       self.DISPLAY_STATUSES_DATES[self.displayed_status])

    def get_shared_ip_with_display(self):
        return list(
            map(
                lambda po: dict(PartnerOrganization.AGENCY_CHOICES).get(
                    po, 'Unknown'), self.shared_ip_with))

    @property
    def unique_id(self):
        engagement_code = 'a' if self.engagement_type == self.TYPES.audit else self.engagement_type
        return '{}/{}/{}/{}/{}'.format(
            connection.tenant.country_short_code or '', self.partner.name[:5],
            engagement_code.upper(), self.created.year, self.id)

    @property
    def reference_number(self):
        return self.unique_id

    def get_mail_context(self, **kwargs):
        object_url = self.get_object_url(**kwargs)

        return {
            'unique_id': self.unique_id,
            'engagement_type': self.get_engagement_type_display(),
            'object_url': object_url,
            'partner': force_text(self.partner),
            'auditor_firm': force_text(self.agreement.auditor_firm),
        }

    def _notify_focal_points(self, template_name, context=None):
        for focal_point in get_user_model().objects.filter(
                groups=UNICEFAuditFocalPoint.as_group()):
            # Build the context in the same order the previous version of the code did,
            # just in case something relies on it (intentionally or not).
            ctx = {
                'focal_point': focal_point.get_full_name(),
            }
            if context:
                ctx.update(context)
            base_context = {
                'engagement': self.get_mail_context(user=focal_point),
                'environment': get_environment(),
            }
            base_context.update(ctx)
            context = base_context

            send_notification_using_email_template(
                recipients=[focal_point.email],
                email_template_name=template_name,
                context=context,
            )

    @transition(status,
                source=STATUSES.partner_contacted,
                target=STATUSES.report_submitted,
                permission=has_action_permission(action='submit'))
    def submit(self):
        self.date_of_report_submit = timezone.now()

        self._notify_focal_points('audit/engagement/reported_by_auditor')

    @transition(status,
                source=[STATUSES.partner_contacted, STATUSES.report_submitted],
                target=STATUSES.cancelled,
                permission=has_action_permission(action='cancel'),
                custom={'serializer': EngagementCancelSerializer})
    def cancel(self, cancel_comment):
        self.date_of_cancel = timezone.now()
        self.cancel_comment = cancel_comment

    @transition(status,
                source=STATUSES.report_submitted,
                target=STATUSES.final,
                permission=has_action_permission(action='finalize'))
    def finalize(self):
        self.date_of_final_report = timezone.now()

    def get_object_url(self, **kwargs):
        return build_frontend_url('ap', 'engagements', self.id, 'overview',
                                  **kwargs)
Ejemplo n.º 8
0
class ActionPoint(TimeStampedModel):
    MODULE_CHOICES = Choices(
        ('t2f', _('Trip Management')),
        ('tpm', 'Third Party Monitoring'),
        ('audit', _('Auditor Portal')),
    )

    STATUSES = Choices(
        ('open', _('Open')),
        ('completed', _('Completed')),
    )

    STATUSES_DATES = {
        STATUSES.open: 'created',
        STATUSES.completed: 'date_of_completion'
    }

    KEY_EVENTS = Choices(
        ('status_update', _('Status Update')),
        ('reassign', _('Reassign')),
    )

    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='created_action_points',
                               verbose_name=_('Author'),
                               on_delete=models.CASCADE,
                               )
    assigned_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='+', verbose_name=_('Assigned By'),
                                    on_delete=models.CASCADE,
                                    )
    assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='assigned_action_points',
                                    verbose_name=_('Assigned To'),
                                    on_delete=models.CASCADE,
                                    )

    status = FSMField(verbose_name=_('Status'), max_length=10, choices=STATUSES, default=STATUSES.open, protected=True)

    description = models.TextField(verbose_name=_('Description'))
    due_date = models.DateField(verbose_name=_('Due Date'), blank=True, null=True)
    high_priority = models.BooleanField(default=False, verbose_name=_('High Priority'))

    section = models.ForeignKey('reports.Sector', verbose_name=_('Section'), blank=True, null=True,
                                on_delete=models.CASCADE,
                                )
    office = models.ForeignKey('users.Office', verbose_name=_('Office'), blank=True, null=True,
                               on_delete=models.CASCADE,
                               )

    location = models.ForeignKey('locations.Location', verbose_name=_('Location'), blank=True, null=True,
                                 on_delete=models.CASCADE,
                                 )
    partner = models.ForeignKey('partners.PartnerOrganization', verbose_name=_('Partner'), blank=True, null=True,
                                on_delete=models.CASCADE,
                                )
    cp_output = models.ForeignKey('reports.Result', verbose_name=_('CP Output'), blank=True, null=True,
                                  on_delete=models.CASCADE,
                                  )
    intervention = models.ForeignKey('partners.Intervention', verbose_name=_('PD/SSFA'), blank=True, null=True,
                                     on_delete=models.CASCADE,
                                     )
    engagement = models.ForeignKey('audit.Engagement', verbose_name=_('Engagement'), blank=True, null=True,
                                   related_name='action_points', on_delete=models.CASCADE,
                                   )
    tpm_activity = models.ForeignKey('tpm.TPMActivity', verbose_name=_('TPM Activity'), blank=True, null=True,
                                     related_name='action_points', on_delete=models.CASCADE,
                                     )
    travel = models.ForeignKey('t2f.Travel', verbose_name=_('Travel'), blank=True, null=True,
                               on_delete=models.CASCADE,
                               )

    date_of_completion = MonitorField(verbose_name=_('Date Action Point Completed'), null=True, blank=True,
                                      default=None, monitor='status', when=[STATUSES.completed])

    comments = GenericRelation('django_comments.Comment', object_id_field='object_pk')

    history = GenericRelation('snapshot.Activity', object_id_field='target_object_id',
                              content_type_field='target_content_type')

    tracker = FieldTracker(fields=['assigned_to'])

    class Meta:
        ordering = ('id', )
        verbose_name = _('Action Point')
        verbose_name_plural = _('Action Points')

    @property
    def engagement_subclass(self):
        return self.engagement.get_subclass() if self.engagement else None

    @property
    def related_object(self):
        return self.engagement_subclass or self.tpm_activity or self.travel

    @property
    def related_module(self):
        if self.engagement:
            return self.MODULE_CHOICES.audit
        if self.tpm_activity:
            return self.MODULE_CHOICES.tpm
        if self.travel:
            return self.MODULE_CHOICES.t2f
        return None

    @property
    def reference_number(self):
        return '{}/{}/{}/APD'.format(
            connection.tenant.country_short_code or '',
            self.created.year,
            self.id,
        )

    def get_object_url(self, **kwargs):
        return build_frontend_url('apd', 'action-points', 'detail', self.id, **kwargs)

    @property
    def status_date(self):
        return getattr(self, self.STATUSES_DATES[self.status])

    def __str__(self):
        return self.reference_number

    def get_meaningful_history(self):
        return self.history.filter(
            models.Q(action=Activity.CREATE) |
            models.Q(models.Q(action=Activity.UPDATE), ~models.Q(change={}))
        )

    def snapshot_additional_data(self, diff):
        key_events = []
        if 'status' in diff:
            key_events.append(self.KEY_EVENTS.status_update)
        if 'assigned_to' in diff:
            key_events.append(self.KEY_EVENTS.reassign)

        return {'key_events': key_events}

    @classmethod
    def get_snapshot_action_display(cls, activity):
        key_events = activity.data.get('key_events')
        if key_events:
            if cls.KEY_EVENTS.status_update in key_events:
                return _('Changed status to {}').format(cls.STATUSES[activity.change['status']['after']])
            elif cls.KEY_EVENTS.reassign in key_events:
                return _('Reassigned to {}').format(
                    get_user_model().objects.get(pk=activity.change['assigned_to']['after']).get_full_name()
                )

        return activity.get_action_display()

    def get_mail_context(self, user=None, include_token=False):
        return {
            'person_responsible': self.assigned_to.get_full_name(),
            'assigned_by': self.assigned_by.get_full_name(),
            'reference_number': self.reference_number,
            'partner': self.partner.name if self.partner else '',
            'description': self.description,
            'due_date': self.due_date.strftime('%d %b %Y') if self.due_date else '',
            'object_url': self.get_object_url(user=user, include_token=include_token),
        }

    def send_email(self, recipient, template_name, additional_context=None):
        context = {
            'environment': get_environment(),
            'action_point': self.get_mail_context(user=recipient),
            'recipient': recipient.get_full_name(),
        }
        context.update(additional_context or {})

        notification = Notification.objects.create(
            sender=self,
            recipients=[recipient.email], template_name=template_name,
            template_data=context
        )
        notification.send_notification()

    def _do_complete(self):
        self.send_email(self.assigned_by, 'action_points/action_point/completed')

    @transition(status, source=STATUSES.open, target=STATUSES.completed,
                permission=has_action_permission(action='complete'),
                conditions=[
                    ActionPointCompleteActionsTakenCheck.as_condition()
                ])
    def complete(self):
        self._do_complete()