diff --git a/ecs/jskult-batch-daily/.env.example b/ecs/jskult-batch-daily/.env.example index 95aef7fe..d95322fb 100644 --- a/ecs/jskult-batch-daily/.env.example +++ b/ecs/jskult-batch-daily/.env.example @@ -11,3 +11,6 @@ ULTMARC_BACKUP_FOLDER=ultmarc JSKULT_CONFIG_BUCKET=********************** JSKULT_CONFIG_CALENDAR_FOLDER=jskult/calendar JSKULT_CONFIG_CALENDAR_HOLIDAY_LIST_FILE_NAME=jskult_holiday_list.txt +JSKULT_CONFIG_CALENDAR_WHOLESALER_STOCK_FILE_NAME=jskult_wholesaler_stock_input_day_list.txt +JSKULT_DATA_BUCKET=********************** +JSKULT_DATA_FOLDER_RECV=********************** diff --git a/ecs/jskult-batch-daily/src/aws/s3.py b/ecs/jskult-batch-daily/src/aws/s3.py index 2ac3efe6..2aebff4b 100644 --- a/ecs/jskult-batch-daily/src/aws/s3.py +++ b/ecs/jskult-batch-daily/src/aws/s3.py @@ -1,3 +1,4 @@ +import io import os.path as path import tempfile @@ -16,7 +17,8 @@ class S3Client: return [] contents = response['Contents'] # 末尾がスラッシュで終わるものはフォルダとみなしてスキップする - objects = [{'filename': content['Key'], 'size': content['Size']} for content in contents if not content['Key'].endswith('/')] + objects = [{'filename': content['Key'], 'size': content['Size']} + for content in contents if not content['Key'].endswith('/')] return objects def copy(self, src_bucket: str, src_key: str, dest_bucket: str, dest_key: str) -> None: @@ -89,6 +91,16 @@ class ConfigBucket(S3Bucket): f.seek(0) return temporary_file_path + def download_wholesaler_stock_list(self): + # 一時ファイルとして保存する + temporary_dir = tempfile.mkdtemp() + temporary_file_path = path.join(temporary_dir, environment.JSKULT_CONFIG_CALENDAR_WHOLESALER_STOCK_FILE_NAME) + holiday_list_key = f'{environment.JSKULT_CONFIG_CALENDAR_FOLDER}/{environment.JSKULT_CONFIG_CALENDAR_WHOLESALER_STOCK_FILE_NAME}' + with open(temporary_file_path, mode='wb') as f: + self._s3_client.download_file(self._bucket_name, holiday_list_key, f) + f.seek(0) + return temporary_file_path + class JskUltBackupBucket(S3Bucket): _bucket_name = environment.JSKULT_BACKUP_BUCKET @@ -96,3 +108,16 @@ class JskUltBackupBucket(S3Bucket): class UltmarcBackupBucket(JskUltBackupBucket): _folder = environment.ULTMARC_BACKUP_FOLDER + + +class VjskBucket(S3Bucket): + # TODO:V実消化バケットから見たり取ってきたりする実装をやる + _bucket_name = environment.JSKULT_DATA_BUCKET + _recv_folder = environment.JSKULT_DATA_FOLDER_RECV + + def get_file_list(self): + return self._s3_client.list_objects(self._bucket_name, self._recv_folder) + + # def download_data_file(self, data_filename: str): + # temporary_dir = tempfile.mkdtemp() + # temporary_file_path = path.join(temporary_dir, f'{data_filename.replace(f"{self._folder}/", "")}') diff --git a/ecs/jskult-batch-daily/src/batch/common/batch_context.py b/ecs/jskult-batch-daily/src/batch/common/batch_context.py index 3b3ac157..b493ecca 100644 --- a/ecs/jskult-batch-daily/src/batch/common/batch_context.py +++ b/ecs/jskult-batch-daily/src/batch/common/batch_context.py @@ -3,6 +3,7 @@ class BatchContext: __syor_date: str # 処理日(yyyy/mm/dd形式) __is_not_business_day: bool # 日次バッチ起動日フラグ __is_ultmarc_imported: bool # アルトマーク取込実施済フラグ + __is_import_target_vjsk_stockslipdata: bool # 卸在庫データ取込対象フラグ def __init__(self) -> None: self.__is_not_business_day = False @@ -37,3 +38,11 @@ class BatchContext: @is_ultmarc_imported.setter def is_ultmarc_imported(self, flag: bool): self.__is_ultmarc_imported = flag + + @property + def is_import_target_vjsk_stockslipdata(self): + return self.__is_import_target_vjsk_stockslipdata + + @is_import_target_vjsk_stockslipdata.setter + def is_import_target_vjsk_stockslipdata(self, flag: bool): + self.__is_import_target_vjsk_stockslipdata = flag diff --git a/ecs/jskult-batch-daily/src/batch/common/calendar_wholestocksaler_file.py b/ecs/jskult-batch-daily/src/batch/common/calendar_wholestocksaler_file.py new file mode 100644 index 00000000..ba687514 --- /dev/null +++ b/ecs/jskult-batch-daily/src/batch/common/calendar_wholestocksaler_file.py @@ -0,0 +1,32 @@ +from src.system_var import constants + + +class CalendarWholwSalerStockFile: + """V実消化卸在庫データ連携日ファイル""" + + __calendar_file_lines: list[str] + + def __init__(self, calendar_file_path): + with open(calendar_file_path) as f: + self.__calendar_file_lines: list[str] = f.readlines() + + def compare_date(self, date_str: str) -> bool: + """与えられた日付がV実消化卸在庫データ連携日ファイル内に含まれているかどうか + V実消化卸在庫データ連携日ファイル内の日付はyyyy/mm/ddで書かれている前提 + コメント(#)が含まれている行は無視される + + Args: + date_str (str): yyyy/mm/dd文字列 + + Returns: + bool: 含まれていればTrue + """ + for calendar_date in self.__calendar_file_lines: + # コメント行が含まれている場合はスキップ + if constants.CALENDAR_COMMENT_SYMBOL in calendar_date: + continue + + if date_str in calendar_date: + return True + + return False diff --git a/ecs/jskult-batch-daily/src/batch/vjsk/vjsk_importer.py b/ecs/jskult-batch-daily/src/batch/vjsk/vjsk_importer.py new file mode 100644 index 00000000..4c2cb0ee --- /dev/null +++ b/ecs/jskult-batch-daily/src/batch/vjsk/vjsk_importer.py @@ -0,0 +1,143 @@ +from src.aws.s3 import ConfigBucket, VjskBucket +from src.batch.common.batch_context import BatchContext +from src.batch.common.calendar_wholestocksaler_file import \ + CalendarWholwSalerStockFile +from src.batch.vjsk.vjsk_recv_file_mapper import VjskRecvFileMapper +from src.error.exceptions import BatchOperationException +from src.logging.get_logger import get_logger + +# from src.batch.datachange import emp_chg_inst_lau + +logger = get_logger('V実消化データ取込') +batch_context = BatchContext.get_instance() +vjsk_recv_bucket = VjskBucket() +vjsk_mapper = VjskRecvFileMapper() + + +def _check_if_file_exists(src_list: list, key: str) -> bool: + pref = vjsk_mapper.get_file_prefix(key) + suff = vjsk_mapper.get_file_suffix(key) + for idx, elem in enumerate(src_list): + buf = elem.get("filename") + filename = buf[buf.rfind("/") + 1:] + if filename.startswith(pref) and filename.endswith(suff): + return True + return False + + +def _check_received_files(): + """V実消化連携データ存在確認処理""" + logger.debug('V実消化連携データ存在確認処理:開始') + + # 実消化&アルトマーク V実消化データ受領バケットにあるファイル一覧を取得 + received_files = vjsk_recv_bucket.get_file_list() + logger.debug(f'ファイル一覧{received_files}') + + # ファイル存在確認 卸在庫データファイル(卸在庫データ処理対象日のみ実施) + if batch_context.is_import_target_vjsk_stockslipdata: + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_STOCK_SLIP_DATA): + raise BatchOperationException(f'卸在庫データファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 卸販売データ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_SLIP_DATA): + raise BatchOperationException(f'卸販売データファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 卸組織変換マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_ORG_CNV_MST): + raise BatchOperationException(f'卸組織変換マスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 施設統合マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_VOP_HCO_MERGE): + raise BatchOperationException(f'施設統合マスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 卸マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_WHS_MST): + raise BatchOperationException(f'卸マスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 卸ホールディングスマスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_HLD_MST): + raise BatchOperationException(f'卸ホールディングスマスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 施設マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_FCL_MST): + raise BatchOperationException(f'施設マスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 メーカー卸組織展開表 + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_MKR_ORG_HORIZON): + raise BatchOperationException(f'メーカー卸組織展開表ファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 取引区分マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_TRAN_KBN_MST): + raise BatchOperationException(f'取引区分マスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 製品マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_PHM_PRD_MST): + raise BatchOperationException(f'製品マスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 製品価格マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_PHM_PRICE_MST): + raise BatchOperationException(f'製品価格マスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 卸得意先情報マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_WHS_CUSTOMER_MST): + raise BatchOperationException(f'卸得意先情報マスタファイルがありません ファイル一覧:{received_files}') + + # ファイル存在確認 MDBコード変換マスタ + if not _check_if_file_exists(received_files, vjsk_mapper.CONDKEY_MDB_CONV_MST): + raise BatchOperationException(f'MDBコード変換マスタファイルがありません ファイル一覧:{received_files}') + + logger.debug('V実消化連携データ存在確認処理:終了') + + return True + + +def _import_file_to_db(): + logger.debug('V実消化取込処理:開始') + + # diff_upsertに変わるやつを呼び出す + # emp_chg_inst_lau.batch_process() みたいに + + logger.debug('V実消化取込処理:終了') + + +def _determine_today_is_stockslipdata_target(): + try: + # 設定ファイル「V実消化卸在庫データ連携日ファイル」の内容を取得して、処理日が該当していればTrueを返却する + today = batch_context.syor_date + + holiday_list_file_path = ConfigBucket().download_wholesaler_stock_list() + targetdays = CalendarWholwSalerStockFile(holiday_list_file_path) + ret = targetdays.compare_date(today) + except Exception as e: + logger.error(f'{e}') + raise e + return ret + + +def exec(): + """V実消化データ取込""" + logger.info('Start Jitsusyouka Torikomi PGM.') + + # 卸在庫データ取込対象日であれば、卸在庫データ処理対象フラグを立てる + logger.debug('卸在庫データ取込対象日であるかを判定') + batch_context.is_import_target_vjsk_stockslipdata = _determine_today_is_stockslipdata_target() + logger.debug(f'判定結果 : {batch_context.is_import_target_vjsk_stockslipdata}') + if batch_context.is_import_target_vjsk_stockslipdata: + logger.info('卸在庫データ取込対象日です') + + # V実消化データファイル受領チェック + logger.debug('V実消化データファイル受領チェック:開始') + try: + _check_received_files() + except BatchOperationException as e: + logger.error('受領したV実消化データファイルに欠落があります') + raise e + logger.debug('V実消化データファイル受領チェック:終了') + + # データベース取込 + logger.debug('V実消化データ取込:開始') + try: + _import_file_to_db() + except Exception as e: + logger.error(f'データベース登録失敗 {e}') + logger.debug('V実消化データ取込:終了') diff --git a/ecs/jskult-batch-daily/src/batch/vjsk/vjsk_recv_file_mapper.py b/ecs/jskult-batch-daily/src/batch/vjsk/vjsk_recv_file_mapper.py new file mode 100644 index 00000000..6a997c9f --- /dev/null +++ b/ecs/jskult-batch-daily/src/batch/vjsk/vjsk_recv_file_mapper.py @@ -0,0 +1,166 @@ +class VjskRecvFileMapper: + CONDKEY_SLIP_DATA = "SLIP_DATA" # 販売実績データ + CONDKEY_HLD_MST = "HLD_MST" # V卸ホールディングスマスタ + CONDKEY_WHS_MST = "WHS_MST" # V卸マスタ + CONDKEY_MKR_ORG_HORIZON = "MKR_ORG_HORIZON" # Vメーカー卸組織展開表 + CONDKEY_ORG_CNV_MST = "ORG_CNV_MST" # V卸組織変換マスタ + CONDKEY_TRAN_KBN_MST = "TRAN_KBN_MST" # V取引区分マスタ + CONDKEY_FCL_MST = "FCL_MST" # V施設マスタ + CONDKEY_PHM_PRD_MST = "PHM_PRD_MST" # V製品マスタ + CONDKEY_PHM_PRICE_MST = "PHM_PRICE_MST" # V製品価格マスタ + CONDKEY_VOP_HCO_MERGE = "VOP_HCO_MERGE" # V施設統合マスタ + CONDKEY_WHS_CUSTOMER_MST = "WHS_CUSTOMER_MST" # V卸得意先情報マスタ + CONDKEY_MDB_CONV_MST = "MDB_CONV_MST" # MDBコード変換表 + CONDKEY_STOCK_SLIP_DATA = "STOCK_SLIP_DATA" # 卸在庫データ + CONDKEY_BIO_SLIP_DATA = "BIO_SLIP_DATA" # 生物由来データ + CONDKEY_LOT_NUM_MS = "LOT_NUM_MS" # ロットマスタデータ + + _KEY_FILE_PREFIX = "file_prefix" + _KEY_FILE_SUFFIX = "file_suffix" + _KEY_ORG_TABLE = "org_table" + _KEY_SRC_TABLE = "src_table" + _VJSK_INTERFACE_MAPPING = { + # 販売実績データ + CONDKEY_SLIP_DATA: { + _KEY_FILE_PREFIX: "slip_data_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.sales", + _KEY_SRC_TABLE: "src05.sales" + }, + + # V卸ホールディングスマスタ + CONDKEY_HLD_MST: { + _KEY_FILE_PREFIX: "hld_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.hld_mst_v", + _KEY_SRC_TABLE: "src05.hld_mst_v" + }, + + # V卸マスタ + CONDKEY_WHS_MST: { + _KEY_FILE_PREFIX: "whs_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.whs_mst_v", + _KEY_SRC_TABLE: "src05.whs_mst_v" + }, + + # Vメーカー卸組織展開表 + CONDKEY_MKR_ORG_HORIZON: { + _KEY_FILE_PREFIX: "mkr_org_horizon_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.mkr_org_horizon_v", + _KEY_SRC_TABLE: "src05.mkr_org_horizon_v" + }, + + # V卸組織変換マスタ + CONDKEY_ORG_CNV_MST: { + _KEY_FILE_PREFIX: "org_cnv_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.org_cnv_mst_v", + _KEY_SRC_TABLE: "src05.org_cnv_mst_v" + }, + + # V取引区分マスタ + CONDKEY_TRAN_KBN_MST: { + _KEY_FILE_PREFIX: "tran_kbn_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.tran_kbn_mst_v", + _KEY_SRC_TABLE: "src05.tran_kbn_mst_v" + }, + + # V施設マスタ + CONDKEY_FCL_MST: { + _KEY_FILE_PREFIX: "fcl_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.fcl_mst_v", + _KEY_SRC_TABLE: "src05.fcl_mst_v" + }, + + # V製品マスタ + CONDKEY_PHM_PRD_MST: { + _KEY_FILE_PREFIX: "phm_prd_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.phm_prd_mst_v", + _KEY_SRC_TABLE: "src05.phm_prd_mst_v" + }, + + # V製品価格マスタ + CONDKEY_PHM_PRICE_MST: { + _KEY_FILE_PREFIX: "phm_price_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.phm_price_mst_v", + _KEY_SRC_TABLE: "src05.phm_price_mst_v" + }, + + # V施設統合マスタ + CONDKEY_VOP_HCO_MERGE: { + _KEY_FILE_PREFIX: "vop_hco_merge_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.vop_hco_merge_v", + _KEY_SRC_TABLE: "src05.vop_hco_merge_v" + }, + + # V卸得意先情報マスタ + CONDKEY_WHS_CUSTOMER_MST: { + _KEY_FILE_PREFIX: "whs_customer_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.whs_customer_mst_v", + _KEY_SRC_TABLE: "src05.whs_customer_mst_v" + }, + + # MDBコード変換表 + CONDKEY_MDB_CONV_MST: { + _KEY_FILE_PREFIX: "mdb_conv_mst_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.mdb_conv_mst_v", + _KEY_SRC_TABLE: "src05.mdb_conv_mst_v" + }, + + # 卸在庫データ + CONDKEY_STOCK_SLIP_DATA: { + _KEY_FILE_PREFIX: "stock_slip_data_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.whole_stock", + _KEY_SRC_TABLE: "src05.whole_stock" + }, + + # 生物由来データ + CONDKEY_BIO_SLIP_DATA: { + _KEY_FILE_PREFIX: "bio_slip_data_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.bio_sales", + _KEY_SRC_TABLE: "src05.bio_sales" + }, + + # ロットマスタデータ + CONDKEY_LOT_NUM_MS: { + _KEY_FILE_PREFIX: "lot_num_ms_", + _KEY_FILE_SUFFIX: ".tsv", + _KEY_ORG_TABLE: "org05.lot_num_mst", + _KEY_SRC_TABLE: "src05.lot_num_mst" + }, + } + + def get_file_prefix(self, condkey: str) -> str: + ret = None + if condkey in self._VJSK_INTERFACE_MAPPING: + ret = self._VJSK_INTERFACE_MAPPING.get(condkey).get(self._KEY_FILE_PREFIX) + return ret + + def get_file_suffix(self, condkey: str) -> str: + ret = None + if condkey in self._VJSK_INTERFACE_MAPPING: + ret = self._VJSK_INTERFACE_MAPPING.get(condkey).get(self._KEY_FILE_SUFFIX) + return ret + + def get_org_table(self, condkey: str) -> str: + ret = None + if condkey in self._VJSK_INTERFACE_MAPPING: + ret = self._VJSK_INTERFACE_MAPPING.get(condkey).get(self._KEY_ORG_TABLE) + return ret + + def get_src_table(self, condkey: str) -> str: + ret = None + if condkey in self._VJSK_INTERFACE_MAPPING: + ret = self._VJSK_INTERFACE_MAPPING.get(condkey).get(self._KEY_SRC_TABLE) + return ret diff --git a/ecs/jskult-batch-daily/src/jobctrl_daily.py b/ecs/jskult-batch-daily/src/jobctrl_daily.py index 370f2179..dc7146b7 100644 --- a/ecs/jskult-batch-daily/src/jobctrl_daily.py +++ b/ecs/jskult-batch-daily/src/jobctrl_daily.py @@ -9,6 +9,7 @@ from src.batch.common.batch_context import BatchContext from src.batch.common.calendar_file import CalendarFile from src.batch.laundering import create_dcf_inst_merge, create_mst_inst from src.batch.ultmarc import ultmarc_process +from src.batch.vjsk import vjsk_importer from src.error.exceptions import BatchOperationException from src.logging.get_logger import get_logger from src.system_var import constants @@ -84,6 +85,7 @@ def exec(): logger.info('日次処理(V実消化)') try: logger.info('V実消化取込:起動') + vjsk_importer.exec() logger.info('V実消化取込:終了') except BatchOperationException as e: logger.exception(f'V実消化取込処理エラー(異常終了){e}') diff --git a/ecs/jskult-batch-daily/src/system_var/environment.py b/ecs/jskult-batch-daily/src/system_var/environment.py index b1730224..6a2fca0b 100644 --- a/ecs/jskult-batch-daily/src/system_var/environment.py +++ b/ecs/jskult-batch-daily/src/system_var/environment.py @@ -15,6 +15,9 @@ ULTMARC_BACKUP_FOLDER = os.environ['ULTMARC_BACKUP_FOLDER'] JSKULT_CONFIG_BUCKET = os.environ['JSKULT_CONFIG_BUCKET'] JSKULT_CONFIG_CALENDAR_FOLDER = os.environ['JSKULT_CONFIG_CALENDAR_FOLDER'] JSKULT_CONFIG_CALENDAR_HOLIDAY_LIST_FILE_NAME = os.environ['JSKULT_CONFIG_CALENDAR_HOLIDAY_LIST_FILE_NAME'] +JSKULT_CONFIG_CALENDAR_WHOLESALER_STOCK_FILE_NAME = os.environ['JSKULT_CONFIG_CALENDAR_WHOLESALER_STOCK_FILE_NAME'] +JSKULT_DATA_BUCKET = os.environ['JSKULT_DATA_BUCKET'] +JSKULT_DATA_FOLDER_RECV = os.environ['JSKULT_DATA_FOLDER_RECV'] # 初期値がある環境変数 LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')