예제 #1
0
class Year(Model):
    MONTHS = [
        "january",
        "february",
        "march",
        "april",
        "may",
        "june",
        "july",
        "august",
        "september",
        "october",
        "november",
        "december",
    ]

    working_time_model = models.ForeignKey(
        WorkingTimeModel,
        on_delete=models.CASCADE,
        verbose_name=_("working time model"),
    )
    year = models.IntegerField(_("year"))
    january = models.DecimalField(_("january"), max_digits=4, decimal_places=2)
    february = models.DecimalField(_("february"),
                                   max_digits=4,
                                   decimal_places=2)
    march = models.DecimalField(_("march"), max_digits=4, decimal_places=2)
    april = models.DecimalField(_("april"), max_digits=4, decimal_places=2)
    may = models.DecimalField(_("may"), max_digits=4, decimal_places=2)
    june = models.DecimalField(_("june"), max_digits=4, decimal_places=2)
    july = models.DecimalField(_("july"), max_digits=4, decimal_places=2)
    august = models.DecimalField(_("august"), max_digits=4, decimal_places=2)
    september = models.DecimalField(_("september"),
                                    max_digits=4,
                                    decimal_places=2)
    october = models.DecimalField(_("october"), max_digits=4, decimal_places=2)
    november = models.DecimalField(_("november"),
                                   max_digits=4,
                                   decimal_places=2)
    december = models.DecimalField(_("december"),
                                   max_digits=4,
                                   decimal_places=2)
    working_time_per_day = HoursField(_("working time per day"))

    class Meta:
        ordering = ["-year"]
        unique_together = (("working_time_model", "year"), )
        verbose_name = _("year")
        verbose_name_plural = _("years")

    def __str__(self):
        return "%s %s" % (self.year, self.working_time_model)

    @property
    def months(self):
        return [getattr(self, field) for field in self.MONTHS]

    @property
    def pretty_working_time_per_day(self):
        return "%s/%s" % (hours(self.working_time_per_day), _("day"))
예제 #2
0
class Milestone(Model):
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        verbose_name=_("project"),
        related_name="milestones",
    )
    date = models.DateField(_("date"))
    title = models.CharField(_("title"), max_length=200)

    phase_starts_on = models.DateField(_("phase starts on"),
                                       blank=True,
                                       null=True)

    estimated_total_hours = HoursField(
        _("planned hours"),
        validators=[MinValueValidator(Decimal("0.0"))],
        default=Decimal("0.0"),
    )

    class Meta:
        ordering = ["date"]
        verbose_name = _("milestone")
        verbose_name_plural = _("milestones")

    def __str__(self):
        return f"{self.title} ({local_date_format(self.date, fmt='l, j.n.')})"
예제 #3
0
class PlannedWork(AbstractPlannedWork):
    open_in_modal = True

    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        verbose_name=_("project"),
        related_name="planned_work",
    )
    offer = models.ForeignKey(
        Offer,
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        verbose_name=_("offer"),
        related_name="planned_work",
    )
    user = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        verbose_name=_("user"),
        related_name="planned_work",
    )
    planned_hours = HoursField(_("planned hours"),
                               validators=[MinValueValidator(Decimal("0.1"))])
    is_provisional = models.BooleanField(_("is provisional"), default=False)

    objects = PlannedWorkQuerySet.as_manager()

    class Meta:
        ordering = ["-pk"]
        verbose_name = _("planned work")
        verbose_name_plural = _("planned work")

    def __str__(self):
        u = self.user.get_short_name()
        h = hours(self.planned_hours)
        return f"{self.title} ({u}, {h}, {self.pretty_from_until})"
예제 #4
0
class LoggedHours(Model):
    service = models.ForeignKey(
        Service,
        on_delete=models.PROTECT,
        related_name="loggedhours",
        verbose_name=_("service"),
    )
    created_at = models.DateTimeField(_("created at"),
                                      default=timezone.now,
                                      db_index=True)
    created_by = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        verbose_name=_("created by"),
        related_name="loggedhours_set",
    )
    rendered_on = models.DateField(_("rendered on"),
                                   default=dt.date.today,
                                   db_index=True)
    rendered_by = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        related_name="loggedhours",
        verbose_name=_("rendered by"),
    )
    hours = HoursField(_("hours"),
                       validators=[MinValueValidator(Decimal("0.1"))])
    description = models.TextField(_("description"))

    invoice_service = models.ForeignKey(
        "invoices.Service",
        on_delete=models.PROTECT,
        blank=True,
        null=True,
        verbose_name=_("invoice service"),
    )
    archived_at = models.DateTimeField(_("archived at"), blank=True, null=True)

    class Meta:
        indexes = [models.Index(fields=["-rendered_on"])]
        ordering = ("-rendered_on", "-created_at")
        verbose_name = _("logged hours")
        verbose_name_plural = _("logged hours")

    def __str__(self):
        return "%s: %s" % (self.service.title, self.description)

    @classmethod
    def allow_delete(cls, instance, request):
        if instance.invoice_service_id or instance.archived_at:
            messages.error(request, _("Cannot delete archived logged hours."))
            return False
        if instance.rendered_on < logbook_lock():
            messages.error(request,
                           _("Cannot delete logged hours from past weeks."))
            return False
        return super().allow_delete(instance, request)

    @classmethod
    def get_redirect_url(cls, instance, request):
        if not request.is_ajax():
            return cls.urls["list"] + "?project={}".format(
                instance.service.project_id if instance else "")
예제 #5
0
class PlanningRequest(Model):
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        verbose_name=_("project"),
        related_name="planning_requests",
    )
    offer = models.ForeignKey(
        Offer,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        verbose_name=_("offer"),
        related_name="planning_requests",
    )
    requested_hours = HoursField(
        _("requested hours"), validators=[MinValueValidator(Decimal("0.1"))])
    planned_hours = HoursField(_("planned hours"))
    earliest_start_on = models.DateField(_("earliest start on"))
    completion_requested_on = models.DateField(_("completion requested on"))
    title = models.CharField(_("title"), max_length=100)
    description = models.TextField(_("description"), blank=True)
    receivers = models.ManyToManyField(
        User,
        verbose_name=_("receivers"),
        related_name="received_planning_requests")

    created_at = models.DateTimeField(_("created at"), default=timezone.now)
    created_by = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        verbose_name=_("created by"),
        related_name="sent_planning_requests",
    )
    closed_at = models.DateTimeField(_("closed at"), blank=True, null=True)
    is_provisional = models.BooleanField(_("is provisional"), default=False)

    objects = PlanningRequestQuerySet.as_manager()

    class Meta:
        ordering = ["-pk"]
        verbose_name = _("planning request")
        verbose_name_plural = _("planning requests")

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if self.pk:
            self.planned_hours = (self.planned_work.order_by().aggregate(
                h=Sum("planned_hours"))["h"] or Z1)
        else:
            self.planned_hours = Z1
        super().save(*args, **kwargs)

    save.alters_data = True

    def clean_fields(self, exclude=None):
        super().clean_fields(exclude=exclude)
        errors = {}

        if self.earliest_start_on.weekday() != 0:
            errors["earliest_start_on"] = _("Only mondays allowed.")
        if self.completion_requested_on.weekday() != 0:
            errors["completion_requested_on"] = _("Only mondays allowed.")

        if self.completion_requested_on <= self.earliest_start_on:
            errors["completion_requested_on"] = _(
                "Allow at least one week for the work please.")

        raise_if_errors(errors, exclude)

    @cached_property
    def weeks(self):
        return list(
            takewhile(
                lambda x: x < self.completion_requested_on,
                recurring(self.earliest_start_on, "weekly"),
            ))

    @cached_property
    def receivers_with_work(self):
        work = {user: [] for user in self.receivers.all()}
        for pw in self.planned_work.select_related("user"):
            work.setdefault(pw.user, []).append(pw)
        return sorted(work.items())

    @property
    def missing_hours(self):
        return self.requested_hours - self.planned_hours

    def html_link(self):
        return format_html('<a href="{}" data-toggle="ajaxmodal">{}</a>',
                           self.get_absolute_url(), self)
예제 #6
0
class PlannedWork(Model):
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        verbose_name=_("project"),
        related_name="planned_work",
    )
    offer = models.ForeignKey(
        Offer,
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        verbose_name=_("offer"),
        related_name="planned_work",
    )
    request = models.ForeignKey(
        PlanningRequest,
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        verbose_name=_("planning request"),
        related_name="planned_work",
    )
    created_at = models.DateTimeField(_("created at"), default=timezone.now)
    user = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        verbose_name=_("user"),
        related_name="planned_work",
    )
    planned_hours = HoursField(_("planned hours"),
                               validators=[MinValueValidator(Decimal("0.1"))])
    title = models.CharField(_("title"), max_length=200)
    notes = models.TextField(_("notes"), blank=True)
    weeks = ArrayField(models.DateField(), verbose_name=_("weeks"))

    class Meta:
        ordering = ["-pk"]
        verbose_name = _("planned work")
        verbose_name_plural = _("planned work")

    def __str__(self):
        return "{} ({})".format(self.title, hours(self.planned_hours))

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._update_request_ids = {self.request_id}

    def _update_requests(self):
        self._update_request_ids.add(self.request_id)
        self._update_request_ids.discard(None)
        if self._update_request_ids:
            for request in PlanningRequest.objects.filter(
                    id__in=self._update_request_ids):
                request.save()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        self._update_requests()

    save.alters_data = False

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        self._update_requests()

    delete.alters_data = False

    def clean_fields(self, exclude=None):
        super().clean_fields(exclude=exclude)
        errors = {}

        if self.weeks:
            no_mondays = [day for day in self.weeks if day.weekday() != 0]
            if no_mondays:
                errors["weeks"] = _(
                    "Only mondays allowed, but field contains %s.") % (
                        ", ".join(
                            local_date_format(day) for day in no_mondays), )

        raise_if_errors(errors, exclude)

    @property
    def pretty_from_until(self):
        return "{} – {}".format(
            local_date_format(min(self.weeks)),
            local_date_format(max(self.weeks) + dt.timedelta(days=6)),
        )

    @property
    def ranges(self):
        def _find_ranges(weeks):
            start = weeks[0]
            maybe_end = weeks[0]
            for day in weeks[1:]:
                if (day - maybe_end).days == 7:
                    maybe_end = day
                else:
                    yield start, maybe_end
                    start = maybe_end = day

            yield start, maybe_end

        for from_, until_ in _find_ranges(self.weeks):
            yield {
                "from":
                from_,
                "until":
                until_,
                "pretty":
                "{} – {}".format(
                    local_date_format(from_),
                    local_date_format(until_ + dt.timedelta(days=6)),
                ),
            }

    @property
    def pretty_planned_hours(self):
        return _(
            "%(planned_hours)s in %(weeks)s weeks (%(per_week)s per week)") % {
                "planned_hours": hours(self.planned_hours),
                "weeks": len(self.weeks),
                "per_week": hours(self.planned_hours / len(self.weeks)),
            }