diff --git a/ecs/jskult-webapp/.env.example b/ecs/jskult-webapp/.env.example index 0630afe4..10844a21 100644 --- a/ecs/jskult-webapp/.env.example +++ b/ecs/jskult-webapp/.env.example @@ -10,6 +10,7 @@ COGNITO_CLIENT_SECRET=****************************** SESSION_TABLE_NAME=*********************** ##S3 BIO_ACCESS_LOG_BUCKET=******************* +MASTER_MAINTENANCE_BUCKET=mbj-newdwh2021-staging-jskult-master-maintenance #MySQL DB_HOST=************ DB_PORT=************ diff --git a/ecs/jskult-webapp/src/controller/master_mainte.py b/ecs/jskult-webapp/src/controller/master_mainte.py index ea972ad6..046fc4ab 100644 --- a/ecs/jskult-webapp/src/controller/master_mainte.py +++ b/ecs/jskult-webapp/src/controller/master_mainte.py @@ -1,8 +1,11 @@ +from io import BytesIO, TextIOWrapper +from typing import Optional + from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import HTMLResponse from starlette import status - from src.depends.services import get_service +from src.logging.get_logger import get_logger from src.model.internal.session import UserSession from src.model.view.inst_emp_csv_download_view_model import \ InstEmpCsvDownloadViewModel @@ -13,9 +16,15 @@ from src.model.view.master_mainte_menu_view_model import \ from src.model.view.table_override_view_model import TableOverrideViewModel from src.router.session_router import AuthenticatedRoute from src.services.batch_status_service import BatchStatusService +from src.services.master_mainte_service import MasterMainteService from src.services.session_service import set_session from src.system_var import constants from src.templates import templates +from src.model.request.master_mainte_csvup import MasterMainteCsvUpModel +from src.model.request.master_mainte_csvdl import MasterMainteCsvDlModel + + +logger = get_logger('マスターメンテ') router = APIRouter() router.route_class = AuthenticatedRoute @@ -85,7 +94,7 @@ def inst_emp_csv_upload_view( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BACKUP_PROCESSING) # 画面表示用のモデル - view_model = InstEmpCsvUploadViewModel() + mainte_csv_up = InstEmpCsvUploadViewModel() # セッション書き換え session.update( actions=[ @@ -98,14 +107,118 @@ def inst_emp_csv_upload_view( 'instEmpCsvUL.html', { 'request': request, - 'view': view_model + 'mainte_csv_up': mainte_csv_up }, headers={'session_key': session.session_key} ) return templates_response -@router.get('/instEmpCsvDL', response_class=HTMLResponse) +@router.post('/instEmpCsvUL', response_class=HTMLResponse) +async def inst_emp_csv_upload( + request: Request, + csv_upload_form: Optional[MasterMainteCsvUpModel] = Depends(MasterMainteCsvUpModel.as_form), + master_mainte_service: MasterMainteService = Depends(get_service(MasterMainteService)), + batch_status_service: BatchStatusService = Depends(get_service(BatchStatusService)) +): + session: UserSession = request.session + + # マスタメンテメニューへのアクセス権がない場合、ログアウトさせる + if session.master_mainte_flg != '1': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + # バッチ処理中の場合、ログアウトさせる + if batch_status_service.is_batch_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail=constants.LOGOUT_REASON_BATCH_PROCESSING_FOR_MAINTE) + # dump処理中の場合、ログアウトさせる + if batch_status_service.is_dump_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BACKUP_PROCESSING) + + # 画面表示用のモデル + error_message_list = [] + content_type: str = csv_upload_form.csv_file.content_type.strip() + if csv_upload_form.csv_file.size == 0: + error_message_list.append('選択されたファイルが見つかりませんでした。') + elif content_type != 'text/csv' and content_type != 'application/vnd.ms-excel' and content_type != 'application/octet-stream': + error_message_list.append('選択されたファイル形式が"csv"ではありません。') + elif csv_upload_form.csv_file.size >= constants.MENTE_CSV_UPLOAD_MAX_FILE_SIZE_BYTE: + error_message_list.append('選択されたCSVファイルサイズが大きいです。100MB未満にしてください。') + else: + mainte_csv_up = master_mainte_service.prepare_mainte_csv_up_view( + TextIOWrapper(BytesIO(await csv_upload_form.csv_file.read()), encoding='utf-8'), + csv_upload_form.csv_file.filename, + csv_upload_form) + + if len(error_message_list) > 0: + mainte_csv_up = InstEmpCsvUploadViewModel( + is_verified=True, + error_message_list=error_message_list, + select_function=csv_upload_form.select_function, + select_table=csv_upload_form.select_table) + + # セッション書き換え + session.update( + actions=[ + UserSession.last_access_time.set(UserSession.new_last_access_time()), + UserSession.record_expiration_time.set(UserSession.new_record_expiration_time()), + ] + ) + set_session(session) + templates_response = templates.TemplateResponse( + 'instEmpCsvUL.html', + { + 'request': request, + 'mainte_csv_up': mainte_csv_up + }, + headers={'session_key': session.session_key} + ) + return templates_response + + +@router.post('/newInst', response_class=HTMLResponse) +def new_inst_result_view( + request: Request, + csv_upload_form: Optional[MasterMainteCsvUpModel] = Depends(MasterMainteCsvUpModel.as_form), + master_mainte_service: MasterMainteService = Depends(get_service(MasterMainteService)), + batch_status_service: BatchStatusService = Depends(get_service(BatchStatusService)) +): + session: UserSession = request.session + + # マスタメンテメニューへのアクセス権がない場合、ログアウトさせる + if session.master_mainte_flg != '1': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + # バッチ処理中の場合、ログアウトさせる + if batch_status_service.is_batch_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail=constants.LOGOUT_REASON_BATCH_PROCESSING_FOR_MAINTE) + # dump処理中の場合、ログアウトさせる + if batch_status_service.is_dump_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BACKUP_PROCESSING) + + # 画面表示用のモデル + mainte_csv_up = master_mainte_service.prepare_mainte_new_inst_view(session.user_id, csv_upload_form) + # セッション書き換え + session.update( + actions=[ + UserSession.last_access_time.set(UserSession.new_last_access_time()), + UserSession.record_expiration_time.set(UserSession.new_record_expiration_time()), + ] + ) + set_session(session) + templates_response = templates.TemplateResponse( + 'instEmpCsvUL.html', + { + 'request': request, + 'mainte_csv_up': mainte_csv_up + }, + headers={'session_key': session.session_key} + ) + return templates_response + + +@ router.get('/instEmpCsvDL', response_class=HTMLResponse) def inst_emp_csv_download_view( request: Request, batch_status_service: BatchStatusService = Depends(get_service(BatchStatusService)) @@ -125,7 +238,9 @@ def inst_emp_csv_download_view( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BACKUP_PROCESSING) # 画面表示用のモデル - view_model = InstEmpCsvDownloadViewModel() + mainte_csv_dl = InstEmpCsvDownloadViewModel( + is_search=False + ) # セッション書き換え session.update( actions=[ @@ -138,7 +253,77 @@ def inst_emp_csv_download_view( 'instEmpCsvDL.html', { 'request': request, - 'view': view_model + 'mainte_csv_dl': mainte_csv_dl + }, + headers={'session_key': session.session_key} + ) + return templates_response + + +@router.post('/download', response_class=HTMLResponse) +async def inst_emp_csv_download( + request: Request, + csv_download_form: Optional[MasterMainteCsvDlModel] = Depends(MasterMainteCsvDlModel.as_form), + master_mainte_service: MasterMainteService = Depends(get_service(MasterMainteService)), + batch_status_service: BatchStatusService = Depends(get_service(BatchStatusService)) +): + session: UserSession = request.session + + # マスタメンテメニューへのアクセス権がない場合、ログアウトさせる + if session.master_mainte_flg != '1': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + # バッチ処理中の場合、ログアウトさせる + if batch_status_service.is_batch_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail=constants.LOGOUT_REASON_BATCH_PROCESSING_FOR_MAINTE) + # dump処理中の場合、ログアウトさせる + if batch_status_service.is_dump_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BACKUP_PROCESSING) + + search_result_df = master_mainte_service.search_emp_chg_inst_data(csv_download_form) + + (result_msg, download_file_url) = master_mainte_service.upload_emp_chg_inst_data_file( + search_result_df, + session.user_id, + csv_download_form.select_table) + + # 画面表示用のモデル + mainte_csv_dl = InstEmpCsvDownloadViewModel( + is_search=True, + ta_cd=csv_download_form.ta_cd, + inst_cd=csv_download_form.inst_cd, + emp_cd=csv_download_form.emp_cd, + apply_date_from=csv_download_form.apply_date_from, + start_date_from=csv_download_form.start_date_from, + start_date_to=csv_download_form.start_date_to, + end_date_from=csv_download_form.end_date_from, + end_date_to=csv_download_form.end_date_to, + create_date_from=csv_download_form.create_date_from, + create_date_to=csv_download_form.create_date_to, + update_date_from=csv_download_form.update_date_from, + update_date_to=csv_download_form.update_date_to, + select_table=csv_download_form.select_table, + data_count=search_result_df.shape[0], + download_file_url=download_file_url, + file_name=constants.MENTE_CSV_DOWNLOAD_FILE_NAME, + result_msg=result_msg + ) + + # セッション書き換え + session.update( + actions=[ + UserSession.last_access_time.set(UserSession.new_last_access_time()), + UserSession.record_expiration_time.set(UserSession.new_record_expiration_time()), + ] + ) + set_session(session) + + templates_response = templates.TemplateResponse( + 'instEmpCsvDL.html', + { + 'request': request, + 'mainte_csv_dl': mainte_csv_dl }, headers={'session_key': session.session_key} ) @@ -165,7 +350,7 @@ def table_override_view( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BACKUP_PROCESSING) # 画面表示用のモデル - view_model = TableOverrideViewModel() + table_override = TableOverrideViewModel() # セッション書き換え session.update( actions=[ @@ -178,7 +363,49 @@ def table_override_view( 'tableOverride.html', { 'request': request, - 'view': view_model + 'table_override': table_override + }, + headers={'session_key': session.session_key} + ) + return templates_response + + +@router.post('/tableOverride', response_class=HTMLResponse) +def table_override_result_view( + request: Request, + master_mainte_service: MasterMainteService = Depends(get_service(MasterMainteService)), + batch_status_service: BatchStatusService = Depends(get_service(BatchStatusService)) +): + session: UserSession = request.session + + # マスタメンテメニューへのアクセス権がない場合、ログアウトさせる + if session.master_mainte_flg != '1': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + # バッチ処理中の場合、ログアウトさせる + if batch_status_service.is_batch_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail=constants.LOGOUT_REASON_BATCH_PROCESSING_FOR_MAINTE) + # dump処理中の場合、ログアウトさせる + if batch_status_service.is_dump_processing(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.LOGOUT_REASON_BACKUP_PROCESSING) + + # 画面表示用のモデル + table_override = master_mainte_service.copy_data_real_to_dummy() + + # セッション書き換え + session.update( + actions=[ + UserSession.last_access_time.set(UserSession.new_last_access_time()), + UserSession.record_expiration_time.set(UserSession.new_record_expiration_time()), + ] + ) + set_session(session) + templates_response = templates.TemplateResponse( + 'tableOverride.html', + { + 'request': request, + 'table_override': table_override }, headers={'session_key': session.session_key} ) diff --git a/ecs/jskult-webapp/src/model/db/master_mente_count.py b/ecs/jskult-webapp/src/model/db/master_mente_count.py new file mode 100644 index 00000000..a3837819 --- /dev/null +++ b/ecs/jskult-webapp/src/model/db/master_mente_count.py @@ -0,0 +1,9 @@ +from typing import Optional + +from src.model.db.base_db_model import BaseDBModel +from src.util.sanitize import sanitize + + +@sanitize +class MasterMenteCountModel(BaseDBModel): + count: Optional[int] diff --git a/ecs/jskult-webapp/src/model/internal/master_mainte_csv.py b/ecs/jskult-webapp/src/model/internal/master_mainte_csv.py new file mode 100644 index 00000000..e288ce3a --- /dev/null +++ b/ecs/jskult-webapp/src/model/internal/master_mainte_csv.py @@ -0,0 +1,572 @@ +import csv +import json + +from io import TextIOWrapper +from datetime import datetime +from abc import ABCMeta, abstractmethod +from src.system_var import constants +from src.util.string_util import is_not_empty +from src.repositories.mst_inst_repository import MstInstRepository +from src.repositories.bu_master_cd_repository import BuMasterRepository +from src.repositories.emp_master_repository import EmpMasterRepository +from src.repositories.emp_chg_inst_repository import EmpChgInstRepository +from src.logging.get_logger import get_logger + +logger = get_logger('マスターメンテ') + + +class MasterMainteCSVItem(metaclass=ABCMeta): + + csv_row: list[str] + table_name: str + line_num: str + mst_inst_repository: MstInstRepository + emp_master_repository: EmpMasterRepository + bu_master_repository: BuMasterRepository + emp_chginst_repository: EmpChgInstRepository + + def __init__( + self, + csv_row: list[str], + table_name: str, + line_num: str, + mst_inst_repository: MstInstRepository, + emp_master_repository: EmpMasterRepository, + bu_master_repository: BuMasterRepository, + emp_chginst_repository: EmpChgInstRepository + ): + self.csv_row = csv_row + self.table_name = table_name + self.line_num = line_num + self.mst_inst_repository = mst_inst_repository + self.emp_master_repository = emp_master_repository + self.bu_master_repository = bu_master_repository + self.emp_chginst_repository = emp_chginst_repository + + def validate(self) -> list[str]: + """ + 項目のバリデーションを行うテンプレートメソッド\n + 各チェックロジックはサブクラスで実装する + エラーが有る場合、[行数、項目名: エラー内容]のリストを返す + """ + error_list = [] + # 項目数チェック + error_list.extend(self.check_item_count()) + if len(error_list) == 0: + # 必須チェック 及び コメントエラーチェック + error_list.extend(self.check_require()) + # 施設コード存在チェック + error_list.extend(self.check_inst_cd_exists()) + # MUID存在チェック + error_list.extend(self.check_emp_cd_exists()) + # BuCd存在チェック + error_list.extend(self.check_bu_cd_exists()) + # 適用開始日 < 適用終了日、実在日チェック + error_list.extend(self.check_existing_date()) + # データ存在チェック + error_list.extend(self.check_data_exists()) + + # エラーのないリストを省いて返す + error_list = [error for error in error_list if len(error) != 0] + return error_list + + def check_csv_item_count(self, item_count: int) -> list[str]: + error_list = [] + + if not len(self.csv_row) == item_count: + error_list.append(f'{self.line_num}行目の項目数が一致しません。項目数を確認してください。') + + return error_list + + def emp_chg_inst_count(self, start_date: str): + return self.emp_chginst_repository.fetch_count(self.inst_cd, self.ta_cd, start_date, self.table_name) + + def is_exist_emp_cd(self, start_date: str) -> bool: + if start_date is None or len(start_date) == 0: + return False + if self.emp_master_repository.fetch_count(self.emp_cd, start_date) == 0: + return True + return False + + def is_exist_inst_cd(self) -> bool: + return True if self.mst_inst_repository.fetch_count(self.inst_cd) > 0 else False + + def is_exist_bu_cd(self) -> bool: + return True if self.bu_master_repository.fetch_count(self.bu_cd) > 0 else False + + def make_require_error_message(self, line_num: str, col_name: str) -> str: + return f'{line_num}行目の{col_name}が入力されておりません。' + + def __parse_str_to_date(self, check_date: str) -> tuple[bool, datetime]: + try: + check_date_time: datetime = datetime.strptime(check_date, '%Y%m%d') + except Exception as e: + logger.debug(f'適用期間の日付が不正{e}') + return (False, None) + + try: + reverse_check_date: str = check_date_time.strftime('%Y%m%d') + except Exception as e: + logger.debug(f'適用期間の日付が不正{e}') + return (False, None) + + if check_date != reverse_check_date: + return (False, None) + + return (True, check_date_time) + + def check_term_date(self, + start_date: str, + end_date: str, + start_date_col_name: str, + end_date_col_name: str) -> tuple[list[str], datetime, datetime]: + error_list = [] + + start_date_time: datetime = None + end_date_time: datetime = None + if is_not_empty(start_date): + (result, start_date_time) = self.__parse_str_to_date(start_date) + if result is False: + error_list.append(f'{self.line_num}行目の{start_date_col_name}が実在しない日付になっています。') + if is_not_empty(end_date): + (result, end_date_time) = self.__parse_str_to_date(end_date) + if result is False: + error_list.append(f'{self.line_num}行目の{end_date_col_name}が実在しない日付になっています。') + + return (error_list, start_date_time, end_date_time) + + def get_csv_value(self, column_no: int): + try: + column_value = self.csv_row[column_no] + except IndexError: + column_value = '' + + return column_value + + @abstractmethod + def csv_row_data(self) -> dict: + pass + ... + + @abstractmethod + def check_require(self) -> list[str]: + """必須チェック""" + pass + ... + + @abstractmethod + def check_inst_cd_exists(self) -> list[str]: + """InstCD存在チェック""" + pass + ... + + @abstractmethod + def check_emp_cd_exists(self) -> list[str]: + """MUID存在チェック""" + pass + ... + + @abstractmethod + def check_bu_cd_exists(self) -> list[str]: + """BuCd存在チェック""" + pass + ... + + @abstractmethod + def check_existing_date(self) -> list[str]: + """適用開始日 < 適用終了日、実在日チェック""" + + @abstractmethod + def check_item_count(self) -> list[str]: + """項目数チェック""" + pass + ... + + @abstractmethod + def check_data_exists(self) -> list[str]: + """データ存在チェック""" + pass + ... + + +class MasterMainteNewInstEmpCSVItem(MasterMainteCSVItem): + """新規施設担当者登録CSV""" + inst_name: str + emp_name_family: str + emp_name_first: str + start_date: str + end_date: str + + def __init__( + self, + csv_row: list[str], + table_name: str, + line_num: str, + mst_inst_repository: MstInstRepository, + emp_master_repository: EmpMasterRepository, + bu_master_repository: BuMasterRepository, + emp_chginst_repository: EmpChgInstRepository + ): + super().__init__( + csv_row, + table_name, + line_num, + mst_inst_repository, + emp_master_repository, + bu_master_repository, + emp_chginst_repository + ) + self.inst_cd = super().get_csv_value(constants.CSV_NEW_INST_CD_COL_NO) + self.inst_name = super().get_csv_value(constants.CSV_NEW_INST_NAME_COL_NO) + self.ta_cd = super().get_csv_value(constants.CSV_NEW_TA_CD_COL_NO) + self.emp_cd = super().get_csv_value(constants.CSV_NEW_EMP_CD_COL_NO) + self.emp_name_family = super().get_csv_value(constants.CSV_NEW_EMP_NAME_FAMILY_COL_NO) + self.emp_name_first = super().get_csv_value(constants.CSV_NEW_EMP_NAME_FIRST_COL_NO) + self.bu_cd = super().get_csv_value(constants.CSV_NEW_BU_CD_COL_NO) + self.start_date = super().get_csv_value(constants.CSV_NEW_START_DATE) + self.end_date = super().get_csv_value(constants.CSV_NEW_END_DATE) + + def csv_row_data(self) -> dict: + return {constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[i]: self.csv_row[i] for i in range(len(self.csv_row))} + + def check_require(self) -> list[str]: + error_list = [] + if len(self.inst_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_INST_CD_COL_NO])) + if len(self.ta_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_TA_CD_COL_NO])) + if len(self.emp_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_EMP_CD_COL_NO])) + if len(self.bu_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_BU_CD_COL_NO])) + if len(self.start_date) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_START_DATE])) + if len(self.end_date) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_END_DATE])) + + return error_list + + def check_inst_cd_exists(self) -> list[str]: + error_list = [] + + if is_not_empty(self.inst_cd) and super().is_exist_inst_cd() is False: + error_list.append( + f'{self.line_num}行目の{constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_INST_CD_COL_NO]}\ +は施設マスタに存在しないコードです。') + return error_list + + def check_emp_cd_exists(self) -> list[str]: + error_list = [] + if not self.start_date or not self.emp_cd: + return error_list + + if super().is_exist_emp_cd(self.start_date) is True: + error_list.append(f'{self.line_num}行目の{constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_EMP_CD_COL_NO]}\ +は従業員マスタに存在しない もしくは 適用期間外のIDです。') + return error_list + + def check_bu_cd_exists(self) -> list[str]: + error_list = [] + + if is_not_empty(self.bu_cd) and super().is_exist_bu_cd() is False: + error_list.append(f'{self.line_num}行目の{constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_BU_CD_COL_NO]}\ +はビジネスユニットマスタに存在しないコードです。') + return error_list + + def check_existing_date(self) -> list[str]: + error_list = [] + if not self.start_date or not self.end_date: + return error_list + + (error_list, start_date_time, end_date_time) = super().check_term_date( + self.start_date, + self.end_date, + constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_START_DATE], + constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_END_DATE]) + if len(error_list) > 0: + return error_list + + if start_date_time > end_date_time: + error_list.append(f'{self.line_num}行目の{constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_START_DATE]}\ +が{constants.NEW_INST_EMP_CSV_LOGICAL_NAMES[constants.CSV_NEW_END_DATE]}よりも後の日付になっています。') + return error_list + + def check_item_count(self) -> list[str]: + return super().check_csv_item_count(len(constants.NEW_INST_EMP_CSV_LOGICAL_NAMES)) + + def check_data_exists(self) -> list[str]: + error_list = [] + if super().emp_chg_inst_count(self.start_date) > 0: + error_list.append(f'{self.line_num}行目の施設コード、領域コード、適用開始日がすべて同一のデータが既に登録されています。') + + return error_list + + +class MasterMainteChangeInstEmpCSVItem(MasterMainteCSVItem): + """施設担当者変更登録CSV""" + bu_name: str + org_cd: str + org_short_name: str + inst_name: str + explain: str + emp_full_name: str + inst_emp_start_date: str + inst_emp_end_date: str + change_end_date: str + comment: str + + def __init__( + self, + csv_row: list[str], + table_name: str, + line_num: str, + mst_inst_repository: MstInstRepository, + emp_master_repository: EmpMasterRepository, + bu_master_repository: BuMasterRepository, + emp_chginst_repository: EmpChgInstRepository + ): + super().__init__( + csv_row, + table_name, + line_num, + mst_inst_repository, + emp_master_repository, + bu_master_repository, + emp_chginst_repository + ) + self.bu_cd = super().get_csv_value(constants.CSV_CHANGE_BU_CD_COL_NO) + self.bu_name = super().get_csv_value(constants.CSV_CHANGE_BU_NAME_COL_NO) + self.org_cd = super().get_csv_value(constants.CSV_CHANGE_ORG_CD_COL_NO) + self.org_short_name = super().get_csv_value(constants.CSV_CHANGE_ORG_SHORT_NAME_COL_NO) + self.inst_cd = super().get_csv_value(constants.CSV_CHANGE_INST_CD_COL_NO) + self.inst_name = super().get_csv_value(constants.CSV_CHANGE_INST_NAME_COL_NO) + self.ta_cd = super().get_csv_value(constants.CSV_CHANGE_TA_CD_COL_NO) + self.explain = super().get_csv_value(constants.CSV_CHANGE_EXPLAIN_COL_NO) + self.emp_cd = super().get_csv_value(constants.CSV_CHANGE_EMP_CD_COL_NO) + self.emp_full_name = super().get_csv_value(constants.CSV_CHANGE_EMP_FULL_NAME_COL_NO) + self.inst_emp_start_date = super().get_csv_value(constants.CSV_CHANGE_INST_EMP_START_DATE_COL_NO) + self.inst_emp_end_date = super().get_csv_value(constants.CSV_CHANGE_INST_EMP_END_DATE_COL_NO) + self.change_end_date = super().get_csv_value(constants.CSV_CHANGE_CHANGE_END_DATE_COL_NO) + self.comment = super().get_csv_value(constants.CSV_CHANGE_COMMENT) + + def csv_row_data(self) -> dict: + return {constants.CHANGE_INST_CSV_LOGICAL_NAMES[i]: self.csv_row[i] for i in range(len(self.csv_row))} + + def check_require(self) -> list[str]: + error_list = [] + if self.comment == '追加': + if len(self.bu_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_BU_CD_COL_NO])) + if len(self.inst_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_INST_CD_COL_NO])) + if len(self.ta_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_TA_CD_COL_NO])) + if len(self.emp_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_EMP_CD_COL_NO])) + if len(self.inst_emp_start_date) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[ + constants.CSV_CHANGE_INST_EMP_START_DATE_COL_NO])) + if len(self.inst_emp_end_date) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[ + constants.CSV_CHANGE_INST_EMP_END_DATE_COL_NO])) + elif self.comment == '終了': + if len(self.inst_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_INST_CD_COL_NO])) + if len(self.ta_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_TA_CD_COL_NO])) + if len(self.inst_emp_start_date) == 0: + error_list.append(self.make_require_error_message( + self.line_num, + constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_INST_EMP_START_DATE_COL_NO])) + if len(self.change_end_date) == 0: + error_list.append(self.make_require_error_message(self.line_num, + constants.CHANGE_INST_CSV_LOGICAL_NAMES[ + constants.CSV_CHANGE_CHANGE_END_DATE_COL_NO])) + elif self.comment == '担当者修正': + if len(self.inst_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_INST_CD_COL_NO])) + if len(self.ta_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_TA_CD_COL_NO])) + if len(self.emp_cd) == 0: + error_list.append(self.make_require_error_message( + self.line_num, constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_EMP_CD_COL_NO])) + if len(self.inst_emp_start_date) == 0: + error_list.append(self.make_require_error_message(self.line_num, + constants.CHANGE_INST_CSV_LOGICAL_NAMES[ + constants.CSV_CHANGE_INST_EMP_START_DATE_COL_NO])) + else: + error_list.append(f'{self.line_num}行目のコメントが不正です。 「追加」「終了」「担当者修正」のいずれかを入力してください。') + return error_list + + def check_inst_cd_exists(self) -> list[str]: + error_list = [] + + if is_not_empty(self.inst_cd) and super().is_exist_inst_cd() is False: + error_list.append(f'{self.line_num}行目の{constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_INST_CD_COL_NO]}\ +は施設マスタに存在しないコードです。') + return error_list + + def check_emp_cd_exists(self) -> list[str]: + error_list = [] + if not self.inst_emp_start_date or not self.emp_cd: + return error_list + + if self.comment != '追加' and self.comment != '担当者修正': + return error_list + + if super().is_exist_emp_cd(self.inst_emp_start_date) is True: + error_list.append(f'{self.line_num}行目の{constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_EMP_CD_COL_NO]}\ +は従業員マスタに存在しない もしくは 適用期間外のIDです。') + return error_list + + def check_bu_cd_exists(self) -> list[str]: + error_list = [] + + if is_not_empty(self.bu_cd) and self.comment == '追加'\ + and super().is_exist_bu_cd() is False: + error_list.append(f'{self.line_num}行目の{constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_BU_CD_COL_NO]}\ +はビジネスユニットマスタに存在しないコードです。') + return error_list + + def check_existing_date(self) -> list[str]: + error_list = [] + start_date = self.inst_emp_start_date + if self.comment == '追加' or self.comment == '終了': + if self.comment == '追加': + end_date = self.inst_emp_end_date + end_date_col_name = constants.CHANGE_INST_CSV_LOGICAL_NAMES[ + constants.CSV_CHANGE_INST_EMP_END_DATE_COL_NO] + compare_error_message = f'\ +{constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_INST_EMP_START_DATE_COL_NO]}が\ +{end_date_col_name}よりも後の日付になっています。' + else: + end_date = self.change_end_date + end_date_col_name = constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_CHANGE_END_DATE_COL_NO] + compare_error_message = f'\ +{constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_INST_EMP_START_DATE_COL_NO]}が\ +{end_date_col_name}よりも後の日付になっています。' + + if not start_date or not end_date: + return error_list + + (error_list, start_date_time, end_date_time) = super().check_term_date( + start_date, + end_date, + constants.CHANGE_INST_CSV_LOGICAL_NAMES[constants.CSV_CHANGE_INST_EMP_START_DATE_COL_NO], + end_date_col_name) + if len(error_list) > 0: + return error_list + + if start_date_time > end_date_time: + error_list.append(f'{self.line_num}行目の{compare_error_message}') + return error_list + + def check_item_count(self) -> list[str]: + return super().check_csv_item_count(len(constants.CHANGE_INST_CSV_LOGICAL_NAMES)) + + def check_data_exists(self) -> list[str]: + error_list = [] + emp_chg_inst_count = super().emp_chg_inst_count(self.inst_emp_start_date) + if self.comment == '追加' and emp_chg_inst_count > 0: + error_list.append(f'{self.line_num}行目の施設コード、領域コード、施設担当_開始日がすべて同一のデータが既に登録されています。') + + elif (self.comment == '終了' or self.comment == '担当者修正') and emp_chg_inst_count == 0: + error_list.append(f'{self.line_num}行目の施設コード、領域コード、施設担当_開始日がすべて同一のデータが存在しないため更新できません。') + + return error_list + + +class MasterMainteCSVItems: + """施設担当者CSVをループで回すためのもの""" + lines: list[MasterMainteCSVItem] + __i: int = 0 + + def to_json(self): + # CSVをjsonに変換 + csv_row_dict_list: list[dict] = self.to_dict() + + # json作成 + return json.dumps(csv_row_dict_list, ensure_ascii=False) + + def to_dict(self): + return [row_item.csv_row_data() for row_item in self.lines] + + def __iter__(self): + return self + + def __next__(self) -> MasterMainteCSVItem: + if self.__i == len(self.lines): + raise StopIteration() + line = self.lines[self.__i] + self.__i += 1 + return line + + def __init__( + self, + file: TextIOWrapper, + select_function: str, + table_name: str, + mst_inst_repository: MstInstRepository, + emp_master_repository: EmpMasterRepository, + bu_master_repository: BuMasterRepository, + emp_chginst_repository: EmpChgInstRepository + ) -> None: + reader = csv.reader(file) + csv_rows = [] + for line_num, row in enumerate(reader, start=0): + if line_num == 0: + continue + csv_rows.append(self.__select_function( + select_function, + row, + table_name, + line_num, + mst_inst_repository, + emp_master_repository, + bu_master_repository, + emp_chginst_repository)) + self.lines = csv_rows + + def __select_function(self, + function_type: str, + row: list[str], + table_name: str, + line_num: int, + mst_inst_repository: MstInstRepository, + emp_master_repository: EmpMasterRepository, + bu_master_repository: BuMasterRepository, + emp_chginst_repository: EmpChgInstRepository) -> MasterMainteCSVItem: + if function_type == 'new': + return MasterMainteNewInstEmpCSVItem( + row, + table_name, + str(line_num), + mst_inst_repository, + emp_master_repository, + bu_master_repository, + emp_chginst_repository) + elif function_type == 'change': + return MasterMainteChangeInstEmpCSVItem( + row, + table_name, + str(line_num), + mst_inst_repository, + emp_master_repository, + bu_master_repository, + emp_chginst_repository) diff --git a/ecs/jskult-webapp/src/model/internal/master_mainte_emp_chg_inst_function.py b/ecs/jskult-webapp/src/model/internal/master_mainte_emp_chg_inst_function.py new file mode 100644 index 00000000..4f4f6165 --- /dev/null +++ b/ecs/jskult-webapp/src/model/internal/master_mainte_emp_chg_inst_function.py @@ -0,0 +1,165 @@ +from abc import ABCMeta, abstractmethod +from src.repositories.emp_chg_inst_repository import EmpChgInstRepository +from src.logging.get_logger import get_logger + +logger = get_logger('マスターメンテ') + + +class MasterMainteEmpChgInstFunction(metaclass=ABCMeta): + insert_data: list[dict] + table_name: str + select_table_message: str + user_name: str + emp_chginst_repository: EmpChgInstRepository + + def __init__( + self, + insert_data, + table_name: str, + select_table_message: str, + user_name: str, + emp_chginst_repository: EmpChgInstRepository + ): + self.insert_data = insert_data + self.table_name = table_name + self.select_table_message = select_table_message + self.user_name = user_name + self.emp_chginst_repository = emp_chginst_repository + + def save(self): + error_list = [] + try: + self.emp_chginst_repository.connect() + self.emp_chginst_repository.begin() + (result_message, error_list) = self.write_emp_chg_inst_table() + if len(error_list) > 0: + self.emp_chginst_repository.rollback() + else: + self.emp_chginst_repository.commit() + except Exception as e: + self.emp_chginst_repository.rollback() + raise e + finally: + self.emp_chginst_repository.disconnect() + + return (result_message, error_list) + + def add_emp_chg_inst_table(self, data, start_date, end_date): + self.emp_chginst_repository.insert_emp_chg_inst( + data['施設コード'], + data['領域コード'], + data['MUID'], + data['ビジネスユニットコード'], + start_date, + end_date, + self.user_name, + self.table_name) + + @abstractmethod + def write_emp_chg_inst_table(self): + pass + + +class NewEmpChgInstFunction(MasterMainteEmpChgInstFunction): + + def __init__( + self, + insert_data_list: list[dict], + table_name: str, + select_table_message: str, + user_name: str, + emp_chginst_repository: EmpChgInstRepository + ): + super().__init__( + insert_data_list, + table_name, + select_table_message, + user_name, + emp_chginst_repository + ) + + def write_emp_chg_inst_table(self): + error_list = [] + + add_count = 0 + for row_no, data in enumerate(self.insert_data, start=1): + try: + self.add_emp_chg_inst_table(data, data['適用開始日'], data['適用終了日']) + add_count += 1 + except Exception as e: + error_list.append(f'{str(row_no)}行目がSQL実行エラーです。CSVファイルを確認してください。') + logger.info(f'新規施設登録時に{row_no}行目でエラーが発生しました: {e}') + + result_message_list = [] + if len(error_list) == 0: + result_message_list.append('新規施設登録を行いました') + result_message_list.append('対象:' + self.select_table_message) + result_message_list.append('追加:' + str(add_count) + '件') + return (result_message_list, error_list) + + +class ChangeEmpChgInstFunction(MasterMainteEmpChgInstFunction): + + def __init__( + self, + insert_data: list[dict], + table_name: str, + select_table_message: str, + user_name: str, + emp_chginst_repository: EmpChgInstRepository + + ): + super().__init__( + insert_data, + table_name, + select_table_message, + user_name, + emp_chginst_repository + ) + + def write_emp_chg_inst_table(self): + add_count = 0 + end_count = 0 + modify_count = 0 + error_list = [] + for row_no, data in enumerate(self.insert_data, start=1): + try: + if data['コメント'] == '追加': + self.add_emp_chg_inst_table(data, data['施設担当_開始日'], data['施設担当_終了日']) + add_count += 1 + elif data['コメント'] == '終了': + self.__end_emp_chg_inst(data) + end_count += 1 + elif data['コメント'] == '担当者修正': + self.__modify_emp_chg_inst(data) + modify_count += 1 + except Exception as e: + error_list.append(f'{str(row_no)}行目がSQL実行エラーです。CSVファイルを確認してください。') + logger.info(f'施設担当者変更時に{row_no}行目でエラーが発生しました: {e}') + + result_message_list = [] + if len(error_list) == 0: + result_message_list.append('施設担当者変更を行いました') + result_message_list.append('対象:' + self.select_table_message) + result_message_list.append('追加:' + str(add_count) + '件') + result_message_list.append('修正:' + str(modify_count) + '件') + result_message_list.append('終了:' + str(end_count) + '件') + return (result_message_list, error_list) + + def __end_emp_chg_inst(self, data: dict): + self.emp_chginst_repository.end_emp_chg_inst( + data['施設コード'], + data['領域コード'], + data['施設担当_開始日'], + data['終了日の変更'], + self.user_name, + self.table_name) + + def __modify_emp_chg_inst(self, data: dict): + self.emp_chginst_repository.modify_emp_chg_inst( + data['施設コード'], + data['領域コード'], + data['施設担当_開始日'], + data['MUID'], + self.user_name, + self.table_name) diff --git a/ecs/jskult-webapp/src/model/request/master_mainte_csvdl.py b/ecs/jskult-webapp/src/model/request/master_mainte_csvdl.py new file mode 100644 index 00000000..4155fa73 --- /dev/null +++ b/ecs/jskult-webapp/src/model/request/master_mainte_csvdl.py @@ -0,0 +1,169 @@ +from typing import Optional + +from fastapi import Form + +from src.util.sanitize import sanitize + +from src.model.request.request_base_model import RequestBaseModel +from src.util.string_util import is_not_empty + + +@sanitize +class MasterMainteCsvDlModel(RequestBaseModel): + ta_cd: Optional[str] + adapt_ta_cd: Optional[str] + inst_cd: Optional[str] + adapt_inst_cd: Optional[str] + emp_cd: Optional[str] + adapt_emp_cd: Optional[str] + apply_date_from: Optional[str] + adapt_apply_date_from: Optional[str] + start_date_from: Optional[str] + adapt_start_date_from: Optional[str] + start_date_to: Optional[str] + adapt_start_date_to: Optional[str] + end_date_from: Optional[str] + adapt_end_date_from: Optional[str] + end_date_to: Optional[str] + adapt_end_date_to: Optional[str] + select_table: Optional[str] + create_date_from: Optional[str] + adapt_create_date_from: Optional[str] + create_date_to: Optional[str] + adapt_create_date_to: Optional[str] + update_date_from: Optional[str] + adapt_update_date_from: Optional[str] + update_date_to: Optional[str] + adapt_update_date_to: Optional[str] + + @classmethod + def as_form( + cls, + ctrl_ta_cd: Optional[str] = Form(None), + ctrl_inst_cd: Optional[str] = Form(None), + ctrl_emp_cd: Optional[str] = Form(None), + ctrl_apply_date_from: Optional[str] = Form(None), + ctrl_start_date_from: Optional[str] = Form(None), + ctrl_start_date_to: Optional[str] = Form(None), + ctrl_end_date_from: Optional[str] = Form(None), + ctrl_end_date_to: Optional[str] = Form(None), + radio_select_table: Optional[str] = Form(None), + ctrl_create_date_from: Optional[str] = Form(None), + ctrl_create_date_to: Optional[str] = Form(None), + ctrl_update_date_from: Optional[str] = Form(None), + ctrl_update_date_to: Optional[str] = Form(None) + ): + return cls.__convert_request_param( + cls, + ctrl_ta_cd, + ctrl_inst_cd, + ctrl_emp_cd, + ctrl_apply_date_from, + ctrl_start_date_from, + ctrl_start_date_to, + ctrl_end_date_from, + ctrl_end_date_to, + radio_select_table, + ctrl_create_date_from, + ctrl_create_date_to, + ctrl_update_date_from, + ctrl_update_date_to + ) + + def __convert_request_param( + cls, + ctrl_ta_cd: str, + ctrl_inst_cd: str, + ctrl_emp_cd: str, + ctrl_apply_date_from: str, + ctrl_start_date_from: str, + ctrl_start_date_to: str, + ctrl_end_date_from: str, + ctrl_end_date_to: str, + radio_select_table: str, + ctrl_create_date_from: str, + ctrl_create_date_to: str, + ctrl_update_date_from: str, + ctrl_update_date_to: str + ): + ctrl_ta_cd = ctrl_ta_cd if is_not_empty(ctrl_ta_cd) else '' + ctrl_inst_cd = ctrl_inst_cd if is_not_empty(ctrl_inst_cd) else '' + ctrl_emp_cd = ctrl_emp_cd if is_not_empty(ctrl_emp_cd) else '' + + adapt_apply_date_from = '' + if is_not_empty(ctrl_apply_date_from): + adapt_apply_date_from = ctrl_apply_date_from.replace('/', '') + else: + ctrl_apply_date_from = '' + + adapt_start_date_from = '' + adapt_start_date_to = '' + if is_not_empty(ctrl_start_date_from): + adapt_start_date_from = ctrl_start_date_from.replace('/', '') + else: + ctrl_start_date_from = '' + if is_not_empty(ctrl_start_date_to): + adapt_start_date_to = ctrl_start_date_to.replace('/', '') + else: + ctrl_start_date_to = '' + + adapt_end_date_from = '' + adapt_end_date_to = '' + if is_not_empty(ctrl_end_date_from): + adapt_end_date_from = ctrl_end_date_from.replace('/', '') + else: + ctrl_end_date_from = '' + if is_not_empty(ctrl_end_date_to): + adapt_end_date_to = ctrl_end_date_to.replace('/', '') + else: + ctrl_end_date_to = '' + + adapt_create_date_from = '' + adapt_create_date_to = '' + if is_not_empty(ctrl_create_date_from): + adapt_create_date_from = ctrl_create_date_from.replace('/', '-') + ' 00:00:00' + else: + ctrl_create_date_from = '' + if is_not_empty(ctrl_create_date_to): + adapt_create_date_to = ctrl_create_date_to.replace('/', '-') + ' 23:59:59' + else: + ctrl_create_date_to = '' + + adapt_update_date_from = '' + adapt_update_date_to = '' + if is_not_empty(ctrl_update_date_from): + adapt_update_date_from = ctrl_update_date_from.replace('/', '-') + ' 00:00:00' + else: + ctrl_update_date_from = '' + if is_not_empty(ctrl_update_date_to): + adapt_update_date_to = ctrl_update_date_to.replace('/', '-') + ' 23:59:59' + else: + ctrl_update_date_to = '' + + return cls( + ta_cd=ctrl_ta_cd, + adapt_ta_cd=ctrl_ta_cd, + inst_cd=ctrl_inst_cd, + adapt_inst_cd=ctrl_inst_cd, + emp_cd=ctrl_emp_cd, + adapt_emp_cd=ctrl_emp_cd, + apply_date_from=ctrl_apply_date_from, + adapt_apply_date_from=adapt_apply_date_from, + start_date_from=ctrl_start_date_from, + adapt_start_date_from=adapt_start_date_from, + start_date_to=ctrl_start_date_to, + adapt_start_date_to=adapt_start_date_to, + select_table=radio_select_table, + end_date_from=ctrl_end_date_from, + adapt_end_date_from=adapt_end_date_from, + end_date_to=ctrl_end_date_to, + adapt_end_date_to=adapt_end_date_to, + create_date_from=ctrl_create_date_from, + adapt_create_date_from=adapt_create_date_from, + create_date_to=ctrl_create_date_to, + adapt_create_date_to=adapt_create_date_to, + update_date_from=ctrl_update_date_from, + adapt_update_date_from=adapt_update_date_from, + update_date_to=ctrl_update_date_to, + adapt_update_date_to=adapt_update_date_to + ) diff --git a/ecs/jskult-webapp/src/model/request/master_mainte_csvup.py b/ecs/jskult-webapp/src/model/request/master_mainte_csvup.py new file mode 100644 index 00000000..2f24bd8b --- /dev/null +++ b/ecs/jskult-webapp/src/model/request/master_mainte_csvup.py @@ -0,0 +1,31 @@ +from typing import Optional, Annotated + +from fastapi import Form + +from src.util.sanitize import sanitize +from fastapi import File, UploadFile + +from src.model.request.request_base_model import RequestBaseModel + + +@sanitize +class MasterMainteCsvUpModel(RequestBaseModel): + csv_file: Optional[Annotated[UploadFile, File()]] + select_function: Optional[str] + select_table: Optional[str] + json_upload_data: Optional[str] + + @classmethod + def as_form( + cls, + ctrl_csv_file: UploadFile = Form(None), + ctrl_select_function: Optional[str] = Form(None), + ctrl_select_table: Optional[str] = Form(None), + ctrl_json_upload_data: Optional[str] = Form(None) + ): + return cls( + csv_file=ctrl_csv_file, + select_function=ctrl_select_function, + select_table=ctrl_select_table, + json_upload_data=ctrl_json_upload_data + ) diff --git a/ecs/jskult-webapp/src/model/view/inst_emp_csv_download_view_model.py b/ecs/jskult-webapp/src/model/view/inst_emp_csv_download_view_model.py index 220294ba..a0bd7f3d 100644 --- a/ecs/jskult-webapp/src/model/view/inst_emp_csv_download_view_model.py +++ b/ecs/jskult-webapp/src/model/view/inst_emp_csv_download_view_model.py @@ -1,5 +1,31 @@ from pydantic import BaseModel +from typing import Optional +from src.util.string_util import is_not_empty class InstEmpCsvDownloadViewModel(BaseModel): subtitle: str = '施設担当者データCSVダウンロード' + is_search: Optional[bool] = False + ta_cd: Optional[str] = '' + inst_cd: Optional[str] = '' + emp_cd: Optional[str] = '' + apply_date_from: Optional[str] = '' + start_date_from: Optional[str] = '' + start_date_to: Optional[str] = '' + end_date_from: Optional[str] = '' + end_date_to: Optional[str] = '' + create_date_from: Optional[str] = '' + create_date_to: Optional[str] = '' + update_date_from: Optional[str] = '' + update_date_to: Optional[str] = '' + select_table: Optional[str] = '' + data_count: Optional[int] = 0 + result_msg: Optional[str] = '' + download_file_url: Optional[str] = '' + file_name: Optional[str] = '' + + def is_select_table_empty(self): + return not is_not_empty(self.select_table) + + def is_download_file_url_empty(self): + return not is_not_empty(self.download_file_url) diff --git a/ecs/jskult-webapp/src/model/view/inst_emp_csv_upload_view_model.py b/ecs/jskult-webapp/src/model/view/inst_emp_csv_upload_view_model.py index 64bde407..b2ab8051 100644 --- a/ecs/jskult-webapp/src/model/view/inst_emp_csv_upload_view_model.py +++ b/ecs/jskult-webapp/src/model/view/inst_emp_csv_upload_view_model.py @@ -1,5 +1,49 @@ +from typing import Optional + from pydantic import BaseModel +from src.system_var import constants class InstEmpCsvUploadViewModel(BaseModel): subtitle: str = '施設担当者データCSVアップロード' + is_verified: Optional[bool] + is_insert: Optional[bool] + error_message_list: Optional[list[str]] + select_function: Optional[str] + select_table: Optional[str] + csv_file_name: Optional[str] + csv_upload_list: Optional[list[dict]] + json_upload_data: Optional[str] + result_message_list: Optional[list[str]] + select_function_message: Optional[str] + + def select_table_message(self): + return self.__dummy_table() if self.select_table == 'dummy' else self.__real_table() + + def upload_data_columns(self) -> list[str]: + return self.__inst_emp_columns() + + def is_select_function_empty(self): + return self.select_function is None or len(self.select_function) == 0 + + def is_select_table_empty(self): + return self.select_table is None or len(self.select_table) == 0 + + def is_error_message_list_empty(self): + return self.error_message_list is None or len(self.error_message_list) == 0 + + def csv_data_count(self): + return 0 if self.csv_upload_list is None else len(self.csv_upload_list) + + def __inst_emp_columns(self) -> list[str]: + if self.select_function == 'new': + return constants.NEW_INST_EMP_CSV_LOGICAL_NAMES + if self.select_function == 'change': + return constants.CHANGE_INST_CSV_LOGICAL_NAMES + return [] + + def __real_table(self): + return constants.CSV_REAL_TABLE_NAME + + def __dummy_table(self): + return constants.CSV_CHANGE_TABLE_NAME diff --git a/ecs/jskult-webapp/src/model/view/table_override_view_model.py b/ecs/jskult-webapp/src/model/view/table_override_view_model.py index e03b1fd0..d97a1928 100644 --- a/ecs/jskult-webapp/src/model/view/table_override_view_model.py +++ b/ecs/jskult-webapp/src/model/view/table_override_view_model.py @@ -1,5 +1,8 @@ from pydantic import BaseModel +from typing import Optional class TableOverrideViewModel(BaseModel): subtitle: str = 'テーブル上書きコピー' + + is_override: Optional[bool] = False diff --git a/ecs/jskult-webapp/src/repositories/bu_master_cd_repository.py b/ecs/jskult-webapp/src/repositories/bu_master_cd_repository.py new file mode 100644 index 00000000..36992192 --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/bu_master_cd_repository.py @@ -0,0 +1,32 @@ +from src.repositories.base_repository import BaseRepository +from src.model.db.master_mente_count import MasterMenteCountModel +from src.logging.get_logger import get_logger + +logger = get_logger('ビジネスユニットマスタ') + + +class BuMasterRepository(BaseRepository): + + FETCH_COUNT_SQL = """\ + SELECT + COUNT(*) AS count + FROM + src05.bu + WHERE + bu.bu_cd = :bu_cd + """ + + def fetch_count(self, bu_cd) -> MasterMenteCountModel: + try: + self._database.connect() + query = self.FETCH_COUNT_SQL + result = self._database.execute_select(query, {'bu_cd': bu_cd}) + models = [MasterMenteCountModel(**r) for r in result] + if len(models) == 0: + return 0 + return models[0].count + except Exception as e: + logger.exception(f"DB Error : Exception={e.args}") + raise e + finally: + self._database.disconnect() diff --git a/ecs/jskult-webapp/src/repositories/emp_chg_inst_repository.py b/ecs/jskult-webapp/src/repositories/emp_chg_inst_repository.py new file mode 100644 index 00000000..404d7bca --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/emp_chg_inst_repository.py @@ -0,0 +1,300 @@ +from src.repositories.base_repository import BaseRepository +from src.db.sql_condition import SQLCondition +from src.db import sql_condition as condition +from src.model.db.master_mente_count import MasterMenteCountModel +from src.model.request.master_mainte_csvdl import MasterMainteCsvDlModel +from src.util.string_util import is_not_empty +from src.logging.get_logger import get_logger + +logger = get_logger('従業員担当施設マスタ') + + +class EmpChgInstRepository(BaseRepository): + + def connect(self): + self._database.connect() + + def begin(self): + self._database.begin() + + def commit(self): + self._database.commit() + + def rollback(self): + self._database.rollback() + + def disconnect(self): + self._database.disconnect() + + INSERT_SQL = """\ + INSERT INTO {table_name} + ( + inst_cd, + ta_cd, + emp_cd, + bu_cd, + start_date, + end_date, + main_chg_flg, + enabled_flg, + creater, + create_date, + updater, + update_date + ) + VALUES ( + :inst_cd, + :ta_cd, + :emp_cd, + :bu_cd, + :start_date, + :end_date, + '1', + 'Y', + :create_user_name, + NOW(), + :update_user_name, + NOW() + ) + """ + + def insert_emp_chg_inst(self, inst_cd, ta_cd, emp_cd, bu_cd, start_date, + end_date, create_user_name, table_name): + try: + query = self.INSERT_SQL.format(table_name=table_name) + self._database.execute(query, { + 'inst_cd': inst_cd, + 'ta_cd': ta_cd, + 'emp_cd': emp_cd, + 'bu_cd': bu_cd, + 'start_date': start_date, + 'end_date': end_date, + 'create_user_name': create_user_name, + 'update_user_name': create_user_name + }) + except Exception as e: + logger.exception(f'DB Error : Exception={e.args}') + raise e + + UPDATE_END_DATE_SQL = """\ + UPDATE + {table_name} + SET + end_date = :end_date, + updater = :update_user_name, + update_date = NOW() + WHERE + inst_cd = :inst_cd + and ta_cd = :ta_cd + and start_date = :start_date + """ + + def end_emp_chg_inst(self, inst_cd, ta_cd, start_date, + end_date, update_user_name, table_name): + try: + query = self.UPDATE_END_DATE_SQL.format(table_name=table_name) + self._database.execute(query, { + 'inst_cd': inst_cd, + 'ta_cd': ta_cd, + 'start_date': start_date, + 'end_date': end_date, + 'update_user_name': update_user_name + }) + except Exception as e: + logger.exception(f'DB Error : Exception={e.args}') + raise e + + UPDATE_EMP_CD_SQL = """\ + UPDATE + {table_name} + SET + emp_cd = :emp_cd, + updater = :update_user_name, + update_date = now() + where + inst_cd = :inst_cd + and ta_cd = :ta_cd + and start_date = :start_date + """ + + def modify_emp_chg_inst(self, inst_cd, ta_cd, start_date, emp_cd, update_user_name, table_name): + try: + query = self.UPDATE_EMP_CD_SQL.format(table_name=table_name) + self._database.execute(query, { + 'inst_cd': inst_cd, + 'ta_cd': ta_cd, + 'start_date': start_date, + 'emp_cd': emp_cd, + 'update_user_name': update_user_name + }) + except Exception as e: + logger.exception(f'DB Error : Exception={e.args}') + raise e + + FETCH_COUNT_SQL = """\ + SELECT + COUNT(*) AS count + FROM + {table_name} + WHERE + inst_cd = :inst_cd + AND ta_cd = :ta_cd + AND start_date = :start_date + """ + + def fetch_count(self, inst_cd, ta_cd, start_date, table_name) -> MasterMenteCountModel: + try: + self._database.connect() + query = self.FETCH_COUNT_SQL.format(table_name=table_name) + result = self._database.execute_select(query, {'inst_cd': inst_cd, 'ta_cd': ta_cd, + 'start_date': start_date}) + models = [MasterMenteCountModel(**r) for r in result] + if len(models) == 0: + return 0 + return models[0].count + except Exception as e: + logger.exception(f'DB Error : Exception={e.args}') + raise e + finally: + self._database.disconnect() + + FETCH_SQL = """\ + SELECT DISTINCT + eci.inst_cd AS inst_cd, + mi.inst_name AS inst_name, + eci.ta_cd AS ta_cd, + eci.emp_cd AS emp_cd, + CONCAT(emp.emp_name_family, " ", emp.emp_name_first) AS emp_name_full, + eci.bu_cd AS bu_cd, + bu.bu_name AS bu_name, + eci.start_date AS start_date, + eci.end_date AS end_date, + eci.creater AS creater, + eci.create_date AS create_date, + eci.updater AS updater, + eci.update_date AS update_date + FROM + {table_name} AS eci + LEFT JOIN mst_inst AS mi + ON eci.inst_cd = mi.inst_cd + LEFT JOIN emp + ON eci.emp_cd = emp.emp_cd + LEFT JOIN bu + ON eci.bu_cd = bu.bu_cd + WHERE + {where_clause} + """ + + def fetch_as_data_frame(self, table_name: str, parameter: MasterMainteCsvDlModel): + try: + self._database.connect() + where_clause = self.__build_condition(parameter) + query = self.FETCH_SQL.format(table_name=table_name, where_clause=where_clause) + logger.debug(f'SQL: {query}') + df = self._to_data_frame(query, parameter) + logger.debug(f'count= {df.shape[0]}') + return df + except Exception as e: + logger.exception(f'DB Error : Exception={e.args}') + raise e + finally: + self._database.disconnect() + + def __build_condition(self, parameter: MasterMainteCsvDlModel): + where_clauses: list[SQLCondition] = [] + + # 領域コードが入力されていた場合 + if is_not_empty(parameter.ta_cd): + parameter.adapt_ta_cd = f'%{parameter.ta_cd}%' + where_clauses.append(SQLCondition('eci.ta_cd', condition.LIKE, 'adapt_ta_cd')) + + # 施設コードが入力されていた場合 + if is_not_empty(parameter.inst_cd): + parameter.adapt_inst_cd = f'%{parameter.inst_cd}%' + where_clauses.append(SQLCondition('eci.inst_cd', condition.LIKE, 'adapt_inst_cd')) + + # MUIDが入力されていた場合 + if is_not_empty(parameter.emp_cd): + parameter.adapt_emp_cd = f'%{parameter.emp_cd}%' + where_clauses.append(SQLCondition('eci.emp_cd', condition.LIKE, 'adapt_emp_cd')) + + # 適用期間内が入力されていた場合 + if is_not_empty(parameter.adapt_apply_date_from): + where_clauses.append(SQLCondition('eci.start_date', + condition.LE, + 'adapt_apply_date_from')) + where_clauses.append(SQLCondition('eci.end_date', + condition.GE, + 'adapt_apply_date_from')) + + # 適用開始日(FROM)が入力されていた場合 + if is_not_empty(parameter.adapt_start_date_from): + where_clauses.append(SQLCondition('eci.start_date', + condition.GE, + 'adapt_start_date_from')) + + # 適用開始日(TO)が入力されていた場合 + if is_not_empty(parameter.adapt_start_date_to): + where_clauses.append(SQLCondition('eci.start_date', + condition.LE, + 'adapt_start_date_to')) + + # 適用終了日(FROM)が入力されていた場合 + if is_not_empty(parameter.adapt_end_date_from): + where_clauses.append(SQLCondition('eci.end_date', + condition.GE, + 'adapt_end_date_from')) + + # 適用終了日(TO)が入力されていた場合 + if is_not_empty(parameter.adapt_end_date_to): + where_clauses.append(SQLCondition('eci.end_date', + condition.LE, + 'adapt_end_date_to')) + + # データ作成日(FROM)が入力されていた場合 + if is_not_empty(parameter.adapt_create_date_from): + where_clauses.append(SQLCondition('eci.create_date', + condition.GE, + 'adapt_create_date_from')) + + # データ作成日(TO)が入力されていた場合 + if is_not_empty(parameter.adapt_create_date_to): + where_clauses.append(SQLCondition('eci.create_date', + condition.LE, + 'adapt_create_date_to')) + + # データ更新日(FROM)が入力されていた場合 + if is_not_empty(parameter.adapt_update_date_from): + where_clauses.append(SQLCondition('eci.update_date', + condition.GE, + 'adapt_update_date_from')) + + # データ更新日(TO)が入力されていた場合 + if is_not_empty(parameter.adapt_update_date_to): + where_clauses.append(SQLCondition('eci.update_date', + condition.LE, + 'adapt_update_date_to')) + + where_clauses_str = ' AND '.join([condition.apply() for condition in where_clauses]) + logger.debug(f'条件設定終了:{where_clauses_str}') + return where_clauses_str + + DELETE_SQL = "DELETE FROM emp_chg_inst_wrk" + + def delete_dummy_table(self): + try: + query = self.DELETE_SQL + self._database.execute(query) + except Exception as e: + logger.exception(f'DB Error : Exception={e.args}') + raise e + + COPY_TABLE_SQL = "INSERT INTO emp_chg_inst_wrk SELECT * FROM emp_chg_inst" + + def copy_real_to_dummy(self): + try: + query = self.COPY_TABLE_SQL + self._database.execute(query) + except Exception as e: + logger.exception(f'DB Error : Exception={e.args}') + raise e diff --git a/ecs/jskult-webapp/src/repositories/emp_master_repository.py b/ecs/jskult-webapp/src/repositories/emp_master_repository.py new file mode 100644 index 00000000..8c278177 --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/emp_master_repository.py @@ -0,0 +1,34 @@ +from src.repositories.base_repository import BaseRepository +from src.model.db.master_mente_count import MasterMenteCountModel +from src.logging.get_logger import get_logger + +logger = get_logger('従業員マスタ') + + +class EmpMasterRepository(BaseRepository): + + FETCH_COUNT_SQL = """\ + SELECT + COUNT(*) AS count + FROM + src05.emp + WHERE + emp.emp_cd = :emp_cd + AND str_to_date(emp.start_date, '%Y%m%d') <= str_to_date(:start_work_date, '%Y%m%d') + AND str_to_date(:start_work_date, '%Y%m%d') <= str_to_date(emp.end_date ,'%Y%m%d') + """ + + def fetch_count(self, emp_cd, start_work_date) -> MasterMenteCountModel: + try: + self._database.connect() + query = self.FETCH_COUNT_SQL + result = self._database.execute_select(query, {'emp_cd': emp_cd, 'start_work_date': start_work_date}) + models = [MasterMenteCountModel(**r) for r in result] + if len(models) == 0: + return 0 + return models[0].count + except Exception as e: + logger.exception(f"DB Error : Exception={e.args}") + raise e + finally: + self._database.disconnect() diff --git a/ecs/jskult-webapp/src/repositories/mst_inst_repository.py b/ecs/jskult-webapp/src/repositories/mst_inst_repository.py new file mode 100644 index 00000000..9c92599c --- /dev/null +++ b/ecs/jskult-webapp/src/repositories/mst_inst_repository.py @@ -0,0 +1,32 @@ +from src.repositories.base_repository import BaseRepository +from src.model.db.master_mente_count import MasterMenteCountModel +from src.logging.get_logger import get_logger + +logger = get_logger('メルク施設マスタ') + + +class MstInstRepository(BaseRepository): + + FETCH_COUNT_SQL = """\ + SELECT + COUNT(*) AS count + FROM + src05.mst_inst + WHERE + mst_inst.inst_cd = :inst_cd + """ + + def fetch_count(self, inst_cd) -> MasterMenteCountModel: + try: + self._database.connect() + query = self.FETCH_COUNT_SQL + result = self._database.execute_select(query, {'inst_cd': inst_cd}) + models = [MasterMenteCountModel(**r) for r in result] + if len(models) == 0: + return 0 + return models[0].count + except Exception as e: + logger.exception(f"DB Error : Exception={e.args}") + raise e + finally: + self._database.disconnect() diff --git a/ecs/jskult-webapp/src/services/master_mainte_service.py b/ecs/jskult-webapp/src/services/master_mainte_service.py new file mode 100644 index 00000000..e9620eda --- /dev/null +++ b/ecs/jskult-webapp/src/services/master_mainte_service.py @@ -0,0 +1,262 @@ +import os +import json +import html +import csv + +import pandas as pd + +from fastapi import HTTPException +from io import TextIOWrapper +from src.aws.aws_api_client import AWSAPIClient +from src.aws.s3 import S3Client +from src.error.exceptions import DBException +from starlette import status +from datetime import datetime +from src.services.base_service import BaseService +from src.system_var import constants, environment +from src.repositories.base_repository import BaseRepository +from src.repositories.mst_inst_repository import MstInstRepository +from src.repositories.bu_master_cd_repository import BuMasterRepository +from src.repositories.emp_master_repository import EmpMasterRepository +from src.repositories.emp_chg_inst_repository import EmpChgInstRepository +from src.model.internal.master_mainte_csv import MasterMainteCSVItems +from src.model.internal.master_mainte_emp_chg_inst_function import NewEmpChgInstFunction +from src.model.internal.master_mainte_emp_chg_inst_function import ChangeEmpChgInstFunction +from src.model.view.inst_emp_csv_upload_view_model import InstEmpCsvUploadViewModel +from src.model.view.table_override_view_model import TableOverrideViewModel +from src.model.request.master_mainte_csvup import MasterMainteCsvUpModel +from src.model.request.master_mainte_csvdl import MasterMainteCsvDlModel +from src.logging.get_logger import get_logger + + +logger = get_logger('マスターメンテ') + + +class MasterMainteService(BaseService): + REPOSITORIES = { + 'mst_inst_repository': MstInstRepository, + 'emp_master_repository': EmpMasterRepository, + 'bu_master_repository': BuMasterRepository, + 'emp_chginst_repository': EmpChgInstRepository, + } + + CLIENTS = { + 's3_client': S3Client + } + + mst_inst_repository: MstInstRepository + emp_master_repository: EmpMasterRepository + bu_master_repository: BuMasterRepository + emp_chginst_repository: EmpChgInstRepository + s3_client: S3Client + + def __init__(self, repositories: dict[str, BaseRepository], clients: dict[str, AWSAPIClient]) -> None: + super().__init__(repositories, clients) + self.mst_inst_repository = repositories['mst_inst_repository'] + self.emp_master_repository = repositories['emp_master_repository'] + self.bu_master_repository = repositories['bu_master_repository'] + self.emp_chginst_repository = repositories['emp_chginst_repository'] + self.s3_client = clients['s3_client'] + + def prepare_mainte_csv_up_view(self, + file: TextIOWrapper, + csv_file_name: str, + csv_upload_form: MasterMainteCsvUpModel) -> InstEmpCsvUploadViewModel: + + if csv_upload_form.select_function != 'new' and csv_upload_form.select_function != 'change': + raise Exception(f'機能の選択値が不正です: {csv_upload_form.select_function}') + if csv_upload_form.select_table != 'dummy' and csv_upload_form.select_table != 'real': + raise Exception(f'登録テーブルの選択値が不正です: {csv_upload_form.select_table}') + + (table_name, selected_table_msg) = self.__choose_target_table(csv_upload_form.select_table) + + csv_items = MasterMainteCSVItems( + file, + csv_upload_form.select_function, + table_name, + self.mst_inst_repository, + self.emp_master_repository, + self.bu_master_repository, + self.emp_chginst_repository + ) + + error_message_list = [] + # CSVファイル0件(ヘッダ行のみ)チェック + if len(csv_items.lines) == 0: + error_message_list.append('選択されたCSVファイルの2行目以降に値が記入されておりません。') + else: + for row_item in csv_items: + error_message_list.extend([data for data in row_item.validate()]) + + csv_upload_list = [] + json_upload_data = '' + if len(error_message_list) == 0: + csv_upload_list: list[dict] = csv_items.to_dict() + # json作成 + json_upload_data = csv_items.to_json() + + mainte_csv_up = InstEmpCsvUploadViewModel( + is_verified=True, + error_message_list=error_message_list, + select_function=csv_upload_form.select_function, + select_table=csv_upload_form.select_table, + csv_upload_list=csv_upload_list, + json_upload_data=json_upload_data, + csv_file_name=csv_file_name, + select_function_message=self.__make_dialog_confirm_message( + csv_upload_form.select_function, + selected_table_msg) + ) + return mainte_csv_up + + def prepare_mainte_new_inst_view(self, + user_name: str, + csv_upload_form: MasterMainteCsvUpModel) -> InstEmpCsvUploadViewModel: + + (table_name, selected_table_msg) = self.__choose_target_table(csv_upload_form.select_table) + + csv_data_list = json.loads(html.unescape(csv_upload_form.unescape().json_upload_data)) + + if csv_upload_form.select_function == 'new': + emp_chg_inst = NewEmpChgInstFunction( + csv_data_list, + table_name, + selected_table_msg, + user_name, + self.emp_chginst_repository) + elif csv_upload_form.select_function == 'change': + emp_chg_inst = ChangeEmpChgInstFunction( + csv_data_list, + table_name, + selected_table_msg, + user_name, + self.emp_chginst_repository) + else: + raise Exception(f'機能の選択値が不正です: {csv_upload_form.select_function}') + + (result_message_list, raw_error_list) = emp_chg_inst.save() + + error_message_list = [] + error_message_list.extend(raw_error_list) + + mainte_csv_up = InstEmpCsvUploadViewModel( + is_insert=True, + result_message_list=result_message_list, + error_message_list=error_message_list + ) + return mainte_csv_up + + def copy_data_real_to_dummy(self) -> TableOverrideViewModel: + try: + self.emp_chginst_repository.connect() + self.emp_chginst_repository.begin() + self.emp_chginst_repository.delete_dummy_table() + self.emp_chginst_repository.copy_real_to_dummy() + self.emp_chginst_repository.commit() + except Exception as e: + self.emp_chginst_repository.rollback() + raise e + finally: + self.emp_chginst_repository.disconnect() + + # コピー完了をマークして画面に返却 + table_override = TableOverrideViewModel( + is_override=True + ) + return table_override + + def search_emp_chg_inst_data(self, csv_download_form: MasterMainteCsvDlModel) -> pd.DataFrame: + try: + csv_download_form.unescape() + # 施設担当者データを検索 + search_result_df = self.search_download_emp_chg_inst_data(csv_download_form) + except DBException as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={'error': 'db_error', 'message': e.args} + ) + + return search_result_df + + def search_download_emp_chg_inst_data(self, csv_download_form: MasterMainteCsvDlModel): + (table_name, _) = self.__choose_target_table(csv_download_form.select_table) + search_result_df = self.emp_chginst_repository.fetch_as_data_frame(table_name, csv_download_form) + return search_result_df + + def write_csv_file(self, data_frame: pd.DataFrame, header: list[str], download_file_name: str): + # csvに書き込み + output_file_path = os.path.join(constants.MENTE_CSV_TEMPORARY_FILE_DIR_PATH, download_file_name) + # 横長のDataFrameとするため、ヘッダーの加工処理 + header_data = {} + for df_column, header_column in zip(data_frame.columns, header): + header_data[df_column] = header_column + + header_df = pd.DataFrame([header_data], index=None) + output_df = pd.concat([header_df, data_frame]) + # ヘッダー行としてではなく、1レコードとして出力する + output_df.to_csv(output_file_path, encoding="utf-8_sig", quoting=csv.QUOTE_ALL, index=False, header=False) + + return output_file_path + + def upload_emp_chg_inst_data_file(self, df: pd.DataFrame, user_id: str, select_table: str) -> tuple[str, str]: + if df.shape[0] == 0: + return '該当データが存在しないためCSVファイルを出力しませんでした', '' + + # ファイル名に使用するタイムスタンプを初期化しておく + current_timestamp = datetime.now() + download_file_name = f'Result_{user_id}_{current_timestamp:%Y%m%d%H%M%S%f}.csv' + + # ファイルを書き出し(CSV) + local_file_path = self.__write_emp_chg_inst_data_to_file(df, download_file_name) + + # ローカルファイルからS3にアップロードし、ダウンロード用URLを取得する + download_file_url = '' + try: + bucket_name = environment.MASTER_MAINTENANCE_BUCKET + file_key = f'data/{os.path.basename(local_file_path)}' + self.s3_client.upload_file(local_file_path, bucket_name, file_key) + # アップロード後、ローカルからは削除する + self.delete_local_file(local_file_path) + download_file_url = self.generate_download_file_url(local_file_path) + except Exception as e: + logger.exception(f'S3 アクセスエラー{e}') + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={'error': 'aws_error', 'message': e.args} + ) + if select_table == 'dummy': + result_msg = f'ダミーテーブルのデータ{df.shape[0]}件をCSVファイルに出力しました' + else: + result_msg = f'本番テーブルのデータ{df.shape[0]}件をCSVファイルに出力しました' + + return result_msg, download_file_url + + def generate_download_file_url(self, local_file_path: str) -> str: + bucket_name = environment.MASTER_MAINTENANCE_BUCKET + file_key = f'data/{os.path.basename(local_file_path)}' + return self.s3_client.generate_presigned_url(bucket_name, file_key, constants.MENTE_CSV_DOWNLOAD_FILE_NAME) + + def __write_emp_chg_inst_data_to_file(self, df: pd.DataFrame, download_file_name: str) -> str: + logger.info('CSVファイルを出力する') + local_file_path = self.write_csv_file( + df, header=constants.MENTE_CSV_DOWNLOAD_HEADER, download_file_name=download_file_name) + + return local_file_path + + def __choose_target_table(self, select_table: str): + if select_table == 'dummy': + table_name = 'src05.emp_chg_inst_wrk' + selected_table_msg = constants.CSV_CHANGE_TABLE_NAME + elif select_table == 'real': + table_name = 'src05.emp_chg_inst' + selected_table_msg = constants.CSV_REAL_TABLE_NAME + else: + raise Exception(f'登録テーブルの選択値が不正です: {select_table}') + return (table_name, selected_table_msg) + + def __make_dialog_confirm_message(self, select_function: str, selected_table_msg: str) -> str: + select_function_msg = '新規施設登録' if select_function == 'new' else '施設担当者変更' + return f'{selected_table_msg}に{select_function_msg}を行いますか?' + + def delete_local_file(self, local_file_path: str): + os.remove(local_file_path) diff --git a/ecs/jskult-webapp/src/static/css/masterMainte.css b/ecs/jskult-webapp/src/static/css/masterMainte.css index 3c15e03d..c6d7be7d 100644 --- a/ecs/jskult-webapp/src/static/css/masterMainte.css +++ b/ecs/jskult-webapp/src/static/css/masterMainte.css @@ -1,3 +1,9 @@ +/* Bootstrap 5.10以降、box-sizingのデフォルト値によってテーブルがずれるため、このページ限定的にリセット */ +/* @see https://bootstrap-guide.com/content/reboot#page-defaults */ +table { + box-sizing: initial; +} + body{ background-color: LightCyan; font-family : "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", "メイリオ", Meiryo, Osaka, "MS Pゴシック", "MS PGothic", sans-serif; @@ -5,6 +11,7 @@ body{ h1{ margin-left : 1%; + font-weight: 700; } @@ -23,6 +30,10 @@ h1{ width: 20%; } +.header_buttonSize{ + width: 7rem; + font-size: initial; +} .buttonSize{ width: 85px; } @@ -45,6 +56,7 @@ h1{ padding-bottom: 1%; border-bottom: solid 1px gray; width: 94%; + border-collapse: initial; } .searchLabelTd{ @@ -88,7 +100,7 @@ h1{ } /*//////////////////////////*/ -/*施設担当者データExcelアップロード*/ +/*施設担当者データCSVアップロード*/ /*//////////////////////////*/ .inputTable{ margin-left: 3%; @@ -97,6 +109,7 @@ h1{ padding-bottom: 1%; border-bottom: solid 1px gray; width: 94%; + border-collapse: initial; } .inputLabelTd{ @@ -160,5 +173,6 @@ table.inputData tbody td { margin-bottom: 2%; border-bottom: solid 1px gray; width: 94%; + border-collapse: initial; } diff --git a/ecs/jskult-webapp/src/static/function/businessLogicScript.js b/ecs/jskult-webapp/src/static/function/businessLogicScript.js index 7ce3b9ba..9983067d 100644 --- a/ecs/jskult-webapp/src/static/function/businessLogicScript.js +++ b/ecs/jskult-webapp/src/static/function/businessLogicScript.js @@ -84,51 +84,54 @@ function selectDropDowList(id, selectedName){ function enableDatePicker() { // カレンダーの表示を日曜日始まりに変更 flatpickr.l10ns.ja.firstDayOfWeek = 0; - $('.date_picker').flatpickr( - { + + $(".date_picker").each(function(i, elem) { + const date_picker_name = elem.name; + + flatpickr(elem, { locale: 'ja', // 日本語カレンダー allowInput: true, // 入力可能にする - dateFormat: 'Y/m/d' // 日付のフォーマットを修正 - } - ) + dateFormat: "YYYY/MM/DD", // 日付のフォーマット + onChange(_dates, currentDateString, _picker, _data) { + }, + parseDate: function(strFormat, format) { + + // yyyyMMddの場合→yyyy/MM/dd + const datePatternMatches = strFormat.match(/^(\d{4})(\d{2})(\d{2})$/); + if (datePatternMatches){ + strFormat = `${datePatternMatches[1]}/${datePatternMatches[2]}/${datePatternMatches[3]}`; + } + + // yyyy/00/00~yyyy/00/00の場合→yyyy/01/01~yyyy/12/31 + // yyyy/MM/00~yyyy/MM/01の場合→yyyy/MM/01~yyyy/MM/末日 + // 開始日の場合 + if (date_picker_name.includes('from')){ + strFormat = strFormat.replace("/00/00", "/01/01"); + strFormat = strFormat.replace("/00", "/01"); + } + // 終了日の場合 + else if (date_picker_name.includes('to')){ + strFormat = strFormat.replace("/00/00", "/12/31"); + const date = new Date(strFormat.slice(0, 4), strFormat.slice(5, 7), 0).getDate(); + strFormat = strFormat.replace("/00", "/"+date.toString()); + } + return new Date(strFormat); + }, + formatDate: (date, format) => { + // 日付の整合性チェック、不正の場合は空表示 + if(isNaN(date.getDate())){ + return; + } + + // フォーマットを設定 + const formatted = flatpickr.formatDate(date,'Y/m/d'); + return formatted; + + } + }) + }); } -// 日付入力チェック -// 引数:チェックするテキストボックスNo -function autoModifyDate($this){ - // 日付フォーマットチェック - - if($this.value === "" || - (!$this.value.match(/^\d{4}\/\d{2}\/\d{2}$/) && !$this.value.match(/^\d{4}\d{2}\d{2}$/))) - { - $this.value = ""; - return; - } - - /** @type { string }*/ - let strFormat = $this.value;; - // yyyyMMddの場合→yyyy/MM/dd - const datePatternMatches = strFormat.match(/^(\d{4})(\d{2})(\d{2})$/); - if (datePatternMatches){ - strFormat = `${datePatternMatches[1]}/${datePatternMatches[2]}/${datePatternMatches[3]}`; - } - // yyyy/00/00~yyyy/00/00の場合→yyyy/01/01~yyyy/12/31 - // yyyy/MM/00~yyyy/MM/01の場合→yyyy/MM/01~yyyy/MM/末日 - // 開始日の場合 - if ($this.name.includes('from')){ - strFormat = strFormat.replace("00/00", "01/01"); - strFormat = strFormat.replace("00", "01"); - } - // 終了日の場合 - else if ($this.name.includes('to')){ - strFormat = strFormat.replace("00/00", "12/31"); - const date = new Date(strFormat.slice(0, 4), strFormat.slice(5, 7), 0).getDate(); - strFormat = strFormat.replace("00", date.toString()); - } - $this.value = strFormat; -} - - // 前のスペースを許さない入力チェック function checkSpaceForm($this) { @@ -179,3 +182,66 @@ function allOff(){ $(".selected").prop("checked", false); $(".send").prop('disabled',true); } + +// 検索結果のところのボタンをチェックが1個でも付いたら押せるようにして、チェックがなければ押せないようにする関数 +// 条件:チェックボックスのクラス名に"selected"というのがついていること +// 条件:ボタンにクラス名 send がついていること +function resultBtDisablead(){ + var cnt1 = $('.checkNum input:checkbox:checked').length; + console.log(cnt1); + if(cnt1 == 0) { + $(".send").prop('disabled',true); + } + else { + $(".send").prop('disabled',false); + } +} + +// 数字-以外を許さない入力チェック +function checkNumberForm($this) +{ + var str=$this.value; + while(str.match(/[^\d\-]/)) + { + str=str.replace(/[^\d\-]/,""); + } + $this.value=str; +} + +// 数字以外を許さない入力チェック +function checkNumberOnlyForm($this) +{ + var str=$this.value; + while(str.match(/[^\d]/)) + { + str=str.replace(/[^\d]/,""); + } + $this.value=str; +} + +// メニューへボタンの関数 +// 機能概要:マスターメンテメニュー画面に遷移する +function backToMainteMenu(){ + sessionStorage.clear(); + location.href = "/masterMainte/masterMainteMenu"; +} + +// 確認ダイアログ +function confirmDialog(strMesssage) { + var result = confirm(strMesssage); + return result; +} + +function formInsertBtDisabled(){ + var validFlg = false; + if(document.getElementById("csvFile").value === ""){ + validFlg = true; + } + + if (validFlg == true) { + document.getElementById("confirm").disabled = true; + } + else { + document.getElementById("confirm").disabled = false; + } +} diff --git a/ecs/jskult-webapp/src/system_var/constants.py b/ecs/jskult-webapp/src/system_var/constants.py index 064d135b..a50d498e 100644 --- a/ecs/jskult-webapp/src/system_var/constants.py +++ b/ecs/jskult-webapp/src/system_var/constants.py @@ -132,3 +132,123 @@ LOGOUT_REASON_MESSAGE_MAP = { LOGOUT_REASON_DB_ERROR: 'DB接続に失敗しました。
再度Loginするか、
管理者にお問い合わせください。', LOGOUT_REASON_UNEXPECTED: '予期しないエラーが発生しました。
再度Loginするか、
管理者に問い合わせてください。' } + +# 新規施設担当者登録CSV(マスターメンテ) +NEW_INST_EMP_CSV_LOGICAL_NAMES = [ + '施設コード', + '施設名', + '領域コード', + 'MUID', + '担当者名(姓)', + '担当者名(名)', + 'ビジネスユニットコード', + '適用開始日', + '適用終了日' +] +# 施設コードの列No +CSV_NEW_INST_CD_COL_NO = 0 +# 施設名の列No +CSV_NEW_INST_NAME_COL_NO = 1 +# 領域コードの列No +CSV_NEW_TA_CD_COL_NO = 2 +# MUIDの列No +CSV_NEW_EMP_CD_COL_NO = 3 +# 担当者名(姓)の列No +CSV_NEW_EMP_NAME_FAMILY_COL_NO = 4 +# 担当者名(名)の列No +CSV_NEW_EMP_NAME_FIRST_COL_NO = 5 +# ビジネスユニットコードの列No +CSV_NEW_BU_CD_COL_NO = 6 +# 適用開始日の列No +CSV_NEW_START_DATE = 7 +# 適用終了日の列No +CSV_NEW_END_DATE = 8 + +# 施設担当者変更登録CSV(マスターメンテ) +CHANGE_INST_CSV_LOGICAL_NAMES = [ + 'ビジネスユニットコード', + 'ビジネスユニット名', + '組織コード', + '組織名略称', + '施設コード', + '施設名', + '領域コード', + '説明', + 'MUID', + '担当者名', + '施設担当_開始日', + '施設担当_終了日', + '終了日の変更', + 'コメント' +] +# ビジネスユニットコードの列No +CSV_CHANGE_BU_CD_COL_NO = 0 +# ビジネスユニット名の列No +CSV_CHANGE_BU_NAME_COL_NO = 1 +# 組織コードの列No +CSV_CHANGE_ORG_CD_COL_NO = 2 +# 組織名略称の列No +CSV_CHANGE_ORG_SHORT_NAME_COL_NO = 3 +# 施設コードの列No +CSV_CHANGE_INST_CD_COL_NO = 4 +# 施設名の列No +CSV_CHANGE_INST_NAME_COL_NO = 5 +# 領域コードの列No +CSV_CHANGE_TA_CD_COL_NO = 6 +# 説明の列No +CSV_CHANGE_EXPLAIN_COL_NO = 7 +# MUIDの列No +CSV_CHANGE_EMP_CD_COL_NO = 8 +# 担当者名の列No +CSV_CHANGE_EMP_FULL_NAME_COL_NO = 9 +# 施設担当_開始日の列No +CSV_CHANGE_INST_EMP_START_DATE_COL_NO = 10 +# 施設担当_終了日の列No +CSV_CHANGE_INST_EMP_END_DATE_COL_NO = 11 +# 終了日の変更の列No +CSV_CHANGE_CHANGE_END_DATE_COL_NO = 12 +# コメントの列No +CSV_CHANGE_COMMENT = 13 + +# CSVアップロードテーブル名(マスターメンテ) +CSV_REAL_TABLE_NAME = '本番テーブル' +CSV_CHANGE_TABLE_NAME = 'ダミーテーブル' + +MENTE_CSV_TEMPORARY_FILE_DIR_PATH = path.join(path.curdir, 'src', 'data') + +MENTE_CSV_DOWNLOAD_EXTRACT_COLUMNS = [ + 'inst_cd', + 'inst_name', + 'ta_cd', + 'emp_cd', + 'emp_name_full', + 'bu_cd', + 'bu_name', + 'start_date', + 'end_date', + 'creater', + 'create_date', + 'updater', + 'update_date' +] + +MENTE_CSV_DOWNLOAD_HEADER = [ + '施設コード', + '施設名', + '領域コード', + 'MUID', + '担当者名', + 'ビジネスユニットコード', + 'ビジネスユニット名', + '適用開始日', + '適用終了日', + '作成者', + '作成日', + '更新者', + '更新日' +] + +MENTE_CSV_DOWNLOAD_FILE_NAME = 'instEmpData.csv' + +# CSVアップロードの制限サイズ=100MB +MENTE_CSV_UPLOAD_MAX_FILE_SIZE_BYTE = 104857600 diff --git a/ecs/jskult-webapp/src/system_var/environment.py b/ecs/jskult-webapp/src/system_var/environment.py index c5bf66ef..bbb40223 100644 --- a/ecs/jskult-webapp/src/system_var/environment.py +++ b/ecs/jskult-webapp/src/system_var/environment.py @@ -11,6 +11,7 @@ COGNITO_CLIENT_SECRET = os.environ['COGNITO_CLIENT_SECRET'] AWS_REGION = os.environ['AWS_REGION'] SESSION_TABLE_NAME = os.environ['SESSION_TABLE_NAME'] BIO_ACCESS_LOG_BUCKET = os.environ['BIO_ACCESS_LOG_BUCKET'] +MASTER_MAINTENANCE_BUCKET = os.environ['MASTER_MAINTENANCE_BUCKET'] DB_HOST = os.environ['DB_HOST'] DB_PORT = int(os.environ['DB_PORT']) diff --git a/ecs/jskult-webapp/src/templates/instEmpCsvDL.html b/ecs/jskult-webapp/src/templates/instEmpCsvDL.html index 7e84fd4c..d8ac4d5b 100644 --- a/ecs/jskult-webapp/src/templates/instEmpCsvDL.html +++ b/ecs/jskult-webapp/src/templates/instEmpCsvDL.html @@ -1,12 +1,187 @@ - {% with subtitle = view.subtitle %} + {% with subtitle = mainte_csv_dl.subtitle %} {% include '_header.html' %} {% endwith %} + + + + + -

施設担当者データCSVダウンロード

+ +

+ + + + + +

施設担当者データCSVダウンロード

+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
領域コード: + + 施設コード: + + MUID: + +
適用期間内: + + 適用開始日: + + ~ + + 適用終了日: + + ~ + +
対象テーブル: + + + データ作成日: + + ~ + + データ更新日: + + ~ + +
+ + +
+
+

+ {% if mainte_csv_dl.is_search %} + {% if mainte_csv_dl.data_count == 0 %} +

{{mainte_csv_dl.result_msg}}
+ {% else %} +
{{mainte_csv_dl.result_msg}}
+ {% endif %} + {% endif %} + +

diff --git a/ecs/jskult-webapp/src/templates/instEmpCsvUL.html b/ecs/jskult-webapp/src/templates/instEmpCsvUL.html index 9ec84207..2cf5770f 100644 --- a/ecs/jskult-webapp/src/templates/instEmpCsvUL.html +++ b/ecs/jskult-webapp/src/templates/instEmpCsvUL.html @@ -1,13 +1,207 @@ - {% with subtitle = view.subtitle %} + {% with subtitle = mainte_csv_up.subtitle %} {% include '_header.html' %} {% endwith %} - + + + -

施設担当者データCSVアップロード

+ +

+ + + + + +

施設担当者データCSVアップロード

+ {% if mainte_csv_up.is_verified and mainte_csv_up.is_error_message_list_empty() %} + + {% else %} + + {% endif %} +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
機能: + + + +
登録テーブル: + + + +
登録CSV: + {% if mainte_csv_up.is_verified and mainte_csv_up.is_error_message_list_empty() %} + {{mainte_csv_up.csv_file_name}} + {% else %} + + {% endif %} + + {% if mainte_csv_up.is_verified and mainte_csv_up.is_error_message_list_empty() and mainte_csv_up.select_table == 'real' %} + +
本番テーブルが選択されています
+ {% elif mainte_csv_up.is_verified and mainte_csv_up.is_error_message_list_empty() and mainte_csv_up.select_table == 'dummy' %} + + + {% else %} + + + {% endif %} +
+{% if mainte_csv_up.is_verified and mainte_csv_up.is_error_message_list_empty() %} + + + +{% endif %} +
+

+ +

+ {% if not mainte_csv_up.is_error_message_list_empty() %} +
+ {% for error_message in mainte_csv_up.error_message_list %} + {{error_message}}
+ {% endfor %} +
+ {% elif mainte_csv_up.is_verified and mainte_csv_up.is_error_message_list_empty() %} +
+
件数:{{mainte_csv_up.csv_data_count()}}件
+ + + + + {% for column_name in mainte_csv_up.upload_data_columns() %} + + {% endfor %} + + {% for item in mainte_csv_up.csv_upload_list %} + + {% for key, value in item.items() %} + + {% endfor %} + + {% endfor %} + +
{{column_name}}
{{value}}
+
+ {% elif mainte_csv_up.is_insert %} +
+ {% for message in mainte_csv_up.result_message_list %} + {{ message }}
+ {% endfor %} +
+ {% endif %} +

diff --git a/ecs/jskult-webapp/src/templates/masterMainteMenu.html b/ecs/jskult-webapp/src/templates/masterMainteMenu.html index 957279fa..0abc4ec3 100644 --- a/ecs/jskult-webapp/src/templates/masterMainteMenu.html +++ b/ecs/jskult-webapp/src/templates/masterMainteMenu.html @@ -9,7 +9,7 @@

MeDaCA
マスターメンテメニュー



- + 施設担当者データCSVアップロード

diff --git a/ecs/jskult-webapp/src/templates/tableOverride.html b/ecs/jskult-webapp/src/templates/tableOverride.html index e473b469..30fac4ea 100644 --- a/ecs/jskult-webapp/src/templates/tableOverride.html +++ b/ecs/jskult-webapp/src/templates/tableOverride.html @@ -1,12 +1,61 @@ - {% with subtitle = view.subtitle %} + {% with subtitle = table_override.subtitle %} {% include '_header.html' %} {% endwith %} + + + -

テーブル上書きコピー

+ +

+ + + + + +

テーブル上書きコピー

+

+ + + + + + + + + +
+ +
+
+ +
+
+ {% if table_override.is_override %} +

+

ダミー従業員担当施設マスタのデータを本番従業員担当施設マスタのデータで上書きしました
+

+ {% endif %} + +