From 09d15e097f14b1e94384e9db94f741a72a5f409b Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 14:51:29 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E5=8A=B4=E5=8B=99=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=B3=BB=E3=81=AE=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/__main__.py | 2 + annofabcli/labor/__init__.py | 0 annofabcli/labor/list_worktime_by_user.py | 209 ++++++++++++++++++++++ annofabcli/labor/subcommand_labor.py | 22 +++ 4 files changed, 233 insertions(+) create mode 100644 annofabcli/labor/__init__.py create mode 100644 annofabcli/labor/list_worktime_by_user.py create mode 100644 annofabcli/labor/subcommand_labor.py diff --git a/annofabcli/__main__.py b/annofabcli/__main__.py index 5627f844..2b614e37 100644 --- a/annofabcli/__main__.py +++ b/annofabcli/__main__.py @@ -15,6 +15,7 @@ import annofabcli.statistics.subcommand_statistics import annofabcli.supplementary.subcommand_supplementary import annofabcli.task.subcommand_task +import annofabcli.labor.subcommand_labor logger = logging.getLogger(__name__) @@ -41,6 +42,7 @@ def main(arguments: Optional[Sequence[str]] = None): annofabcli.inspection_comment.subcommand_inspection_comment.add_parser(subparsers) annofabcli.instruction.subcommand_instruction.add_parser(subparsers) annofabcli.job.subcommand_job.add_parser(subparsers) + annofabcli.labor.subcommand_labor.add_parser(subparsers) annofabcli.organization_member.subcommand_organization_member.add_parser(subparsers) annofabcli.project.subcommand_project.add_parser(subparsers) annofabcli.project_member.subcommand_project_member.add_parser(subparsers) diff --git a/annofabcli/labor/__init__.py b/annofabcli/labor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py new file mode 100644 index 00000000..65cd8c08 --- /dev/null +++ b/annofabcli/labor/list_worktime_by_user.py @@ -0,0 +1,209 @@ +import argparse +import logging +from dataclasses import dataclass +from dataclasses_json import dataclass_json + +from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +import pandas +from annofabapi.models import SupplementaryData, OrganizationMember, Project + +import more_itertools +import annofabcli +from annofabcli import AnnofabApiFacade +from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login, \ + get_list_from_args +from annofabcli.common.enums import FormatArgument +import datetime + +logger = logging.getLogger(__name__) + + +def str_to_datetime(d: str) -> datetime.datetime: + """ + 文字列 `YYYY-MM-DDD` をdatetime.datetimeに変換する。 + """ + return datetime.datetime.strptime(d, '%Y-%m-%d') + + +@dataclass_json +@dataclass(frozen=True) +class LaborWorktime: + """ + 出力用の作業時間情報 + """ + date: str + organization_id: str + organization_name: int + project_id: str + project_title: str + account_id: str + user_id: str + username: str + worktime_plan_hour: float + worktime_result_hour: float + + +class ListWorktimeByUser(AbstractCommandLineInterface): + DATE_FORMAT = "%Y-%m-%d" + + """ + 作業時間をユーザごとに出力する。 + """ + + @annofabcli.utils.allow_404_error + def get_supplementary_data_list(self, project_id: str, input_data_id: str) -> SupplementaryData: + supplementary_data_list, _ = self.service.api.get_supplementary_data_list(project_id, input_data_id) + return supplementary_data_list + + def get_all_supplementary_data_list(self, project_id: str, + input_data_id_list: List[str]) -> List[SupplementaryData]: + """ + 補助情報一覧を取得する。 + """ + all_supplementary_data_list: List[SupplementaryData] = [] + + logger.debug(f"{len(input_data_id_list)}件の入力データに紐づく補助情報を取得します。") + for index, input_data_id in enumerate(input_data_id_list): + if (index + 1) % 100 == 0: + logger.debug(f"{index + 1} 件目の入力データを取得します。") + + supplementary_data_list = self.get_supplementary_data_list(project_id, input_data_id) + if supplementary_data_list is not None: + all_supplementary_data_list.extend(supplementary_data_list) + else: + logger.warning(f"入力データ '{input_data_id}' に紐づく補助情報が見つかりませんでした。") + + return all_supplementary_data_list + + @staticmethod + def get_member_from_user_id(organization_member_list: List[OrganizationMember], user_id: str) -> Optional[ + OrganizationMember]: + member = more_itertools.first_true(organization_member_list, pred=lambda e: e["user_id"] == user_id) + return member + + @staticmethod + def get_member_from_account_id(organization_member_list: List[OrganizationMember], account_id: str) -> Optional[ + OrganizationMember]: + member = more_itertools.first_true(organization_member_list, pred=lambda e: e["account_id"] == account_id) + return member + + @staticmethod + def get_project_title(project_list: List[Project], project_id: str) -> str: + project = more_itertools.first_true(project_list, pred=lambda e: e["project_id"] == project_id) + if project is not None: + return project["title"] + else: + return "" + + @staticmethod + def get_worktime_hour(working_time_by_user: Dict[str, Any], key: str) -> float: + value = working_time_by_user.get(key) + if value is None: + return 0 + else: + return value / 3600 / 1000 + + def get_labor_list(self, organization_name: str, start_date: str, end_date: str) -> List[LaborWorktime]: + new_labor_list = [] + organization, _ = self.service.api.get_organization(organization_name) + organization_id = organization["organization_id"] + organization_member_list = self.service.wrapper.get_all_organization_members(organization_name) + project_list = self.service.wrapper.get_all_projects_of_organization(organization_name) + + labor_list = \ + self.service.api.get_labor_control( + {"organization_id": organization_id, "from": start_date, "to": end_date})[0][ + "list"] + for labor in labor_list: + member = self.get_member_from_account_id(organization_member_list, labor["account_id"]) + + new_labor = LaborWorktime( + date=labor["date"], + organization_id=labor["organization_id"], + organization_name=labor["organization_name"], + project_id=labor["project_id"], + project_title=self.get_project_title(project_list, labor["project_id"]), + account_id=labor["account_id"], + user_id=member["user_id"] if member is not None else "", + username=member["username"] if member is not None else "", + worktime_plan_hour=self.get_worktime_hour(labor["working_time_by_user"], "plans"), + worktime_result_hour=self.get_worktime_hour(labor["working_time_by_user"], "results"), + ) + new_labor_list.append(new_labor) + + return new_labor_list + + @staticmethod + def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_date: str, end_date: str): + sum_labor_list = [] + for date in pandas.date_range(start=start_date, end=end_date): + str_date = date.strftime(ListWorktimeByUser.DATE_FORMAT) + filtered_list = [e for e in labor_list if e.user_id == user_id and e.date == str_date] + worktime_plan_hour = sum([e["worktime_plan_hour"] for e in filtered_list]) + worktime_result_hour = sum([e["worktime_result_hour"] for e in filtered_list]) + + labor = LaborWorktime(user_id=user_id, date=date, worktime_plan_hour=worktime_plan_hour, worktime_result_hour=worktime_result_hour) + sum_labor_list.append(labor) + + return sum_labor_list + + def print_labor_worktime_list(self, organization_name_list: List[str], user_id_list: List[str], start_date: str, + end_date: str) -> None: + """ + 作業時間の一覧を出力する + """ + labor_list = [] + for organization_name in organization_name_list: + labor_list.extend(self.get_labor_list(organization_name, start_date=start_date, end_date=end_date)) + + reform_dict = { + ("date",):[e.strftime(ListWorktimeByUser.DATE_FORMAT) for e in pandas.date_range(start=start_date, end=end_date)], + ("dayofweek",): [e.strftime("%a") for e in pandas.date_range(start=start_date, end=end_date)], + } + + for user_id in user_id_list: + sum_worktime_list = self.get_sum_worktime_list(labor_list, user_id=user_id, start_date=start_date, end_date=end_date) + reform_dict.update({(user_id, "worktime_plan_hour"): [e.worktime_plan_hour for e in sum_worktime_list], + (user_id, "worktime_result_hour"): [e.worktime_result_hour for e in sum_worktime_list]}) + + sum_worktime_df = pandas.DataFrame(reform_dict) + self.print_csv(sum_worktime_df) + + def main(self): + args = self.args + user_id_list = get_list_from_args(args.user_id) + organization_name_list = get_list_from_args(args.organization) + self.print_labor_worktime_list(organization_name_list=organization_name_list, user_id_list=user_id_list, start_date=args.start_date, end_date=args.end_date) + + +def main(args): + service = build_annofabapi_resource_and_login() + facade = AnnofabApiFacade(service) + ListWorktimeByUser(service, facade, args).main() + + +def parse_args(parser: argparse.ArgumentParser): + argument_parser = ArgumentParser(parser) + + parser.add_argument('-org', '--organization', type=str, nargs='+', required=True, + help='集計対象の組織名を指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + + parser.add_argument('-u', '--user_id', type=str, nargs='+', required=True, + help='集計対象のユーザのuser_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + + parser.add_argument("--start_date", type=str, required=True, help="集計期間の開始日(%%Y-%%m-%%d)") + parser.add_argument("--end_date", type=str, required=True, help="集計期間の終了日(%%Y-%%m-%%d)") + + argument_parser.add_output() + argument_parser.add_csv_format() + + parser.set_defaults(subcommand_func=main) + + +def add_parser(subparsers: argparse._SubParsersAction): + subcommand_name = "list_worktime_by_user" + subcommand_help = "ユーザごとに作業時間を出力します。" + description = ("ユーザごとに作業時間を出力します。") + + parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) + parse_args(parser) diff --git a/annofabcli/labor/subcommand_labor.py b/annofabcli/labor/subcommand_labor.py new file mode 100644 index 00000000..b3b60e6a --- /dev/null +++ b/annofabcli/labor/subcommand_labor.py @@ -0,0 +1,22 @@ +import argparse + +import annofabcli +import annofabcli.common.cli +import annofabcli.labor.list_worktime_by_user + + +def parse_args(parser: argparse.ArgumentParser): + + subparsers = parser.add_subparsers(dest='subcommand_name') + + # サブコマンドの定義 + annofabcli.labor.list_worktime_by_user.add_parser(subparsers) + + +def add_parser(subparsers: argparse._SubParsersAction): + subcommand_name = "labor" + subcommand_help = "労務管理関係のサブコマンド" + description = "労務管理関係のサブコマンド" + + parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) + parse_args(parser) From 3017382468ba8b54b47d2b3412ab9e66bc444f84 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 15:39:27 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=E5=BE=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/labor/list_worktime_by_user.py | 63 +++++++++-------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index 65cd8c08..c2fc21aa 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -29,7 +29,7 @@ def str_to_datetime(d: str) -> datetime.datetime: @dataclass(frozen=True) class LaborWorktime: """ - 出力用の作業時間情報 + 労務管理情報 """ date: str organization_id: str @@ -42,6 +42,17 @@ class LaborWorktime: worktime_plan_hour: float worktime_result_hour: float +@dataclass_json +@dataclass(frozen=True) +class SumLaborWorktime: + """ + 出力用の作業時間情報 + """ + date: str + user_id: str + worktime_plan_hour: float + worktime_result_hour: float + class ListWorktimeByUser(AbstractCommandLineInterface): DATE_FORMAT = "%Y-%m-%d" @@ -50,31 +61,6 @@ class ListWorktimeByUser(AbstractCommandLineInterface): 作業時間をユーザごとに出力する。 """ - @annofabcli.utils.allow_404_error - def get_supplementary_data_list(self, project_id: str, input_data_id: str) -> SupplementaryData: - supplementary_data_list, _ = self.service.api.get_supplementary_data_list(project_id, input_data_id) - return supplementary_data_list - - def get_all_supplementary_data_list(self, project_id: str, - input_data_id_list: List[str]) -> List[SupplementaryData]: - """ - 補助情報一覧を取得する。 - """ - all_supplementary_data_list: List[SupplementaryData] = [] - - logger.debug(f"{len(input_data_id_list)}件の入力データに紐づく補助情報を取得します。") - for index, input_data_id in enumerate(input_data_id_list): - if (index + 1) % 100 == 0: - logger.debug(f"{index + 1} 件目の入力データを取得します。") - - supplementary_data_list = self.get_supplementary_data_list(project_id, input_data_id) - if supplementary_data_list is not None: - all_supplementary_data_list.extend(supplementary_data_list) - else: - logger.warning(f"入力データ '{input_data_id}' に紐づく補助情報が見つかりませんでした。") - - return all_supplementary_data_list - @staticmethod def get_member_from_user_id(organization_member_list: List[OrganizationMember], user_id: str) -> Optional[ OrganizationMember]: @@ -110,39 +96,37 @@ def get_labor_list(self, organization_name: str, start_date: str, end_date: str) organization_member_list = self.service.wrapper.get_all_organization_members(organization_name) project_list = self.service.wrapper.get_all_projects_of_organization(organization_name) - labor_list = \ - self.service.api.get_labor_control( - {"organization_id": organization_id, "from": start_date, "to": end_date})[0][ - "list"] + labor_list, _ = self.service.api.get_labor_control( + {"organization_id": organization_id, "from": start_date, "to": end_date}) + logger.info(f"'{organization_name}'組織の労務管理情報の件数: {len(labor_list)}") for labor in labor_list: member = self.get_member_from_account_id(organization_member_list, labor["account_id"]) - new_labor = LaborWorktime( date=labor["date"], organization_id=labor["organization_id"], - organization_name=labor["organization_name"], + organization_name=organization_name, project_id=labor["project_id"], project_title=self.get_project_title(project_list, labor["project_id"]), account_id=labor["account_id"], user_id=member["user_id"] if member is not None else "", username=member["username"] if member is not None else "", - worktime_plan_hour=self.get_worktime_hour(labor["working_time_by_user"], "plans"), - worktime_result_hour=self.get_worktime_hour(labor["working_time_by_user"], "results"), + worktime_plan_hour=self.get_worktime_hour(labor["values"]["working_time_by_user"], "plans"), + worktime_result_hour=self.get_worktime_hour(labor["values"]["working_time_by_user"], "results"), ) new_labor_list.append(new_labor) return new_labor_list @staticmethod - def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_date: str, end_date: str): + def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_date: str, end_date: str) -> List[SumLaborWorktime]: sum_labor_list = [] for date in pandas.date_range(start=start_date, end=end_date): str_date = date.strftime(ListWorktimeByUser.DATE_FORMAT) filtered_list = [e for e in labor_list if e.user_id == user_id and e.date == str_date] - worktime_plan_hour = sum([e["worktime_plan_hour"] for e in filtered_list]) - worktime_result_hour = sum([e["worktime_result_hour"] for e in filtered_list]) + worktime_plan_hour = sum([e.worktime_plan_hour for e in filtered_list]) + worktime_result_hour = sum([e.worktime_result_hour for e in filtered_list]) - labor = LaborWorktime(user_id=user_id, date=date, worktime_plan_hour=worktime_plan_hour, worktime_result_hour=worktime_result_hour) + labor = SumLaborWorktime(user_id=user_id, date=date, worktime_plan_hour=worktime_plan_hour, worktime_result_hour=worktime_result_hour) sum_labor_list.append(labor) return sum_labor_list @@ -194,8 +178,7 @@ def parse_args(parser: argparse.ArgumentParser): parser.add_argument("--start_date", type=str, required=True, help="集計期間の開始日(%%Y-%%m-%%d)") parser.add_argument("--end_date", type=str, required=True, help="集計期間の終了日(%%Y-%%m-%%d)") - argument_parser.add_output() - argument_parser.add_csv_format() + parser.add_argument('-o', '--output_dir', type=str, required=True, help='出力先のディレクトリのパス') parser.set_defaults(subcommand_func=main) From 85981850b9ed878dfbe16589e29d55846ef2e303 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 16:21:50 +0900 Subject: [PATCH 03/10] format --- Makefile | 2 +- annofabcli/__main__.py | 2 +- annofabcli/labor/list_worktime_by_user.py | 104 ++++++++++++++-------- 3 files changed, 71 insertions(+), 37 deletions(-) diff --git a/Makefile b/Makefile index 81410aa6..67a45b29 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,8 @@ format: pipenv run yapf --verbose --in-place --recursive ${FORMAT_FILES} lint: - pipenv run flake8 ${LINT_FILES} pipenv run mypy ${LINT_FILES} --config-file setup.cfg + pipenv run flake8 ${LINT_FILES} pipenv run pylint ${LINT_FILES} --rcfile setup.cfg test: diff --git a/annofabcli/__main__.py b/annofabcli/__main__.py index 2b614e37..9b88342d 100644 --- a/annofabcli/__main__.py +++ b/annofabcli/__main__.py @@ -9,13 +9,13 @@ import annofabcli.inspection_comment.subcommand_inspection_comment import annofabcli.instruction.subcommand_instruction import annofabcli.job.subcommand_job +import annofabcli.labor.subcommand_labor import annofabcli.organization_member.subcommand_organization_member import annofabcli.project.subcommand_project import annofabcli.project_member.subcommand_project_member import annofabcli.statistics.subcommand_statistics import annofabcli.supplementary.subcommand_supplementary import annofabcli.task.subcommand_task -import annofabcli.labor.subcommand_labor logger = logging.getLogger(__name__) diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index c2fc21aa..43754dd2 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -1,30 +1,21 @@ import argparse import logging from dataclasses import dataclass -from dataclasses_json import dataclass_json - +from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import -import pandas -from annofabapi.models import SupplementaryData, OrganizationMember, Project import more_itertools +import pandas +from annofabapi.models import OrganizationMember, Project +from dataclasses_json import dataclass_json + import annofabcli from annofabcli import AnnofabApiFacade -from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login, \ - get_list_from_args -from annofabcli.common.enums import FormatArgument -import datetime +from annofabcli.common.cli import AbstractCommandLineInterface, build_annofabapi_resource_and_login, get_list_from_args logger = logging.getLogger(__name__) -def str_to_datetime(d: str) -> datetime.datetime: - """ - 文字列 `YYYY-MM-DDD` をdatetime.datetimeに変換する。 - """ - return datetime.datetime.strptime(d, '%Y-%m-%d') - - @dataclass_json @dataclass(frozen=True) class LaborWorktime: @@ -33,7 +24,7 @@ class LaborWorktime: """ date: str organization_id: str - organization_name: int + organization_name: str project_id: str project_title: str account_id: str @@ -42,6 +33,7 @@ class LaborWorktime: worktime_plan_hour: float worktime_result_hour: float + @dataclass_json @dataclass(frozen=True) class SumLaborWorktime: @@ -55,21 +47,27 @@ class SumLaborWorktime: class ListWorktimeByUser(AbstractCommandLineInterface): - DATE_FORMAT = "%Y-%m-%d" - """ 作業時間をユーザごとに出力する。 """ + DATE_FORMAT = "%Y-%m-%d" + @staticmethod - def get_member_from_user_id(organization_member_list: List[OrganizationMember], user_id: str) -> Optional[ - OrganizationMember]: + def create_required_columns(df, prior_columns): + remained_columns = list(df.columns.difference(prior_columns)) + all_columns = prior_columns + remained_columns + return all_columns + + @staticmethod + def get_member_from_user_id(organization_member_list: List[OrganizationMember], + user_id: str) -> Optional[OrganizationMember]: member = more_itertools.first_true(organization_member_list, pred=lambda e: e["user_id"] == user_id) return member @staticmethod - def get_member_from_account_id(organization_member_list: List[OrganizationMember], account_id: str) -> Optional[ - OrganizationMember]: + def get_member_from_account_id(organization_member_list: List[OrganizationMember], + account_id: str) -> Optional[OrganizationMember]: member = more_itertools.first_true(organization_member_list, pred=lambda e: e["account_id"] == account_id) return member @@ -96,8 +94,11 @@ def get_labor_list(self, organization_name: str, start_date: str, end_date: str) organization_member_list = self.service.wrapper.get_all_organization_members(organization_name) project_list = self.service.wrapper.get_all_projects_of_organization(organization_name) - labor_list, _ = self.service.api.get_labor_control( - {"organization_id": organization_id, "from": start_date, "to": end_date}) + labor_list, _ = self.service.api.get_labor_control({ + "organization_id": organization_id, + "from": start_date, + "to": end_date + }) logger.info(f"'{organization_name}'組織の労務管理情報の件数: {len(labor_list)}") for labor in labor_list: member = self.get_member_from_account_id(organization_member_list, labor["account_id"]) @@ -118,7 +119,8 @@ def get_labor_list(self, organization_name: str, start_date: str, end_date: str) return new_labor_list @staticmethod - def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_date: str, end_date: str) -> List[SumLaborWorktime]: + def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_date: str, + end_date: str) -> List[SumLaborWorktime]: sum_labor_list = [] for date in pandas.date_range(start=start_date, end=end_date): str_date = date.strftime(ListWorktimeByUser.DATE_FORMAT) @@ -126,38 +128,71 @@ def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_d worktime_plan_hour = sum([e.worktime_plan_hour for e in filtered_list]) worktime_result_hour = sum([e.worktime_result_hour for e in filtered_list]) - labor = SumLaborWorktime(user_id=user_id, date=date, worktime_plan_hour=worktime_plan_hour, worktime_result_hour=worktime_result_hour) + labor = SumLaborWorktime(user_id=user_id, date=date, worktime_plan_hour=worktime_plan_hour, + worktime_result_hour=worktime_result_hour) sum_labor_list.append(labor) return sum_labor_list + @staticmethod + def write_sum_worktime_list(sum_worktime_df: pandas.DataFrame, output_dir: Path): + sum_worktime_df.to_csv(str(output_dir / "ユーザごとの作業時間.csv"), encoding="utf_8_sig", index=False) + + @staticmethod + def write_worktime_list(worktime_df: pandas.DataFrame, output_dir: Path): + worktime_df = worktime_df.rename(columns={"worktime_plan_hour": "作業予定時間", "worktime_result_hour": "作業実績時間"}) + columns = ListWorktimeByUser.create_required_columns( + worktime_df, ["date", "organization_name", "project_title", "username", "作業予定時間", "作業実績時間"]) + worktime_df[columns].to_csv(str(output_dir / "作業時間の詳細一覧.csv"), encoding="utf_8_sig", index=False) + def print_labor_worktime_list(self, organization_name_list: List[str], user_id_list: List[str], start_date: str, - end_date: str) -> None: + end_date: str, output_dir: Path) -> None: """ 作業時間の一覧を出力する """ labor_list = [] + member_list: List[OrganizationMember] = [] for organization_name in organization_name_list: labor_list.extend(self.get_labor_list(organization_name, start_date=start_date, end_date=end_date)) + member_list.extend(self.service.wrapper.get_all_organization_members(organization_name)) reform_dict = { - ("date",):[e.strftime(ListWorktimeByUser.DATE_FORMAT) for e in pandas.date_range(start=start_date, end=end_date)], - ("dayofweek",): [e.strftime("%a") for e in pandas.date_range(start=start_date, end=end_date)], + ("date", ""): [ + e.strftime(ListWorktimeByUser.DATE_FORMAT) for e in pandas.date_range(start=start_date, end=end_date) + ], + ("dayofweek", ""): [e.strftime("%a") for e in pandas.date_range(start=start_date, end=end_date)], } for user_id in user_id_list: - sum_worktime_list = self.get_sum_worktime_list(labor_list, user_id=user_id, start_date=start_date, end_date=end_date) - reform_dict.update({(user_id, "worktime_plan_hour"): [e.worktime_plan_hour for e in sum_worktime_list], - (user_id, "worktime_result_hour"): [e.worktime_result_hour for e in sum_worktime_list]}) + sum_worktime_list = self.get_sum_worktime_list(labor_list, user_id=user_id, start_date=start_date, + end_date=end_date) + member = self.get_member_from_user_id(member_list, user_id) + if member is not None: + username = member["username"] + else: + logger.warning(f"user_idが'{user_id}'のユーザは存在しません。") + username = user_id + + reform_dict.update({ + (username, "作業予定"): [e.worktime_plan_hour for e in sum_worktime_list], + (username, "作業実績"): [e.worktime_result_hour for e in sum_worktime_list] + }) sum_worktime_df = pandas.DataFrame(reform_dict) - self.print_csv(sum_worktime_df) + self.write_sum_worktime_list(sum_worktime_df, output_dir) + + worktime_df = pandas.DataFrame([e.to_dict() for e in labor_list]) # type: ignore + self.write_worktime_list(worktime_df, output_dir) def main(self): args = self.args user_id_list = get_list_from_args(args.user_id) organization_name_list = get_list_from_args(args.organization) - self.print_labor_worktime_list(organization_name_list=organization_name_list, user_id_list=user_id_list, start_date=args.start_date, end_date=args.end_date) + output_dir = Path(args.output_dir) + output_dir.mkdir(exist_ok=True, parents=True) + + self.print_labor_worktime_list(organization_name_list=organization_name_list, user_id_list=user_id_list, + start_date=args.start_date, end_date=args.end_date, output_dir=output_dir) def main(args): @@ -167,7 +202,6 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): - argument_parser = ArgumentParser(parser) parser.add_argument('-org', '--organization', type=str, nargs='+', required=True, help='集計対象の組織名を指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') From cbb9f9b9fc40efbb3b46950cbef163d3bc19ab8a Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 16:23:18 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=E5=90=8D=E5=89=8D=E3=81=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/labor/list_worktime_by_user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index 43754dd2..1e5c9d9d 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -204,10 +204,10 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): parser.add_argument('-org', '--organization', type=str, nargs='+', required=True, - help='集計対象の組織名を指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + help='集計対象の組織名を指定してください。`file://`を先頭に付けると、組織名の一覧が記載されたファイルを指定できます。') parser.add_argument('-u', '--user_id', type=str, nargs='+', required=True, - help='集計対象のユーザのuser_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + help='集計対象のユーザのuser_idを指定してください。`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。') parser.add_argument("--start_date", type=str, required=True, help="集計期間の開始日(%%Y-%%m-%%d)") parser.add_argument("--end_date", type=str, required=True, help="集計期間の終了日(%%Y-%%m-%%d)") From cf1e8a4ca7404ae7b123707918029b163cabb3fa Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 21:44:54 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=81=A7=E3=82=82=E9=9B=86=E8=A8=88=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/common/cli.py | 8 +- annofabcli/labor/list_worktime_by_user.py | 118 +++++++++++++++++++--- 2 files changed, 106 insertions(+), 20 deletions(-) diff --git a/annofabcli/common/cli.py b/annofabcli/common/cli.py index c9aa57c7..67864d50 100644 --- a/annofabcli/common/cli.py +++ b/annofabcli/common/cli.py @@ -89,14 +89,14 @@ def create_parent_parser() -> argparse.ArgumentParser: return parent_parser -def get_list_from_args(str_list: Optional[List[str]] = None) -> List[str]: +def get_list_from_args(str_list: Optional[List[str]] = None) -> Optional[List[str]]: """ 文字列のListのサイズが1で、プレフィックスが`file://`ならば、ファイルパスとしてファイルを読み込み、行をListとして返す。 - そうでなければ、引数の値をそのままかえす。 - ただしNoneの場合は空Listを変えす + そうでなければ、引数の値をそのまま返す。 + ただしNoneの場合はNoneを返す。 """ if str_list is None or len(str_list) == 0: - return [] + return None if len(str_list) > 1: return str_list diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index 1e5c9d9d..65f40b91 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -1,5 +1,6 @@ import argparse import logging +import sys from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import @@ -87,11 +88,49 @@ def get_worktime_hour(working_time_by_user: Dict[str, Any], key: str) -> float: else: return value / 3600 / 1000 - def get_labor_list(self, organization_name: str, start_date: str, end_date: str) -> List[LaborWorktime]: + def _get_labor_worktime(self, labor: Dict[str, Any], member: OrganizationMember, project_title: str, + organization_name: str) -> LaborWorktime: + new_labor = LaborWorktime( + date=labor["date"], + organization_id=labor["organization_id"], + organization_name=organization_name, + project_id=labor["project_id"], + project_title=project_title, + account_id=labor["account_id"], + user_id=member["user_id"] if member is not None else "", + username=member["username"] if member is not None else "", + worktime_plan_hour=self.get_worktime_hour(labor["values"]["working_time_by_user"], "plans"), + worktime_result_hour=self.get_worktime_hour(labor["values"]["working_time_by_user"], "results"), + ) + return new_labor + + def get_labor_list_from_project_id(self, project_id: str, member_list: List[OrganizationMember], start_date: str, + end_date: str) -> List[LaborWorktime]: + labor_list, _ = self.service.api.get_labor_control({ + "project_id": project_id, + "from": start_date, + "to": end_date + }) + project_title = self.service.api.get_project(project_id)[0]["title"] + + logger.info(f"'{project_title}'プロジェクト('{project_id}')の労務管理情報の件数: {len(labor_list)}") + new_labor_list = [] + for labor in labor_list: + member = self.get_member_from_account_id(member_list, labor["account_id"]) + + organization, _ = self.service.api.get_organization_of_project(project_id) + organization_name = organization["organization_name"] + new_labor = self._get_labor_worktime(labor, member=member, project_title=project_title, + organization_name=organization_name) + new_labor_list.append(new_labor) + + return new_labor_list + + def get_labor_list_from_organization_name(self, organization_name: str, member_list: List[OrganizationMember], + start_date: str, end_date: str) -> List[LaborWorktime]: organization, _ = self.service.api.get_organization(organization_name) organization_id = organization["organization_id"] - organization_member_list = self.service.wrapper.get_all_organization_members(organization_name) project_list = self.service.wrapper.get_all_projects_of_organization(organization_name) labor_list, _ = self.service.api.get_labor_control({ @@ -100,8 +139,9 @@ def get_labor_list(self, organization_name: str, start_date: str, end_date: str) "to": end_date }) logger.info(f"'{organization_name}'組織の労務管理情報の件数: {len(labor_list)}") + new_labor_list = [] for labor in labor_list: - member = self.get_member_from_account_id(organization_member_list, labor["account_id"]) + member = self.get_member_from_account_id(member_list, labor["account_id"]) new_labor = LaborWorktime( date=labor["date"], organization_id=labor["organization_id"], @@ -145,16 +185,44 @@ def write_worktime_list(worktime_df: pandas.DataFrame, output_dir: Path): worktime_df, ["date", "organization_name", "project_title", "username", "作業予定時間", "作業実績時間"]) worktime_df[columns].to_csv(str(output_dir / "作業時間の詳細一覧.csv"), encoding="utf_8_sig", index=False) - def print_labor_worktime_list(self, organization_name_list: List[str], user_id_list: List[str], start_date: str, + def get_organization_member_list(self, organization_name_list: Optional[List[str]], + project_id_list: Optional[List[str]]) -> List[OrganizationMember]: + member_list: List[OrganizationMember] = [] + + if project_id_list is not None: + tmp_organization_name_list = [] + for project_id in project_id_list: + organization, _ = self.service.api.get_organization_of_project(project_id) + tmp_organization_name_list.append(organization["organization_name"]) + + organization_name_list = list(set(tmp_organization_name_list)) + + if organization_name_list is not None: + for organization_name in organization_name_list: + member_list.extend(self.service.wrapper.get_all_organization_members(organization_name)) + + return member_list + + def print_labor_worktime_list(self, organization_name_list: Optional[List[str]], + project_id_list: Optional[List[str]], user_id_list: List[str], start_date: str, end_date: str, output_dir: Path) -> None: """ 作業時間の一覧を出力する """ labor_list = [] - member_list: List[OrganizationMember] = [] - for organization_name in organization_name_list: - labor_list.extend(self.get_labor_list(organization_name, start_date=start_date, end_date=end_date)) - member_list.extend(self.service.wrapper.get_all_organization_members(organization_name)) + member_list = self.get_organization_member_list(organization_name_list, project_id_list) + + if project_id_list is not None: + for project_id in project_id_list: + labor_list.extend( + self.get_labor_list_from_project_id(project_id, member_list=member_list, start_date=start_date, + end_date=end_date)) + + elif organization_name_list is not None: + for organization_name in organization_name_list: + labor_list.extend( + self.get_labor_list_from_organization_name(organization_name, member_list=member_list, + start_date=start_date, end_date=end_date)) reform_dict = { ("date", ""): [ @@ -184,15 +252,29 @@ def print_labor_worktime_list(self, organization_name_list: List[str], user_id_l worktime_df = pandas.DataFrame([e.to_dict() for e in labor_list]) # type: ignore self.write_worktime_list(worktime_df, output_dir) + @staticmethod + def validate(args: argparse.Namespace) -> bool: + if args.organization is not None and args.user_id is None: + print("ERROR: argument --user_id: " "`--organization`を指定しているときは、`--user_id`オプションは必須です。", file=sys.stderr) + return False + + return True + def main(self): args = self.args + + if not self.validate(args): + return + user_id_list = get_list_from_args(args.user_id) + project_id_list = get_list_from_args(args.project_id) organization_name_list = get_list_from_args(args.organization) output_dir = Path(args.output_dir) output_dir.mkdir(exist_ok=True, parents=True) - self.print_labor_worktime_list(organization_name_list=organization_name_list, user_id_list=user_id_list, - start_date=args.start_date, end_date=args.end_date, output_dir=output_dir) + self.print_labor_worktime_list(organization_name_list=organization_name_list, project_id_list=project_id_list, + user_id_list=user_id_list, start_date=args.start_date, end_date=args.end_date, + output_dir=output_dir) def main(args): @@ -202,12 +284,16 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): + target_group = parser.add_mutually_exclusive_group(required=True) + target_group.add_argument('-org', '--organization', type=str, nargs='+', + help='集計対象の組織名を指定してください。`file://`を先頭に付けると、組織名の一覧が記載されたファイルを指定できます。') - parser.add_argument('-org', '--organization', type=str, nargs='+', required=True, - help='集計対象の組織名を指定してください。`file://`を先頭に付けると、組織名の一覧が記載されたファイルを指定できます。') + target_group.add_argument('-p', '--project_id', type=str, nargs='+', + help='集計対象のプロジェクトを指定してください。`file://`を先頭に付けると、project_idの一覧が記載されたファイルを指定できます。') - parser.add_argument('-u', '--user_id', type=str, nargs='+', required=True, - help='集計対象のユーザのuser_idを指定してください。`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。') + parser.add_argument( + '-u', '--user_id', type=str, nargs='+', + help='集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。') parser.add_argument("--start_date", type=str, required=True, help="集計期間の開始日(%%Y-%%m-%%d)") parser.add_argument("--end_date", type=str, required=True, help="集計期間の終了日(%%Y-%%m-%%d)") @@ -219,8 +305,8 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list_worktime_by_user" - subcommand_help = "ユーザごとに作業時間を出力します。" - description = ("ユーザごとに作業時間を出力します。") + subcommand_help = "ユーザごとに作業予定時間、作業実績時間を出力します。" + description = ("ユーザごとに作業予定時間、作業実績時間を出力します。") parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) From 14119ea35f73f7a98cae2e59eff5f44ecd025419 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 21:59:04 +0900 Subject: [PATCH 06/10] format --- annofabcli/common/cli.py | 6 +++--- annofabcli/labor/list_worktime_by_user.py | 22 +++++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/annofabcli/common/cli.py b/annofabcli/common/cli.py index 67864d50..a1115cd8 100644 --- a/annofabcli/common/cli.py +++ b/annofabcli/common/cli.py @@ -89,14 +89,14 @@ def create_parent_parser() -> argparse.ArgumentParser: return parent_parser -def get_list_from_args(str_list: Optional[List[str]] = None) -> Optional[List[str]]: +def get_list_from_args(str_list: Optional[List[str]] = None) -> List[str]: """ 文字列のListのサイズが1で、プレフィックスが`file://`ならば、ファイルパスとしてファイルを読み込み、行をListとして返す。 そうでなければ、引数の値をそのまま返す。 - ただしNoneの場合はNoneを返す。 + ただしNoneの場合は空Listを変えす """ if str_list is None or len(str_list) == 0: - return None + return [] if len(str_list) > 1: return str_list diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index 65f40b91..b1940bed 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -260,15 +260,26 @@ def validate(args: argparse.Namespace) -> bool: return True + def get_user_id_list_from_project_id_list(self, project_id_list: List[str]) -> List[str]: + member_list = [] + for project_id in project_id_list: + member_list.extend(self.service.wrapper.get_all_project_members(project_id)) + user_id_list = [e["user_id"] for e in member_list] + return list(set(user_id_list)) + def main(self): args = self.args if not self.validate(args): return - user_id_list = get_list_from_args(args.user_id) - project_id_list = get_list_from_args(args.project_id) - organization_name_list = get_list_from_args(args.organization) + user_id_list = get_list_from_args(args.user_id) if args.user_id is not None else None + project_id_list = get_list_from_args(args.project_id) if args.project_id is not None else None + organization_name_list = get_list_from_args(args.organization) if args.organization is not None else None + + if user_id_list is None and project_id_list is not None: + user_id_list = self.get_user_id_list_from_project_id_list(project_id_list) + output_dir = Path(args.output_dir) output_dir.mkdir(exist_ok=True, parents=True) @@ -292,8 +303,9 @@ def parse_args(parser: argparse.ArgumentParser): help='集計対象のプロジェクトを指定してください。`file://`を先頭に付けると、project_idの一覧が記載されたファイルを指定できます。') parser.add_argument( - '-u', '--user_id', type=str, nargs='+', - help='集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。') + '-u', '--user_id', type=str, nargs='+', help= + '集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。指定しない場合は、プロジェクトメンバが指定されます。`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。' + ) parser.add_argument("--start_date", type=str, required=True, help="集計期間の開始日(%%Y-%%m-%%d)") parser.add_argument("--end_date", type=str, required=True, help="集計期間の終了日(%%Y-%%m-%%d)") From 34af4d2a7d57d1ab92dcc13601e6c12aee2bec13 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 22:00:49 +0900 Subject: [PATCH 07/10] format --- annofabcli/labor/list_worktime_by_user.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index b1940bed..91e9c237 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -261,7 +261,7 @@ def validate(args: argparse.Namespace) -> bool: return True def get_user_id_list_from_project_id_list(self, project_id_list: List[str]) -> List[str]: - member_list = [] + member_list: List[Dict[str, Any]] = [] for project_id in project_id_list: member_list.extend(self.service.wrapper.get_all_project_members(project_id)) user_id_list = [e["user_id"] for e in member_list] @@ -303,8 +303,10 @@ def parse_args(parser: argparse.ArgumentParser): help='集計対象のプロジェクトを指定してください。`file://`を先頭に付けると、project_idの一覧が記載されたファイルを指定できます。') parser.add_argument( - '-u', '--user_id', type=str, nargs='+', help= - '集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。指定しない場合は、プロジェクトメンバが指定されます。`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。' + '-u', '--user_id', type=str, nargs='+', + help='集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。' + '指定しない場合は、プロジェクトメンバが指定されます。' + '`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。' ) parser.add_argument("--start_date", type=str, required=True, help="集計期間の開始日(%%Y-%%m-%%d)") From 9d37b124ccbb01f215f4821cbcd7644f9f5d0862 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 22:19:06 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=E6=89=80=E5=B1=9E=E7=B5=84=E7=B9=94?= =?UTF-8?q?=E3=82=92=E9=9B=86=E8=A8=88=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/labor/list_worktime_by_user.py | 37 +++++++++++------------ 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index 91e9c237..f17ec3e7 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -106,8 +106,12 @@ def _get_labor_worktime(self, labor: Dict[str, Any], member: OrganizationMember, def get_labor_list_from_project_id(self, project_id: str, member_list: List[OrganizationMember], start_date: str, end_date: str) -> List[LaborWorktime]: + organization, _ = self.service.api.get_organization_of_project(project_id) + organization_name = organization["organization_name"] + labor_list, _ = self.service.api.get_labor_control({ "project_id": project_id, + "organization_id": organization["organization_id"], "from": start_date, "to": end_date }) @@ -119,8 +123,6 @@ def get_labor_list_from_project_id(self, project_id: str, member_list: List[Orga for labor in labor_list: member = self.get_member_from_account_id(member_list, labor["account_id"]) - organization, _ = self.service.api.get_organization_of_project(project_id) - organization_name = organization["organization_name"] new_labor = self._get_labor_worktime(labor, member=member, project_title=project_title, organization_name=organization_name) new_labor_list.append(new_labor) @@ -142,18 +144,9 @@ def get_labor_list_from_organization_name(self, organization_name: str, member_l new_labor_list = [] for labor in labor_list: member = self.get_member_from_account_id(member_list, labor["account_id"]) - new_labor = LaborWorktime( - date=labor["date"], - organization_id=labor["organization_id"], - organization_name=organization_name, - project_id=labor["project_id"], - project_title=self.get_project_title(project_list, labor["project_id"]), - account_id=labor["account_id"], - user_id=member["user_id"] if member is not None else "", - username=member["username"] if member is not None else "", - worktime_plan_hour=self.get_worktime_hour(labor["values"]["working_time_by_user"], "plans"), - worktime_result_hour=self.get_worktime_hour(labor["values"]["working_time_by_user"], "results"), - ) + project_title = self.get_project_title(project_list, labor["project_id"]) + new_labor = self._get_labor_worktime(labor, member=member, project_title=project_title, + organization_name=organization_name) new_labor_list.append(new_labor) return new_labor_list @@ -224,6 +217,9 @@ def print_labor_worktime_list(self, organization_name_list: Optional[List[str]], self.get_labor_list_from_organization_name(organization_name, member_list=member_list, start_date=start_date, end_date=end_date)) + # 集計対象ユーザで絞り込む + labor_list = [e for e in labor_list if e.user_id in user_id_list] + reform_dict = { ("date", ""): [ e.strftime(ListWorktimeByUser.DATE_FORMAT) for e in pandas.date_range(start=start_date, end=end_date) @@ -250,7 +246,10 @@ def print_labor_worktime_list(self, organization_name_list: Optional[List[str]], self.write_sum_worktime_list(sum_worktime_df, output_dir) worktime_df = pandas.DataFrame([e.to_dict() for e in labor_list]) # type: ignore - self.write_worktime_list(worktime_df, output_dir) + if len(worktime_df) > 0: + self.write_worktime_list(worktime_df, output_dir) + else: + logger.warning(f"労務管理情報が0件のため、作業時間の詳細一覧.csv は出力しません。") @staticmethod def validate(args: argparse.Namespace) -> bool: @@ -303,11 +302,9 @@ def parse_args(parser: argparse.ArgumentParser): help='集計対象のプロジェクトを指定してください。`file://`を先頭に付けると、project_idの一覧が記載されたファイルを指定できます。') parser.add_argument( - '-u', '--user_id', type=str, nargs='+', - help='集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。' - '指定しない場合は、プロジェクトメンバが指定されます。' - '`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。' - ) + '-u', '--user_id', type=str, nargs='+', help='集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。' + '指定しない場合は、プロジェクトメンバが指定されます。' + '`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。') parser.add_argument("--start_date", type=str, required=True, help="集計期間の開始日(%%Y-%%m-%%d)") parser.add_argument("--end_date", type=str, required=True, help="集計期間の終了日(%%Y-%%m-%%d)") From 8ae9545d0ffe7bde2b23d3d31c53827ca21b6dc0 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 23:11:12 +0900 Subject: [PATCH 09/10] update README --- README.md | 14 ++++++++++++++ annofabcli/__version__.py | 2 +- annofabcli/labor/list_worktime_by_user.py | 12 ++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 88d7421f..eb251db0 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ $ docker run -it -e ANNOFAB_USER_ID=XXXX -e ANNOFAB_PASSWORD=YYYYY annofab-cli a |instruction| upload | HTMLファイルを作業ガイドとして登録します。 |チェッカー/オーナ| |job|list | ジョブ一覧を出力します。 |-| |job|list_last | 複数のプロジェクトに対して、最新のジョブを出力します。 |-| +|labor|list_worktime_by_user | ユーザごとに作業予定時間、作業実績時間を出力します。 || |organization_member|list | 組織メンバ一覧を出力します。 |-| |project| copy | プロジェクトをコピーします。 |オーナ and 組織管理者/組織オーナ| |project| diff | プロジェクト間の差分を表示します。 |チェッカー/オーナ| @@ -645,6 +646,19 @@ $ annofabcli job list_last --project_id prj1 --job_type gen-annotation --add_det ``` +### labor list_worktime_by_user + +ユーザごとに作業予定時間、作業実績時間を出力します。 + +``` +# 組織org1, org2に対して、user1, user2の作業時間を集計します。 +$ annofabcli labor list_worktime_by_user --organization org1 org2 --user_id user1 user2 \ + --start_date 2019-10-01 --end_date 2019-10-31 --output_dir /tmp/output + +# プロジェクトprj1, prj2に対して作業時間を集計します。集計対象のユーザはプロジェクトに所属するメンバです。 +$ annofabcli labor list_worktime_by_user --project_id prj1 prj2 --user_id user1 user2 \ + --start_date 2019-10-01 --end_date 2019-10-31 --output_dir /tmp/output +``` ### organization_member list diff --git a/annofabcli/__version__.py b/annofabcli/__version__.py index e4f2ad49..1c19d78b 100644 --- a/annofabcli/__version__.py +++ b/annofabcli/__version__.py @@ -1 +1 @@ -__version__ = '1.14.0' +__version__ = '1.15.0' diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index f17ec3e7..c4a2c087 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -169,13 +169,17 @@ def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_d @staticmethod def write_sum_worktime_list(sum_worktime_df: pandas.DataFrame, output_dir: Path): - sum_worktime_df.to_csv(str(output_dir / "ユーザごとの作業時間.csv"), encoding="utf_8_sig", index=False) + sum_worktime_df.round(3).to_csv(str(output_dir / "ユーザごとの作業時間.csv"), encoding="utf_8_sig", index=False) @staticmethod def write_worktime_list(worktime_df: pandas.DataFrame, output_dir: Path): - worktime_df = worktime_df.rename(columns={"worktime_plan_hour": "作業予定時間", "worktime_result_hour": "作業実績時間"}) - columns = ListWorktimeByUser.create_required_columns( - worktime_df, ["date", "organization_name", "project_title", "username", "作業予定時間", "作業実績時間"]) + worktime_df = worktime_df.rename(columns={ + "worktime_plan_hour": "作業予定時間", + "worktime_result_hour": "作業実績時間" + }).round(3) + columns = [ + "date", "organization_name", "project_title", "project_id", "username", "user_id", "作業予定時間", "作業実績時間" + ] worktime_df[columns].to_csv(str(output_dir / "作業時間の詳細一覧.csv"), encoding="utf_8_sig", index=False) def get_organization_member_list(self, organization_name_list: Optional[List[str]], From d0d25fc9df7d194b2a5d63a51bb9ddc9f56a1a91 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Mon, 18 Nov 2019 23:23:40 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=E5=8A=B4=E5=8B=99=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=B3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_main.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 4995a382..90d75fd2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -25,10 +25,7 @@ out_path = Path('./tests/out') data_path = Path('./tests/data') - -def get_organization_name(project_id: str) -> str: - organization, _ = service.api.get_organization_of_project(project_id) - return organization["organization_name"] +organization_name = service.api.get_organization_of_project(project_id)[0]["organization_name"] class TestAnnotation: @@ -165,12 +162,29 @@ def test_list_last_job(self): ]) +class TestLabor: + def test_list_worktime_by_user_with_project_id(self): + output_dir = str(out_path / 'labor') + main([ + 'labor', 'list_worktime_by_user', '--project_id', project_id, '--user_id', service.api.login_user_id, + '--start_date', '2019-09-01', '--end_date', '2019-09-01', '--output_dir', + str(output_dir) + ]) + + def test_list_worktime_by_user_with_organization_name(self): + output_dir = str(out_path / 'labor') + main([ + 'labor', 'list_worktime_by_user', '--organization', organization_name, '--user_id', + service.api.login_user_id, '--start_date', '2019-09-01', '--end_date', '2019-09-01', '--output_dir', + str(output_dir) + ]) + + class TestOrganizationMember: def test_list_organization_member(self): out_file = str(out_path / 'organization_member.csv') main([ - 'organization_member', 'list', '--organization', - get_organization_name(project_id), '--format', 'csv', '--output', out_file + 'organization_member', 'list', '--organization', organization_name, '--format', 'csv', '--output', out_file ]) @@ -203,7 +217,6 @@ def test_download_project_full_annotation(self): main(['project', 'download', 'full_annotation', '--project_id', project_id, '--output', out_file]) def test_list_project(self): - organization_name = get_organization_name(project_id) out_file = str(out_path / 'project.csv') main([ 'project', 'list', '--organization', organization_name, '--project_query', '{"status": "active"}', @@ -218,7 +231,6 @@ def test_put_project_member(self): def test_list_project_member(self): main(['project_member', 'list', '--project_id', project_id]) - organization_name = get_organization_name(project_id) main(['project_member', 'list', '--organization', organization_name]) def test_copy_project_member(self):