週報書いている人、今すぐやめて。サボり方教えます。

## はじめに
業務をやっていると週報を書く必要が出てきますよね。
週報だけならともかく、日報も書く必要がある時ってすごいめんどくさいですよね…

その場合の週報のサボり方を今回紹介します。完全無料でできます。

:::note warn
この記事はWindowsの環境を想定しています。
Windows10の環境では動作することを確認しています。
:::

## 実現方法
Gemini、Python、Excel、Teams、タスクスケジューラ
これらを使ってサボります。

Excel：日報管理に使います。
Gemini：日報をまとめて週報を作ってもらいます。(無料枠があるので)
Python：Excel、Gemini、Teamsの制御に使用します。
タスクスケジューラ：pythonの定期実行に使用します。
Teams：週報の出力先として使用します。

以下が仕様です。
・今日の日付から過去５日の週報をまとめる。
　例えば、週報を書くのが水曜日の場合、
　土日を除く、水、火、月、先週の金、先週の木を対象とする。(祝日も除く)
・先週の日付が月を跨ぐ場合、それを考慮して日付を取得する。
　先月のシートがない場合は、無視する。今月分だけで作成する。
・年を跨ぐ場合は無視する(1月の最初の週のみなので)
・５日中１日しか日報が取得できなかった場合は、作成しない。その旨をTeamsで伝える。

## 手順
各ツールのインストール&設定をしてください。

### Pythonの準備
今更なのでここでは説明しませんが、以下のサイト等を見てインストールしてください。

https://www.python.jp/install/windows/install.html

PythonからGeminiを使うために以下のコマンドを打ってください。

```sh
!pip install -q -U google-generativeai
```

その他以下のライブラリが必要になりますので、インストールしてください。

```sh
pip install openpyxl requests pandas jpholiday
```

### Geminiの準備
以下のサイトを見てGemini APIキーを取得してください。

https://monomonotech.jp/kurage/memo/m240725_get_gemini_api_key.html

APIキーの取得が完了したら、環境変数にこのAPIキーを登録してください。
環境変数名は、「GOOGLE_API_KEY」で登録してください。

### Teamsの準備
TeamsのWorkflowsを使用します。以下の記事からフローを作成してください。

https://qiita.com/k_adc/items/62d27d7941cec604d3de

### Excelの準備
日報のテンプレートを用意しましたので、このテンプレートを真似してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/34c3dfb9-9928-4f4f-8618-3b5efeede8d6.png)

A1セルに”日付”を入力する。
B1セルに”内容”を入力する。内容欄に日々の日報を書いていく感じです。

A2セルには以下の関数を入力してください。

```
=IF(ROW(A2)-1 > DAY(EOMONTH(DATE(YEAR(TODAY()), MID(CELL("filename", A2), FIND("]", CELL("filename", A2)) + 1, LEN(CELL("filename", A2)) - FIND("]", CELL("filename", A2)) - 1) + 0, 1), 0)), "", DATE(YEAR(TODAY()), MID(CELL("filename", A2), FIND("]", CELL("filename", A2)) + 1, LEN(CELL("filename", A2)) - FIND("]", CELL("filename", A2)) - 1) + 0, ROW(A2)-1))
```

A3セルに以下の関数を入力し、A32セルまでオートフィルしてください。

```
=IF(A2<>"", IF(A2+1 <= EOMONTH(A2, 0), A2+1, ""), "")
```

シート名に該当の月を入力してください。(日報は月ごとの管理になります)
すると、A列に日付が自動的に埋まります。
例)　4月、8月など

こんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/48ed2ab7-14f5-4253-ad82-db6ef7c4babd.png)

あと、Excelファイルの名前ですが、「YYYY_xxx.xlsx」としてください。
”YYYY_”部分でファイルを検索、取得しています。
例）2025_report.xlsx

### タスクスケジューラの準備
以下のサイトを見てpythonをタスクに登録してください。
(これはpythonのコードが用意できてからやってください)

https://zenn.dev/tonokokko/articles/3b65c0c4a5408b

これで事前準備は完了です。

## ソースコード

```python
import os
import google.generativeai as genai
import glob
import openpyxl
import requests
import datetime
import json
import pandas as pd
import datetime
import jpholiday

## 任意で設定 #########################################################################

# WorkFlowsのHTTPエンドポイント
WORKFLOWS_URL = "各自のURLを入力してください"

# Geminiのバージョン
GEMINI_VERSION = "gemini-1.5-flash"

# Teamsに送信するメッセージのテンプレート
msg = {
"attachments": [
    {
    "contentType": "application/vnd.microsoft.card.adaptive",
    "content": {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.2",
        "body": [
        {
            "type": "TextBlock",
            "text": "",
            "size": "Large",
            "weight": "Bolder"
        },
        {
            "type": "TextBlock",
            "text": "",
            "wrap": True,
            "markdown": True
        }
        ]
    }
    }
]
}

## 編集不要の設定 #####################################################################
# Google API Keyの設定
GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
genai.configure(api_key=GOOGLE_API_KEY)

## 関数定義 #########################################################################
def glob_files(year):
    # 指定した年のExcelファイルを取得する
    files = glob.glob(f"{year}_*.xlsx")
    return files

def get_last_5_business_days(file_path, today):
    business_days = []
    current_date = today
    all_filtered_data = []

    while len(business_days) < 5:
        # 年をまたいだ場合は終了（前年に突入した場合）
        if current_date.year < today.year:
            print("前年に突入したため、処理を中止します")
            break

        # 土日・祝日を除外
        if current_date.weekday() < 5 and not jpholiday.is_holiday(current_date):
            business_days.append(current_date)

            # シート名を決定（現在の月）
            sheet_name = f"{current_date.month}月"

            try:
                # Excelの該当シートを読み込む
                df = pd.read_excel(file_path, sheet_name=sheet_name)
                df['日付'] = pd.to_datetime(df['日付'], errors='coerce').dt.date

                # 該当する日付のデータを取得
                filtered_df = df[df['日付'] == current_date]
                if not filtered_df.empty:
                    all_filtered_data.append(filtered_df)

            except ValueError:
                # シートが存在しない場合はスキップ
                continue

        current_date -= datetime.timedelta(days=1) # 1日戻る

    # 全データを統合
    if all_filtered_data:
        return pd.concat(all_filtered_data)
    else:
        return pd.DataFrame() # データがなかった場合は空の DataFrame を返す


def send_message(url, msg):
    # POSTリクエストを送信
    response = requests.post(
        url=url,
        data=json.dumps(msg),
        headers={"Content-Type": "application/json"}
    )

    # レスポンスを確認
    if response.status_code == 200:
        print("メッセージを送信しました")
    elif response.status_code == 202:
        print("メッセージの送信を受け付けました")
    else:
        print(f"エラーが発生しました: {response.status_code}, {response.text}")

    return response.status_code

def read_excel(file_path):
    wb = openpyxl.load_workbook(file_path)
    data = {}

    for sheet in wb.sheetnames:
        ws = wb[sheet]
        sheet_data = []
        for row in ws.iter_rows(values_only=True):
            sheet_data.append(row)
        data[sheet] = sheet_data

    return data

def call_gemini(filtered_df):
    # 過去5営業日のデータを文字列に変換し、Geminiに要約を依頼する
    prompt = (
        "以下のExcelデータを箇条書きで要約してください。\n"
        "【注意点】\n"
        "・箇条書きは各項目の内容ごとではなく、同じような内容のものをまとめて書く\n"
        "：\n\n"
    )

    # DataFrameからプロンプトを作成
    for _, row in filtered_df.iterrows():
        prompt += f"{row['内容']}\n" # 内容欄のデータを追加

    # Geminiに送信
    model = genai.GenerativeModel(GEMINI_VERSION)
    response = model.generate_content(prompt)

    return response.text

if __name__ == "__main__":
    # 今日の日付を取得
    today = datetime.date.today()

    # 読み込むExcelファイルを今日の年から取得
    file_path = glob_files(today.year)[0]

    # 過去5営業日を取得
    filtered_df = get_last_5_business_days(file_path, today)
    nan_count = filtered_df.isnull().sum(axis=1).sum()

    # 過去5営業日のデータが4つ以上欠損している場合は週報を作成しない
    if nan_count >= 4:
        summary = "データが不足しているため、週報を作成できませんでした。"
    else:
        summary = call_gemini(filtered_df)

    title = datetime.date.today().strftime('%Y/%m/%d') + "の週報"

    msg["attachments"][0]["content"]["body"][0]["text"] = title
    msg["attachments"][0]["content"]["body"][1]["text"] = summary

    # Teamsにメッセージを送信
    status = send_message(WORKFLOWS_URL, msg)
```

基本的にはこれをコピペして使用すればOKですが、Geminiに対しての指示は各自で修正したほうがいいかなと思います。載せてるのはあくまで参考程度にぐらいです。
call_gemini()関数のprompt変数の内容を修正すればOKです。

## 終わりに
最後に一言、**いい週報レスライフを！！**