【初心者🔰】Copilotを使ってPythonでツールを作ってみました

# 1. きっかけ

営業サポート事務をしていた際、納期確認を取引先に行っていました。
Excelに納期日を設定してメール送信📧、返信してもらうのが希望なのですが、
取引先さんから自社の管理表と照らし合わせながら行ってて・・・と電話口📱で口頭回答頂くことがあったのを思い出しました。
納期日が書いてあるファイルの日付が問い合わせファイルに自動的に入るといいななどと思い出しつつ、Copilotと一緒にならば簡単に作れるのではと思い、作成してみました。

# 2. 処理内容

- 納期回答の催促回答
- 作業用フォルダ（固定）に2つのファイルを格納
- ツールを使用することで納期日が入力されていないファイルに納期日が設定される

### 追加した処理
- フォルダパスはdefault固定。変更したい場合は処理開始時に変更設定可能

# 3. 前提条件

- PC用
- 品目番号が2つのファイルで同一
- 品目番号は英数字で設定された8文字
- 作業フォルダに2つのファイルがある
- 1つのファイルには納期設定入力済

# 4. Copilotに作ってもらったもの

① 2つのファイルのデータ
-条件
- 英数字8桁、
- 2つのファイルのデータの並びは不一致であること
- 件数は20件程

② Pythonコード
③ テスト洗い出しと一覧

:::note
①のコマンドプロンプトはこのような文章
あなたはデータ作成のスペシャリストです
あるツールを作成するために架空のExcelデータを作りたいです。それぞれのデータには同じ8桁の部品番号、部品の名称が記入されています。
① 取引先からの納入納期日の回答を依頼されたもの（納期日は空白）
② 自社で作成している納期日が記入されているもの
条件
1. 数字と記号で設定された8桁の部品番号、20個を作成してください
2. ②には2025年1月30日以降の納期日を適当に設定してください
3. 上記2つの項目が設定された表を①と②ように2つ用意
4. 3の部品番号の並びは2つとも別々にしてください
:::

# 5. Pythonコード
:::note warn
**注意：このコードは動作保証を行いません。
環境や設定によっては正常に動作しない場合がありますので、
ご自身の責任においてご使用ください。**
:::
```python
import os
import pandas as pd
import logging

# ロギングの設定
logging.basicConfig(filename='error.log', level=logging.DEBUG, format='%(asctime)s %(levelname)s: %(message)s')

def get_excel_files(folder_path):
    try:
        files = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if f.endswith('.xlsx')]
        if len(files) != 2:
            raise ValueError("フォルダにExcelファイルが2つ以上存在する必要があります。")
        return files
    except Exception as e:
        logging.critical(f"エラーが発生しました: {e}")
        return None

def find_part_number_column(df, pattern=r'^[A-Za-z0-9]{8}$'):
    try:
        for col in df.columns:
            if df[col].astype(str).str.match(pattern).any():
                return col
        raise ValueError("部品番号カラムが見つかりませんでした。")
    except Exception as e:
        logging.critical(f"エラーが発生しました: {e}")
        return None

def set_due_date(df1, df2, part_number_col1, part_number_col2):
    for index, row in df1.iterrows():
        part_number = row[part_number_col1]
        if pd.isna(part_number):
            logging.warning(f"部品番号が見つかりません。行: {index}, ファイル: {df1.name}")
            continue

        matching_row = df2[df2[part_number_col2] == part_number]
        if not matching_row.empty:
            due_date = matching_row.iloc[0]['納期日']
            if isinstance(due_date, pd.Timestamp):
                due_date = due_date.strftime('%Y/%m/%d')  # 日付の形式を 'YYYY/MM/DD' に変更
            df1.at[index, '納期日'] = due_date  # 納期日をそのままコピー

def main():
    try:
        # デフォルトの入力フォルダと出力フォルダのパスを設定
        default_input_folder = r'C:\Users\test\test2\Desktop\hensyumae'
        default_output_folder = r'C:\Users\test\test2\Desktop\copy_after'
        
        # 入力フォルダを変更するかどうか尋ねる
        change_input_folder = input(f"デフォルトの入力フォルダ ({default_input_folder}) を使用しますか？ (y/n): ").strip().lower()
        if change_input_folder == 'n':
            input_folder_path = input("新しい入力フォルダのパスを入力してください: ").strip()
        else:
            input_folder_path = default_input_folder

        # 出力フォルダを変更するかどうか尋ねる
        change_output_folder = input(f"デフォルトの出力フォルダ ({default_output_folder}) を使用しますか？ (y/n): ").strip().lower()
        if change_output_folder == 'n':
            output_folder_path = input("新しい出力フォルダのパスを入力してください: ").strip()
        else:
            output_folder_path = default_output_folder

        # フォルダ内の2つのExcelファイルを取得
        files = get_excel_files(input_folder_path)
        if files is None:
            logging.critical("入力フォルダのファイル読み込みに失敗しました。")
            return
        
        file1, file2 = files
        
        # Excelファイルを読み込む
        try:
            df1 = pd.read_excel(file1)
            df2 = pd.read_excel(file2)
        except Exception as e:
            logging.critical(f"Excelファイルの読み込み中にエラーが発生しました: {e}")
            return
        
        # 部品番号カラムを検出
        part_number_col1 = find_part_number_column(df1)
        part_number_col2 = find_part_number_column(df2)
        if part_number_col1 is None or part_number_col2 is None:
            logging.critical("部品番号カラムの検出に失敗しました。")
            return
        
        # 全ての行に部品番号が存在するか確認
        if df1[part_number_col1].isna().all():
            raise ValueError("全ての行に部品番号が存在しません。")
        
        # 部品番号を基に2つのデータフレームを照合し、納期日を設定
        set_due_date(df1, df2, part_number_col1, part_number_col2)
        
        # 結果を新しいExcelファイルに保存
        try:
            output_path = os.path.join(output_folder_path, 'updated_excel_file.xlsx')
            df1.to_excel(output_path, index=False)
            print(f"結果を {output_path} に保存しました。")
        except Exception as e:
            logging.critical(f"結果の保存中にエラーが発生しました: {e}")
            return

    except Exception as e:
        logging.critical(f"予期せぬエラーが発生しました: {e}")
        return

if __name__ == "__main__":
    main()
```

# 6. テスト一覧

| No | テストパターン                               | シナリオ                                                                                 | 期待結果                                                                           | 結果         | コメント                         |
|----|---------------------------------------------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|------------|------------------------------|
| 1  | 正常系テスト                                   | デフォルトの入力フォルダと出力フォルダを使用して正常に処理を実行する                                 | エラーが発生せず、指定した出力フォルダに更新されたExcelファイルが保存される                              |            |                              |
| 2  | 入力フォルダ変更テスト                           | デフォルトの入力フォルダを変更し、新しい入力フォルダを指定して処理を実行する                             | エラーが発生せず、新しい入力フォルダからExcelファイルが読み込まれ、指定した出力フォルダに更新されたExcelファイルが保存される |            |                              |
| 3  | 出力フォルダ変更テスト                           | デフォルトの出力フォルダを変更し、新しい出力フォルダを指定して処理を実行する                             | エラーが発生せず、指定した新しい出力フォルダに更新されたExcelファイルが保存される                             |            |                              |
| 4  | 入力フォルダが存在しない場合のテスト                 | 存在しない入力フォルダを指定して処理を実行する                                                   | フォルダが見つからない旨のエラーメッセージが表示され、処理が終了する                                           |            |                              |
| 5  | 出力フォルダが存在しない場合のテスト                 | 存在しない出力フォルダを指定して処理を実行する                                                   | 出力フォルダが自動的に作成され、更新されたExcelファイルが保存される                                           |            |                              |
| 6  | 部品番号カラムが存在しない場合のテスト                   | 部品番号カラムが存在しないExcelファイルを使用して処理を実行する                                      | 部品番号カラムが見つからない旨のエラーメッセージが表示され、処理が終了する                                           |            |                              |
| 7  | Excelファイルの形式が異なる場合のテスト              | Excelファイルの形式が異なる（例：.xlsや.csv）場合に処理を実行する                                  | ファイル形式がサポートされていない旨のエラーメッセージが表示され、処理が終了する                                      |            |                              |
| 8  | ファイルが不足している場合のテスト                   | 指定したフォルダにExcelファイルが1つしか存在しない場合に処理を実行する                              | 必要なファイルが見つからない旨のエラーメッセージが表示され、処理が終了する                                       |            |                              |
| 9  | 納期日のカラムが存在しない場合の                  | 納期日のカラムが存在しないExcelファイルを使用して処理を実行する                              | エラーメッセージが表示され、処理が終了する                                      |            |                              |


# 7. 修正・変更履歴
①コピー後の納期日の時間が不要だと思い、形式をyyyy/mm/ddで固定
②Google　Geminiにもコードについて聞いたところ、ロギングの設定があると良いとアドバイス有、設定