領収書、請求書、見積書を LINEで送るだけ自動処理!スモールビジネスの経理を楽にするシステム

こんにちは!
九保すこひです(フリーランスのITコンサルタント、エンジニア)

さてさて、このところローカルAIに感動していろいろとシステムを試していますが、社外秘を全面に押し出していたので、やっていないことがありました。

それは、もちろん・・・・・・

インターネットとの連携

です。

というのも、あるYouTube動画を見たときに出演してた方が

AIって日常使いする場所にないと使ってもらえないんですよね!

とおっしゃっていたからです。

そして、日常使いと聞いて1番に思いつくのはLINEです。

ということで、アイデアを考えていたら

LINEで領収書を送れば、自動処理するシステム

がつくってみたくなりました。

ということで、今回は次のような人に向けて記事を書いています。

  • 「経理のために毎月時間をムダにしている気がする」
  • 「LINEで完結すると聞くと『いや、それなら使うかも』と感じた」
  • 「経理は重要だけど、やりたくない仕事のトップだと思う」
  • 「外注しても戻ってくる確認作業で疲弊してる」
  • 「時間単価が高い人が経理処理までしてて、もったいない」
  • 「LINEなら開くけど会計ソフトは開かないと思う」
  • 「経理はAIに任せて、本業を伸ばす時代が来たと感じている」

「同級生とカラオケいっても、
Switch2でゲームばっかしてます😂」

前提として

今回は以下3つを使って実装します。
先に準備をしておいてください。

  • LINE Messaging API:アクセストークン&ウェブフック
  • ngrok:ウェブフックをローカルに転送
  • Ollama:localhost:11434で動いてる想定

処理をする流れ

今回は以下の流れで処理を自動化します。

  1. LINEに{領収書、請求書、見積書}を送信
  2. LINEからのウェブフックをngrokでローカルに転送
  3. FastAPIでデータを受けとる
  4. PDFをダウンロードし、Doclingでテキスト抽出
  5. Ollama(gemma4n:e4b)でデータ加工
  6. 電子帳簿保存法に対応するファイル名で、特定フォルダへ振り分け

ちょっと長く感じるかもしれませんが、FastAPIは1ファイルで実装できます。
楽しんでやっていきましょう!

FastAPI部分をつくる

では、いきなりコード部分です。
まずは必要なパッケージのインストールです。

pip install fastapi uvicorn line-bot-sdk docling requests

次に、適当なフォルダをつくってmain.pyとして保存してください。

main.py

import os
import re
import json
import requests
import tempfile
from datetime import datetime
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import PlainTextResponse
from docling.document_converter import DocumentConverter

app = FastAPI()
converter = DocumentConverter()

LINE_CHANNEL_ACCESS_TOKEN = "(ここにLINEのチャネルアクセストークン)"
LINE_CONTENT_API = "https://api-data.line.me/v2/bot/message/{message_id}/content"
OLLAMA_API_URL = "http://localhost:11434/api/generate"

TYPE_ITEMS = {
"estimate": {"label": "見積書", "folder": "estimate"},
"invoice": {"label": "請求書", "folder": "invoice"},
"receipt": {"label": "領収書", "folder": "receipt"},
}

OUTPUT_DIRS = {
key: value["folder"] for key, value in TYPE_ITEMS.items()
}

for dir_name in OUTPUT_DIRS.values():
os.makedirs(dir_name, exist_ok=True)


@app.post("/webhook", response_class=PlainTextResponse)
async def webhook(request: Request):
body = await request.json()
events = body.get("events", [])
if not events:
return "OK"

event = events[0]
if event.get("type") != "message":
return "OK"

message = event.get("message", {})
message_type = message.get("type")
message_id = message.get("id")
file_name = message.get("fileName")

if message_type != "file":
return "OK"

if not message_id:
raise HTTPException(status_code=400, detail="message id not found")

pdf_bytes = download_line_content_sync(message_id)
md_text = convert_pdf_bytes_to_markdown(pdf_bytes)
llm_reply = call_ollama_gemma(md_text)

try:
parsed = extract_json(llm_reply)
except (json.JSONDecodeError, ValueError):
print("Failed to parse JSON from LLM")
return "OK"

save_path = save_pdf_by_type(pdf_bytes, parsed, file_name)
print(f"Saved to: {save_path}")
return "OK"


def download_line_content_sync(message_id: str) -> bytes:
url = LINE_CONTENT_API.format(message_id=message_id)
headers = {"Authorization": f"Bearer {LINE_CHANNEL_ACCESS_TOKEN}"}
resp = requests.get(url, headers=headers)
resp.raise_for_status()
return resp.content


def convert_pdf_bytes_to_markdown(data: bytes) -> str:
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=True) as tmp:
tmp.write(data)
tmp.flush()
result = converter.convert(tmp.name)
return result.document.export_to_markdown()


def call_ollama_gemma(md_text: str) -> str:
print("Processing PDF text with Ollama...")
prompt = f"""PDFから抽出したテキストを以下フォーマットで出力してください。
{{
"company_name": "(会社名)",
"date": "(発生日:YYYY/MM/DD)",
"total_amount": "(合計金額)",
"type": "document type (estimate / invoice / receipt)",
}}

【PDFから抽出したテキスト】
{md_text}"""
payload = {
"model": "gemma3n:e4b",
"prompt": prompt,
"stream": False,
}
resp = requests.post(OLLAMA_API_URL, json=payload, timeout=120)
resp.raise_for_status()
data = resp.json()
return data.get("response", "")


def extract_json(text: str) -> dict:
match = re.search(r"```(?:json)?\s*\n?(\{.*?\})\s*```", text, re.DOTALL)
if match:
json_str = match.group(1)
else:
brace_match = re.search(r"\{.*\}", text, re.DOTALL)
if brace_match:
json_str = brace_match.group(0)
else:
raise ValueError("JSON not found")

return json.loads(json_str)


def save_pdf_by_type(pdf_bytes: bytes, parsed: dict, original_name: str) -> str:
doc_type = parsed.get("type", "").strip().lower()

if doc_type not in TYPE_ITEMS:
print(f"Unknown type: {doc_type}")
return "OK"

meta = TYPE_ITEMS[doc_type]
suffix = meta["label"]
folder = OUTPUT_DIRS[doc_type]

date_str = parsed.get("date", "")
match = re.search(r"\d{4}/\d{1,2}/\d{1,2}", date_str)
ymd = match.group(0).replace("/", "") if match else datetime.now().strftime("%Y%m%d")

company = parsed.get("company_name", "").strip() or "Unknown"
amount = re.sub(r"[^0-9]", "", parsed.get("total_amount", "").strip()) or "0"
safe_company = re.sub(r'[\\/:*?"<>|]', "_", company)

filename = f"{ymd}_{safe_company}_{amount}_{suffix}.pdf"
filepath = os.path.join(folder, filename)

with open(filepath, "wb") as f:
f.write(pdf_bytes)

return filepath

なお、LINEのアクセストークンは「チャネル名 > Messaging API設定」のチャネルアクセストークンで取得できます。

すべてを起動して連携できるようにする

では、大きく以下3つが連携してシステムを動かしてみましょう。

  • ngrok
  • LINEウェブフック
  • Ollama

ngrokはインストールしたら以下コマンドで簡単に起動することができます。

ngrok http 8000

すると、ngrokのサブドメインを割り当ててくれますので、これをLINEウェブフックに設定します。

たとえば、Forwardingに表示されたURL

https://abcdefgh123.ngrok-free.app

だったとしたら、LINEウェブフックに設定するのは、

https://abcdefgh123.ngrok-free.app/webhook

となります。

これでLINEチャネルにメッセージが来たら、ローカルパソコンにデータ転送してくれるようになります。

※自動応答が初期状態で有効になっていると思うので、開発するときはOFFにしといたほうがいいです。超ウザいので(笑)

では、続いてFastAPIです。
以下コマンドを実行してください。

uvicorn main:app --reload --port 8000

これで、LINEngrokFastAPIが繋がりました。

では最後にFastAPIからアクセスすることになるOllamaです。
今回はOpen WebUIdocker compose)で起動します。

docker compose up -d

docker composeの構成は以下ページと同じです(Qdrantは不要)

参考ページ:オフラインで完結する「社内マニュアル」AIチャットをつくってみた!

また、使用するモデルは軽量なのに賢いGoogle製の「gemma3n:e4b」を使います。以下でインストールできます。

docker exec -it $(docker ps -qf "name=ollama") ollama pull gemma3n:e4b

これで準備は完了です!

テストしてみる

では、テスト用のPDFを送信して、うまくいくか見てみましょう!

すると、ウェブフック→ngrok経由でFastAPIが動き出しました!

十数秒ほど時間が経って・・・・・・

はい!
処理が終わったと表示がでました😊

実際にフォルダを見てみましょう!

はい!

ちゃんとestimateのフォルダに入ってますし、ファイル名は「20251218_九保すこひ_67897_見積書.pdf」と、電子帳簿保存法に対応すべく検索しやすいファイル名になっています。

すべて成功です😊✨

企業様へのご提案

今回のように、日常使いしているアプリとローカルのAIを連携させることで業務の効率化、人的コストの削減につなげることができます。

これからは、電子的なシステムだけでなく全てのものが「AIを前提とした構築をするようになる」とも言われており、ライバル企業との競争にも影響するかと思います。

もしローカルAIを使ったご相談がありましたら、お気軽にお問い合わせからご連絡ください。

お待ちしております😊✨

おわりに

ということで、今回はLINEngrokFastAPIOllamaという結構なロングジャーニーなシステム構築をしてみました。

当初はDifyのウェブフック・トリガーを使えばローコードでもいけるかなと思ってたんですが、どうやらファイルを保存する機能はDifyにはない(コードの実行でもダメみたいです)ので、やはりAIとの連携をするには、まだまだプログラムが必要なんだなと再認識しました。

正直なところ、FastAPIのコードは生成AIにつくってもらいましたが、山ほど修正&リファクタリングしてますし、これからの時代はそこが価値になっていきそうな気がしてます。

となると、1行ずつコードを書いてた経験は、結構有利なんじゃないかなとも考えてます(どうでしょうか…🤔)

ホントに変化の早い時代に生きていますが、いつか逆に「ほぼ変化なんてしない世界」が来たりするんでしょうか。うーん…。

ではでは〜!

「冷凍たこ焼き+松茸のお吸い物
で、明石焼きとして食べてます👍」

このエントリーをはてなブックマークに追加       follow us in feedly  
お問い合わせ、お待ちしております。
開発のご依頼はこちら: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ