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', )
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.', }, }
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', )
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', )
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)
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', )
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.', }, }
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', )
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.', }, }
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)
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.', }, }