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)
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)
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
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)
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)
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)
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)
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()