Exemple #1
0
class MinutesOfMeetingRevision(MetadataRevision):
    metadata = models.ForeignKey('MinutesOfMeeting')
    status = ConfigurableChoiceField(_('Status'),
                                     max_length=20,
                                     list_index='STATUS_COR_MOM')

    class Meta:
        app_label = 'default_documents'
Exemple #2
0
class TransmittalRevision(MetadataRevision):
    metadata = models.ForeignKey('Transmittal')
    trs_status = ConfigurableChoiceField(_('Status'),
                                         max_length=20,
                                         default='opened',
                                         list_index='STATUS_TRANSMITTALS')

    class Meta:
        app_label = 'transmittals'
Exemple #3
0
class GtgMetadataRevision(TransmittableMixin, MetadataRevision):
    metadata = models.ForeignKey('GtgMetadata')
    status = ConfigurableChoiceField(_('Status'),
                                     max_length=3,
                                     list_index='GTG_STATUSES',
                                     null=True,
                                     blank=True)
    final_revision = models.NullBooleanField(_('Is final revision?'),
                                             choices=BOOLEANS,
                                             null=True,
                                             blank=True)

    def get_first_revision_number(self):
        """See `MetadataRevision.get_first_revision_number`"""
        return 1
Exemple #4
0
class ContractorDeliverableRevision(TransmittableMixin, MetadataRevision):
    # Revision
    metadata = models.ForeignKey('ContractorDeliverable')
    status = ConfigurableChoiceField(verbose_name="Status",
                                     default="STD",
                                     max_length=3,
                                     list_index='STATUSES',
                                     null=True,
                                     blank=True)
    final_revision = models.NullBooleanField(_('Is final revision?'),
                                             choices=BOOLEANS,
                                             null=True,
                                             blank=True)

    class Meta:
        app_label = 'default_documents'
Exemple #5
0
class CorrespondenceRevision(MetadataRevision):
    metadata = models.ForeignKey('Correspondence')
    status = ConfigurableChoiceField(_('Status'),
                                     max_length=20,
                                     list_index='STATUS_COR_MOM')
    under_review = models.NullBooleanField(_('Under Review'),
                                           choices=BOOLEANS,
                                           null=True,
                                           blank=True)
    overdue = models.NullBooleanField(_('Overdue'),
                                      choices=BOOLEANS,
                                      null=True,
                                      blank=True)
    leader = models.ForeignKey(User,
                               verbose_name=_('Leader'),
                               related_name='leading_correspondance',
                               null=True,
                               blank=True)

    class Meta:
        app_label = 'default_documents'
Exemple #6
0
class GtgMetadata(Metadata):
    latest_revision = models.ForeignKey('GtgMetadataRevision',
                                        null=True,
                                        verbose_name=_('Latest revision'))
    title = models.TextField(_('title'))
    originator = models.ForeignKey('accounts.Entity',
                                   verbose_name=_('Originator'),
                                   limit_choices_to={'type': 'originator'})
    unit = ConfigurableChoiceField(verbose_name="Unit",
                                   default="000",
                                   max_length=3,
                                   list_index='GTG_UNITS')
    discipline = ConfigurableChoiceField(_('Discipline'),
                                         max_length=6,
                                         list_index='GTG_DISCIPLINES',
                                         blank=True,
                                         null=True)
    document_type = ConfigurableChoiceField(_('Document Type'),
                                            max_length=3,
                                            list_index='GTG_DOCUMENT_TYPES')

    # Related docs
    related_documents = models.ManyToManyField(
        'documents.Document', related_name='gtg_related_documents', blank=True)

    # Schedule
    status_ifr_planned_date = models.DateField(_('Status IFR Planned Date'),
                                               null=True,
                                               blank=True)
    status_ifr_forecast_date = models.DateField(_('Status IFR Forecast Date'),
                                                null=True,
                                                blank=True)
    status_ifr_actual_date = models.DateField(_('Status IFR Actual Date'),
                                              null=True,
                                              blank=True)

    status_ifa_planned_date = models.DateField(_('Status IFA Planned Date'),
                                               null=True,
                                               blank=True)
    status_ifa_forecast_date = models.DateField(_('Status IFA Forecast Date'),
                                                null=True,
                                                blank=True)
    status_ifa_actual_date = models.DateField(_('Status IFA Actual Date'),
                                              null=True,
                                              blank=True)

    status_ifi_planned_date = models.DateField(_('Status IFI Planned Date'),
                                               null=True,
                                               blank=True)
    status_ifi_forecast_date = models.DateField(_('Status IFI Forecast Date'),
                                                null=True,
                                                blank=True)
    status_ifi_actual_date = models.DateField(_('Status IFI Actual Date'),
                                              null=True,
                                              blank=True)

    status_ife_planned_date = models.DateField(_('Status IFE Planned Date'),
                                               null=True,
                                               blank=True)
    status_ife_forecast_date = models.DateField(_('Status IFE Forecast Date'),
                                                null=True,
                                                blank=True)
    status_ife_actual_date = models.DateField(_('Status IFE Actual Date'),
                                              null=True,
                                              blank=True)

    status_ifp_planned_date = models.DateField(_('Status IFP Planned Date'),
                                               null=True,
                                               blank=True)
    status_ifp_forecast_date = models.DateField(_('Status IFP Forecast Date'),
                                                null=True,
                                                blank=True)
    status_ifp_actual_date = models.DateField(_('Status IFP Actual Date'),
                                              null=True,
                                              blank=True)

    status_fin_planned_date = models.DateField(_('Status FIN Planned Date'),
                                               null=True,
                                               blank=True)
    status_fin_forecast_date = models.DateField(_('Status FIN Forecast Date'),
                                                null=True,
                                                blank=True)
    status_fin_actual_date = models.DateField(_('Status FIN Actual Date'),
                                              null=True,
                                              blank=True)

    status_asb_planned_date = models.DateField(_('Status ASB Planned Date'),
                                               null=True,
                                               blank=True)
    status_asb_forecast_date = models.DateField(_('Status ASB Forecast Date'),
                                                null=True,
                                                blank=True)
    status_asb_actual_date = models.DateField(_('Status ASB Actual Date'),
                                              null=True,
                                              blank=True)

    class Meta:
        verbose_name = _('Gtg deliverable')
        verbose_name_plural = _('Gtg deliverables')
        ordering = ('document_number', )

    class PhaseConfig:
        filter_fields = [
            'originator', 'discipline', 'document_type', 'status', 'unit',
            'leader', 'approver', 'under_review', 'overdue'
        ]
        filter_fields_order_ = [
            'search_terms', 'originator', 'discipline', 'document_type',
            'status', 'unit', 'leader', 'approver', 'under_review', 'overdue'
        ]

        column_fields = (
            ('Document Number', 'document_number'),
            ('Title', 'title'),
            ('Status', 'status'),
            ('Rev.', 'current_revision'),
            ('Document type', 'document_type'),
            ('Originator', 'originator'),
            ('Review start date', 'review_start_date'),
            ('Review due date', 'review_due_date'),
            ('Under review', 'under_review'),
            ('Leader', 'leader'),
            ('Approver', 'approver'),
        )

    def natural_key(self):
        return self.document_key

    def generate_document_key(self):
        # If document key is not suppplied by user, we generate a uuid
        return '{}'.format(uuid.uuid4())

    @property
    def status(self):
        return self.latest_revision.status

    @property
    def final_revision(self):
        return self.latest_revision.final_revision

    @property
    def review_start_date(self):
        return self.latest_revision.review_start_date

    @property
    def review_due_date(self):
        return self.latest_revision.review_due_date

    @property
    def under_review(self):
        return self.latest_revision.is_under_review()

    @property
    def overdue(self):
        return self.latest_revision.is_overdue()

    @property
    def leader(self):
        return self.latest_revision.leader

    @property
    def approver(self):
        return self.latest_revision.approver

    @property
    def received_date(self):
        return self.latest_revision.received_date

    @property
    def leader_step_closed(self):
        return self.latest_revision.leader_step_closed

    @property
    def review_end_date(self):
        return self.latest_revision.review_end_date

    @property
    def return_code(self):
        return self.latest_revision.return_code

    @classmethod
    def get_batch_actions(cls, category, user):
        actions = super(GtgMetadata, cls).get_batch_actions(category, user)
        actions['start_review'] = MenuItem(
            'start-review',
            _('Start review'),
            reverse('batch_start_reviews',
                    args=[category.organisation.slug, category.slug]),
            ajax=True,
            modal='batch-review-modal',
            progression_modal=True,
            icon='eye-open',
        )
        actions['cancel_review'] = MenuItem(
            'cancel-review',
            'Cancel review',
            reverse('batch_cancel_reviews',
                    args=[category.organisation.slug, category.slug]),
            ajax=True,
            modal='cancel-review-modal',
            progression_modal=True,
            icon='eye-close',
        )
        if user.has_perm('transmittals.add_outgoingtransmittal'):
            actions['create_transmittal'] = MenuItem(
                'create-transmittal',
                'Create transmittal',
                reverse('transmittal_create',
                        args=[category.organisation.slug, category.slug]),
                ajax=True,
                modal='create-transmittal-modal',
                progression_modal=True,
                icon='transfer',
            )
        return actions

    @classmethod
    def get_batch_actions_modals(cls):
        templates = super(GtgMetadata, cls).get_batch_actions_modals()
        return templates + [
            'reviews/document_list_cancel_review_modal.html',
            'reviews/document_list_batch_review_modal.html',
            'transmittals/document_list_create_transmittal_modal.html'
        ]
Exemple #7
0
class MinutesOfMeeting(Metadata):
    latest_revision = models.ForeignKey('MinutesOfMeetingRevision',
                                        null=True,
                                        verbose_name=_('Latest revision'))

    # General information
    subject = models.TextField(_('Subject'))
    meeting_date = models.DateField(_('Meeting date'))
    received_sent_date = models.DateField(_('Received / sent date'))
    # We keep it for a while
    contract_number_old = ConfigurableChoiceField(_('Contract Number'),
                                                  max_length=8,
                                                  list_index='CONTRACT_NBS',
                                                  blank=True,
                                                  null=True)
    contract_number = models.CharField(verbose_name='Contract Number',
                                       max_length=50)
    originator = ConfigurableChoiceField(_('Originator'),
                                         default='FWF',
                                         max_length=3,
                                         list_index='ORIGINATORS')
    recipient = ConfigurableChoiceField(_('Recipient'),
                                        max_length=50,
                                        list_index='RECIPIENTS')
    document_type = ConfigurableChoiceField(_('Document Type'),
                                            default="PID",
                                            max_length=3,
                                            list_index='DOCUMENT_TYPES')
    sequential_number = models.CharField(
        verbose_name="sequential Number",
        help_text=_('Type in a four digit number'),
        default="0001",
        max_length=4,
        validators=[StringNumberValidator(4)])
    prepared_by = ConfigurableChoiceField(_('Prepared by'),
                                          null=True,
                                          blank=True,
                                          max_length=250,
                                          list_index='AUTHORS')
    signed = models.NullBooleanField(_('Signed'),
                                     null=True,
                                     blank=True,
                                     choices=BOOLEANS)

    # Response reference
    # TODO Check the queryset
    response_reference = models.ManyToManyField(
        'Correspondence',
        related_name='mom_correspondence_related_set',
        blank=True)

    class Meta:
        ordering = ('document_number', )
        app_label = 'default_documents'
        unique_together = ((
            "contract_number",
            "originator",
            "recipient",
            "document_type",
            "sequential_number",
        ), )

    class PhaseConfig:
        filter_fields = (
            'originator',
            'recipient',
            'status',
            'signed',
            'prepared_by',
        )
        column_fields = (
            ('Reference', 'document_number'),
            ('Subject', 'subject'),
            ('Meeting date', 'meeting_date'),
            ('Rec./sent date', 'received_sent_date'),
            ('Originator', 'originator'),
            ('Recipient', 'recipient'),
            ('Document type', 'document_type'),
            ('Prepared by', 'prepared_by'),
            ('Signed', 'signed'),
            ('Status', 'status'),
        )

    def generate_document_key(self):
        return slugify("{contract_number}-{originator}-{recipient}"
                       "{document_type}-{sequential_number}".format(
                           contract_number=self.contract_number,
                           originator=self.originator,
                           recipient=self.recipient,
                           document_type=self.document_type,
                           sequential_number=self.sequential_number)).upper()

    def natural_key(self):
        return (self.document_key, )

    @property
    def status(self):
        return self.latest_revision.status

    @property
    def title(self):
        return self.subject
Exemple #8
0
class Correspondence(Metadata):
    latest_revision = models.ForeignKey('CorrespondenceRevision',
                                        null=True,
                                        verbose_name=_('Latest revision'))

    # General information
    subject = models.TextField(_('Subject'))
    correspondence_date = models.DateField(_('Correspondence date'))
    received_sent_date = models.DateField(_('Received / sent date'))
    contract_number_old = ConfigurableChoiceField(_('Contract Number'),
                                                  max_length=8,
                                                  list_index='CONTRACT_NBS',
                                                  null=True,
                                                  blank=True)
    contract_number = models.CharField(verbose_name='Contract Number',
                                       max_length=50)
    originator = ConfigurableChoiceField(_('Originator'),
                                         default='FWF',
                                         max_length=3,
                                         list_index='ORIGINATORS')
    recipient = ConfigurableChoiceField(_('Recipient'),
                                        max_length=50,
                                        list_index='RECIPIENTS')
    document_type = ConfigurableChoiceField(_('Document Type'),
                                            default="PID",
                                            max_length=3,
                                            list_index='DOCUMENT_TYPES')
    sequential_number = models.CharField(
        verbose_name="sequential Number",
        help_text=_('Type in a four digit number'),
        default="0001",
        max_length=4,
        validators=[StringNumberValidator(4)])
    author = ConfigurableChoiceField(_('Author'),
                                     null=True,
                                     blank=True,
                                     max_length=250,
                                     list_index='AUTHORS')
    addresses = ConfigurableChoiceField(_('Addresses'),
                                        null=True,
                                        blank=True,
                                        list_index='ADDRESSES')
    response_required = models.NullBooleanField(_('Response required'),
                                                null=True,
                                                blank=True)
    due_date = models.DateField(_('Due date'), null=True, blank=True)
    external_reference = models.TextField(_('External reference'),
                                          null=True,
                                          blank=True)

    # Related documents
    related_documents = models.ManyToManyField(
        'documents.Document',
        related_name='correspondence_related_set',
        blank=True)

    class Meta:
        ordering = ('id', )
        app_label = 'default_documents'
        unique_together = ((
            "contract_number",
            "originator",
            "recipient",
            "document_type",
            "sequential_number",
        ), )

    class PhaseConfig:
        filter_fields = ('originator', 'recipient', 'status', 'overdue',
                         'leader')
        column_fields = (
            ('Reference', 'document_number'),
            ('Subject', 'subject'),
            ('Rec./Sent date', 'received_sent_date'),
            ('Resp. required', 'response_required'),
            ('Due date', 'due_date'),
            ('Status', 'status'),
            ('Under review', 'status'),
            ('Overdue', 'overdue'),
            ('Leader', 'leader'),
            ('Originator', 'originator'),
            ('Recipient', 'recipient'),
            ('Document type', 'document_type'),
        )

    def generate_document_key(self):
        return slugify("{contract_number}-{originator}-{recipient}-"
                       "{document_type}-{sequential_number}".format(
                           contract_number=self.contract_number,
                           originator=self.originator,
                           recipient=self.recipient,
                           document_type=self.document_type,
                           sequential_number=self.sequential_number)).upper()

    def natural_key(self):
        return (self.document_key, )

    @property
    def status(self):
        return self.latest_revision.status

    @property
    def overdue(self):
        return self.latest_revision.overdue

    @property
    def leader(self):
        return self.latest_revision.leader

    @property
    def title(self):
        return self.subject

    def get_initial_empty(self):
        empty_fields = ('final_revision', )
        return super(ContractorDeliverableRevision,
                     self).get_initial_empty() + empty_fields
Exemple #9
0
class ContractorDeliverable(ScheduleMixin, Metadata):
    latest_revision = models.ForeignKey('ContractorDeliverableRevision',
                                        null=True,
                                        verbose_name=_('Latest revision'))

    # General information
    title = models.TextField(verbose_name="Title")
    # Let's keep this field for a while
    contract_number_old = ConfigurableChoiceField(
        verbose_name="Contract Number",
        max_length=15,
        list_index='CONTRACT_NBS',
        null=True,
        blank=True)
    contract_number = models.CharField(verbose_name='Contract Number',
                                       max_length=50)
    originator = models.ForeignKey('accounts.Entity',
                                   verbose_name=_('Originator'))
    unit = ConfigurableChoiceField(verbose_name="Unit",
                                   default="000",
                                   max_length=3,
                                   list_index='UNITS')
    discipline = ConfigurableChoiceField(verbose_name="Discipline",
                                         default="PCS",
                                         max_length=3,
                                         list_index='DISCIPLINES')
    document_type = ConfigurableChoiceField(verbose_name="Document Type",
                                            default="PID",
                                            max_length=3,
                                            list_index='DOCUMENT_TYPES')
    sequential_number = models.CharField(
        verbose_name="sequential Number",
        help_text=_('Select a four digit number'),
        default="0001",
        max_length=4,
        validators=[StringNumberValidator(4)])
    system = ConfigurableChoiceField(verbose_name="System",
                                     list_index='SYSTEMS',
                                     null=True,
                                     blank=True)
    wbs = ConfigurableChoiceField(verbose_name="WBS",
                                  max_length=20,
                                  list_index='WBS',
                                  null=True,
                                  blank=True)
    weight = models.IntegerField(verbose_name="Weight", null=True, blank=True)

    # Related documents
    related_documents = models.ManyToManyField(
        'documents.Document', related_name='cd_related_documents', blank=True)

    # Schedule
    status_std_planned_date = models.DateField(
        verbose_name="Status STD Planned Date", null=True, blank=True)
    status_std_forecast_date = models.DateField(
        verbose_name="Status STD Forecast Date", null=True, blank=True)
    status_std_actual_date = models.DateField(
        verbose_name="Status STD Actual Date", null=True, blank=True)
    status_idc_planned_date = models.DateField(
        verbose_name="Status IDC Planned Date", null=True, blank=True)
    status_idc_forecast_date = models.DateField(
        verbose_name="Status IDC Forecast Date", null=True, blank=True)
    status_idc_actual_date = models.DateField(
        verbose_name="Status IDC Actual Date", null=True, blank=True)
    status_ifr_planned_date = models.DateField(
        verbose_name="Status IFR Planned Date", null=True, blank=True)
    status_ifr_forecast_date = models.DateField(
        verbose_name="Status IFR Forecast Date", null=True, blank=True)
    status_ifr_actual_date = models.DateField(
        verbose_name="Status IFR Actual Date", null=True, blank=True)
    status_ifa_planned_date = models.DateField(
        verbose_name="Status IFA Planned Date", null=True, blank=True)
    status_ifa_forecast_date = models.DateField(
        verbose_name="Status IFA Forecast Date", null=True, blank=True)
    status_ifa_actual_date = models.DateField(
        verbose_name="Status IFA Actual Date", null=True, blank=True)
    status_ifd_planned_date = models.DateField(
        verbose_name="Status IFD Planned Date", null=True, blank=True)
    status_ifd_forecast_date = models.DateField(
        verbose_name="Status IFD Forecast Date", null=True, blank=True)
    status_ifd_actual_date = models.DateField(
        verbose_name="Status IFD Actual Date", null=True, blank=True)
    status_ifc_planned_date = models.DateField(
        verbose_name="Status IFC Planned Date", null=True, blank=True)
    status_ifc_forecast_date = models.DateField(
        verbose_name="Status IFC Forecast Date", null=True, blank=True)
    status_ifc_actual_date = models.DateField(
        verbose_name="Status IFC Actual Date", null=True, blank=True)
    status_ifi_planned_date = models.DateField(
        verbose_name="Status IFI Planned Date", null=True, blank=True)
    status_ifi_forecast_date = models.DateField(
        verbose_name="Status IFI Forecast Date", null=True, blank=True)
    status_ifi_actual_date = models.DateField(
        verbose_name="Status IFI Actual Date", null=True, blank=True)
    status_asb_planned_date = models.DateField(
        verbose_name="Status ASB Planned Date", null=True, blank=True)
    status_asb_forecast_date = models.DateField(
        verbose_name="Status ASB Forecast Date", null=True, blank=True)
    status_asb_actual_date = models.DateField(
        verbose_name="Status ASB Actual Date", null=True, blank=True)

    class PhaseConfig:
        filter_fields = ('docclass', 'status', 'unit', 'discipline',
                         'document_type', 'under_review', 'overdue', 'leader',
                         'approver')
        indexable_fields = ['is_existing', 'can_be_transmitted']
        es_field_types = {
            'overdue': 'boolean',
        }
        column_fields = (
            ('', 'under_preparation_by'),
            ('Document Number', 'document_number'),
            ('Title', 'title'),
            ('Rev.', 'current_revision'),
            ('Status', 'status'),
            ('Class', 'docclass'),
            ('Unit', 'unit'),
            ('Discipline', 'discipline'),
            ('Document type', 'document_type'),
            ('Review start date', 'review_start_date'),
            ('Review due date', 'review_due_date'),
            ('Under review', 'under_review'),
            ('Overdue', 'overdue'),
            ('Leader', 'leader'),
            ('Approver', 'approver'),
            ('Final revision', 'final_revision'),
        )
        transmittal_columns = {
            'Document Number': 'document_key',
            'Title': 'title',
            'Contract Number': 'contract_number',
            'Originator': 'originator',
            'Unit': 'unit',
            'Discipline': 'discipline',
            'Document Type': 'document_type',
            'Sequential Number': 'sequential_number',
            'Class': 'docclass',
            'Revision': 'revision',
            'Status': 'status',
            'Received Date': 'received_date',
            'Created': 'created_on',
        }
        export_fields = OrderedDict((
            ('Document number', 'document_number'),
            ('Title', 'title'),
            ('Revision', 'revision_name'),
            ('Revision date', 'revision_date'),
            ('Status', 'status'),
            ('Doc category', 'category'),
            ('Class', 'docclass'),
            ('Contract Number', 'contract_number'),
            ('Originator', 'originator'),
            ('Unit', 'unit'),
            ('Discipline', 'discipline'),
            ('Document type', 'document_type'),
            ('Sequential number', 'sequential_number'),
            ('System', 'system'),
            ('WBS', 'wbs'),
            ('Weight', 'weight'),
            ('Is final revision', 'final_revision'),
            ('Received date', 'received_date'),
            ('Created on', 'created_on'),
            ('Review start date', 'review_start_date'),
            ('Review due date', 'review_due_date'),
            ('Leader', 'leader'),
            ('Approver', 'approver'),
            ('Return code', 'return_code'),
            ('STD Planned', 'status_std_planned_date'),
            ('IDC Planned', 'status_idc_planned_date'),
            ('IFR Planned', 'status_ifr_planned_date'),
            ('IFA Planned', 'status_ifa_planned_date'),
            ('IFD Planned', 'status_ifd_planned_date'),
            ('IFC Planned', 'status_ifc_planned_date'),
            ('IFI Planned', 'status_ifi_planned_date'),
            ('ASB Planned', 'status_asb_planned_date'),
            ('STD Forecast', 'status_std_forecast_date'),
            ('IDC Forecast', 'status_idc_forecast_date'),
            ('IFR Forecast', 'status_ifr_forecast_date'),
            ('IFA Forecast', 'status_ifa_forecast_date'),
            ('IFD Forecast', 'status_ifd_forecast_date'),
            ('IFC Forecast', 'status_ifc_forecast_date'),
            ('IFI Forecast', 'status_ifi_forecast_date'),
            ('ASB Forecast', 'status_asb_forecast_date'),
            ('STD Actual', 'status_std_actual_date'),
            ('IFR Actual', 'status_ifr_actual_date'),
            ('IDC Actual', 'status_idc_actual_date'),
            ('IFA Actual', 'status_ifa_actual_date'),
            ('IFD Actual', 'status_ifd_actual_date'),
            ('IFC Actual', 'status_ifc_actual_date'),
            ('IFI Actual', 'status_ifi_actual_date'),
            ('ASB Actual', 'status_asb_actual_date'),
        ))
        custom_filters = OrderedDict((('show_cld_spd', {
            'field': forms.BooleanField,
            'label': _('Show CLD/SPD docs'),
            'filters': {
                True: None,
                False: Q('term', is_existing=True),
                None: Q('term', is_existing=True)
            }
        }), ('outgoing_trs', {
            'field': forms.BooleanField,
            'label': _('Ready for outgoing TRS'),
            'filters': {
                True: Q('term', can_be_transmitted=True),
                False: None,
                None: None,
            }
        })))

    class Meta:
        ordering = ('document_number', )
        app_label = 'default_documents'
        unique_together = ((
            "contract_number",
            "originator",
            "unit",
            "discipline",
            "document_type",
            "sequential_number",
        ), )

    def natural_key(self):
        return self.document_key

    def generate_document_key(self):
        return slugify("{contract_number}-{originator}-{unit}-{discipline}-"
                       "{document_type}-{sequential_number}".format(
                           contract_number=self.contract_number,
                           originator=self.originator.trigram,
                           unit=self.unit,
                           discipline=self.discipline,
                           document_type=self.document_type,
                           sequential_number=self.sequential_number)).upper()

    def get_all_revisions(self):
        """Return all revisions data of this document."""
        revisions = super(ContractorDeliverable, self) \
            .get_all_revisions() \
            .select_related(
                'metadata',
                'metadata__document',
                'metadata__document__category__organisation',
                'leader',
                'approver',
                'approver') \
            .prefetch_related('reviewers', 'transmittals__document')

        return revisions

    @property
    def status(self):
        return self.latest_revision.status

    @property
    def is_existing(self):
        return self.status not in ('CLD', 'SPD')

    @property
    def final_revision(self):
        return self.latest_revision.final_revision

    @property
    def review_start_date(self):
        return self.latest_revision.review_start_date

    @property
    def review_due_date(self):
        return self.latest_revision.review_due_date

    @property
    def under_review(self):
        return self.latest_revision.is_under_review()

    @property
    def overdue(self):
        return self.latest_revision.is_overdue()

    @property
    def leader(self):
        return self.latest_revision.leader

    @property
    def approver(self):
        return self.latest_revision.approver

    @property
    def docclass(self):
        return self.latest_revision.docclass

    @classmethod
    def get_batch_actions(cls, category, user):
        actions = super(ContractorDeliverable,
                        cls).get_batch_actions(category, user)

        if user.has_perm('documents.can_control_document') and user.has_perm(
                'reviews.can_add_review'):
            actions['start_review'] = MenuItem(
                'start-review',
                _('Start review'),
                reverse('batch_start_reviews',
                        args=[category.organisation.slug, category.slug]),
                ajax=True,
                modal='batch-review-modal',
                progression_modal=True,
                icon='eye-open',
            )
            actions['cancel_review'] = MenuItem(
                'cancel-review',
                _('Cancel review'),
                reverse('batch_cancel_reviews',
                        args=[category.organisation.slug, category.slug]),
                ajax=True,
                modal='cancel-review-modal',
                progression_modal=True,
                icon='eye-close',
            )

        if user.has_perm('transmittals.add_outgoingtransmittal'):
            actions['prepare_transmittal'] = MenuItem(
                'prepare-transmittal',
                _('Prepare outgoing transmittal'),
                reverse('transmittal_prepare',
                        args=[category.organisation.slug, category.slug]),
                ajax=False,
                progression_modal=False,
                icon='hand-up')
            actions['create_transmittal'] = MenuItem(
                'create-transmittal',
                'Create transmittal',
                reverse('transmittal_create',
                        args=[category.organisation.slug, category.slug]),
                ajax=True,
                modal='create-transmittal-modal',
                progression_modal=True,
                icon='transfer',
            )
        return actions

    @classmethod
    def get_batch_actions_modals(cls):
        templates = super(ContractorDeliverable,
                          cls).get_batch_actions_modals()
        return templates + [
            'reviews/document_list_cancel_review_modal.html',
            'reviews/document_list_batch_review_modal.html',
            'transmittals/document_list_create_transmittal_modal.html'
        ]
Exemple #10
0
class TransmittableMixin(ReviewMixin):
    """Define behavior of revisions that can be embedded in transmittals.

    Only reviewable documents can be transmitted, hence the mixin
    inheritance.

    """

    PURPOSE_OF_ISSUE_CHOICES = Choices(
        ('FR', _('For review')),
        ('FI', _('For information')))

    transmittal = models.ForeignKey(
        'transmittals.OutgoingTransmittal',
        verbose_name='transmittal',
        null=True, blank=True,
        on_delete=models.SET_NULL)
    transmittals = models.ManyToManyField(
        'transmittals.OutgoingTransmittal',
        verbose_name='transmittals',
        related_name="%(app_label)s_%(class)s_related"
    )
    transmittal_sent_date = models.DateField(
        _('Transmittal sent date'),
        null=True, blank=True)
    trs_return_code = ConfigurableChoiceField(
        _('Final return code'),
        max_length=3,
        null=True, blank=True,
        list_index='REVIEW_RETURN_CODES')
    file_transmitted = PrivateFileField(
        _('File Transmitted'),
        null=True, blank=True,
        upload_to=file_transmitted_file_path)
    under_preparation_by = models.ForeignKey(
        'accounts.User',
        verbose_name=_('Under preparation by'),
        related_name='+',
        null=True, blank=True)
    internal_review = models.BooleanField(
        _('Internal review only?'),
        choices=Choices(
            (False, 'No'),
            (True, 'Yes')
        ),
        default=False)
    purpose_of_issue = models.CharField(
        _('Purpose of issue'),
        max_length=2,
        blank=True,
        choices=PURPOSE_OF_ISSUE_CHOICES,
        default=PURPOSE_OF_ISSUE_CHOICES.FR)
    external_review_due_date = models.DateField(
        _('External due date'),
        null=True, blank=True)

    class Meta:
        abstract = True

    def get_final_return_code(self):
        """Returns the latest available return code."""
        if self.trs_return_code:
            rc = self.trs_return_code
        elif hasattr(self, 'return_code'):
            rc = self.return_code
        else:
            rc = ''
        return rc

    @property
    def can_be_reviewed(self):
        """Can this revision be reviewed.

        A revision that was already embedded in a transmittal cannot
        be reviewed anymore.

        """
        # Turns out, simply calling "self.can_be_transmitted" leads to
        # an infinite recursion error.
        return all((
            super(TransmittableMixin, self).can_be_reviewed,
            not self.transmittals.count()))

    @property
    def can_be_transmitted(self):
        """Is this rev ready to be embedded in an outgoing trs?"""
        return all((
            not self.internal_review,
            not self.transmittals.count(),
            self.document.current_revision == self.revision))

    @property
    def last_review_closed(self):
        return all((
            not self.internal_review,
            self.document.current_revision == self.revision,
            self.review_end_date))

    def can_be_transmitted_to_recipient(self, recipient):
        """Is this rev ready to be embedded in an outgoing trs?"""
        return all((
            not self.internal_review,
            recipient not in [trs.recipient for trs in self.transmittals.all()],
            self.document.current_revision == self.revision))

    def get_initial_empty(self):
        """New revision initial data that must be empty."""
        empty_fields = super(TransmittableMixin, self).get_initial_empty()
        return empty_fields + (
            'trs_return_code',
            'file_transmitted',
            'external_review_due_date',
        )
Exemple #11
0
class OutgoingTransmittal(Metadata):
    """Represents an outgoing transmittal.

    In the end, Transmittal and OutgoingTransmittal should be refactored into a
    single class. However, the incoming trs class contains too much specific
    code and is kept isolated for now.

    """
    EXTERNAL_REVIEW_DURATION = 13

    latest_revision = models.ForeignKey(
        'OutgoingTransmittalRevision',
        null=True,
        verbose_name=_('Latest revision'))

    revisions_category = models.ForeignKey(
        'categories.Category',
        verbose_name=_('From category'),
        on_delete=models.PROTECT)
    # We'll have to delete it, but let's keep this one for a while
    contract_number_old = ConfigurableChoiceField(
        verbose_name='Contract Number',
        max_length=8,
        list_index='CONTRACT_NBS',
        null=True,
        blank=True
    )
    contract_number = models.CharField(
        verbose_name='Contract Number',
        max_length=50)
    originator = models.CharField(
        _('Originator'),
        max_length=3)
    recipient = models.ForeignKey(
        'accounts.Entity',
        verbose_name=_('Recipient'))
    sequential_number = models.PositiveIntegerField(
        _('sequential number'),
        null=True, blank=True)
    ack_of_receipt_date = models.DateField(
        _('Acknowledgment of receipt date'),
        null=True, blank=True)
    ack_of_receipt_author = models.ForeignKey(
        'accounts.User',
        verbose_name=_('Acknowledgment of receipt author'),
        null=True, blank=True,
        on_delete=models.PROTECT)
    archived_pdf = OgtFileField(
        verbose_name=_("Archived PDF"),
        null=True, blank=True)

    class Meta:
        app_label = 'transmittals'
        ordering = ('document_number',)
        verbose_name = _('Outgoing transmittal')
        verbose_name_plural = _('Outgoing transmittals')

    class PhaseConfig:
        filter_fields = ('recipient', 'ack_of_receipt')
        custom_filters = OrderedDict((
            ('has_errors', {
                'field': forms.ChoiceField,
                'field_kwargs': {
                    'choices': (
                        ('', '----------'),
                        ('true', 'Yes'),
                        ('false', 'No'),
                    )
                },
                'label': _('Has errors?'),
                'filters': {
                    '': None,
                    'true': Q('term', has_error=True),
                    'false': Q('term', has_error=False)
                }
            }),)
        )
        column_fields = (
            ('Reference', 'document_number'),
            ('Created', 'created_on'),
            ('Originator', 'originator'),
            ('Recipient', 'recipient'),
            ('Acknowledgment of receipt', 'ack_of_receipt'),
            ('Has error', 'has_error'),
        )
        export_fields = OrderedDict((
            ('Document number', 'document_number'),
            ('Contract Number', 'contract_number'),
            ('Originator', 'originator'),
            ('Recipient', 'recipient'),
            ('Ack. of receipt', 'ack_of_receipt'),
            ('Ack. of receipt date', 'ack_of_receipt_date'),
            ('Ack. of receipt author', 'ack_of_receipt_author'),
            ('Revision', 'revision_name'),
            ('Has error', 'has_error'),
        ))

    def __str__(self):
        return self.document_key

    def get_revisions(self):
        _class = self.get_revisions_class()
        revisions = _class.objects.filter(transmittals=self).select_related()
        return revisions

    def get_last_revisions(self):
        """This is ugly. Should find a better way.
            Return last revision of each linked document
        """
        revisions = self.get_revisions()
        docs = [rev.metadata for rev in revisions]
        last_revs = [doc.get_all_revisions().first() for doc in docs]
        return list(set(last_revs))

    def get_revisions_class(self):
        return self.revisions_category.revision_class()

    @property
    def ack_of_receipt(self):
        return bool(self.ack_of_receipt_date)

    def get_ack_of_receipt_display(self):
        return 'Yes' if self.ack_of_receipt else 'No'
    get_ack_of_receipt_display.short_description = 'Acknowledgment of receipt'

    def generate_document_key(self):
        key = '{}-{}-{}-TRS-{:0>5d}'.format(
            self.contract_number,
            self.originator,
            self.recipient.trigram,
            self.sequential_number)
        return key

    @property
    def title(self):
        return self.document_key

    @classmethod
    def get_document_download_form(cls, data, queryset):
        from transmittals.forms import TransmittalDownloadForm
        return TransmittalDownloadForm(data, queryset=queryset)

    @classmethod
    def compress_documents(cls, documents, **kwargs):
        """See `documents.models.Metadata.compress_documents`"""
        content = kwargs.get('content', 'transmittal')
        revisions = kwargs.get('revisions', 'latest')

        temp_file = tempfile.TemporaryFile()

        with zipfile.ZipFile(temp_file, mode='w') as zip_file:
            for document in documents:
                dirname = document.document_key
                revision = document.get_latest_revision()

                # Should we embed the transmittal pdf?
                if content in ('transmittal', 'both'):

                    # All revisions or just the latest?
                    if revisions == 'latest':
                        revs = [revision]
                    elif revisions == 'all':
                        revs = document.get_all_revisions()

                    # Embed the file in the zip archive
                    for rev in revs:
                        pdf_file = rev.pdf_file
                        pdf_basename = os.path.basename(pdf_file.name)

                        # Avoiding to break export process
                        if not pdf_file:
                            continue

                        # If file is gone, don't break export process
                        try:
                            zip_file.write(
                                pdf_file.path,
                                '{}/{}'.format(dirname, pdf_basename),
                                compress_type=zipfile.ZIP_DEFLATED)
                        except OSError:
                            logger.warning(
                                'File: {} missing in export process'.format(pdf_file))

                # Should we embed review comments?
                if content in ('revisions', 'both'):
                    meta = document.get_metadata()
                    exported_revs = meta.get_revisions()
                    for rev in exported_revs:
                        if rev.file_transmitted:
                            comments_file = rev.file_transmitted
                            comments_basename = os.path.basename(comments_file.path)
                            zip_file.write(
                                comments_file.path,
                                '{}/{}/{}'.format(
                                    dirname,
                                    rev.document.document_key,
                                    comments_basename),
                                compress_type=zipfile.ZIP_DEFLATED)

        return temp_file

    def link_to_revisions(self, revisions):
        """Set the given revisions as related documents.

        The revisions MUST be valid:
         - belong to the same category
         - be transmittable objects

        """
        ids = []
        index_data = []
        for revision in revisions:
            ids.append(revision.id)

            # Update ES index to make sure the "can_be_transmitted"
            # filter is up to date
            index_datum = build_index_data(revision)
            index_datum['_source']['can_be_transmitted'] = False
            index_datum['_source']['last_review_closed'] = False
            index_data.append(index_datum)
        with transaction.atomic():
            today = timezone.now()
            later = today + datetime.timedelta(days=self.EXTERNAL_REVIEW_DURATION)

            # Mark revisions as transmitted
            Revision = type(revisions[0])
            Revision.objects \
                .filter(id__in=ids) \
                .update(
                    transmittal=self,
                    transmittal_sent_date=timezone.now(),
                    external_review_due_date=Case(
                        When(purpose_of_issue='FR', then=Value(later)),
                        When(purpose_of_issue='FI', then=Value(None)),
                    ))
            for rev in Revision.objects.filter(id__in=ids):
                rev.transmittals.add(self)
            bulk_actions(index_data)

    @classmethod
    def get_batch_actions(cls, category, user):
        actions = super(OutgoingTransmittal, cls).get_batch_actions(
            category, user)

        if user.is_external:
            actions['ack_transmittals'] = MenuItem(
                'ack-transmittals',
                _('Ack receipt'),
                reverse('transmittal_batch_ack_of_receipt', args=[
                    category.organisation.slug,
                    category.slug]),
                ajax=False,
                icon='eye-open',
            )

        return actions

    @classmethod
    def get_batch_actions_modals(cls):
        """Returns a list of templates used in batch actions."""
        return ['transmittals/document_list_download_modal.html',
                'documents/document_list_export_modal.html']

    def ack_receipt(self, user, save=True):
        """Acknowledge receipt of this transmittal."""
        self.ack_of_receipt_date = timezone.now().date()
        self.ack_of_receipt_author = user

        if save:
            self.save()
Exemple #12
0
class Transmittal(Metadata):
    """Represents and incoming transmittal.

    Transmittals are created when a contractor upload documents.

    """
    STATUSES = Choices(
        ('new', _('New')),
        ('invalid', _('Invalid')),
        ('tobechecked', _('To be checked')),
        ('rejected', _('Rejected')),
        ('processing', _('Processing')),
        ('accepted', _('Accepted')),
    )

    latest_revision = models.ForeignKey(
        'TransmittalRevision',
        null=True,
        verbose_name=_('Latest revision'))

    transmittal_key = models.CharField(
        _('Transmittal key'),
        max_length=250)

    # General informations
    transmittal_date = models.DateField(
        _('Transmittal date'),
        null=True, blank=True)
    ack_of_receipt_date = models.DateField(
        _('Acknowledgment of receipt date'),
        null=True, blank=True)
    # We'll keep it for a while
    contract_number_old = ConfigurableChoiceField(
        verbose_name='Contract Number',
        max_length=8,
        list_index='CONTRACT_NBS',
        null=True,
        blank=True)
    contract_number = models.CharField(
        verbose_name='Contract Number',
        max_length=50)
    originator = ConfigurableChoiceField(
        _('Originator'),
        default='CTR',
        max_length=3,
        list_index='ORIGINATORS')
    recipient = ConfigurableChoiceField(
        _('Recipient'),
        max_length=50,
        list_index='RECIPIENTS')
    sequential_number = models.PositiveIntegerField(
        _('sequential number'),
        null=True, blank=True)
    document_type = ConfigurableChoiceField(
        _('Document Type'),
        default="PID",
        max_length=3,
        list_index='DOCUMENT_TYPES')
    status = models.CharField(
        max_length=20,
        choices=STATUSES,
        default=STATUSES.tobechecked)

    # Related documents
    related_documents = models.ManyToManyField(
        'documents.Document',
        related_name='transmittals_related_set',
        blank=True)

    contractor = models.CharField(max_length=255, null=True, blank=True)
    tobechecked_dir = models.CharField(max_length=255, null=True, blank=True)
    accepted_dir = models.CharField(max_length=255, null=True, blank=True)
    rejected_dir = models.CharField(max_length=255, null=True, blank=True)

    class Meta:
        app_label = 'transmittals'
        ordering = ('document_number',)
        verbose_name = _('Transmittal')
        verbose_name_plural = _('Transmittals')
        index_together = (
            ('contract_number', 'originator', 'recipient', 'sequential_number',
             'status'),
        )

    class PhaseConfig:
        filter_fields = (
            'originator', 'recipient', 'status',
        )
        column_fields = (
            ('Reference', 'document_number'),
            ('Transmittal date', 'transmittal_date'),
            ('Acknowledgment of receipt', 'ack_of_receipt_date'),
            ('Originator', 'originator'),
            ('Recipient', 'recipient'),
            ('Document type', 'document_type'),
            ('Status', 'status'),
        )

    def __str__(self):
        return self.document_key

    def save(self, *args, **kwargs):
        if not self.transmittal_key:
            if not self.document_key:
                self.document_key = self.generate_document_key()
            self.transmittal_key = self.document_key
        super(Transmittal, self).save(*args, **kwargs)

    @property
    def full_tobechecked_name(self):
        return os.path.join(self.tobechecked_dir, self.transmittal_key)

    @property
    def full_accepted_name(self):
        return os.path.join(self.accepted_dir, self.transmittal_key)

    @property
    def full_rejected_name(self):
        return os.path.join(self.rejected_dir, self.transmittal_key)

    def generate_document_key(self):
        key = '{}-{}-{}-TRS-{:0>5d}'.format(
            self.contract_number,
            self.originator,
            self.recipient,
            self.sequential_number)
        return key

    @property
    def title(self):
        return self.document_key

    @transaction.atomic
    def reject(self):
        """Mark the transmittal as rejected.

        Upon rejecting the transmittal, we must perform the following
        operations:

          - update the transmittal status in db
          - move the corresponding files in the correct "refused" directory
          - send the notifications to the email list.

        """
        # Only transmittals with a pending validation can be refused
        if self.status != 'tobechecked':
            error_msg = 'The transmittal {} cannot be rejected ' \
                        'it it\'s current status ({})'.format(
                            self.document_key, self.status)
            raise RuntimeError(error_msg)

        # If an existing version already exists in rejected, we delete it before
        if os.path.exists(self.full_rejected_name):
            # Let's hope we got correct data and the process does not run
            # as root. Who would do something that stupid anyway?
            logger.info('Deleteting directory {}'.format(self.full_rejected_name))
            shutil.rmtree(self.full_rejected_name)

        # Move to rejected directory
        if os.path.exists(self.full_tobechecked_name):
            try:
                os.rename(self.full_tobechecked_name, self.full_rejected_name)
            except OSError as e:
                logger.error('Cannot reject transmittal {} ({})'.format(
                    self, e))
                raise e
        else:
            # If the directory cannot be found in tobechecked, that's weird but we
            # won't trigger an error
            logger.warning('Transmittal {} files are gone'.format(self))

        # Since the document_key "unique" constraint is enforced in the parent
        # class (Metadata), we need to update this object's key to allow a
        # new transmittal submission with the same transmittal key.
        new_key = '{}-{}'.format(
            self.document_key,
            uuid.uuid4())

        self.document_key = new_key
        self.status = 'rejected'
        self.save()

        self.document.document_key = new_key
        self.document.save()

    def accept(self):
        """Starts the transmittal import process.

        Since the import can be quite long, we delegate the work to
        a celery task.

        """
        from transmittals.tasks import process_transmittal

        if self.status != 'tobechecked':
            error_msg = 'The transmittal {} cannot be accepted ' \
                        'in it\'s current state ({})'.format(
                            self.document_key, self.get_status_display())
            raise RuntimeError(error_msg)

        self.status = 'processing'
        self.save()

        process_transmittal.delay(self.pk)
Exemple #13
0
class TrsRevision(models.Model):
    """Stores data imported from a single line in the csv."""
    transmittal = models.ForeignKey(
        Transmittal,
        verbose_name=_('Transmittal'))
    document = models.ForeignKey(
        Document,
        null=True, blank=True,
        verbose_name=_('Document'))
    document_key = models.SlugField(
        _('Document number'),
        max_length=250)
    category = models.ForeignKey('categories.Category')
    title = models.TextField(
        verbose_name=_('Title'))
    revision = models.PositiveIntegerField(
        verbose_name=_('Revision'),
        default=0)
    revision_date = models.DateField(
        _('Revision date'),
        null=True, blank=True)
    received_date = models.DateField(
        _('Received date'),
        null=True, blank=True)
    created_on = models.DateField(
        _('Created on'),
        null=True, blank=True)
    accepted = models.NullBooleanField(
        verbose_name=_('Accepted?'))
    comment = models.TextField(
        verbose_name=_('Comment'),
        null=True, blank=True)
    is_new_revision = models.BooleanField(
        _('Is new revision?'))

    # We'll keep it for a while.
    # Those are fields that will one day be configurable
    # but are static for now.
    contract_number_old = ConfigurableChoiceField(
        verbose_name='Contract Number',
        max_length=8,
        list_index='CONTRACT_NBS',
        null=True,
        blank=True)
    contract_number = models.CharField(
        verbose_name='Contract Number',
        max_length=50)
    originator = models.ForeignKey(
        'accounts.Entity',
        verbose_name=_('Originator'))
    unit = ConfigurableChoiceField(
        verbose_name=_('Unit'),
        default='000',
        max_length=3,
        list_index='UNITS')
    discipline = ConfigurableChoiceField(
        verbose_name=_('Discipline'),
        default='PCS',
        max_length=3,
        list_index='DISCIPLINES')
    document_type = ConfigurableChoiceField(
        verbose_name=_('Document Type'),
        default='PID',
        max_length=3,
        list_index='DOCUMENT_TYPES')
    sequential_number = models.CharField(
        verbose_name="sequential Number",
        help_text=_('Select a four digit number'),
        default="0001",
        max_length=4,
        validators=[StringNumberValidator(4)],
        null=True, blank=True)
    system = ConfigurableChoiceField(
        _('System'),
        list_index='SYSTEMS',
        null=True, blank=True)
    wbs = ConfigurableChoiceField(
        _('Wbs'),
        max_length=20,
        list_index='WBS',
        null=True, blank=True)
    weight = models.IntegerField(
        _('Weight'),
        null=True, blank=True)
    docclass = models.IntegerField(
        verbose_name=_('Class'),
        default=1,
        choices=CLASSES)
    status = ConfigurableChoiceField(
        verbose_name=_('Status'),
        default='STD',
        max_length=3,
        list_index='STATUSES',
        null=True, blank=True)
    return_code = models.PositiveIntegerField(
        _('Return code'),
        null=True, blank=True)
    review_start_date = models.DateField(
        _('Review start date'),
        null=True, blank=True)
    review_due_date = models.DateField(
        _('Review due date'),
        null=True, blank=True)
    review_leader = models.CharField(
        _('Review leader'),
        max_length=150,
        null=True, blank=True)
    leader_comment_date = models.DateField(
        _('Leader comment date'),
        null=True, blank=True)
    review_approver = models.CharField(
        _('Review approver'),
        max_length=150,
        null=True, blank=True)
    approver_comment_date = models.DateField(
        _('Approver comment date'),
        null=True, blank=True)
    review_trs = models.CharField(
        verbose_name=_('Review transmittal name'),
        max_length=255,
        null=True, blank=True)
    review_trs_status = models.CharField(
        verbose_name=_('Review transmittal status'),
        max_length=50,
        null=True, blank=True)
    outgoing_trs = models.CharField(
        verbose_name=_('Outgoing transmittal name'),
        max_length=255,
        null=True, blank=True)
    outgoing_trs_status = models.CharField(
        verbose_name=_('Outgoing transmittal status'),
        max_length=50,
        null=True, blank=True)
    outgoing_trs_sent_date = models.DateField(
        verbose_name=_('Outgoing transmittal sent date'),
        null=True, blank=True)
    doc_category = models.CharField(
        max_length=50,
        verbose_name=_('Doc category'))
    pdf_file = TransmittalFileField(
        verbose_name=_('Pdf file'))
    native_file = TransmittalFileField(
        verbose_name=_('Native file'),
        null=True, blank=True, max_length=255)

    class Meta:
        app_label = 'transmittals'
        verbose_name = _('Trs Revision')
        verbose_name_plural = _('Trs Revisions')
        unique_together = ('transmittal', 'document_key', 'revision')

    def __str__(self):
        return '{} ({:02d})'.format(self.document_key, self.revision)

    def get_absolute_url(self):
        return reverse('transmittal_revision_diff', args=[
            self.transmittal.pk, self.transmittal.document_key,
            self.document_key, self.revision])

    def get_document_fields(self):
        """Return a dict of fields that will be passed to the document form."""
        columns = self.category.get_transmittal_columns()
        fields = list(columns.values())
        fields_dict = dict([(field, getattr(self, field)) for field in fields])

        # XXX
        # This is a HACK and should be refactored somehow
        fields_dict.update({
            'originator': self.originator.id,
            'sequential_number': '{:04}'.format(int(self.sequential_number))
        })

        files_dict = {
            'native_file': self.native_file,
            'pdf_file': self.pdf_file}

        return fields_dict, files_dict

    def save_to_document(self):
        """Use self data to create / update the corresponding revision."""

        fields, files = self.get_document_fields()
        kwargs = {
            'category': self.category,
            'data': fields,
            'files': files
        }

        # The document was created earlier during
        # the batch import
        if self.document is None and self.revision > 0:
            self.document = Document.objects.get(document_key=self.document_key)

        metadata = getattr(self.document, 'metadata', None)
        kwargs.update({'instance': metadata})
        Form = self.category.get_metadata_form_class()
        metadata_form = Form(**kwargs)

        # If there is no such revision, the method will return None
        # which is fine.
        revision = metadata.get_revision(self.revision) if metadata else None

        kwargs.update({'instance': revision})
        RevisionForm = self.category.get_revision_form_class()
        revision_form = RevisionForm(**kwargs)

        doc, meta, rev = save_document_forms(
            metadata_form, revision_form, self.category)

        # Performs custom import action
        rev.post_trs_import(self)
Exemple #14
0
class Review(models.Model):
    # Yes, two statuses with the same label.
    # See Trello#173
    STATUSES = Choices(
        ('void', ''),  # Only for dummy review in document form
        ('pending', _('Pending')),
        ('progress', _('In progress')),
        ('reviewed', _('Reviewed')),
        ('commented', _('Reviewed')),
        ('not_reviewed', _('Not reviewed')),
    )
    STEPS = Choices(
        ('pending', ''),
        ('reviewer', _('Reviewer')),
        ('leader', _('Leader')),
        ('approver', _('Approver')),
        ('closed', _('Closed')),
    )
    ROLES = Choices(
        ('reviewer', _('Reviewer')),
        ('leader', _('Leader')),
        ('approver', _('Approver')),
    )

    reviewer = models.ForeignKey(
        User,
        verbose_name=_('User'),
    )
    role = models.CharField(_('Role'),
                            max_length=8,
                            choices=ROLES,
                            default=ROLES.reviewer)
    document = models.ForeignKey(Document, verbose_name=_('Document'))
    revision = models.PositiveIntegerField(_('Revision'))
    received_date = models.DateField(_('Review received date'),
                                     null=True,
                                     blank=True)
    start_date = models.DateField(_('Review start date'),
                                  null=True,
                                  blank=True)
    due_date = models.DateField(_('Review due date'), null=True, blank=True)
    docclass = models.IntegerField(verbose_name=u"Class",
                                   default=1,
                                   choices=CLASSES)
    status = models.CharField(_('Status'),
                              max_length=30,
                              choices=STATUSES,
                              default=STATUSES.pending)
    revision_status = models.CharField(_('Revision status'),
                                       max_length=30,
                                       null=True,
                                       blank=True)
    closed_on = models.DateTimeField(_('Closed on'), null=True, blank=True)
    amended_on = models.DateTimeField(_('Amended on'), null=True, blank=True)
    comments = PrivateFileField(_('Comments'),
                                null=True,
                                blank=True,
                                upload_to=review_comments_file_path)
    return_code = ConfigurableChoiceField(_('Return code'),
                                          max_length=3,
                                          null=True,
                                          blank=True,
                                          list_index='REVIEW_RETURN_CODES')
    created_on = models.DateTimeField(_('Created on'), default=timezone.now)

    class Meta:
        verbose_name = _('Review')
        verbose_name_plural = _('Reviews')
        index_together = (('reviewer', 'document', 'revision', 'role'), )
        unique_together = ('reviewer', 'document', 'revision')
        app_label = 'reviews'

    def save(self, *args, **kwargs):
        cache_key = 'all_reviews_{}'.format(self.document_id)
        cache.delete(cache_key)
        super(Review, self).save(*args, **kwargs)

    @property
    def revision_name(self):
        return '%02d' % self.revision

    def post_review(self, comments, return_code=None, save=True):
        self.comments = comments
        self.return_code = return_code

        if self.closed_on is None:
            self.closed_on = timezone.now()
        else:
            self.amended_on = timezone.now()

        if comments:
            self.status = self.STATUSES.commented
        else:
            self.status = self.STATUSES.reviewed

        if save:
            self.save()

    def is_overdue(self):
        """Tells if the review is overdue.

        A review is overdue only if it's ongoing (ended reviews cannot
        be overdue) and the due date is past.

        """
        today = timezone.now().date()
        return self.due_date < today and self.closed_on is None

    def days_of_delay(self):
        """Gets the number of days between the due date and the review end.

        If the review has ended, returns delay between the due date and
        the completion date.

        If the review is ongoing, returns delay between the due date and the
        present day.

        """
        if self.closed_on:
            checked_date = self.closed_on
        else:
            checked_date = timezone.now().date()

        delta = checked_date - self.due_date
        return delta.days

    def get_comments_url(self):
        return reverse(
            'download_review_comments',
            args=[self.document.document_key, self.revision, self.id])
Exemple #15
0
class ReviewMixin(models.Model):
    """A Mixin to use to define reviewable document types.
    The review duration is configurable via a tuple matching the CLASSES tuple.

    The duration is extracted.
    REVIEW_DURATIONS = (
        (1, 4),
        (2, 8),
        (3, 13),
        (4, 13),
    )
    e.g: if docclass field value is 2 then duration is 8, etc.
    """

    review_start_date = models.DateField(_('Review start date'),
                                         null=True,
                                         blank=True)
    review_due_date = models.DateField(_('Review due date'),
                                       null=True,
                                       blank=True)
    reviewers_step_closed = models.DateField(_('Reviewers step closed'),
                                             null=True,
                                             blank=True)
    leader_step_closed = models.DateField(_('Leader step closed'),
                                          null=True,
                                          blank=True)
    review_end_date = models.DateField(_('Review end date'),
                                       null=True,
                                       blank=True)
    reviewers = models.ManyToManyField(User,
                                       verbose_name=_('Reviewers'),
                                       blank=True)
    leader = models.ForeignKey(
        User,
        verbose_name=_('Leader'),
        related_name='%(app_label)s_%(class)s_related_leader',
        null=True,
        blank=True)
    approver = models.ForeignKey(
        User,
        verbose_name=_('Approver'),
        related_name='%(app_label)s_%(class)s_related_approver',
        null=True,
        blank=True)
    docclass = models.IntegerField(verbose_name=u"Class",
                                   default=1,
                                   choices=CLASSES)
    return_code = ConfigurableChoiceField(_('Return code'),
                                          max_length=3,
                                          null=True,
                                          blank=True,
                                          list_index='REVIEW_RETURN_CODES')

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        cache_key = 'all_reviews_{}'.format(self.metadata.document_id)
        cache.delete(cache_key)
        super(ReviewMixin, self).save(*args, **kwargs)

    @cached_property
    def can_be_reviewed(self):
        """Is this revision ready to be reviewed.

        A revision can only be reviewed if at least a leader was defined.

        Also, a revision can only be reviewed once.

        """
        return all(
            (self.received_date, self.leader_id, not self.review_start_date))

    @transaction.atomic
    def start_review(self, at_date=None, due_date=None):
        """Starts the review process.

        This methods initiates the review process. We don't check whether the
        document can be reviewed or not, or if the process was already
        initiated. It's up to the developer to perform those checks before
        calling this method.

        """
        start_date = at_date or timezone.now()
        self.review_start_date = start_date

        duration = self.get_review_duration()
        self.review_due_date = due_date or \
            self.received_date + \
            datetime.timedelta(days=duration)

        reviewers = self.reviewers.all()
        for reviewer in reviewers:
            Review.objects.create(reviewer=reviewer,
                                  document=self.document,
                                  revision=self.revision,
                                  received_date=self.received_date,
                                  start_date=start_date,
                                  due_date=self.review_due_date,
                                  docclass=self.docclass,
                                  status='progress',
                                  revision_status=self.status)

        # If no reviewers, close reviewers step immediatly
        if len(reviewers) == 0:
            self.reviewers_step_closed = start_date
            leader_review_status = 'progress'
        else:
            leader_review_status = 'pending'

        # Leader is mandatory, no need to test it
        Review.objects.create(reviewer_id=self.leader_id,
                              role=Review.ROLES.leader,
                              document=self.document,
                              revision=self.revision,
                              received_date=self.received_date,
                              start_date=start_date,
                              due_date=self.review_due_date,
                              status=leader_review_status,
                              docclass=self.docclass,
                              revision_status=self.status)

        # Approver is not mandatory
        if self.approver_id:
            Review.objects.create(reviewer_id=self.approver_id,
                                  role=Review.ROLES.approver,
                                  document=self.document,
                                  revision=self.revision,
                                  received_date=self.received_date,
                                  start_date=start_date,
                                  due_date=self.review_due_date,
                                  status=leader_review_status,
                                  docclass=self.docclass,
                                  revision_status=self.status)

        self.reload_reviews()
        self.save(update_document=True)

    @transaction.atomic
    def cancel_review(self):
        """Stops the review process.

        This methods reverts the "start_review" process. It simply deletes all
        data related to the current review, and leaves the document in the
        state it was before starting the review.

        This method can cause data loss.

        """
        Review.objects \
            .filter(document=self.document) \
            .filter(revision=self.revision) \
            .delete()

        self.review_start_date = None
        self.review_due_date = None
        self.review_end_date = None
        self.reviewers_step_closed = None
        self.leader_step_closed = None
        self.save(update_document=True)

        from reviews.signals import review_canceled
        review_canceled.send(sender=self.__class__, instance=self)

    @transaction.atomic
    def end_reviewers_step(self, at_date=None, save=True):
        """Ends the first step of the review."""
        end_date = at_date or timezone.now()
        self.reviewers_step_closed = end_date

        Review.objects \
            .filter(document=self.document) \
            .filter(revision=self.revision) \
            .filter(role=Review.ROLES.reviewer) \
            .filter(closed_on=None) \
            .update(closed_on=end_date, status='not_reviewed')

        Review.objects \
            .filter(document=self.document) \
            .filter(revision=self.revision) \
            .filter(role=Review.ROLES.leader) \
            .update(status='progress')

        self.reload_reviews()
        if save:
            self.save(update_document=True)

    @transaction.atomic
    def end_leader_step(self, at_date=None, save=True):
        """Ends the second step of the review.

        Also ends the first step if it wasn't already done.

        """
        if self.reviewers_step_closed is None:
            self.end_reviewers_step(save=False, at_date=at_date)

        end_date = at_date or timezone.now()

        self.leader_step_closed = end_date

        Review.objects \
            .filter(document=self.document) \
            .filter(revision=self.revision) \
            .filter(role=Review.ROLES.leader) \
            .filter(closed_on=None) \
            .update(closed_on=end_date, status='not_reviewed')

        Review.objects \
            .filter(document=self.document) \
            .filter(revision=self.revision) \
            .filter(role=Review.ROLES.approver) \
            .update(status='progress')

        if not self.approver_id:
            self.review_end_date = end_date

        self.reload_reviews()
        if save:
            self.save(update_document=True)

    @transaction.atomic
    def send_back_to_leader_step(self, save=True):
        """Send the review back to the leader step."""
        self.leader_step_closed = None

        Review.objects \
            .filter(document=self.document) \
            .filter(revision=self.revision) \
            .filter(role=Review.ROLES.leader) \
            .update(closed_on=None, status='progress')

        self.reload_reviews()
        if save:
            self.save(update_document=True)

    @transaction.atomic
    def end_review(self, at_date=None, save=True):
        """Ends the review.

        Also ends the steps before.

        """
        if self.leader_step_closed is None:
            self.end_leader_step(save=False, at_date=at_date)

        end_date = at_date or timezone.now()
        self.review_end_date = end_date

        Review.objects \
            .filter(document=self.document) \
            .filter(revision=self.revision) \
            .filter(role=Review.ROLES.approver) \
            .filter(closed_on=None) \
            .update(closed_on=end_date, status='not_reviewed')

        self.reload_reviews()
        if save:
            self.save(update_document=True)

    @transaction.atomic
    def sync_reviews(self):
        """Update Review objects so it's coherent with current object state.

        If the distribution list (reviewers, leader, approver) was modified in
        the document form, the corresponding Review objects must be created /
        deleted to stay in sync.

        """
        # Sync leader
        leader_review = self.get_leader_review()
        if leader_review.reviewer_id != self.leader_id:
            leader_review.reviewer = self.leader
            leader_review.save()

        # Sync approver
        # Several cases here
        #  * an existing approver review was deleted
        #  * an existing approver review was modified
        #  * an approver review was created
        approver_review = self.get_approver_review()
        if approver_review:
            if approver_review.reviewer_id != self.approver_id:
                # A new approver was submitted
                if self.approver:
                    approver_review.reviewer = self.approver
                    approver_review.save()
                # The approver was deleted
                else:
                    approver_review.delete()
                    # If we were at the last review step, end the review completely
                    if self.is_at_review_step(Review.STEPS.approver):
                        self.end_review()
        else:
            if self.approver:
                Review.objects.create(reviewer=self.approver,
                                      role=Review.ROLES.approver,
                                      document=self.document,
                                      revision=self.revision,
                                      received_date=self.received_date,
                                      start_date=self.review_start_date,
                                      due_date=self.review_due_date,
                                      docclass=self.docclass,
                                      status='pending',
                                      revision_status=self.status)

        # Sync reviewers
        old_reviews = self.get_reviewers_reviews()
        old_reviewers = set(review.reviewer for review in old_reviews)
        current_reviewers = set(self.reviewers.all())

        # Create Review objects for new reviewers
        new_reviewers = current_reviewers - old_reviewers
        for reviewer in new_reviewers:
            Review.objects.create(reviewer=reviewer,
                                  document=self.document,
                                  revision=self.revision,
                                  received_date=self.received_date,
                                  start_date=self.review_start_date,
                                  due_date=self.review_due_date,
                                  docclass=self.docclass,
                                  status='progress',
                                  revision_status=self.status)

        # Remove Review objects for deleted reviewers
        deleted_reviewers = old_reviewers - current_reviewers
        if len(deleted_reviewers) > 0:
            for reviewer in deleted_reviewers:
                review = self.get_review(reviewer)
                # Check that we only delete review with no comments
                # This condition is enforced in the ReviewMixinForm anyway
                if review.status != 'progress':
                    raise RuntimeError('Cannot delete a review with comments')
                review.delete()

            # Should we end the reviewers step?
            waiting_reviews = self.get_filtered_reviews(
                lambda rev: rev.id and rev.role == 'reviewer' and rev.status ==
                'progress')
            if len(waiting_reviews) == 0:
                self.end_reviewers_step()

        self.reload_reviews()

    def is_under_review(self):
        """It's under review only if review has started but not ended."""
        return bool(self.review_start_date) != bool(self.review_end_date)

    is_under_review.short_description = _('Under review')

    def is_overdue(self):
        """Tells if the review is overdue.

        A review is overdue only if it's ongoing (ended reviews cannot
        be overdue) and the due date is past.

        """
        today = timezone.now().date()
        return self.is_under_review() and self.review_due_date < today

    is_overdue.short_description = _('Overdue')

    def current_review_step(self):
        """Return a string representing the current step."""
        if self.review_start_date is None:
            return Review.STEPS.pending

        if self.reviewers_step_closed is None:
            return Review.STEPS.reviewer

        if self.leader_step_closed is None:
            return Review.STEPS.leader

        if self.review_end_date is None:
            return Review.STEPS.approver

        return Review.STEPS.closed

    current_review_step.short_description = _('Current review step')

    def get_current_review_step_display(self):
        step = self.current_review_step()
        return dict(Review.STEPS)[step]

    get_current_review_step_display.short_description = _(
        'Current review step')

    def is_at_review_step(self, step):
        return step == self.current_review_step()

    def get_reviews(self):
        """Get all reviews associated with this revision."""
        if not hasattr(self, '_reviews'):
            qs = Review.objects \
                .filter(document=self.document) \
                .filter(revision=self.revision) \
                .order_by('id') \
                .select_related('reviewer')
            self._reviews = qs
        return self._reviews

    def reload_reviews(self):
        """Reload the review cache."""
        if hasattr(self, '_reviews'):
            del self._reviews

    def get_review_duration(self):
        """We can define `REVIEW_DURATIONS` tuple in class attributes. Then we
         return the duration matched by `docclass` field. If `REVIEW_DURATIONS`
          is not present, the global value `REVIEW_DURATION` is in settings."""

        # If REVIEW_DURATIONS is not defined we get the settings value
        if not hasattr(self, 'REVIEW_DURATIONS'):
            return settings.REVIEW_DURATION

        review_durations = dict(self.REVIEW_DURATIONS)

        # We try to get the duration from the tuple
        review_duration = review_durations.get(self.docclass, None)
        if review_duration is None:
            raise ImproperlyConfigured(
                'Define {0}.REVIEW_DURATIONS to match reviews.models.CLASSES'
                'or define settings.LOGIN_URL or '
                'override {0}.get_review_duration().'.format(
                    self.__class__.__name__))
        return review_duration

    def get_review(self, user):
        """Get the review from this specific user."""
        reviews = self.get_reviews()
        rev = next((rev for rev in reviews if rev.reviewer == user), None)
        return rev

    def get_filtered_reviews(self, filter):
        reviews = self.get_reviews()
        filtered = [rev for rev in reviews if filter(rev)]
        return filtered

    def get_reviewers_reviews(self):
        reviews = self.get_reviews()
        return [rev for rev in reviews if rev.role == 'reviewer']

    def get_leader_review(self):
        reviews = self.get_reviews()
        rev = next((rev for rev in reviews if rev.role == 'leader'), None)
        return rev

    def get_approver_review(self):
        reviews = self.get_reviews()
        rev = next((rev for rev in reviews if rev.role == 'approver'), None)
        return rev

    def is_reviewer(self, user):
        return user in self.reviewers.all()

    def get_initial_ignored_fields(self):
        ignored = (
            'review_start_date',
            'review_due_date',
        )
        return super(ReviewMixin, self).get_initial_ignored_fields() + ignored

    def get_new_revision_initial(self, form):
        initial = super(ReviewMixin, self).get_new_revision_initial(form)
        initial.update({
            'leader':
            self.leader_id,
            'approver':
            self.approver_id,
            'reviewers':
            self.reviewers.values_list('id', flat=True),
        })

        return initial

    def post_trs_import(self, trs_revision):
        """See `documents.models.MetadataRevision.post_trs_import`

        If we are importing a revision with review data, we need to make sure
        Phase objects are left in a consistent state.

        We need to create `Review` objects if the leader and approver review
        data is set in the trs_revision object.

        """
        category = self.document.category
        user_qs = User.objects.filter(categories=category)

        # Is there a defined leader?
        if trs_revision.review_leader:
            self.leader = user_qs.get(name=trs_revision.review_leader)

        # Is there a defined approver
        if trs_revision.review_approver:
            self.approver = user_qs.get(name=trs_revision.review_approver)

        # Was the review started?
        if trs_revision.review_start_date:
            self.start_review(at_date=trs_revision.review_start_date,
                              due_date=trs_revision.review_due_date)

        # Did the leader already submit a comment?
        if trs_revision.leader_comment_date:
            self.end_leader_step(at_date=trs_revision.leader_comment_date,
                                 save=False)

        # Is there an approver and did he submit a comment?
        if self.approver_id and trs_revision.approver_comment_date:
            self.end_review(at_date=trs_revision.approver_comment_date,
                            save=False)

        self.save()

    def detail_view_context(self, request):
        """@see `MetadataRevision.detail_view_context`"""
        context = super(ReviewMixin, self).detail_view_context(request)
        user_review = self.get_review(request.user)
        review_closed_on = user_review.closed_on if user_review else None
        context.update({
            'user_review': user_review,
            'review_closed_on': review_closed_on
        })
        return context

    def get_review_fields(self):
        """Return data to display on the review form."""
        fields = [
            (_('Category'), self.document.category),
            (_('Document number'), self.document.document_number),
            (_('Title'), self.document.title),
            (_('Revision'), self.name),
            (_('Status'), self.status),
        ]
        return fields

    def get_actions(self, metadata, user):
        actions = super(ReviewMixin, self).get_actions(metadata, user)
        category = self.document.category

        if self.is_under_review():

            if user.has_perm('documents.can_control_document'):
                actions.insert(
                    -3,
                    MenuItem('cancel-review',
                             _('Cancel review'),
                             reverse('document_cancel_review',
                                     args=[self.document.document_key]),
                             modal='cancel-review-modal'))

            user_review = self.get_review(user)
            review_closed_on = user_review.closed_on if user_review else None
            if review_closed_on:
                actions.insert(
                    -3,
                    MenuItem(
                        'update-comment',
                        _('Modify your comment'),
                        reverse('review_document',
                                args=[self.document.document_key]),
                        method='GET',
                    ))

        else:  # revision is not under review
            if self.can_be_reviewed and \
                    user.has_perm('documents.can_control_document'):

                actions.insert(
                    -3,
                    MenuItem(
                        'start-review',
                        _('Start review'),
                        reverse('document_start_review',
                                args=[
                                    category.organisation.slug, category.slug,
                                    self.document.document_key
                                ]),
                    ))

                actions.insert(
                    -3,
                    MenuItem('start-review-remark',
                             _('Start review w/ remark'),
                             reverse('document_start_review',
                                     args=[
                                         category.organisation.slug,
                                         category.slug,
                                         self.document.document_key
                                     ]),
                             modal='start-comment-review'))

        return actions