예제 #1
0
def campaign_planning(campaign, *, external_view=False):
    projects = Project.objects.filter(campaign=campaign)
    projects_ids = ([project.id for project in projects],)

    with connections["default"].cursor() as cursor:
        cursor.execute(
            """\
WITH sq AS (
    SELECT unnest(weeks) AS week
    FROM planning_plannedwork
    WHERE project_id = ANY %s

    UNION ALL

    SELECT date_trunc('week', date)::date
    FROM planning_milestone
    WHERE project_id = ANY %s
)
SELECT MIN(week), MAX(week) FROM sq
            """,
            [projects_ids, projects_ids],
        )
        result = list(cursor)[0]

    if result[0]:
        result = (min(result[0], monday() - dt.timedelta(days=14)), result[1])
        weeks = list(
            islice(
                recurring(result[0], "weekly"),
                2 + (result[1] - result[0]).days // 7,
            )
        )
    else:
        weeks = list(islice(recurring(monday() - dt.timedelta(days=14), "weekly"), 80))

    planning = Planning(weeks=weeks, projects=projects, external_view=external_view)

    planning.add_planned_work_and_milestones(
        PlannedWork.objects.filter(project__campaign=campaign).select_related(
            "service_type"
        ),
        Milestone.objects.filter(project__campaign=campaign),
        ExternalWork.objects.filter(project__campaign=campaign).select_related(
            "service_type"
        ),
    )
    if not external_view:
        planning.add_worked_hours(LoggedHours.objects.all())
    planning.add_absences(Absence.objects.all())
    planning.add_public_holidays()
    planning.add_milestones(Milestone.objects.all())
    return planning.report()
예제 #2
0
    def test_recurring(self):
        """The recurring() utilty returns expected values"""
        self.assertEqual(
            list(islice(recurring(dt.date(2016, 2, 29), "yearly"), 5)),
            [
                dt.date(2016, 2, 29),
                dt.date(2017, 3, 1),
                dt.date(2018, 3, 1),
                dt.date(2019, 3, 1),
                dt.date(2020, 2, 29),
            ],
        )

        self.assertEqual(
            list(islice(recurring(dt.date(2016, 1, 31), "quarterly"), 5)),
            [
                dt.date(2016, 1, 31),
                dt.date(2016, 5, 1),
                dt.date(2016, 7, 31),
                dt.date(2016, 10, 31),
                dt.date(2017, 1, 31),
            ],
        )

        self.assertEqual(
            list(islice(recurring(dt.date(2016, 1, 31), "monthly"), 5)),
            [
                dt.date(2016, 1, 31),
                dt.date(2016, 3, 1),
                dt.date(2016, 3, 31),
                dt.date(2016, 5, 1),
                dt.date(2016, 5, 31),
            ],
        )

        self.assertEqual(
            list(islice(recurring(dt.date(2016, 1, 1), "weekly"), 5)),
            [
                dt.date(2016, 1, 1),
                dt.date(2016, 1, 8),
                dt.date(2016, 1, 15),
                dt.date(2016, 1, 22),
                dt.date(2016, 1, 29),
            ],
        )

        with self.assertRaises(ValueError):
            list(islice(recurring(dt.date(2016, 1, 1), "unknown"), 5))
예제 #3
0
def create_accruals_for_last_month():
    today = dt.date.today()
    start = today.replace(year=today.year - 2, day=1)

    for day in recurring(start, "monthly"):
        if day > today:
            break
        Accruals.objects.for_cutoff_date(day - dt.timedelta(days=1))
예제 #4
0
def user_planning(user):
    weeks = list(
        islice(recurring(monday() - dt.timedelta(days=14), "weekly"), 80))
    planning = Planning(weeks=weeks, users=[user])
    planning.add_planned_work(user.planned_work.all())
    planning.add_planning_requests(user.received_planning_requests.all())
    planning.add_worked_hours(user.loggedhours.all())
    planning.add_absences(user.absences.all())
    return planning.report()
예제 #5
0
def project_planning(project):
    with connections["default"].cursor() as cursor:
        cursor.execute(
            """\
WITH sq AS (
    SELECT unnest(weeks) AS week
    FROM planning_plannedwork
    WHERE project_id=%s

    UNION ALL

    SELECT earliest_start_on AS week
    FROM planning_planningrequest
    WHERE project_id=%s

    UNION ALL

    SELECT completion_requested_on - 7 AS week
    FROM planning_planningrequest
    WHERE project_id=%s
)
SELECT MIN(week), MAX(week) FROM sq
            """,
            [project.id, project.id, project.id],
        )
        result = list(cursor)[0]

    if result[0]:
        result = (min(result[0], monday() - dt.timedelta(days=14)), result[1])
        weeks = list(
            islice(
                recurring(result[0], "weekly"),
                2 + (result[1] - result[0]).days // 7,
            ))
    else:
        weeks = list(
            islice(recurring(monday() - dt.timedelta(days=14), "weekly"), 80))

    planning = Planning(weeks=weeks)
    planning.add_planned_work(project.planned_work.all())
    planning.add_planning_requests(project.planning_requests.all())
    planning.add_worked_hours(LoggedHours.objects.all())
    planning.add_absences(Absence.objects.all())
    return planning.report()
예제 #6
0
def offers_hourly_rate(date_range):
    margin_m = defaultdict(lambda: Z2)
    hours_m = defaultdict(lambda: Z1)
    margin_y = defaultdict(lambda: Z2)
    hours_y = defaultdict(lambda: Z1)

    def month(day):
        return (day.year, day.month)

    for offer in (Offer.objects.accepted().filter(
            closed_on__range=date_range).prefetch_related("services")):
        margin = offer.total_excl_tax - sum(
            (service.third_party_costs
             for service in offer.services.all() if service.third_party_costs),
            Z2,
        )
        hours = sum(
            (service.service_hours for service in offer.services.all()),
            Z1,
        )

        margin_m[month(offer.closed_on)] += margin
        hours_m[month(offer.closed_on)] += hours
        margin_y[offer.closed_on.year] += margin
        hours_y[offer.closed_on.year] += hours

    months = [
        month(m) for m in takewhile(
            lambda day: day < date_range[1],
            recurring(date_range[0], "monthly"),
        )
    ]
    return {
        "by_month": {
            month: {
                "gross_margin":
                margin_m[month],
                "hours":
                hours_m[month],
                "hourly_rate":
                margin_m[month] / hours_m[month] if hours_m[month] else Z2,
            }
            for month in months
        },
        "by_year": {
            year: {
                "gross_margin":
                margin_y[year],
                "hours":
                hours_y[year],
                "hourly_rate":
                margin_y[year] / hours_y[year] if hours_y[year] else Z2,
            }
            for year in range(date_range[0].year, date_range[1].year + 1)
        },
    }
예제 #7
0
def team_planning(team):
    weeks = list(
        islice(recurring(monday() - dt.timedelta(days=14), "weekly"), 80))
    planning = Planning(weeks=weeks, users=list(team.members.active()))
    planning.add_planned_work(PlannedWork.objects.filter(user__teams=team))
    planning.add_planning_requests(
        PlanningRequest.objects.filter(
            Q(receivers__teams=team) | Q(planned_work__user__teams=team)))
    planning.add_worked_hours(
        LoggedHours.objects.filter(rendered_by__teams=team))
    planning.add_absences(Absence.objects.filter(user__teams=team))
    return planning.report()
예제 #8
0
def team_planning(team, date_range):
    start, end = date_range
    weeks = list(takewhile(lambda x: x <= end, recurring(monday(start), "weekly")))
    planning = Planning(weeks=weeks, users=list(team.members.active()))
    planning.add_planned_work_and_milestones(
        PlannedWork.objects.filter(user__teams=team).select_related("service_type"),
        Milestone.objects.filter(project__planned_work__user__teams=team),
    )
    planning.add_worked_hours(LoggedHours.objects.filter(rendered_by__teams=team))
    planning.add_absences(Absence.objects.filter(user__teams=team))
    planning.add_public_holidays()
    planning.add_milestones(Milestone.objects.all())
    return planning.report()
예제 #9
0
def user_planning(user, date_range):
    start, end = date_range
    weeks = list(takewhile(lambda x: x <= end, recurring(monday(start), "weekly")))
    planning = Planning(weeks=weeks, users=[user])
    planning.add_planned_work_and_milestones(
        user.planned_work.select_related("service_type"),
        Milestone.objects.filter(
            Q(project__planned_work__user=user) | Q(project__owned_by=user)
        ),
    )
    planning.add_worked_hours(user.loggedhours.all())
    planning.add_absences(user.absences.all())
    planning.add_public_holidays()
    planning.add_milestones(Milestone.objects.all())
    return planning.report()
예제 #10
0
def gross_margin_by_month(date_range):
    gross = gross_profit_by_month(date_range)
    third = third_party_costs_by_month(date_range)
    accruals = accruals_by_month(date_range)
    fte = full_time_equivalents_by_month()

    pi = projected_invoices()

    first_of_months = list(
        takewhile(
            lambda day: day < date_range[1],
            recurring(date_range[0], "monthly"),
        ))

    profit = []
    for day in first_of_months:
        month = (day.year, day.month)
        row = {
            "month": month,
            "key": "%s-%s" % month,
            "date": day,
            "gross_profit": gross[month],
            "third_party_costs": third[month],
            "accruals": accruals.get(month) or {
                "accrual": None,
                "delta": Z2
            },
            "fte": fte.get(day, Z2),
            "projected_invoices": pi["monthly_overall"].get(month),
        }
        if not any((
                row["gross_profit"],
                row["third_party_costs"],
                row["accruals"]["delta"],
                row["projected_invoices"],
        )):
            continue

        row["gross_margin"] = (row["gross_profit"] + row["third_party_costs"] +
                               row["accruals"]["delta"])
        row["margin_per_fte"] = row["gross_margin"] / row["fte"] if row[
            "fte"] else None
        profit.append(row)

    return profit
예제 #11
0
 def create_invoices(self):
     invoices = []
     days = recurring(
         max(filter(None, (self.next_period_starts_on, self.starts_on))),
         self.periodicity,
     )
     generate_until = min(
         filter(None, (in_days(-self.create_invoice_on_day), self.ends_on)))
     this_period = next(days)
     while True:
         if this_period > generate_until:
             break
         next_period = next(days)
         invoices.append(
             self.create_single_invoice(
                 period_starts_on=this_period,
                 period_ends_on=next_period - dt.timedelta(days=1),
             ))
         self.next_period_starts_on = next_period
         this_period = next_period
     self.save()
     return invoices
예제 #12
0
class ExternalWorkForm(ModelForm):
    class Meta:
        model = ExternalWork
        fields = (
            "provided_by",
            "title",
            "service_type",
            "notes",
            "milestone",
        )
        widgets = {
            "provided_by": Autocomplete(model=Organization),
            "notes": Textarea,
        }

    def __init__(self, *args, **kwargs):
        initial = kwargs.setdefault("initial", {})
        request = kwargs["request"]

        self.project = kwargs.pop("project", None)
        if not self.project:  # Updating
            self.project = kwargs["instance"].project
        else:
            initial["provided_by"] = self.project.customer_id

        if service_id := request.GET.get("service"):
            try:
                service = self.project.services.get(pk=service_id)
            except (self.project.services.model.DoesNotExist, TypeError, ValueError):
                pass
            else:
                initial.update(
                    {
                        "title": f"{self.project.title}: {service.title}",
                        "notes": service.description,
                        "planned_hours": service.service_hours,
                    }
                )

        # if pk := request.GET.get("copy"):
        #     try:
        #         pw = ExternalWork.objects.get(pk=pk)
        #     except (ExternalWork.DoesNotExist, TypeError, ValueError):
        #         pass
        #     else:
        #         initial.update(
        #             {
        #                 "project": pw.project_id,
        #                 "offer": pw.offer_id,
        #                 "title": pw.title,
        #                 "notes": pw.notes,
        #                 "planned_hours": pw.planned_hours,
        #                 "weeks": pw.weeks,
        #                 "is_provisional": pw.is_provisional,
        #                 "service_type": pw.service_type_id,
        #                 "milestone": pw.milestone_id,
        #             }
        #         )

        super().__init__(*args, **kwargs)
        self.instance.project = self.project

        self.fields["milestone"].queryset = self.project.milestones.all()
        # self.fields["service_type"].required = True

        date_from_options = [
            monday(),
            self.instance.weeks and min(self.instance.weeks),
            initial.get("weeks") and min(initial["weeks"]),
        ]
        date_from = min(filter(None, date_from_options)) - dt.timedelta(days=21)

        self.fields["weeks"] = forms.TypedMultipleChoiceField(
            label=capfirst(_("weeks")),
            choices=[
                (
                    day,
                    "KW{} ({} - {})".format(
                        local_date_format(day, fmt="W"),
                        local_date_format(day),
                        local_date_format(day + dt.timedelta(days=6)),
                    ),
                )
                for day in islice(recurring(date_from, "weekly"), 80)
            ],
            widget=forms.SelectMultiple(attrs={"size": 20}),
            initial=self.instance.weeks or [monday()],
            coerce=parse_date,
        )
예제 #13
0
            initial.get("weeks") and min(initial["weeks"]),
        ]
        date_from = min(filter(None, date_from_options)) - dt.timedelta(days=21)

        self.fields["weeks"] = forms.TypedMultipleChoiceField(
            label=capfirst(_("weeks")),
            choices=[
                (
                    day,
                    "KW{} ({} - {})".format(
                        local_date_format(day, fmt="W"),
                        local_date_format(day),
                        local_date_format(day + dt.timedelta(days=6)),
                    ),
                )
                for day in islice(recurring(date_from, "weekly"), 80)
            ],
            widget=forms.SelectMultiple(attrs={"size": 20}),
            initial=self.instance.weeks or [monday()],
            coerce=parse_date,
        )

    def clean(self):
        data = super().clean()

        if (weeks := data.get("weeks")) and (milestone := data.get("milestone")):
            if after := [week for week in weeks if week >= monday(milestone.date)]:
                self.add_warning(
                    _(
                        "The milestone is scheduled on %(date)s, but work is"
                        " planned in the same or following week(s): %(weeks)s"
예제 #14
0
 def weeks(self):
     return list(
         takewhile(
             lambda x: x < self.completion_requested_on,
             recurring(self.earliest_start_on, "weekly"),
         ))