class SubscriptionProjectSerializer(ModelSerializer):
    purchased_time = SerializerMethodField(source="get_purchased_time")
    spent_time = SerializerMethodField(source="get_spent_time")

    def get_purchased_time(self, obj):
        """
        Calculate purchased time for given project.

        Only acknowledged hours are included.
        """
        orders = Order.objects.filter(project=obj, acknowledged=True)
        data = orders.aggregate(purchased_time=Sum("duration"))
        return duration_string(data["purchased_time"] or timedelta(0))

    def get_spent_time(self, obj):
        """
        Calculate spent time for given project.

        Reports which are not billable or are in review are excluded.
        """
        reports = Report.objects.filter(task__project=obj,
                                        not_billable=False,
                                        review=False)
        data = reports.aggregate(spent_time=Sum("duration"))
        return duration_string(data["spent_time"] or timedelta())

    included_serializers = {
        "billing_type": "timed.projects.serializers.BillingTypeSerializer",
        "customer": "timed.projects.serializers.CustomerSerializer",
        "orders": "timed.subscription.serializers.OrderSerializer",
    }

    class Meta:
        model = Project
        resource_name = "subscription-projects"
        fields = (
            "name",
            "billing_type",
            "purchased_time",
            "spent_time",
            "customer",
            "orders",
        )
Exemple #2
0
class WorktimeBalanceSerializer(Serializer):
    date = SerializerMethodField()
    balance = SerializerMethodField()
    user    = relations.ResourceRelatedField(
        model=get_user_model(), read_only=True, source='id'
    )

    def get_date(self, instance):
        user = instance.id
        today = date.today()

        if instance.date is not None:
            return instance.date

        # calculate last reported day if no specific date is set
        max_absence_date = Absence.objects.filter(
            user=user, date__lt=today).aggregate(date=Max('date'))
        max_report_date = Report.objects.filter(
            user=user, date__lt=today).aggregate(date=Max('date'))

        last_reported_date = max(
            max_absence_date['date'] or date.min,
            max_report_date['date'] or date.min
        )

        instance.date = last_reported_date
        return instance.date

    def get_balance(self, instance):
        balance_date = self.get_date(instance)
        start = date(balance_date.year, 1, 1)

        # id is mapped to user instance
        _, _, balance = instance.id.calculate_worktime(start, balance_date)
        return duration_string(balance)

    included_serializers = {
        'user': '******'
    }

    class Meta:
        resource_name = 'worktime-balances'
Exemple #3
0
class SubscriptionProjectSerializer(ModelSerializer):
    purchased_time = SerializerMethodField(source='get_purchased_time')
    spent_time = SerializerMethodField(source='get_spent_time')

    def get_purchased_time(self, obj):
        """
        Calculate purchased time for given project.

        Only acknowledged hours are included.
        """
        orders = Order.objects.filter(project=obj, acknowledged=True)
        data = orders.aggregate(purchased_time=Sum('duration'))
        return duration_string(data['purchased_time'] or timedelta(0))

    def get_spent_time(self, obj):
        """
        Calculate spent time for given project.

        Reports which are not billable or are in review are excluded.
        """
        reports = Report.objects.filter(task__project=obj,
                                        not_billable=False,
                                        review=False)
        data = reports.aggregate(spent_time=Sum('duration'))
        return duration_string(data['spent_time'] or timedelta())

    included_serializers = {
        'billing_type': 'timed.projects.serializers.BillingTypeSerializer',
        'customer': 'timed.projects.serializers.CustomerSerializer',
        'orders': 'timed.subscription.serializers.OrderSerializer'
    }

    class Meta:
        model = Project
        resource_name = 'subscription-projects'
        fields = ('name', 'billing_type', 'purchased_time', 'spent_time',
                  'customer', 'orders')
class AbsenceSerializer(ModelSerializer):
    """Absence serializer."""

    duration = SerializerMethodField(source="get_duration")
    type = ResourceRelatedField(queryset=AbsenceType.objects.all())
    user = CurrentUserResourceRelatedField()

    included_serializers = {
        "user": "******",
        "type": "timed.employment.serializers.AbsenceTypeSerializer",
    }

    def get_duration(self, instance):
        try:
            employment = Employment.objects.get_at(instance.user,
                                                   instance.date)
        except Employment.DoesNotExist:
            # absence is invalid if no employment exists on absence date
            return duration_string(timedelta())

        return duration_string(instance.calculate_duration(employment))

    def validate_date(self, value):
        """Only owner is allowed to change date."""
        if self.instance is not None:
            user = self.context["request"].user
            owner = self.instance.user
            if self.instance.date != value and user != owner:
                raise ValidationError(_("Only owner may change date"))

        return value

    def validate_type(self, value):
        """Only owner is allowed to change type."""
        if self.instance is not None:
            user = self.context["request"].user
            owner = self.instance.user
            if self.instance.date != value and user != owner:
                raise ValidationError(_("Only owner may change absence type"))

        return value

    def validate(self, data):
        """Validate the absence data.

        An absence should not be created on a public holiday or a weekend.

        :returns: The validated data
        :rtype:   dict
        """
        instance = self.instance
        user = data.get("user", instance and instance.user)
        try:
            location = Employment.objects.get_at(user,
                                                 data.get("date")).location
        except Employment.DoesNotExist:
            raise ValidationError(
                _("You can't create an absence on an unemployed day."))

        if PublicHoliday.objects.filter(location_id=location.id,
                                        date=data.get("date")).exists():
            raise ValidationError(
                _("You can't create an absence on a public holiday"))

        workdays = [int(day) for day in location.workdays]
        if data.get("date").isoweekday() not in workdays:
            raise ValidationError(
                _("You can't create an absence on a weekend"))

        return data

    class Meta:
        """Meta information for the absence serializer."""

        model = models.Absence
        fields = ["comment", "date", "duration", "type", "user"]
class ReportIntersectionSerializer(Serializer):
    """
    Serializer of report intersections.

    Serializes a representation of all fields which are the same
    in given Report objects. If values of one field are not the same
    in all objects it will be represented as None.

    Serializer expect instance to have a queryset value.
    """

    customer = relations.SerializerMethodResourceRelatedField(
        source="get_customer", model=Customer, read_only=True)
    project = relations.SerializerMethodResourceRelatedField(
        source="get_project", model=Project, read_only=True)
    task = relations.SerializerMethodResourceRelatedField(source="get_task",
                                                          model=Task,
                                                          read_only=True)
    comment = SerializerMethodField()
    review = SerializerMethodField()
    not_billable = SerializerMethodField()
    billed = SerializerMethodField()
    verified = SerializerMethodField()

    def _intersection(self, instance, field, model=None):
        """Get intersection of given field.

        :return: Returns value of field if objects have same value;
                 otherwise None
        """
        value = None
        queryset = instance["queryset"]
        values = queryset.values(field).distinct()
        if values.count() == 1:
            value = values.first()[field]
            if model:
                value = model.objects.get(pk=value)

        return value

    def get_customer(self, instance):
        return self._intersection(instance, "task__project__customer",
                                  Customer)

    def get_project(self, instance):
        return self._intersection(instance, "task__project", Project)

    def get_task(self, instance):
        return self._intersection(instance, "task", Task)

    def get_comment(self, instance):
        return self._intersection(instance, "comment")

    def get_review(self, instance):
        return self._intersection(instance, "review")

    def get_not_billable(self, instance):
        return self._intersection(instance, "not_billable")

    def get_billed(self, instance):
        return self._intersection(instance, "billed")

    def get_verified(self, instance):
        queryset = instance["queryset"]
        queryset = queryset.annotate(verified=Case(
            When(verified_by_id__isnull=True, then=False),
            default=True,
            output_field=BooleanField(),
        ))
        instance["queryset"] = queryset
        return self._intersection(instance, "verified")

    def get_root_meta(self, resource, many):
        """Add number of results to meta."""
        queryset = self.instance["queryset"]
        return {"count": queryset.count()}

    included_serializers = {
        "customer": "timed.projects.serializers.CustomerSerializer",
        "project": "timed.projects.serializers.ProjectSerializer",
        "task": "timed.projects.serializers.TaskSerializer",
    }

    class Meta:
        resource_name = "report-intersections"
class AbsenceBalanceSerializer(Serializer):
    credit = SerializerMethodField()
    used_days = SerializerMethodField()
    used_duration = SerializerMethodField()
    balance = SerializerMethodField()

    user = relations.ResourceRelatedField(model=get_user_model(),
                                          read_only=True)

    absence_type = relations.ResourceRelatedField(model=models.AbsenceType,
                                                  read_only=True,
                                                  source="id")

    absence_credits = relations.SerializerMethodResourceRelatedField(
        source="get_absence_credits",
        model=models.AbsenceCredit,
        many=True,
        read_only=True,
    )

    def _get_start(self, instance):
        return date(instance.date.year, 1, 1)

    def get_credit(self, instance):
        """
        Calculate how many days are approved for given absence type.

        For absence types which fill worktime this will be None.
        """
        if "credit" in instance:
            return instance["credit"]

        # id is mapped to absence type
        absence_type = instance.id

        start = self._get_start(instance)

        # avoid multiple calculations as get_balance needs it as well
        instance["credit"] = absence_type.calculate_credit(
            instance.user, start, instance.date)
        return instance["credit"]

    def get_used_days(self, instance):
        """
        Calculate how many days are used of given absence type.

        For absence types which fill worktime this will be None.
        """
        if "used_days" in instance:
            return instance["used_days"]

        # id is mapped to absence type
        absence_type = instance.id

        start = self._get_start(instance)

        # avoid multiple calculations as get_balance needs it as well
        instance["used_days"] = absence_type.calculate_used_days(
            instance.user, start, instance.date)
        return instance["used_days"]

    def get_used_duration(self, instance):
        """
        Calculate duration of absence type.

        For absence types which fill worktime this will be None.
        """
        # id is mapped to absence type
        absence_type = instance.id
        if not absence_type.fill_worktime:
            return None

        start = self._get_start(instance)
        absences = sum(
            [
                absence.calculate_duration(
                    models.Employment.objects.get_at(instance.user,
                                                     absence.date))
                for absence in Absence.objects.filter(
                    user=instance.user,
                    date__range=[start, instance.date],
                    type_id=instance.id,
                ).select_related("type")
            ],
            timedelta(),
        )
        return duration_string(absences)

    def get_absence_credits(self, instance):
        """Get the absence credits for the user and type."""
        if "absence_credits" in instance:
            return instance["absence_credits"]

        # id is mapped to absence type
        absence_type = instance.id

        start = self._get_start(instance)
        absence_credits = models.AbsenceCredit.objects.filter(
            absence_type=absence_type,
            user=instance.user,
            date__range=[start, instance.date],
        ).select_related("user")

        # avoid multiple calculations when absence credits need to be included
        instance["absence_credits"] = absence_credits

        return absence_credits

    def get_balance(self, instance):
        # id is mapped to absence type
        absence_type = instance.id
        if absence_type.fill_worktime:
            return None

        return self.get_credit(instance) - self.get_used_days(instance)

    included_serializers = {
        "absence_type": "timed.employment.serializers.AbsenceTypeSerializer",
        "absence_credits":
        "timed.employment.serializers.AbsenceCreditSerializer",
    }

    class Meta:
        resource_name = "absence-balances"
Exemple #7
0
class AbsenceSerializer(ModelSerializer):
    """Absence serializer."""

    duration = SerializerMethodField(source='get_duration')
    type = ResourceRelatedField(queryset=AbsenceType.objects.all())
    user = ResourceRelatedField(read_only=True, default=CurrentUserDefault())

    included_serializers = {
        'user': '******',
        'type': 'timed.employment.serializers.AbsenceTypeSerializer',
    }

    def get_duration(self, instance):
        try:
            employment = Employment.objects.get_at(instance.user,
                                                   instance.date)
        except Employment.DoesNotExist:
            # absence is invalid if no employment exists on absence date
            return duration_string(timedelta())

        return duration_string(instance.calculate_duration(employment))

    def validate_date(self, value):
        """Only owner is allowed to change date."""
        if self.instance is not None:
            user = self.context['request'].user
            owner = self.instance.user
            if self.instance.date != value and user != owner:
                raise ValidationError(_('Only owner may change date'))

        return value

    def validate_type(self, value):
        """Only owner is allowed to change type."""
        if self.instance is not None:
            user = self.context['request'].user
            owner = self.instance.user
            if self.instance.date != value and user != owner:
                raise ValidationError(_('Only owner may change absence type'))

        return value

    def validate(self, data):
        """Validate the absence data.

        An absence should not be created on a public holiday or a weekend.

        :returns: The validated data
        :rtype:   dict
        """
        try:
            location = Employment.objects.get_at(data.get('user'),
                                                 data.get('date')).location
        except Employment.DoesNotExist:
            raise ValidationError(
                _('You can\'t create an absence on an unemployed day.'))

        if PublicHoliday.objects.filter(location_id=location.id,
                                        date=data.get('date')).exists():
            raise ValidationError(
                _('You can\'t create an absence on a public holiday'))

        workdays = [int(day) for day in location.workdays]
        if data.get('date').isoweekday() not in workdays:
            raise ValidationError(
                _('You can\'t create an absence on a weekend'))

        return data

    class Meta:
        """Meta information for the absence serializer."""

        model = models.Absence
        fields = [
            'comment',
            'date',
            'duration',
            'type',
            'user',
        ]
Exemple #8
0
class AbsenceCreditSerializer(ModelSerializer):
    """Absence credit serializer."""

    absence_type = ResourceRelatedField(read_only=True)
    user         = ResourceRelatedField(read_only=True)
    used         = SerializerMethodField()
    balance      = SerializerMethodField()

    def get_used_raw(self, instance):
        """Calculate the total of used time since the date of the requested credit.

        This is the sum of all durations of reports, which are assigned to the
        credits user, absence type and were created at or after the date of
        this credit.

        :return: The total of used time
        :rtype:  datetime.timedelta
        """
        request            = self.context.get('request')
        requested_end_date = request.query_params.get('until')

        end_date = (
            datetime.strptime(requested_end_date, '%Y-%m-%d').date()
            if requested_end_date
            else date.today()
        )

        reports = Absence.objects.filter(
            user=instance.user,
            type=instance.absence_type,
            date__gte=instance.date,
            date__lte=end_date
        ).values_list('duration', flat=True)

        return sum(reports, timedelta())

    def get_balance_raw(self, instance):
        """Calculate the balance of the requested credit.

        This is the difference between the credits duration and the total used
        time.

        :return: The balance
        :rtype:  datetime.timedelta
        """
        return (
            instance.duration - self.get_used_raw(instance)
            if instance.duration
            else None
        )

    def get_used(self, instance):
        """Format the total of used time.

        :return: The formatted total of used time
        :rtype:  str
        """
        used = self.get_used_raw(instance)

        return duration_string(used)

    def get_balance(self, instance):
        """Format the balance.

        This is None if we don't have a duration.

        :return: The formatted balance
        :rtype:  str or None
        """
        balance = self.get_balance_raw(instance)

        return duration_string(balance) if balance else None

    included_serializers = {
        'absence_type': 'timed.employment.serializers.AbsenceTypeSerializer'
    }

    class Meta:
        """Meta information for the absence credit serializer."""

        model  = models.AbsenceCredit
        fields = [
            'user',
            'absence_type',
            'date',
            'duration',
            'used',
            'balance',
        ]
Exemple #9
0
class UserSerializer(ModelSerializer):
    """User serializer."""

    employments      = ResourceRelatedField(many=True, read_only=True)
    absence_credits  = ResourceRelatedField(many=True, read_only=True)
    worktime_balance = SerializerMethodField()

    def get_worktime_balance_raw(self, instance):
        """Calculate the worktime balance for the user.

        1.  Determine the current employment of the user
        2.  Take the latest of those two as start date:
             * The start of the year
             * The start of the current employment
        3.  Take the delivered date if given or the current date as end date
        4.  Determine the count of workdays within start and end date
        5.  Determine the count of public holidays within start and end date
        6.  The expected worktime consists of following elements:
             * Workdays
             * Subtracted by holidays
             * Multiplicated with the worktime per day of the employment
        7.  Determine the overtime credit duration within start and end date
        8.  The reported worktime is the sum of the durations of all reports
            for this user within start and end date
        9.  The absences are all absences for this user between the start and
            end time
        10. The balance is the reported time plus the absences plus the
            overtime credit minus the expected worktime

        :returns: The worktime balance of the user
        :rtype:   datetime.timedelta
        """
        employment = models.Employment.objects.get(
            user=instance,
            end_date__isnull=True
        )
        location = employment.location

        request            = self.context.get('request')
        requested_end_date = request.query_params.get('until')

        start_date = max(employment.start_date, date(date.today().year, 1, 1))
        end_date   = (
            datetime.strptime(requested_end_date, '%Y-%m-%d').date()
            if requested_end_date
            else date.today()
        )

        # workdays is in isoweekday, byweekday expects Monday to be zero
        week_workdays = [int(day) - 1 for day in employment.location.workdays]
        workdays = rrule.rrule(
            rrule.DAILY,
            dtstart=start_date,
            until=end_date,
            byweekday=week_workdays
        ).count()

        # converting workdays as db expects 1 (Sunday) to 7 (Saturday)
        workdays_db = [
            # special case for Sunday
            int(day) == 7 and 1 or int(day) + 1
            for day in location.workdays
        ]
        holidays = models.PublicHoliday.objects.filter(
            location=location,
            date__gte=start_date,
            date__lte=end_date,
            date__week_day__in=workdays_db
        ).count()

        expected_worktime = employment.worktime_per_day * (workdays - holidays)

        overtime_credit = sum(
            models.OvertimeCredit.objects.filter(
                user=instance,
                date__gte=start_date,
                date__lte=end_date
            ).values_list('duration', flat=True),
            timedelta()
        )

        reported_worktime = sum(
            Report.objects.filter(
                user=instance,
                date__gte=start_date,
                date__lte=end_date
            ).values_list('duration', flat=True),
            timedelta()
        )

        absences = sum(
            Absence.objects.filter(
                user=instance,
                date__gte=start_date,
                date__lte=end_date
            ).values_list('duration', flat=True),
            timedelta()
        )

        return (
            reported_worktime +
            absences +
            overtime_credit -
            expected_worktime
        )

    def get_worktime_balance(self, instance):
        """Format the worktime balance.

        :return: The formatted worktime balance.
        :rtype:  str
        """
        worktime_balance = self.get_worktime_balance_raw(instance)

        return duration_string(worktime_balance)

    included_serializers = {
        'employments':
            'timed.employment.serializers.EmploymentSerializer',
        'absence_credits':
            'timed.employment.serializers.AbsenceCreditSerializer'
    }

    class Meta:
        """Meta information for the user serializer."""

        model  = get_user_model()
        fields = [
            'username',
            'first_name',
            'last_name',
            'email',
            'employments',
            'absence_credits',
            'worktime_balance',
        ]
Exemple #10
0
class UserSerializer(ModelSerializer):
    """User serializer."""

    employments = relations.ResourceRelatedField(many=True, read_only=True)
    worktime_balance = SerializerMethodField()
    user_absence_types = relations.SerializerMethodResourceRelatedField(
        source='get_user_absence_types',
        model=models.UserAbsenceType,
        many=True,
        read_only=True)

    def get_user_absence_types(self, instance):
        """Get the user absence types for this user.

        :returns: All absence types for this user
        """
        request = self.context.get('request')

        end = datetime.strptime(
            request.query_params.get('until',
                                     date.today().strftime('%Y-%m-%d')),
            '%Y-%m-%d').date()

        try:
            employment = models.Employment.objects.for_user(instance, end)
        except models.Employment.DoesNotExist:
            return models.UserAbsenceType.objects.none()

        start = max(employment.start_date, date(date.today().year, 1, 1))

        return models.UserAbsenceType.objects.with_user(instance, start, end)

    def get_worktime(self, user, start=None, end=None):
        """Calculate the reported, expected and balance for user.

        1.  Determine the current employment of the user
        2.  Take the latest of those two as start date:
             * The start of the year
             * The start of the current employment
        3.  Take the delivered date if given or the current date as end date
        4.  Determine the count of workdays within start and end date
        5.  Determine the count of public holidays within start and end date
        6.  The expected worktime consists of following elements:
             * Workdays
             * Subtracted by holidays
             * Multiplicated with the worktime per day of the employment
        7.  Determine the overtime credit duration within start and end date
        8.  The reported worktime is the sum of the durations of all reports
            for this user within start and end date
        9.  The absences are all absences for this user between the start and
            end time
        10. The balance is the reported time plus the absences plus the
            overtime credit minus the expected worktime

        :param user:       user to get worktime from
        :param start_date: worktime starting on  given day;
                           if not set when employment started resp. begining of
                           the year
        :param end_date:   worktime till day or if not set today
        :returns: tuple of 3 values reported, expected and balance in given
                  time frame
        """
        end = end or date.today()

        try:
            employment = models.Employment.objects.for_user(user, end)
        except models.Employment.DoesNotExist:
            # If there is no active employment, set the balance to 0
            return timedelta(), timedelta(), timedelta()

        location = employment.location

        if start is None:
            start = max(employment.start_date, date(date.today().year, 1, 1))

        # workdays is in isoweekday, byweekday expects Monday to be zero
        week_workdays = [int(day) - 1 for day in employment.location.workdays]
        workdays = rrule.rrule(rrule.DAILY,
                               dtstart=start,
                               until=end,
                               byweekday=week_workdays).count()

        # converting workdays as db expects 1 (Sunday) to 7 (Saturday)
        workdays_db = [
            # special case for Sunday
            int(day) == 7 and 1 or int(day) + 1 for day in location.workdays
        ]
        holidays = models.PublicHoliday.objects.filter(
            location=location,
            date__gte=start,
            date__lte=end,
            date__week_day__in=workdays_db).count()

        expected_worktime = employment.worktime_per_day * (workdays - holidays)

        overtime_credit = sum(
            models.OvertimeCredit.objects.filter(user=user,
                                                 date__gte=start,
                                                 date__lte=end).values_list(
                                                     'duration', flat=True),
            timedelta())

        reported_worktime = sum(
            Report.objects.filter(user=user, date__gte=start,
                                  date__lte=end).values_list('duration',
                                                             flat=True),
            timedelta())

        absences = sum(
            Absence.objects.filter(user=user, date__gte=start,
                                   date__lte=end).values_list('duration',
                                                              flat=True),
            timedelta())

        reported = reported_worktime + absences + overtime_credit

        return (reported, expected_worktime, reported - expected_worktime)

    def get_worktime_balance(self, instance):
        """Format the worktime balance.

        :return: The formatted worktime balance.
        :rtype:  str
        """
        request = self.context.get('request')
        until = request.query_params.get('until')
        end_date = until and datetime.strptime(until, '%Y-%m-%d').date()

        _, _, balance = self.get_worktime(instance, None, end_date)
        return duration_string(balance)

    included_serializers = {
        'employments':
        'timed.employment.serializers.EmploymentSerializer',
        'user_absence_types':
        'timed.employment.serializers.UserAbsenceTypeSerializer'
    }

    class Meta:
        """Meta information for the user serializer."""

        model = get_user_model()
        fields = [
            'username',
            'first_name',
            'last_name',
            'email',
            'employments',
            'worktime_balance',
            'is_staff',
            'is_active',
            'user_absence_types',
        ]