Пример #1
0
class NoteSerializer(serializers.ModelSerializer):
    """
    Serializer for the contact model.
    """
    # Show string versions of fields.
    author = serializers.StringRelatedField(read_only=True)
    content_type = serializers.PrimaryKeyRelatedField(
        queryset=ContentType.objects.filter(model__in=NOTABLE_MODELS),
        write_only=True)
    object_id = serializers.IntegerField(write_only=True)
    content = SanitizedHtmlCharField()

    def create(self, validated_data):
        user = self.context.get('request').user

        validated_data.update({
            'author_id': user.pk,
        })

        return super(NoteSerializer, self).create(validated_data)

    class Meta:
        model = Note
        fields = (
            'id',
            'author',
            'content',
            'content_type',
            'created',
            'is_pinned',
            'modified',
            'object_id',
            'type',
        )
Пример #2
0
class NoteSerializer(serializers.ModelSerializer):
    """
    Serializer for the note model.
    """
    # Show string versions of fields.
    content_type = ContentTypeSerializer(read_only=True)
    author = serializers.SerializerMethodField()
    gfk_content_type = serializers.PrimaryKeyRelatedField(
        queryset=ContentType.objects.filter(model__in=NOTABLE_MODELS),
        write_only=True,
        help_text='Content type of the object the note is linked to.',
    )
    gfk_object_id = serializers.IntegerField(
        write_only=True,
        help_text='ID of the object the note is linked to.',
    )
    content = SanitizedHtmlCharField(
        required=True,
        help_text='The actual contents of the note (supports Markdown).',
    )

    def create(self, validated_data):
        user = self.context.get('request').user

        validated_data.update({
            'author_id': user.pk,
        })

        return super(NoteSerializer, self).create(validated_data)

    def get_author(self, obj):
        return {
            'id': obj.author.id,
            'full_name': obj.author.full_name,
            'profile_picture': obj.author.profile_picture,
        }

    class Meta:
        model = Note
        fields = (
            'id',
            'author',
            'content',
            'content_type',
            'created',
            'gfk_content_type',
            'gfk_object_id',
            'is_pinned',
            'modified',
            'type',
        )
        extra_kwargs = {
            'is_pinned': {
                'help_text':
                'If true the note will show at the top of the activity stream.',
            },
        }
Пример #3
0
class AccountSerializer(WritableNestedSerializer):
    """
    Serializer for the account model.
    """
    # Read only fields.
    content_type = ContentTypeSerializer(read_only=True)
    flatname = serializers.CharField(read_only=True)
    contacts = ContactForAccountSerializer(many=True, read_only=True)

    # Related fields
    addresses = RelatedAddressSerializer(many=True, required=False, create_only=True)
    assigned_to = RelatedLilyUserSerializer(required=False, assign_only=True)
    email_addresses = RelatedEmailAddressSerializer(many=True, required=False, create_only=True)
    phone_numbers = RelatedPhoneNumberSerializer(many=True, required=False, create_only=True)
    social_media = RelatedSocialMediaSerializer(many=True, required=False, create_only=True)
    websites = RelatedWebsiteSerializer(many=True, required=False, create_only=True)
    status = RelatedAccountStatusSerializer(assign_only=True)
    tags = RelatedTagSerializer(many=True, required=False, create_only=True)

    # Custom fields.
    name = serializers.CharField(validators=[DuplicateAccountName()])
    description = SanitizedHtmlCharField()

    class Meta:
        model = Account
        fields = (
            'addresses',
            'assigned_to',
            'bankaccountnumber',
            'bic',
            'cocnumber',
            'contacts',
            'content_type',
            'customer_id',
            'description',
            'email_addresses',
            'flatname',
            'iban',
            'legalentity',
            # 'logo',
            'id',
            'modified',
            'name',
            'phone_numbers',
            'social_media',
            'status',
            'tags',
            'taxnumber',
            'websites',
        )
Пример #4
0
class AccountSerializer(PhoneNumberFormatMixin, WritableNestedSerializer):
    """
    Serializer for the account model.
    """
    # Read only fields.
    content_type = ContentTypeSerializer(read_only=True)
    flatname = serializers.CharField(read_only=True)
    contacts = ContactForAccountSerializer(many=True, read_only=True)

    # Related fields
    addresses = RelatedAddressSerializer(many=True,
                                         required=False,
                                         create_only=True)
    assigned_to = RelatedLilyUserSerializer(required=False, assign_only=True)
    email_addresses = RelatedEmailAddressSerializer(many=True,
                                                    required=False,
                                                    create_only=True)
    phone_numbers = RelatedPhoneNumberSerializer(many=True,
                                                 required=False,
                                                 create_only=True)
    social_media = RelatedSocialMediaSerializer(many=True,
                                                required=False,
                                                create_only=True)
    websites = RelatedWebsiteSerializer(many=True,
                                        required=False,
                                        create_only=True)
    status = RelatedAccountStatusSerializer(assign_only=True)
    tags = RelatedTagSerializer(many=True, required=False, create_only=True)

    # Custom fields.
    name = serializers.CharField(validators=[DuplicateAccountName()])
    description = SanitizedHtmlCharField()

    def create(self, validated_data):
        tenant = self.context.get('request').user.tenant
        account_count = Account.objects.filter(is_deleted=False).count()

        if tenant.billing.is_free_plan and account_count >= settings.FREE_PLAN_ACCOUNT_CONTACT_LIMIT:
            raise serializers.ValidationError({
                'limit_reached':
                _('You\'ve reached the limit of accounts for the free plan.'),
            })

        instance = super(AccountSerializer, self).create(validated_data)

        return instance

    class Meta:
        model = Account
        fields = (
            'id',
            'addresses',
            'assigned_to',
            'bankaccountnumber',
            'bic',
            'cocnumber',
            'contacts',
            'content_type',
            'customer_id',
            'description',
            'email_addresses',
            'flatname',
            'iban',
            'is_deleted',
            'legalentity',
            # 'logo',
            'modified',
            'name',
            'phone_numbers',
            'social_media',
            'status',
            'tags',
            'taxnumber',
            'websites',
        )
        read_only_fields = ('is_deleted', )
Пример #5
0
class ContactSerializer(WritableNestedSerializer):
    """
    Serializer for the contact model.
    """
    # Custom fields.
    description = SanitizedHtmlCharField()

    # Set non mutable fields.
    created = serializers.DateTimeField(read_only=True)
    modified = serializers.DateTimeField(read_only=True)
    full_name = serializers.CharField(read_only=True)
    content_type = ContentTypeSerializer(read_only=True)

    # Related fields.
    phone_numbers = RelatedPhoneNumberSerializer(many=True,
                                                 required=False,
                                                 create_only=True)
    addresses = RelatedAddressSerializer(many=True,
                                         required=False,
                                         create_only=True)
    email_addresses = RelatedEmailAddressSerializer(many=True,
                                                    required=False,
                                                    create_only=True)
    social_media = RelatedSocialMediaSerializer(many=True,
                                                required=False,
                                                create_only=True)
    accounts = RelatedAccountSerializer(many=True, required=False)
    tags = RelatedTagSerializer(many=True, required=False, create_only=True)

    # Show string versions of fields.
    gender_display = serializers.CharField(source='get_gender_display',
                                           read_only=True)
    salutation_display = serializers.CharField(source='get_salutation_display',
                                               read_only=True)

    class Meta:
        model = Contact
        fields = (
            'id',
            'accounts',
            'addresses',
            'content_type',
            'created',
            'description',
            'email_addresses',
            'first_name',
            'full_name',
            'gender',
            'gender_display',
            'last_name',
            'modified',
            'phone_numbers',
            'salutation',
            'salutation_display',
            'social_media',
            'tags',
            'title',
        )

    def validate(self, data):
        if not isinstance(data, dict):
            data = {'id': data}

        # Check if we are related and if we only passed in the id, which means user just wants new reference.
        if not (len(data) == 1 and 'id' in data
                and hasattr(self, 'is_related_serializer')):
            if not self.partial:
                first_name = data.get('first_name', None)
                last_name = data.get('last_name', None)

                # Not just a new reference, so validate if contact is set properly.
                if not any([first_name, last_name]):
                    raise serializers.ValidationError({
                        'first_name':
                        _('Please enter a valid first name.'),
                        'last_name':
                        _('Please enter a valid last name.')
                    })

        return super(ContactSerializer, self).validate(data)

    def create(self, validated_data):
        instance = super(ContactSerializer, self).create(validated_data)

        credentials = get_credentials('moneybird')

        if credentials and credentials.integration_context.get('auto_sync'):
            self.send_moneybird_contact(validated_data, instance, credentials)

        return instance

    def update(self, instance, validated_data):
        # Save the current data for later use.
        original_data = {
            'full_name': instance.full_name,
        }

        email_addresses = instance.email_addresses.all()

        if len(email_addresses) == 1:
            original_data.update(
                {'original_email_address': email_addresses[0].email_address})

        instance = super(ContactSerializer,
                         self).update(instance, validated_data)

        credentials = get_credentials('moneybird')

        if credentials and credentials.integration_context.get('auto_sync'):
            self.send_moneybird_contact(validated_data, instance, credentials,
                                        original_data)

        return instance

    def send_moneybird_contact(self,
                               validated_data,
                               instance,
                               credentials,
                               original_data=None):
        administration_id = credentials.integration_context.get(
            'administration_id')
        contact_url = 'https://moneybird.com/api/v2/%s/contacts'

        if original_data:
            full_name = original_data.get('full_name')
        else:
            full_name = instance.full_name

        search_url = (contact_url + '?query=%s') % (administration_id,
                                                    full_name)
        response = send_get_request(search_url, credentials)
        data = response.json()

        patch = False
        params = {}

        if data:
            data = data[0]
            moneybird_id = data.get('id')
            post_url = (contact_url + '/%s') % (administration_id,
                                                moneybird_id)

            params = {
                'id': moneybird_id,
            }

            # Existing Moneybird contact found so we want to PATCH.
            patch = True
        else:
            post_url = contact_url % administration_id

        if 'first_name' in validated_data:
            params.update({'firstname': validated_data.get('first_name')})

        if 'last_name' in validated_data:
            params.update({'lastname': validated_data.get('last_name')})

        accounts = instance.accounts.all()

        if 'accounts' in validated_data and len(accounts) == 1:
            params.update({'company_name': accounts[0].name})

        if 'phone_numbers' in validated_data:
            phone_numbers = []

            for validated_number in validated_data.get('phone_numbers'):
                for phone_number in instance.phone_numbers.all():
                    if validated_number.get('number') == phone_number.number:
                        phone_numbers.append(phone_number)
                        break

            if phone_numbers:
                params.update({'phone': phone_numbers[0].number})

        if 'addresses' in validated_data:
            addresses = []

            for validated_address in validated_data.get('addresses'):
                for address in instance.addresses.all():
                    if validated_address.get('address') == address.address:
                        addresses.append(address)
                        break

            if addresses:
                address = addresses[0]
                params.update({
                    'address1': address.get('address'),
                    'zipcode': address.get('postal_code'),
                    'city': address.get('city'),
                    'country': address.get('country'),
                })

        if 'email_addresses' in validated_data:
            validated_email_addresses = validated_data.get('email_addresses')
            original_email_address = original_data.get(
                'original_email_address')

            if len(validated_email_addresses) == 1 and original_email_address:
                if data:
                    invoices_email = data.get('send_invoices_to_email')
                    estimates_email = data.get('send_estimates_to_email')
                    validated_email_address = validated_email_addresses[0].get(
                        'email_address')

                    if invoices_email == estimates_email and invoices_email == original_email_address:
                        params.update({
                            'send_invoices_to_email':
                            validated_email_address,
                            'send_estimates_to_email':
                            validated_email_address,
                        })
                    elif invoices_email == original_email_address:
                        params.update({
                            'send_invoices_to_email':
                            validated_email_address,
                        })
                    elif estimates_email == original_email_address:
                        params.update({
                            'send_estimates_to_email':
                            validated_email_address,
                        })

        params = {'contact': params, 'administration_id': administration_id}

        response = send_post_request(post_url, credentials, params, patch,
                                     True)
Пример #6
0
class CaseSerializer(WritableNestedSerializer):
    """
    Serializer for the case model.
    """
    # Set non mutable fields.
    created = serializers.DateTimeField(read_only=True)
    created_by = RelatedLilyUserSerializer(read_only=True)
    modified = serializers.DateTimeField(read_only=True)
    content_type = ContentTypeSerializer(read_only=True)

    # Custom fields.
    description = SanitizedHtmlCharField()

    # Related fields.
    account = RelatedAccountSerializer(required=False, allow_null=True)
    contact = RelatedContactSerializer(required=False, allow_null=True)
    assigned_to = RelatedLilyUserSerializer(required=False,
                                            allow_null=True,
                                            assign_only=True)
    assigned_to_teams = RelatedTeamSerializer(many=True,
                                              required=False,
                                              assign_only=True)
    type = RelatedCaseTypeSerializer(assign_only=True)
    status = RelatedCaseStatusSerializer(assign_only=True)
    tags = RelatedTagSerializer(many=True, required=False, create_only=True)

    # Show string versions of fields.
    priority_display = serializers.CharField(source='get_priority_display',
                                             read_only=True)

    def validate(self, attrs):
        contact_id = attrs.get('contact', {})
        if isinstance(contact_id, dict):
            contact_id = contact_id.get('id')

        account_id = attrs.get('account', {})
        if isinstance(account_id, dict):
            account_id = account_id.get('id')

        if contact_id and account_id:
            if not Function.objects.filter(contact_id=contact_id,
                                           account_id=account_id).exists():
                raise serializers.ValidationError(
                    {'contact': _('Given contact must work at the account.')})

        return super(CaseSerializer, self).validate(attrs)

    def create(self, validated_data):
        user = self.context.get('request').user
        assigned_to = validated_data.get('assigned_to')

        validated_data.update({
            'created_by_id': user.pk,
        })

        if assigned_to:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.dumps({
                    'event': 'case-assigned',
                }),
            })

            if assigned_to.get('id') != user.pk:
                validated_data.update({
                    'newly_assigned': True,
                })

        else:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.dumps({
                    'event': 'case-unassigned',
                }),
            })

        return super(CaseSerializer, self).create(validated_data)

    def update(self, instance, validated_data):
        user = self.context.get('request').user
        status_id = validated_data.get('status', instance.status_id)
        assigned_to = validated_data.get('assigned_to')

        if assigned_to:
            assigned_to = assigned_to.get('id')

        if isinstance(status_id, dict):
            status_id = status_id.get('id')

        status = CaseStatus.objects.get(pk=status_id)

        # Automatically archive the case if the status is set to 'Closed'.
        if status.name == 'Closed' and 'is_archived' not in validated_data:
            validated_data.update({'is_archived': True})

        # Check if the case being reassigned. If so we want to notify that user.
        if assigned_to and assigned_to != user.pk:
            validated_data.update({
                'newly_assigned': True,
            })
        elif 'assigned_to' in validated_data and not assigned_to:
            # Case is unassigned, so clear newly assigned flag.
            validated_data.update({
                'newly_assigned': False,
            })

        if 'assigned_to' in validated_data or instance.assigned_to_id:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'case-assigned',
                }),
            })

        if (not instance.assigned_to_id
                or instance.assigned_to_id and 'assigned_to' in validated_data
                and not validated_data.get('assigned_to')):
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'case-unassigned',
                }),
            })

        return super(CaseSerializer, self).update(instance, validated_data)

    class Meta:
        model = Case
        fields = (
            'id',
            'account',
            'assigned_to',
            'assigned_to_teams',
            'contact',
            'content_type',
            'created',
            'created_by',
            'description',
            'expires',
            'is_archived',
            'modified',
            'newly_assigned',
            'priority',
            'priority_display',
            'status',
            'tags',
            'subject',
            'type',
        )
Пример #7
0
class DealSerializer(WritableNestedSerializer):
    """
    Serializer for the deal model.
    """
    # Set non mutable fields.
    created_by = RelatedLilyUserSerializer(read_only=True)
    content_type = ContentTypeSerializer(
        read_only=True,
        help_text='This is what the object is identified as in the back-end.',
    )

    # Custom fields.
    description = SanitizedHtmlCharField(
        help_text='Any extra text to describe the deal (supports Markdown).', )

    # Related fields.
    account = RelatedAccountSerializer(
        required=False,
        allow_null=True,
        help_text='Account for which the deal is being created.',
    )
    contact = RelatedContactSerializer(
        required=False,
        allow_null=True,
        help_text='Contact for which the deal is being created.',
    )
    assigned_to = RelatedLilyUserSerializer(
        required=False,
        allow_null=True,
        assign_only=True,
        help_text='Person which the deal is assigned to.',
    )
    assigned_to_teams = RelatedTeamSerializer(
        many=True,
        required=False,
        assign_only=True,
        help_text='List of teams the deal is assigned to.',
    )
    next_step = RelatedDealNextStepSerializer(
        assign_only=True,
        help_text=
        'Allows the user to set what the next action is for the deal.',
    )
    tags = RelatedTagSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Any tags used to further categorize the deal.',
    )
    why_lost = RelatedDealWhyLostSerializer(
        assign_only=True,
        allow_null=True,
        required=False,
        help_text='Allows the user to set why the deal was lost.',
    )
    why_customer = RelatedDealWhyCustomerSerializer(
        assign_only=True,
        allow_null=True,
        required=False,
        help_text='Why does someone want to become a customer.',
    )
    found_through = RelatedDealFoundThroughSerializer(
        assign_only=True,
        allow_null=True,
        required=False,
        help_text='How did the customer find your company.',
    )
    contacted_by = RelatedDealContactedBySerializer(
        assign_only=True,
        allow_null=True,
        required=False,
        help_text='How did the customer contact your company.',
    )
    status = RelatedDealStatusSerializer(
        assign_only=True,
        help_text='The status of the deal.',
    )

    # Show string versions of fields.
    currency_display = serializers.CharField(source='get_currency_display',
                                             read_only=True)

    amount_once = RegexDecimalField(
        max_digits=19,
        decimal_places=2,
        required=True,
        help_text='One time payment for the deal.',
    )
    amount_recurring = RegexDecimalField(
        max_digits=19,
        decimal_places=2,
        required=True,
        help_text='Recurring costs for the deal.',
    )

    def validate(self, data):
        new_business = data.get('new_business')
        why_customer = data.get('why_customer')
        found_through = data.get('found_through')
        contacted_by = data.get('contacted_by')

        if new_business:
            errors = {}

            if not found_through:
                errors.update(
                    {'found_through': _('This field may not be empty.')})

            if not contacted_by:
                errors.update(
                    {'contacted_by': _('This field may not be empty.')})

            if not why_customer:
                errors.update(
                    {'why_customer': _('This field may not be empty.')})

            if errors:
                raise serializers.ValidationError(errors)

        contact_id = data.get('contact', {})
        if isinstance(contact_id, dict):
            contact_id = contact_id.get('id')

        account_id = data.get('account', {})
        if isinstance(account_id, dict):
            account_id = account_id.get('id')

        if contact_id and account_id:
            if not Function.objects.filter(contact_id=contact_id,
                                           account_id=account_id).exists():
                raise serializers.ValidationError(
                    {'contact': _('Given contact must work at the account.')})

        # Check if we are related and if we only passed in the id, which means user just wants new reference.
        errors = {
            'account': _('Please enter an account and/or contact.'),
            'contact': _('Please enter an account and/or contact.'),
        }

        if not self.partial:
            # For POST or PUT we always want to check if either is set.
            if not (account_id or contact_id):
                raise serializers.ValidationError(errors)
        else:
            # For PATCH only check the data if both account and contact are passed.
            if ('account' in data
                    and 'contact' in data) and not (account_id or contact_id):
                raise serializers.ValidationError(errors)

        status_id = data.get('status', {})
        if isinstance(status_id, dict):
            status_id = status_id.get('id')

        why_lost_id = data.get('why_lost', {})
        if isinstance(why_lost_id, dict):
            why_lost_id = why_lost_id.get('id')

        if status_id:
            status = DealStatus.objects.get(pk=status_id)

            if status.is_lost and why_lost_id is None and DealWhyLost.objects.exists(
            ):
                raise serializers.ValidationError(
                    {'why_lost': _('This field may not be empty.')})

        return super(DealSerializer, self).validate(data)

    def create(self, validated_data):
        user = self.context.get('request').user
        status_id = validated_data.get('status').get('id')
        status = DealStatus.objects.get(pk=status_id)
        closed_date = validated_data.get('closed_date')

        # Set closed_date if status is lost/won and not manually provided.
        if (status.is_won or status.is_lost) and not closed_date:
            closed_date = datetime.datetime.utcnow().replace(tzinfo=utc)
        else:
            closed_date = None

        validated_data.update({
            'created_by_id': user.pk,
            'closed_date': closed_date,
        })

        assigned_to = validated_data.get('assigned_to')

        if assigned_to:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'deal-assigned',
                }),
            })

            if assigned_to.get('id') != user.pk:
                validated_data.update({
                    'newly_assigned': True,
                })

        else:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'deal-unassigned',
                }),
            })

        instance = super(DealSerializer, self).create(validated_data)

        # Track newly ceated accounts in segment.
        if not settings.TESTING:
            analytics.track(
                user.id,
                'deal-created',
                {
                    'assigned_to_id':
                    instance.assigned_to_id if instance.assigned_to else '',
                    'status':
                    instance.status.name,
                    'next_step':
                    instance.next_step.name,
                    'creation_type':
                    'automatic' if is_external_referer(
                        self.context.get('request')) else 'manual',
                },
            )

        return instance

    def update(self, instance, validated_data):
        user = self.context.get('request').user
        status_id = validated_data.get('status', instance.status_id)
        next_step = validated_data.get('next_step')

        if isinstance(status_id, dict):
            status_id = status_id.get('id')

        status = DealStatus.objects.get(pk=status_id)
        closed_date = validated_data.get('closed_date', instance.closed_date)
        assigned_to = validated_data.get('assigned_to')

        if assigned_to:
            assigned_to = assigned_to.get('id')

        # Set closed_date after changing status to lost/won and reset it when it's any other status.
        if status.is_won or status.is_lost:
            if not closed_date:
                closed_date = datetime.datetime.utcnow().replace(tzinfo=utc)
        else:
            closed_date = None

        validated_data.update({
            'closed_date': closed_date,
        })

        # Check if the deal is being reassigned. If so we want to notify that user.
        if assigned_to and assigned_to != user.pk:
            validated_data.update({
                'newly_assigned': True,
            })
        elif 'assigned_to' in validated_data and not assigned_to:
            # Deal is unassigned, so clear newly assigned flag.
            validated_data.update({
                'newly_assigned': False,
            })

        if (('status' in validated_data and status.name == 'Open')
                or ('is_archived' in validated_data
                    and not validated_data.get('is_archived'))):
            # Deal is reopened or unarchived, so we want to notify the user again.
            validated_data.update({
                'newly_assigned': True,
            })

        try:
            none_step = DealNextStep.objects.get(name='None')
        except DealNextStep.DoesNotExist:
            pass

        if next_step:
            try:
                next_step = DealNextStep.objects.get(pk=next_step.get('id'))
            except DealNextStep.DoesNotExist:
                raise serializers.ValidationError(
                    {'why_lost': _('This field may not be empty.')})
            else:
                if next_step.date_increment != 0:
                    validated_data.update({
                        'next_step_date':
                        add_business_days(next_step.date_increment),
                    })
                elif none_step and next_step.id == none_step.id:
                    validated_data.update({
                        'next_step_date': None,
                    })

        if 'assigned_to' in validated_data or instance.assigned_to_id:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'deal-assigned',
                }),
            })

        if (not instance.assigned_to_id
                or instance.assigned_to_id and 'assigned_to' in validated_data
                and not validated_data.get('assigned_to')):
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'deal-unassigned',
                }),
            })

        return super(DealSerializer, self).update(instance, validated_data)

    class Meta:
        model = Deal
        fields = (
            'id',
            'account',
            'amount_once',
            'amount_recurring',
            'assigned_to',
            'assigned_to_teams',
            'card_sent',
            'closed_date',
            'contact',
            'contacted_by',
            'content_type',
            'created',
            'created_by',
            'currency',
            'currency_display',
            'description',
            'found_through',
            'is_archived',
            'is_checked',
            'is_deleted',
            'modified',
            'name',
            'new_business',
            'newly_assigned',
            'next_step',
            'next_step_date',
            'quote_id',
            'status',
            'tags',
            'twitter_checked',
            'why_customer',
            'why_lost',
        )
        extra_kwargs = {
            'created': {
                'help_text':
                'Shows the date and time when the deal was created.',
            },
            'modified': {
                'help_text':
                'Shows the date and time when the deal was last modified.',
            },
            'next_step_date': {
                'help_text':
                'Shows the date and time for when the next step should be executed.',
            },
            'closed_date': {
                'help_text':
                'Shows the date and time when the deal was set to \'Won\' or \'Closed\'.',
            },
            'newly_assigned': {
                'help_text':
                'True if the assignee was changed and that person hasn\'t accepted yet.',
            },
            'name': {
                'help_text': 'A short description of the deal.',
            },
        }
Пример #8
0
class DealSerializer(WritableNestedSerializer):
    """
    Serializer for the deal model.
    """
    # Set non mutable fields.
    created = serializers.DateTimeField(read_only=True)
    created_by = RelatedLilyUserSerializer(read_only=True)
    modified = serializers.DateTimeField(read_only=True)
    content_type = ContentTypeSerializer(read_only=True)

    # Custom fields.
    description = SanitizedHtmlCharField()

    # Related fields.
    account = RelatedAccountSerializer()
    contact = RelatedContactSerializer(required=False, allow_null=True)
    assigned_to = RelatedLilyUserSerializer(required=False,
                                            allow_null=True,
                                            assign_only=True)
    assigned_to_teams = RelatedTeamSerializer(many=True,
                                              required=False,
                                              assign_only=True)
    next_step = RelatedDealNextStepSerializer(assign_only=True)
    tags = RelatedTagSerializer(many=True, required=False, create_only=True)
    why_lost = RelatedDealWhyLostSerializer(assign_only=True,
                                            allow_null=True,
                                            required=False)
    why_customer = RelatedDealWhyCustomerSerializer(assign_only=True,
                                                    allow_null=True,
                                                    required=False)
    found_through = RelatedDealFoundThroughSerializer(assign_only=True,
                                                      allow_null=True,
                                                      required=False)
    contacted_by = RelatedDealContactedBySerializer(assign_only=True,
                                                    allow_null=True,
                                                    required=False)
    status = RelatedDealStatusSerializer(assign_only=True)

    # Show string versions of fields.
    currency_display = serializers.CharField(source='get_currency_display',
                                             read_only=True)

    amount_once = RegexDecimalField(max_digits=19,
                                    decimal_places=2,
                                    required=True)
    amount_recurring = RegexDecimalField(max_digits=19,
                                         decimal_places=2,
                                         required=True)

    def validate(self, attrs):
        new_business = attrs.get('new_business')
        why_customer = attrs.get('why_customer')
        found_through = attrs.get('found_through')
        contacted_by = attrs.get('contacted_by')

        if new_business:
            errors = {}

            if not found_through:
                errors.update(
                    {'found_through': _('This field may not be empty.')})

            if not contacted_by:
                errors.update(
                    {'contacted_by': _('This field may not be empty.')})

            if not why_customer:
                errors.update(
                    {'why_customer': _('This field may not be empty.')})

            if errors:
                raise serializers.ValidationError(errors)

        contact_id = attrs.get('contact', {})
        if isinstance(contact_id, dict):
            contact_id = contact_id.get('id')

        account_id = attrs.get('account', {})
        if isinstance(account_id, dict):
            account_id = account_id.get('id')

        if contact_id and account_id:
            if not Function.objects.filter(contact_id=contact_id,
                                           account_id=account_id).exists():
                raise serializers.ValidationError(
                    {'contact': _('Given contact must work at the account.')})

        status_id = attrs.get('status', {})
        if isinstance(status_id, dict):
            status_id = status_id.get('id')

        why_lost_id = attrs.get('why_lost', {})
        if isinstance(why_lost_id, dict):
            why_lost_id = why_lost_id.get('id')

        if status_id:
            status = DealStatus.objects.get(pk=status_id)

            if status.is_lost and why_lost_id is None and DealWhyLost.objects.exists(
            ):
                raise serializers.ValidationError(
                    {'why_lost': _('This field may not be empty.')})

        return super(DealSerializer, self).validate(attrs)

    def create(self, validated_data):
        user = self.context.get('request').user
        status_id = validated_data.get('status').get('id')
        status = DealStatus.objects.get(pk=status_id)
        closed_date = validated_data.get('closed_date')

        # Set closed_date if status is lost/won and not manually provided.
        if (status.is_won or status.is_lost) and not closed_date:
            closed_date = datetime.datetime.utcnow().replace(tzinfo=utc)
        else:
            closed_date = None

        validated_data.update({
            'created_by_id': user.pk,
            'closed_date': closed_date,
        })

        assigned_to = validated_data.get('assigned_to')

        if assigned_to:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'deal-assigned',
                }),
            })

            if assigned_to.get('id') != user.pk:
                validated_data.update({
                    'newly_assigned': True,
                })

        else:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'deal-unassigned',
                }),
            })

        return super(DealSerializer, self).create(validated_data)

    def update(self, instance, validated_data):
        user = self.context.get('request').user
        status_id = validated_data.get('status', instance.status_id)
        next_step = validated_data.get('next_step')

        if isinstance(status_id, dict):
            status_id = status_id.get('id')

        status = DealStatus.objects.get(pk=status_id)
        closed_date = validated_data.get('closed_date', instance.closed_date)
        assigned_to = validated_data.get('assigned_to')

        if assigned_to:
            assigned_to = assigned_to.get('id')

        # Set closed_date after changing status to lost/won and reset it when it's any other status.
        if status.is_won or status.is_lost:
            if not closed_date:
                closed_date = datetime.datetime.utcnow().replace(tzinfo=utc)
        else:
            closed_date = None

        validated_data.update({
            'closed_date': closed_date,
        })

        # Check if the deal is being reassigned. If so we want to notify that user.
        if assigned_to and assigned_to != user.pk:
            validated_data.update({
                'newly_assigned': True,
            })
        elif 'assigned_to' in validated_data and not assigned_to:
            # Deal is unassigned, so clear newly assigned flag.
            validated_data.update({
                'newly_assigned': False,
            })

        try:
            none_step = DealNextStep.objects.get(name='None')
        except DealNextStep.DoesNotExist:
            pass

        if next_step:
            try:
                next_step = DealNextStep.objects.get(pk=next_step.get('id'))
            except DealNextStep.DoesNotExist:
                raise serializers.ValidationError(
                    {'why_lost': _('This field may not be empty.')})
            else:
                if next_step.date_increment != 0:
                    validated_data.update({
                        'next_step_date':
                        add_business_days(next_step.date_increment),
                    })
                elif none_step and next_step.id == none_step.id:
                    validated_data.update({
                        'next_step_date': None,
                    })

        if 'assigned_to' in validated_data or instance.assigned_to_id:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'deal-assigned',
                }),
            })

        if (not instance.assigned_to_id
                or instance.assigned_to_id and 'assigned_to' in validated_data
                and not validated_data.get('assigned_to')):
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'deal-unassigned',
                }),
            })

        return super(DealSerializer, self).update(instance, validated_data)

    class Meta:
        model = Deal
        fields = (
            'id',
            'account',
            'amount_once',
            'amount_recurring',
            'assigned_to',
            'assigned_to_teams',
            'card_sent',
            'closed_date',
            'contact',
            'contacted_by',
            'content_type',
            'created',
            'created_by',
            'currency',
            'currency_display',
            'description',
            'found_through',
            'is_archived',
            'is_checked',
            'is_deleted',
            'modified',
            'name',
            'new_business',
            'newly_assigned',
            'next_step',
            'next_step_date',
            'quote_id',
            'status',
            'tags',
            'twitter_checked',
            'why_customer',
            'why_lost',
        )
Пример #9
0
class AccountSerializer(PhoneNumberFormatMixin, WritableNestedSerializer):
    """
    Serializer for the account model.
    """
    # Read only fields.
    content_type = ContentTypeSerializer(
        read_only=True,
        help_text='This is what the object is identified as in the back-end.')
    contacts = ContactForAccountSerializer(
        many=True,
        read_only=True,
        help_text='Contains all contacts which work for this account.',
    )

    # Related fields
    addresses = RelatedAddressSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Addresses belonging to the account.',
    )
    assigned_to = RelatedLilyUserSerializer(
        required=False,
        allow_null=True,
        assign_only=True,
        help_text='Person which the account is assigned to.',
    )
    email_addresses = RelatedEmailAddressSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Email addresses belonging to the account.',
    )
    phone_numbers = RelatedPhoneNumberSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Phone numbers belonging to the account.',
    )
    social_media = RelatedSocialMediaSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Social media accounts belonging to the account.',
    )
    websites = RelatedWebsiteSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Any websites that belong to the account.',
    )
    status = RelatedAccountStatusSerializer(
        assign_only=True,
        help_text='ID of an AccountStatus instance.',
    )
    tags = RelatedTagSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Any tags used to further categorize the account.',
    )

    # Custom fields.
    name = serializers.CharField(
        validators=[DuplicateAccountName()],
        help_text='The name of the account.',
    )
    description = SanitizedHtmlCharField(
        help_text='Any extra text to describe the account (supports Markdown).',
    )

    def create(self, validated_data):
        tenant = self.context.get('request').user.tenant
        account_count = Account.objects.filter(is_deleted=False).count()

        if tenant.billing.is_free_plan and account_count >= settings.FREE_PLAN_ACCOUNT_CONTACT_LIMIT:
            raise serializers.ValidationError({
                'limit_reached':
                _('You\'ve reached the limit of accounts for the free plan.'),
            })

        instance = super(AccountSerializer, self).create(validated_data)

        # Track newly ceated accounts in segment.
        if not settings.TESTING:
            analytics.track(
                self.context.get('request').user.id,
                'account-created',
                {
                    'assigned_to_id':
                    instance.assigned_to.id if instance.assigned_to else '',
                    'creation_type':
                    'automatic' if is_external_referer(
                        self.context.get('request')) else 'manual',
                },
            )

        return instance

    class Meta:
        model = Account
        fields = (
            'id',
            'addresses',
            'assigned_to',
            'contacts',
            'content_type',
            'created',
            'customer_id',
            'description',
            'email_addresses',
            'is_deleted',
            'modified',
            'name',
            'phone_numbers',
            'social_media',
            'status',
            'tags',
            'websites',
        )
        read_only_fields = ('is_deleted', )
        extra_kwargs = {
            'created': {
                'help_text':
                'Shows the date and time when the account was created.',
            },
            'modified': {
                'help_text':
                'Shows the date and time when the account was last modified.',
            },
            'customer_id': {
                'help_text':
                'An extra identifier for the account which can be used to link to an external source.',
            },
        }
Пример #10
0
class ContactSerializer(PhoneNumberFormatMixin, NewWritableNestedSerializer):
    """
    Serializer for the contact model.
    """
    # Set non mutable fields.
    full_name = serializers.CharField(
        read_only=True,
        help_text='Read-only property to reduce the need to concatenate.')
    content_type = ContentTypeSerializer(
        read_only=True,
        help_text='This is what the object is identified as in the back-end.',
    )

    # Related fields.
    phone_numbers = RelatedPhoneNumberSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Phone numbers belonging to the contact.',
    )
    addresses = RelatedAddressSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Addresses belonging to the contact.',
    )
    email_addresses = RelatedEmailAddressSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Email addresses belonging to the contact.',
    )
    social_media = RelatedSocialMediaSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Social media accounts belonging to the contact.',
    )
    accounts = RelatedAccountSerializer(
        many=True,
        required=False,
        help_text='Accounts the contact works at.',
    )
    tags = RelatedTagSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Any tags used to further categorize the contact.',
    )
    functions = RelatedFunctionSerializer(
        many=True,
        required=False,
        create_only=True,
    )
    description = SanitizedHtmlCharField(
        help_text='Any extra text to describe the contact (supports Markdown).',
    )

    # Show string versions of fields.
    gender_display = serializers.CharField(
        source='get_gender_display',
        read_only=True,
        help_text='Human readable value of the contact\'s gender.',
    )
    salutation_display = serializers.CharField(
        source='get_salutation_display',
        read_only=True,
        help_text='Human readable value of the contact\'s salutation.',
    )

    primary_email = RelatedEmailAddressSerializer(read_only=True)
    phone_number = RelatedPhoneNumberSerializer(read_only=True)

    class Meta:
        model = Contact
        fields = (
            'id',
            'accounts',
            'addresses',
            'content_type',
            'created',
            'description',
            'email_addresses',
            'first_name',
            'full_name',
            'gender',
            'gender_display',
            'is_deleted',
            'last_name',
            'modified',
            'phone_numbers',
            'salutation',
            'salutation_display',
            'social_media',
            'tags',
            'functions',
            'primary_email',
            'phone_number',
        )
        read_only_fields = ('is_deleted', )
        extra_kwargs = {
            'created': {
                'help_text':
                'Shows the date and time when the contact was created.',
            },
            'modified': {
                'help_text':
                'Shows the date and time when the contact was last modified.',
            },
            'first_name': {
                'help_text': 'The first name of the contact.',
            },
            'last_name': {
                'help_text': 'The last name of the contact.',
            },
        }

    def validate(self, data):
        if not isinstance(data, dict):
            data = {'id': data}

        # Check if we are related and if we only passed in the id, which means user just wants new reference.
        if not (len(data) == 1 and 'id' in data
                and hasattr(self, 'is_related_serializer')):
            errors = {
                'first_name': _('Please enter a valid first name.'),
                'last_name': _('Please enter a valid last name.')
            }

            if not self.partial:
                first_name = data.get('first_name', None)
                last_name = data.get('last_name', None)

                # Not just a new reference, so validate if contact is set properly.
                if not any([first_name, last_name]):
                    raise serializers.ValidationError(errors)
            else:
                if 'first_name' in data and 'last_name' in data:
                    first_name = data.get('first_name', None)
                    last_name = data.get('last_name', None)

                    if not (first_name or last_name):
                        raise serializers.ValidationError(errors)

        return super(ContactSerializer, self).validate(data)

    def create(self, validated_data):
        tenant = self.context.get('request').user.tenant
        contact_count = Contact.objects.filter(is_deleted=False).count()

        if tenant.billing.is_free_plan and contact_count >= settings.FREE_PLAN_ACCOUNT_CONTACT_LIMIT:
            raise serializers.ValidationError({
                'limit_reached':
                _('You\'ve reached the limit of contacts for the free plan.'),
            })

        instance = super(ContactSerializer, self).create(validated_data)

        credentials = get_credentials('moneybird')

        if has_required_tier(
                2) and credentials and credentials.integration_context.get(
                    'auto_sync'):
            self.send_moneybird_contact(validated_data, instance, credentials)

        # Track newly ceated accounts in segment.
        if not settings.TESTING:
            analytics.track(
                self.context.get('request').user.id,
                'contact-created',
                {
                    'creation_type':
                    'automatic' if is_external_referer(
                        self.context.get('request')) else 'manual',
                },
            )

        return instance

    def update(self, instance, validated_data):
        # Save the current data for later use.
        original_data = {
            'full_name': instance.full_name,
        }

        email_addresses = instance.email_addresses.all()

        if len(email_addresses) == 1:
            original_data.update(
                {'original_email_address': email_addresses[0].email_address})

        instance = super(ContactSerializer,
                         self).update(instance, validated_data)

        credentials = get_credentials('moneybird')

        if has_required_tier(
                2) and credentials and credentials.integration_context.get(
                    'auto_sync'):
            self.send_moneybird_contact(validated_data, instance, credentials,
                                        original_data)

        return instance

    def send_moneybird_contact(self,
                               validated_data,
                               instance,
                               credentials,
                               original_data=None):
        administration_id = credentials.integration_context.get(
            'administration_id')
        contact_url = 'https://moneybird.com/api/v2/%s/contacts'

        if original_data:
            full_name = original_data.get('full_name')
        else:
            full_name = instance.full_name

        search_url = (contact_url + '?query=%s') % (administration_id,
                                                    full_name)
        response = send_get_request(search_url, credentials)
        data = response.json()

        patch = False
        params = {}

        if data:
            data = data[0]
            moneybird_id = data.get('id')
            post_url = (contact_url + '/%s') % (administration_id,
                                                moneybird_id)

            params = {
                'id': moneybird_id,
            }

            # Existing Moneybird contact found so we want to PATCH.
            patch = True
        else:
            post_url = contact_url % administration_id

        if 'first_name' in validated_data:
            params.update({'firstname': validated_data.get('first_name')})

        if 'last_name' in validated_data:
            params.update({'lastname': validated_data.get('last_name')})

        accounts = instance.accounts.all()

        if 'accounts' in validated_data and len(accounts) == 1:
            params.update({'company_name': accounts[0].name})

        if 'phone_numbers' in validated_data:
            phone_numbers = []

            for validated_number in validated_data.get('phone_numbers'):
                for phone_number in instance.phone_numbers.all():
                    if validated_number.get('number') == phone_number.number:
                        phone_numbers.append(phone_number)
                        break

            if phone_numbers:
                params.update({'phone': phone_numbers[0].number})

        if 'addresses' in validated_data:
            addresses = []

            for validated_address in validated_data.get('addresses'):
                for address in instance.addresses.all():
                    if validated_address.get('address') == address.address:
                        addresses.append(address)
                        break

            if addresses:
                address = addresses[0]
                params.update({
                    'address1': address.address,
                    'zipcode': address.postal_code,
                    'city': address.city,
                    'country': address.country,
                })

        if 'email_addresses' in validated_data:
            original_email_address = None
            validated_email_addresses = validated_data.get('email_addresses')

            if original_data:
                original_email_address = original_data.get(
                    'original_email_address')

            if len(validated_email_addresses) == 1:
                validated_email_address = validated_email_addresses[0].get(
                    'email_address')

                if data and original_email_address:
                    invoices_email = data.get('send_invoices_to_email')
                    estimates_email = data.get('send_estimates_to_email')

                    if invoices_email == estimates_email and invoices_email == original_email_address:
                        params.update({
                            'send_invoices_to_email':
                            validated_email_address,
                            'send_estimates_to_email':
                            validated_email_address,
                        })
                    elif invoices_email == original_email_address:
                        params.update({
                            'send_invoices_to_email':
                            validated_email_address,
                        })
                    elif estimates_email == original_email_address:
                        params.update({
                            'send_estimates_to_email':
                            validated_email_address,
                        })
                else:
                    params.update({
                        'send_invoices_to_email':
                        validated_email_address,
                        'send_estimates_to_email':
                        validated_email_address,
                    })

        params = {'contact': params, 'administration_id': administration_id}

        response = send_post_request(post_url, credentials, params, patch,
                                     True)
Пример #11
0
class CaseSerializer(WritableNestedSerializer):
    """
    Serializer for the case model.
    """
    # Set non mutable fields.
    created_by = RelatedLilyUserSerializer(read_only=True)
    content_type = ContentTypeSerializer(
        read_only=True,
        help_text='This is what the object is identified as in the back-end.',
    )

    # Related fields.
    account = RelatedAccountSerializer(
        required=False,
        allow_null=True,
        help_text='Account for which the case is being created.',
    )
    contact = RelatedContactSerializer(
        required=False,
        allow_null=True,
        help_text='Contact for which the case is being created.',
    )
    assigned_to = RelatedLilyUserSerializer(
        required=False,
        allow_null=True,
        assign_only=True,
        help_text='Person which the case is assigned to.',
    )
    assigned_to_teams = RelatedTeamSerializer(
        many=True,
        required=False,
        assign_only=True,
        help_text='List of teams the case is assigned to.',
    )
    type = RelatedCaseTypeSerializer(
        assign_only=True,
        help_text='The type of case.',
    )
    status = RelatedCaseStatusSerializer(
        assign_only=True,
        help_text='Status of the case.',
    )
    tags = RelatedTagSerializer(
        many=True,
        required=False,
        create_only=True,
        help_text='Any tags used to further categorize the case.',
    )

    description = SanitizedHtmlCharField(
        help_text='Any extra text to describe the case (supports Markdown).', )

    # Show string versions of fields.
    priority_display = serializers.CharField(
        source='get_priority_display',
        read_only=True,
        help_text='Human readable value of the case\'s priority.',
    )

    def validate(self, data):
        contact_id = data.get('contact', {})
        if isinstance(contact_id, dict):
            contact_id = contact_id.get('id')

        account_id = data.get('account', {})
        if isinstance(account_id, dict):
            account_id = account_id.get('id')

        if contact_id and account_id:
            if not Function.objects.filter(contact_id=contact_id,
                                           account_id=account_id).exists():
                raise serializers.ValidationError(
                    {'contact': _('Given contact must work at the account.')})

        # Check if we are related and if we only passed in the id, which means user just wants new reference.
        errors = {
            'account': _('Please enter an account and/or contact.'),
            'contact': _('Please enter an account and/or contact.'),
        }

        if not self.partial:
            # For POST or PUT we always want to check if either is set.
            if not (account_id or contact_id):
                raise serializers.ValidationError(errors)
        else:
            # For PATCH only check the data if both account and contact are passed.
            if ('account' in data
                    and 'contact' in data) and not (account_id or contact_id):
                raise serializers.ValidationError(errors)

        return super(CaseSerializer, self).validate(data)

    def create(self, validated_data):
        user = self.context.get('request').user
        assigned_to = validated_data.get('assigned_to')

        validated_data.update({
            'created_by_id': user.pk,
        })

        if assigned_to:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.dumps({
                    'event': 'case-assigned',
                }),
            })

            if assigned_to.get('id') != user.pk:
                validated_data.update({
                    'newly_assigned': True,
                })

        else:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.dumps({
                    'event': 'case-unassigned',
                }),
            })

        instance = super(CaseSerializer, self).create(validated_data)

        # Track newly ceated accounts in segment.
        if not settings.TESTING:
            analytics.track(
                user.id,
                'case-created',
                {
                    'expires':
                    instance.expires,
                    'assigned_to_id':
                    instance.assigned_to_id if instance.assigned_to else '',
                    'creation_type':
                    'automatic' if is_external_referer(
                        self.context.get('request')) else 'manual',
                },
            )

        return instance

    def update(self, instance, validated_data):
        user = self.context.get('request').user
        status_id = validated_data.get('status', instance.status_id)
        assigned_to = validated_data.get('assigned_to')

        if assigned_to:
            assigned_to = assigned_to.get('id')

        if isinstance(status_id, dict):
            status_id = status_id.get('id')

        status = CaseStatus.objects.get(pk=status_id)

        # Automatically archive the case if the status is set to 'Closed'.
        if status.name == 'Closed' and 'is_archived' not in validated_data:
            validated_data.update({'is_archived': True})

        # Check if the case being reassigned. If so we want to notify that user.
        if assigned_to and assigned_to != user.pk:
            validated_data.update({
                'newly_assigned': True,
            })
        elif 'assigned_to' in validated_data and not assigned_to:
            # Case is unassigned, so clear newly assigned flag.
            validated_data.update({
                'newly_assigned': False,
            })

        if (('status' in validated_data and status.name == 'Open')
                or ('is_archived' in validated_data
                    and not validated_data.get('is_archived'))):
            # Case is reopened or unarchived, so we want to notify the user again.
            validated_data.update({
                'newly_assigned': True,
            })

        if 'assigned_to' in validated_data or instance.assigned_to_id:
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'case-assigned',
                }),
            })

        if (not instance.assigned_to_id
                or instance.assigned_to_id and 'assigned_to' in validated_data
                and not validated_data.get('assigned_to')):
            Group('tenant-%s' % user.tenant.id).send({
                'text':
                anyjson.serialize({
                    'event': 'case-unassigned',
                }),
            })

        return super(CaseSerializer, self).update(instance, validated_data)

    class Meta:
        model = Case
        fields = (
            'id',
            'account',
            'assigned_to',
            'assigned_to_teams',
            'contact',
            'content_type',
            'created',
            'created_by',
            'description',
            'expires',
            'is_archived',
            'modified',
            'newly_assigned',
            'priority',
            'priority_display',
            'status',
            'tags',
            'subject',
            'type',
        )
        extra_kwargs = {
            'created': {
                'help_text':
                'Shows the date and time when the deal was created.',
            },
            'expires': {
                'help_text':
                'Shows the date and time for when the case should be completed.',
            },
            'modified': {
                'help_text':
                'Shows the date and time when the case was last modified.',
            },
            'newly_assigned': {
                'help_text':
                'True if the assignee was changed and that person hasn\'t accepted yet.',
            },
            'subject': {
                'help_text': 'A short description of the case.',
            },
        }