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