easy_patents 応用例その2 拒絶対応 応答期限チェック

# 初めに
easy_patents の応用例を紹介する。

# 概要
エクセルで自社の案件の拒絶対応の管理をしているような場面と想定する。
本スクリプトは、以下の機能を持つ。
出願番号をもとに、拒絶理由の発行状況、意見書の応答状況、応答期限、警告を出力する。
また、新たな拒絶理由通知を受けた案件、応答期限まで14日未満の案件についてはメールでリマインダーをする。

## INPUT エクセルファイル
![input_excel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513217/d4c4d452-ddec-947e-93ce-14a1058cd4d9.png)

本スクリプトで継続的に確認しているため、前回の記録が残っていると仮定する。

なお、上記の案件は、ランダムに選んだ説明に適した案件(拒絶理由通知を受けていて、期限が近かったり、応答済みであったり、期限が切れていたりした案件)である。

## コード

```sample_check_due_date.py
from easy_patents.get_info import app_progress
from easy_patents.errors import NoDocumentError, TooManyAccessError, ParameterError, UnknownError
import configparser
from mail_notify import email
import pandas as pd
from datetime import datetime, timedelta


def check_due_date(excel_path, sheet_name="Sheet1", from_addr="", to_addrs=[]):
    '''
    中間処理の対応状況を確認する
    最新の拒絶理由に対して、その後に意見書が提出されているかをチェックし、
    その状況をエクセルに記録する。
    新しい拒絶理由、日付の近い拒絶理由に対してはメール送信

    Parameters
    ----------
    excel_path: str
        更新対象のエクセルのパス
    sheet_name: str or int
        シート名またはシート番号
    '''
    # 指定されたエクセルを読み出す
    df = pd.read_excel(excel_path, sheet_name=sheet_name)

    # インストールしたpandasのバージョンによっては、以下のようにencodingを指定する必要がある。
    # df = pd.read_excel(excel_path,
    #        sheet_name=sheet_name, encoding="utf-8_sig")

    for index, row in df.iterrows():

        last_rejection_date = row['rejection_date']
        # データの初期化
        row['rejection_date'] = ''
        row['due_date'] = ''
        row['warning'] = ''
        row['opinion_submit_date'] = ''
        row['invention_title'] = ''
        subject = ""
        body = ""

        # 読み込んだエクセルから出願番号を取得
        app_number = row['app_number']

        # 特許経過情報の取得
        try:
            progress_info = app_progress(app_number, reget_date=7)
        except NoDocumentError:
            # 未公開情報などでドキュメントが取得できない場合にはスキップ
            continue
        except ParameterError as e:
            # パラメーターエラーの場合（出願番号の記載に誤りがあるような場合）には、
            # メッセージを出してスキップ
            print(e)
            continue
        except (TooManyAccessError, UnknownError) as e:
            # アクセス数超過、原因不明エラーの場合には
            # エラーメッセージを出力して終了
            print(e)
            break


        # 発明の名称を設定
        row['invention_title'] = progress_info['result']['data']['inventionTitle']

        # 拒絶理由通知書と意見書の書類データの抽出
        document_list = get_documents_info(progress_info)
        rejection_list = pickup_documents_info(document_list, "拒絶理由通知書")
        opinion_list = pickup_documents_info(document_list, "意見書")

        # 拒絶理由がない場合には、次のループへ
        if len(rejection_list) == 0:
            continue

        # legalDateで並び替え
        # legalDateは日付を示す8桁の数字の文字列(例:"20220216")であるが、
        # 辞書順にこれらの文字列を並べると最新の日付が最後になる。
        sort_key = lambda x : x['legalDate']
        rejection_list.sort(key=sort_key)
        opinion_list.sort(key=sort_key)

        # 最新の拒絶理由を取得
        latest_rejection = rejection_list[-1]

        # 拒絶理由通知の日付を文字列からdatetimeオブジェクトに変換(日付の計算を行うため)
        rejection_date = datetime.strptime(latest_rejection['legalDate'], "%Y%m%d")
        row['rejection_date'] = rejection_date

        # 応答期限を取得(本例では、暫定的に60日期限,土日等の繰り越しなし)
        due_date = rejection_date + timedelta(days=60)
        row['due_date'] = due_date

        # 拒絶理由通知の日付以後の意見書提出がないことを示すフラグを設定
        not_submitted_flag = True

        if len(opinion_list) != 0:
            latest_opinion = opinion_list[-1]
            opinion_date = datetime.strptime(latest_opinion['legalDate'], "%Y%m%d")
            # 意見書の日付が拒絶理由通知の日付以後の場合に、
            # opinion_submit_dateに日付を記入
            # これ以外の場合には追記しない。
            if opinion_date >= rejection_date:
                row['opinion_submit_date'] = opinion_date
                not_submitted_flag = False

        # 応答期限までの日数が0日以上の場合には、あと〇日という警告を出力
        now = datetime.now()
        days_by_due_date = due_date - now
        if -1 < days_by_due_date.days and not_submitted_flag:
            row['warning'] = "あと%s日" % days_by_due_date.days

        # 応答期限までの日数が-1から-30日の場合には、期限切れという警告を出力
        if -30 <= days_by_due_date.days <= -1 and not_submitted_flag:
            row['warning'] = "期限切れ"

        # データフレームの対象行を取得した情報でアップデート
        df.loc[index] = row

        # メール設定読み込み
        config = configparser.ConfigParser()
        config.read('mail_config.ini')
        user = config['Email']['user']
        password = config['Email']['password']
        smtp_server = config['Email']['smtp_server']
        smtp_port = config['Email']['smtp_port']
        is_ssl = config['Email']['is_ssl'] == "True"

        # 応答期限まで14日未満の場合、メール送信
        if  -1 < days_by_due_date.days < 14:
            subject = "拒絶理由応答期限まであと%s日" % days_by_due_date.days
            body = "出願番号: %s\n対応期限: %s" % (app_number, due_date)
        # 拒絶理由の日付が違う場合、すなわち、新たな拒絶理由を受けた場合、メール送信
        elif rejection_date != last_rejection_date:
            subject = "新たな拒絶理由通知を受けました"
            body = "出願番号: %s\n対応期限: %s" % (app_number, due_date)

        if subject:
            email(
                   fromaddr=from_addr,
                   toaddrs=to_addrs,
                   smtp_server=smtp_server,
                   smtp_port=smtp_port,
                   subject=subject,
                   body=body,
                   is_ssl=is_ssl,
                   user=user,
                   password=password,
            )

    # エクセルに反映
    df.to_excel(excel_path,
            sheet_name=sheet_name, index=False)

    # インストールしたpandasのバージョンによっては、以下のようにencodingを指定する必要がある。
    # df.to_excel(excel_path,
    #      sheet_name=sheet_name, index=False, encoding="utf-8_sig")


def get_documents_info(progress_info):
    '''
    bibliographyInformation以下のdocumentデータを一つのリストにまとめる関数

    Parameters
    ----------
    progress_info: json
        app_progressで取得されるjsonデータ

    Returns
    -------
    list
        書類情報データのリスト
    '''
    document_list = []
    for binfo in progress_info['result']['data']['bibliographyInformation']:
        document_list += binfo['documentList']
    return document_list


def pickup_documents_info(document_list, document_name):
    '''
    document_listからdocumentDescriptionがdocument_nameであるものを抽出する関数

    Parameters
    ----------
    document_list: list
        app_progressのbibliographyInformation以下のdocumentListを一つにまとめたリスト
        もしくは、app_progressのbibliographyInformation以下のいずれかのdocumentList
    document_name: str
        抽出対象の書類名

    Returns
    -------
    list
        抽出対象の書類データのリスト
    '''
    ret = []
    for document in document_list:
        if document['documentDescription'] == document_name:
            ret.append(document)
    return ret


if __name__ == "__main__":
    import sys
    excel_path = sys.argv[1]
    sheet_name = sys.argv[2]
    from_addr = sys.argv[3]
    to_addrs = sys.argv[4:]
    check_due_date(excel_path, sheet_name, from_addr, to_addrs)
```

```mail_notify.py
import smtplib
import ssl
from email.mime.text import MIMEText


def make_mimetext(fromaddr, toaddrs, subject, body):
    jp = 'iso-2022-jp'
    msg = MIMEText(body.encode(jp, "ignore"), 'plain', jp,)
    msg['Subject'] = subject
    msg['From'] = fromaddr
    msg['To'] = ','.join(toaddrs)
    return msg


def send_email(smtp_server, smtp_port, fromaddr, toaddrs, mimetext, is_ssl=False, user=None, password=None):
    if is_ssl:
        context = ssl.create_default_context()
        server = smtplib.SMTP_SSL(smtp_server, smtp_port, context=context)
    else:
        server = smtplib.SMTP(smtp_server, smtp_port)
    # server.set_debuglevel(2)
    if user:
        server.login(user, password)
    server.sendmail(fromaddr, toaddrs, mimetext.as_string())


def email(fromaddr, toaddrs, smtp_server, smtp_port, subject, body, is_ssl=False, user=None, password=None):
    mimetext = make_mimetext(fromaddr, toaddrs, subject, body)
    send_email(smtp_server, smtp_port, fromaddr, toaddrs, mimetext, is_ssl, user, password)


if __name__ == "__main__":
    pass
```

## 設定ファイル
以下の内容の設定ファイルが必要である。なお、本スクリプトはSSLのみに対応している。

```mail_config.ini
[Email]
user = <SMTPサーバのログインユーザ名>
password = <SMTPサーバのログインパスワードまたはトークン>
smtp_server = <SMTPサーバ>
smtp_port = <SMTPサーバのポート番号>
is_ssl = True
```

gmailのパスワード取得については、例えば、[2段階認証に未対応のアプリからのログイン用にアプリパスワードを生成する](https://www.howtonote.jp/google-account/2step-verify/index6.html)を参照のこと。
※セキュリティを下げることになるので、アプリ用のアカウントを使ってそれで試した方が良いかもしれない。

gmailだと、SMTPサーバは、smtp.gmail.com STMPポートは465で動作することを確認している。

## 実行コマンド
```
cd <sample_check_due_date.pyがあるディレクトリ>
python sample_check_due_date.py エクセルパス シート名 送信元メールアドレス 送信先メールアドレス・・・ 
```

例えば、エクセルパスがcheck_due_date.xlsx、シート名がSheet1で、送信元メールアドレスがsystem@example.com、送信先メールアドレスがuserA@example.com, userB@example.comにメールを送信する場合

```
python sample_check_due_date.py check_due_date.xlsx Sheet1 system@example.com userA@example.com userB@example.com
```

## OUTPUT　エクセルファイル
![output_excel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513217/30f7122b-99e9-44c9-d302-a9b908d57993.png)


## メール
以下のようなメールが案件ごとに受信できる。
![sample_mail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513217/abe7a9e0-b830-f20f-3770-8630db545938.png)