Example #1
0
    def apply_holidays(self):
        for h in self.bh.holidays:
            if h[1] == "Holidays":
                continue

            d = h[0]

            if not (self.start <= d <= self.end):
                continue

            weekday = d.weekday() + 1
            if weekday in self.bh.weekends:
                log.warning(f"Skipping weekend day {d}")
                continue

            if d not in self.days:
                log.info(f"Adding new day entry list for day {d}")
                self.days[d] = StrictList(Entry)

            dwh = self.bh.get_daily_working_hours() - self.bh.breaks
            dwstart = self.bh.worktimings[0]
            dtime = add_hours(d, dwstart)
            dtime = self.get_timezone().localize(dtime)

            self.project_seconds[h[1]] += tdelta(hours=dwh).total_seconds()
            self.total_time += tdelta(hours=dwh)
            entry = Entry(h[1], dtime, add_hours(dtime, dwh),
                          tdelta(hours=dwh), ["off"])
            pause_entry = Entry(h[1], entry.end,
                                add_hours(entry.end, self.bh.breaks),
                                tdelta(hours=self.bh.breaks), ["pause", "off"])
            log.info(f"Adding entry {entry} to {d}")
            self.days[d].append(entry)
Example #2
0
 def checks(self):
     log.info(
         f"Performing checks on {', '.join([str(s) for s in self.days.keys()])}"
     )
     check_for_expected_hours(self.days, self.get_labor_hours)
     check_for_gaps_and_overlaps(self.days)
     check_for_completeness(self.days, self.bh.get_actual_work_days())
     check_weekends(self.days, self.weekends)
Example #3
0
    def calculate_percents(self):
        for ws in self.ws:
            ws_obj = Workspace(ws)

            log.info(mk_headline(f"Times in Workspace {ws}", "*"))
            # seconds = {}
            self.total_time = tdelta(0)

            self.project_seconds = {
                "Vacations": 0.0,
                "Courses": 0.0,
                "Sick": 0.0
            }

            for project in ws_obj.native_projects:
                p_name = project.name
                times = project.time_entries.list()

                for i in times:
                    if not hasattr(i, "stop"):
                        log.warn("Entry %s seems to still be running" %
                                 i.description)
                        continue
                    start = i.start.replace(tzinfo=pytz.timezone("UTC"))
                    end = i.stop.replace(tzinfo=pytz.timezone("UTC"))

                    if start.date() < self.start or end.date() > self.end:
                        continue

                    dur = end - start

                    if dur.total_seconds() / 3600. > 11:
                        log.warn("Warning: the entry seems to be too long:")
                        log.warn(
                            f"{p_name} from {start} to {end}; duration {dur}")

                    if start.date() not in self.days:
                        self.days[start.date()] = StrictList(Entry)

                    if p_name not in self.project_seconds:
                        self.project_seconds[p_name] = 0.0

                    e = None

                    tags = list(set([t.lower() for t in i.tags])) if hasattr(
                        i, "tags") else []

                    e = Entry(p_name, start, end, dur, tags)
                    add_dur = not (p_name == "Holidays" or
                                   (hasattr(i, "tags") and "Pause" in i.tags))
                    if add_dur:
                        self.project_seconds[p_name] += dur.total_seconds()

                    self.days[start.date()].append(e)
Example #4
0
def check_weekends(days, weekends):
    okay = True
    for d in days:
        at_work_entries = [
            e.name not in ["Vacations", "Sick"] for e in days[d]
        ]
        if d.weekday() + 1 in weekends and any(at_work_entries):
            log.warn(
                f"Seems like {dt.strftime(d, '%d.%m.%Y')} is set as a weekend day. Were you really working then?"
            )
            log.info(f"Entry: {days[d]}")
            okay = False
    return okay
Example #5
0
 def wrapper(*args, **kwargs):
     m = f"Check: {msg}"
     log.info(mk_headline(m, ">", indent=5))
     okay = func(*args, **kwargs)
     if not okay:
         log.warn(
             mk_headline(
                 f"{Colour.RED}{Colour.BOLD}Not OK!{Colour.END}"))
     else:
         log.info(
             mk_headline(f"{Colour.GREEN}{Colour.BOLD}OK!{Colour.END}"))
     log.info(mk_headline(sgn="<"))
     log.info("")
     return okay
Example #6
0
    def output_results(self):
        log.info(mk_headline(f"Resulting resource distribution", "="))
        log.info(mk_headline(sgn="-"))

        project_seconds, total_seconds = self.calc_project_and_total_seconds()

        all_hours = self.bh.get_actual_working_hours()
        log.info(f"Total hours in month {self.start.month}: {all_hours}")
        print(mk_headline(sgn="-"))
        perc_sum = 0.0
        for p in project_seconds:
            perc = round(project_seconds[p] / total_seconds * 100.)
            perc = round(perc / 5) * 5
            perc_sum += perc
            if perc > 0.0:
                print(f"==> Project {p}: {perc}%")
        print(mk_headline(sgn="-"))
        print(f"Sum: {perc_sum}%")
        print(mk_headline(sgn="="))
Example #7
0
def check_for_expected_hours(days, get_working_hours_func):
    days_sorted = sorted(days.keys())
    total_hours = 0.0
    overunder_sum = 0.0
    for d in days_sorted:
        pause_hours = 0.0
        expected_day_hours = get_working_hours_func(d)
        actual_day_hours = 0.0
        special_day_type = None

        for e in days[d]:  # type: Entry
            if "pause" in e.tags:
                pause_hours += e.duration.total_seconds() / 3600.0
                continue
            if "off" in e.tags:
                special_day_type = e.name

            entry_time = e.duration
            entry_hours = entry_time.total_seconds() / 3600.0
            actual_day_hours += entry_hours

        total_hours += actual_day_hours
        overunder = actual_day_hours - expected_day_hours
        if overunder < 0.0:
            p = pause_hours
            overunder += p
            pause_hours -= p

        overunder_sum += overunder

        c = Colour.RED if overunder < 0.0 else Colour.BLUE
        overunder_str = f"{Colour.BOLD}{c}{overunder:>+5.2f}{Colour.END}"

        c = Colour.RED if pause_hours < 1.0 else Colour.BLUE
        pause_str = f"{Colour.BOLD}{c}{pause_hours:>5.2f}{Colour.END}"

        result = [dt.strftime(d, '%d.%m.%Y')]

        if special_day_type is None:
            result.append(f"{actual_day_hours:>5.2f}h")
            result.append(f"breaks: {pause_str}h => +/- {overunder_str}h")
        else:
            if special_day_type == "Vacations" or special_day_type == "Holidays":
                result.append(
                    f"{Colour.GREEN}{Colour.BOLD}is a day off{Colour.END}")
            elif special_day_type == "Sick":
                result.append(
                    f"{Colour.YELLOW}{Colour.BOLD}Hope you got well!{Colour.END}"
                )

        if pause_hours < 1.0 and not special_day_type:
            result.append(
                f"{Colour.BOLD}You need sufficient breaks!{Colour.END}")

        log.info("; ".join(result))

    log.info(mk_headline("Sum of daily hours"))
    log.info(f" Sum: {total_hours:>6.1f}h")
    log.info(f" +/- {overunder_sum:>6.1f}h")

    log.info(mk_headline("So..."))
    if overunder_sum < 0.0:
        log.warn("  You have a negative time record in the given period!")
    elif overunder_sum > 0.0:
        log.info("  You have worked overtime in the given period!")

    return overunder_sum >= 0.0
Example #8
0
    def __init__(self, start_date, end_date, config):
        self.special_projects = ["Vacations", "Sick", "Courses"]
        self.config = config

        self.api_key = config.api["api_key"]
        self.ezve_rounding = config.settings["ezve_rounding"]
        self.projects = config.projects
        self.holidays = parse_holidays(config.holidays)
        self.api = Api(self.api_key)
        self.ws = self.api.workspaces
        self.start = start_date
        self.end = end_date
        self.worktimings = config.settings["worktimings"]
        self.weekends = config.settings["weekends"]
        self.productivity_mappings = config.productivity_mappings

        self.days = StrictDict(StrictList)

        self.bh = WorkingHours(self.start,
                               self.end,
                               weekends=self.weekends,
                               worktimings=self.worktimings,
                               holidays=self.holidays)
        # Workspace.working_hours = self.bh
        log.info(mk_headline(sgn="="))
        log.info(mk_headline("Resource Planner", "#"))
        log.info(mk_headline(sgn="="))
        log.info("")
        log.info(
            " Expected working hours in time span %s to %s: %s (%s days)" %
            (dt.strftime(
                self.start, "%d.%m.%Y"), dt.strftime(
                    self.end, "%d.%m.%Y"), self.bh.get_actual_working_hours(),
             self.bh.get_number_of_actual_workdays()))
        log.info(mk_headline(sgn="="))
Example #9
0
    def add_from_csv(self, csv_file):
        pd = pandas.read_csv(csv_file, index_col=False)
        for i, row in pd.iterrows():
            print(row)
            wid = row["WID"]
            ws_ids = [w.id for w in self.ws]
            if wid not in ws_ids:
                raise Exception(f"WS ID {wid} not in Workspaces ({ws_ids})")
            else:
                assert isinstance(wid, int)
                ws = self.ws.get(wid)

            inf = self.get_timezone()

            start = parse(f"{row['Date']}, {row['From']}+2:00")

            try:
                to = parse(f"{row['To']}").time()
            except ValueError:
                to = None

            try:
                duration = parse_time(row['Duration'])
            except ValueError:
                duration = None

            if duration is not None:
                delta = tdelta(hours=duration.hour,
                               minutes=duration.minute,
                               seconds=duration.second)
            else:
                shours = start.time().hour
                sminut = start.time().minute
                ssecon = start.time().second
                ehours = to.hour
                eminut = to.minute
                esecon = to.second
                delta = tdelta(hours=ehours - shours,
                               minutes=eminut - sminut,
                               seconds=esecon - ssecon)

            end = start + delta

            pname = row["Project"]

            # print(pids)

            log.info(
                f"Adding entry on {start.date().strftime('%d.%m.%Y')} from {start.time()} to {end.time()} (Duration: {duration}"
            )

            project = pname
            projects_by_name = dict([(p.name, p) for p in ws.projects
                                     if p.name == project])
            project_obj = projects_by_name[project]

            e_pid = project_obj.id
            e_start = start.isoformat()

            e_dur = delta.total_seconds()
            e_desc = row["Description"]
            e_tags = row["Tags"].split("|")

            log.info(f"Project: {project}")
            log.info(f"Tags   : {', '.join(e_tags)}")
            log.info(f"Description: {e_desc}")
            log.info(f"Creating entry...")

            self.api.time_entries.create(
                time_entry={
                    "wid": wid,
                    "pid": e_pid,
                    "billable": False,
                    "start": e_start,
                    "duration": e_dur,
                    "description": e_desc,
                    "tags": e_tags,
                    "created_with": "resource_planner"
                })
Example #10
0
    def output_ezve(self, outfile):
        days = sorted(self.days)
        lines = []
        for d in days:
            log.info(mk_headline(str(d)))
            day = self.days[d]

            project_seconds = DefaultDict(0.0)
            day_seconds = 0.0

            for e in day:
                if "pause" in e.tags:
                    continue

                project = self.projects.get_by_name(e.name)
                project_seconds[project.code] += e.duration.seconds
                day_seconds += e.duration.seconds

            prod_mapping_names = [m.code for m in self.productivity_mappings]
            mapped_seconds = DefaultDict(0.0)
            for s in project_seconds:
                prod_proj = self.projects.get_by_code(s)
                ps = project_seconds[s]
                if s in prod_mapping_names:
                    mappings = self.productivity_mappings[s].mappings
                    for m in mappings:
                        prod_proj = self.projects.get_by_code(
                            m.productive_project)
                        mapped_seconds[(prod_proj.code,
                                        prod_proj.ccenter)] += ps * m.fraction
                else:
                    mapped_seconds[(prod_proj.code, prod_proj.ccenter)] += ps

            day_sum = 0
            codes = list(mapped_seconds.keys())
            percents = [0] * len(codes)
            percents = dict(zip(codes, percents))

            def ezve_round(v):
                return int(self.ezve_rounding *
                           round(float(v * 100.) / self.ezve_rounding)) / 100.

            for ms in mapped_seconds:
                ps = mapped_seconds[ms]
                percents[ms] = ps / day_seconds
                percents[ms] = ezve_round(percents[ms])

            remains = DefaultDict(0.0)
            for pc in percents:
                c, cc = pc
                prj = self.projects.get_by_code(c)
                remains[pc] = min(1.0, prj.max) - percents[pc]

            underfull_projects = dict([(i, remains[i]) for i in remains
                                       if remains[i] > 0.0])
            overfull_projects = dict([(i, remains[i]) for i in remains
                                      if remains[i] < 0.0])
            for ofp in overfull_projects:
                c, cc = ofp
                prj = self.projects.get_by_code(c)
                percents[ofp] = prj.max
                if not any(underfull_projects):
                    log.warning(
                        f"There are no unfilled projects on this day that could take the remaining {remains[ofp]*-100}% of {prj.name}"
                    )
                    log.warning(
                        f"Please make sure that at least one other entry is given in Toggl that can take the overflow"
                    )
                    continue

                while remains[ofp] > 0:
                    r = remains[ofp]
                    subt = r / len(underfull_projects)
                    subt = ezve_round(subt)
                    for ufp in underfull_projects:
                        if subt > remains[ufp]:
                            subt = remains[ufp]

                        r -= subt
                        remains[ufp] -= subt
                        percents[ufp] += subt

            for c, cc in percents:
                prj = self.projects.get_by_code(c)
                if prj.ezve_ignore:
                    continue

                p = percents[(c, cc)]
                if p * 100 <= 1e-2:
                    log.info(f"{p:>8.3f}% {c} {cc} skipped")
                    continue

                day_sum += int(p * 100)
                # log.info(f"  {c:15s} -> CC: {str(cc):15s} {p*100:>8.0f}%")
                if outfile is not None:
                    lines.append(
                        f"{d.year}\t{d.month}\t{d.day}\t{cc:05d}\t{p*100}")
                else:
                    if not any(lines):
                        lines.append("Date\t\tProject\t\tPercent")
                        lines.append(mk_headline())

                    lines.append(
                        f"{dt.strftime(d, '%d.%m.%Y')}\t{c:20}\t{cc:8}\t{p*100:5.0f}%"
                    )

            if day_sum < 100:
                log.warning(f"This day is only filled to {day_sum}%!")
            elif day_sum > 100:
                log.error(f"This day is overfilled to {day_sum}%!")

            if outfile is None:
                lines.append(mk_headline())

        if outfile is not None:
            with open(outfile, "w") as f:
                for l in lines:
                    f.write(l + "\n")
        else:
            for l in lines:
                log.info(l.strip())
Example #11
0
    def output_planning(self):
        from string import Template
        import locale
        locale.setlocale(locale.LC_ALL, '')
        mtemp_lines = self.config.templates["mail"]
        mtemp = "\n".join(mtemp_lines)
        temp = Template(mtemp)
        dtemp = Template(self.config.templates["project_line"])
        stemp = Template(self.config.templates["sum_line"])

        pdate = self.start
        _, mdays = monthrange(pdate.year, pdate.month)
        ndate = self.start + tdelta(days=mdays)

        log.info(f"Planning for {pdate} and {ndate}")

        pmonth = dt.strftime(pdate, "%B")
        nmonth = dt.strftime(ndate, "%B")
        pyear = dt.strftime(pdate, "%Y")
        nyear = dt.strftime(ndate, "%Y")
        pyears = dt.strftime(pdate, "%y")
        nyears = dt.strftime(ndate, "%y")

        if pdate.year == ndate.year:
            pyear = ""

        pvacation_days_no = len(self.bh.get_vacations(pdate.month, pdate.year))
        nvacation_days_no = len(self.bh.get_vacations(ndate.month, ndate.year))
        pcourse_days_no = len(self.bh.get_course_days(pdate.month, pdate.year))
        ncourse_days_no = len(self.bh.get_course_days(ndate.month, ndate.year))
        psick_days_no = len(self.bh.get_sick_days(pdate.month, pdate.year))
        nsick_days_no = len(self.bh.get_sick_days(ndate.month, ndate.year))

        pall_days_no = self.bh.get_number_of_actual_workdays(
            pdate.month, pdate.year)
        nall_days_no = self.bh.get_number_of_actual_workdays(
            ndate.month, ndate.year)

        pvacation_perc = pvacation_days_no / pall_days_no
        nvacation_perc = nvacation_days_no / nall_days_no
        psick_perc = psick_days_no / pall_days_no
        pcourses_perc = pcourse_days_no / pall_days_no
        ncourses_perc = ncourse_days_no / nall_days_no

        project_seconds, total_seconds = self.calc_project_and_total_seconds()

        pdata = {}
        ndata = {}

        included_prj_count = len(project_seconds)
        for p in project_seconds:
            s = project_seconds[p]
            perc = s / total_seconds * 100.
            pdata[p] = {"project": p, "perc": (round(perc / 5) * 5)}

        pdata["Vacations"] = {
            "project": "Urlaub",
            "perc": (round((pvacation_perc * 100) / 5) * 5)
        }

        pdata["Courses"] = {
            "project": "Seminare/Weiterbildung",
            "perc": (round((pcourses_perc * 100) / 5) * 5)
        }

        # pdata["Sick"] = {
        #     "project": "Krank",
        #     "perc": (round((psick_perc * 100) / 5) * 5)
        # }
        #
        ndata["Courses"] = {
            "project": "Seminare/Weiterbildung",
            "perc": (round((ncourses_perc * 100) / 5) * 5)
        }

        ndata["Vacations"] = {
            "project": "Urlaub",
            "perc": (round((nvacation_perc * 100) / 5) * 5)
        }

        def perc_sum(data):
            return sum([data[p]["perc"] for p in data])

        non_special_pdata = [
            p for p in pdata if p not in self.special_projects
        ]
        if len(non_special_pdata) > 0:
            while (100 - perc_sum(pdata) > 0):
                diff = int(100 - perc_sum(pdata))
                if diff > included_prj_count:
                    diff = diff // included_prj_count
                    for p in pdata:
                        if p not in self.special_projects:
                            pdata[p]["perc"] += diff
                else:
                    keys = list(pdata.keys())
                    for i in range(diff):
                        key = random.choice(keys)
                        keys.remove(key)
                        pdata[key]["perc"] += 1

        for p in list(pdata.keys()):
            if pdata[p]["perc"] < 5:
                del pdata[p]

        for p in list(ndata.keys()):
            if ndata[p]["perc"] < 5:
                del ndata[p]

        for p in pdata:
            pdata[p]["percentage"] = "%3d" % pdata[p]["perc"]

        for p in ndata:
            ndata[p]["percentage"] = "%3d" % ndata[p]["perc"]

        pdata_str = [dtemp.substitute(pdata[p]) for p in pdata]
        ndata_str = [dtemp.substitute(ndata[p]) for p in ndata]

        psum_str = stemp.substitute({"percsum": perc_sum(pdata)})

        nsum = perc_sum(ndata)
        rest = 100 - nsum
        d = {
            'pmonth': pmonth,
            'pyear': pyear,
            'nmonth': nmonth,
            'nyear': nyear,
            'pyears': pyears,
            'nyears': nyears,
            'pdata': "\n".join(pdata_str),
            'psum': psum_str,
            'ndata': "\n".join(ndata_str),
            'rest': "%3d" % rest
        }
        result = temp.substitute(d)
        log.info(result)

        md_result = markdown2.markdown(result).replace("\n", "")

        from subprocess import Popen
        recipients = self.config.settings["mail_summary_recipients"]
        recipients = ",".join(recipients)
        p = Popen([
            "thunderbird", "-compose",
            f"to='{recipients}',subject='Ressourcenplanung {pmonth} {pyear} und {nmonth} {nyear}',body='{md_result}'"
        ])
        log.info(f"Thunderbird returned: {p.returncode}")
Example #12
0
    def output_results_old(self):
        log.info(mk_headline(f"Resulting Resource Distribution", "="))
        log.info(mk_headline(sgn="-"))
        perc_sum = 0.0

        percents = {}
        hours = {}

        for p_name in self.project_seconds:
            phours = self.project_seconds[p_name] / 3600.
            percent = phours / self.bh.get_actual_working_hours(
                self.start.month) * 100.
            perc_sum += percent

            if p_name not in self.special_projects:
                self.total_time += tdelta(hours=phours)

            percents[p_name] = percent
            hours[p_name] = phours

        bigger_5p = [
            p for p in percents
            if percents[p] >= 5.0 and p not in self.special_projects
        ]
        smallr_5p = [
            p for p in percents
            if percents[p] < 5.0 and p not in self.special_projects
        ]

        sum_bigger = sum([percents[p] for p in bigger_5p])

        for p_name in smallr_5p:
            smallr_perc = percents[p_name]
            smallr_hours = hours[p_name]
            log.info(
                f"Rebooking {smallr_perc:>8.1f}% ({smallr_hours:>8.1f} hours) of project {p_name} to the bigger projects"
            )
            for bigger_p_name in bigger_5p:
                factor = 1. - (percents[bigger_p_name] / sum_bigger)
                hours_plus = smallr_hours * factor
                percs_plus = smallr_perc * factor
                hours[bigger_p_name] += hours_plus
                percents[bigger_p_name] += percs_plus
                log.info(
                    f"  {factor*100.:>8.1f}% of {smallr_hours:>8.1f} hours of {p_name} go to {bigger_p_name}"
                )

        for p_name in bigger_5p:
            log.info(
                f"    {p_name:<20s}: {percents[p_name]:>3.0f}% (hours: {hours[p_name]:>10.1f})"
            )

        log.info(mk_headline("Total hours in the given period", "-"))
        total_hours = self.total_time.total_seconds() / 3600.
        log.info(
            f" {total_hours:>3.1f} hours => {total_hours/self.bh.get_actual_working_hours() * 100.:>3.0f}%"
        )
        log.info(mk_headline("Smaller projects", sgn="-"))

        for p_name in smallr_5p:
            log.info(
                f"    {p_name:<20s}: {percents[p_name]:>3.0f}% (hours: {hours[p_name]:>10.1f})"
            )

        log.info(mk_headline(sgn="="))