Accessの一覧フォームで「PDF作成→Outlookメール送信」を1件ずつ手作業で繰り返していませんか。90件以上になるとクリックとコピー&ペーストだけで時間もミスも増えがちです。この記事では、一覧の複数行を順番に処理してPDF出力とメール送信を自動化する、現実的で運用が安定しやすい作り方をまとめます。
やりたいことを整理:一覧フォームの各行を「PDF→メール」を連続実行したい
相談で多いパターンは、連続フォーム(一覧)に「顧客へ送る明細(ステートメント)」が並んでいて、現状は次の流れを何十回も繰り返している状態です。
- 一覧の行から電話番号(実質、顧客IDのようなキー)をコピー
- 「E-mail PDF」ボタンを押す
- ポップアップの入力欄に貼り付ける
- Outlookの送信メールが開くので送信する
これを「ボタン1回で、一覧の各行を順にPDF出力→メール送信→次の行…」と回したい、という要望です。結論から言うと実現できます。ただし、設計を間違えると「途中で止まる」「二重送信」「どれが送れたか分からない」が起きやすいので、作り方の型を押さえるのが重要です。
結論:一括処理は“ループ”が必要。マクロだけで完結は厳しく、VBAが現実的
Accessのマクロ(UIマクロ)は、単発の操作(フォームを開く・ボタンを押す・クエリを実行する等)は得意です。一方で「複数件を順番に処理する」「途中エラーでも止めずに次へ進む」「条件分岐でスキップする」「ログを残す」など、一括送信に必須の制御は、VBAのほうが圧倒的に扱いやすいです。
| 手段 | 向いている範囲 | 強み | 弱点(大量処理で詰まりやすい点) |
|---|---|---|---|
| Accessマクロ(ボタンに設定) | 単発のPDF出力、単発のメール作成 | 作りやすい/変更しやすい | 繰り返し処理・例外処理・詳細ログが作りにくい |
| VBA(プロシージャ) | 複数件の連続処理、一括送信、条件分岐、ログ | ループ・エラー制御・DB更新・Outlook制御が自由 | 設計が雑だと保守が辛い(だからこそ型が大事) |
| マクロ+VBA(RunCode等) | 操作はマクロ、実体はVBAで一括送信 | UIは簡単、処理は堅牢にできる | 「見た目はマクロ」でも中身の品質はVBA次第 |
おすすめは、既存の「1件送信(現状の仕組み)」は残しつつ、別ボタンで「一括送信」をVBAで用意する方法です。既存を壊さずに追加でき、テストもしやすく、運用が安定します。
安全に一括送信するための設計:最初に“送信キュー”と“送信済み管理”を作る
一括送信で一番痛いのは「どれを送ったか分からない」「エラーで止まった後に再実行したら二重送信」「メールアドレスが空の行も巻き込んで止まる」です。これを避けるには、送信対象の抽出と送信結果の記録を、最初から設計に入れておくのが近道です。
送信対象をクエリで抽出する
まずは「送るべき明細だけ」を確実に抽出するクエリを作ります。例えば次のような条件です。
- メールアドレスがNull/空文字ではない
- 未送信フラグがTrue(または送信日時がNull)
- 対象期間や締日など、業務ルールに沿う
例:送信キュー用クエリ(概念)
SELECT
S.StatementID,
S.CustomerKey, -- 電話番号や顧客IDなど
C.EmailAddress,
S.StatementDate,
S.SendFlag
FROM
T_Statement AS S
INNER JOIN T_Customer AS C
ON S.CustomerKey = C.CustomerKey
WHERE
(C.EmailAddress Is Not Null)
AND (C.EmailAddress <> "")
AND (S.SendFlag = False);
上のSQLは概念なので、実際のテーブル名・項目名に合わせて調整してください。ポイントは「このクエリ結果=送ってよいもの」という状態を作ることです。
送信状態とログを残す項目(またはテーブル)を用意する
最低限、次のどちらかは入れておくと運用が安定します。
- 送信フラグ(True/False)+送信日時
- ログテーブル(成功/失敗、エラー内容、PDFパス、宛先など)
| 管理方法 | 入れておくと強い項目 | おすすめ度 | 理由 |
|---|---|---|---|
| 明細テーブルに送信状態を持つ | SendFlag(Yes/No), SentAt(Date/Time) | 高 | 再実行しても未送信のみ処理できる |
| ログテーブルを別に持つ | Status, ErrorNumber, ErrorDescription, PdfPath, EmailAddress, CreatedAt | 非常に高 | 失敗だけを抽出して再送・原因特定ができる |
「90〜100件を送ることがある」なら、ログテーブルはほぼ必須です。送信の成否が見えるだけで、運用のストレスが一気に下がります。
実装の基本フロー:Recordsetで1件ずつ回し、PDF出力→Outlook送信→送信済み更新
一括送信の処理は、次の型で考えるとブレません。
- 送信対象(キュー)をクエリで抽出
- DAO.Recordsetで開く
- Do While Not rs.EOF で1件ずつループ
- キー(電話番号/顧客ID)でレポートを絞り込みPDF出力
- Outlookを自動操作して宛先・件名・本文・添付をセットし送信
- 送信済みフラグ更新+ログ記録
- 次のレコードへ
ここからは、WordPressに貼ってそのまま使える形で、実務で詰まりやすい点も織り込みながらサンプルを示します(テーブル・レポート名は適宜読み替えてください)。
PDF出力の実装:レポートを“対象レコードで絞ってから”OutputToする
AccessでPDFを安定して作るには、「対象レコードで絞った状態でレポートを開く」→「OutputToでPDF保存」→「レポートを閉じる」という順番が安全です。いきなりOutputToだけを呼ぶと、意図しない範囲のデータが出たり、フィルタが効かないことがあります。
保存フォルダーの基本ルール(ミスが多いポイント)
| 落とし穴 | 起きる現象 | 対策 |
|---|---|---|
| パスの区切り「\」が抜ける | C:\TempStatements123.pdf のように連結され失敗する | フォルダー末尾に必ず「\」を付ける/結合関数を作る |
| 同名PDFで上書き | 別顧客のPDFが上書きされる | 顧客キー+明細ID+日時でファイル名を一意化 |
| ファイル名に禁止文字 | 出力エラー | 「:」「/」「\」などを含めない(日時はyyyymmdd_hhnnss等) |
PDF出力(サンプル関数)
レポート名を rptStatement とし、電話番号や顧客IDに相当するキーを CustomerKey、明細の主キーを StatementID とします。
Option Compare Database
Option Explicit
' 顧客キーと明細IDで対象を絞り込み、PDFを出力する
Public Function ExportStatementPdf(ByVal customerKey As Variant, ByVal statementId As Long, ByVal pdfPath As String) As Boolean
On Error GoTo EH
Dim whereCond As String
' customerKey が文字列(電話番号をTextで持つ等)の場合を想定してクォート
' 数値キーなら、SqlQuote を使わず "CustomerID=" & customerId でOK
whereCond = "CustomerKey=" & SqlQuote(customerKey) & " AND StatementID=" & statementId
' レポートを非表示で開く → 出力 → 閉じる
DoCmd.OpenReport "rptStatement", acViewPreview, , whereCond, , acHidden
DoCmd.OutputTo acOutputReport, "rptStatement", acFormatPDF, pdfPath, False
DoCmd.Close acReport, "rptStatement", acSaveNo
ExportStatementPdf = (Len(Dir$(pdfPath)) > 0)
Exit Function
EH:
On Error Resume Next
DoCmd.Close acReport, "rptStatement", acSaveNo
ExportStatementPdf = False
End Function
' 文字列をSQL用にシングルクォートで囲み、シングルクォートをエスケープする
Public Function SqlQuote(ByVal v As Variant) As String
SqlQuote = "'" & Replace(Nz(v, ""), "'", "''") & "'"
End Function
電話番号をキーにしている場合、先頭ゼロやハイフンの扱いで事故が起きやすいので、DB上はText型で持ち、検索条件も文字列で合わせる方が安定します(「090…」が数値扱いで「90…」になる、といった事故を避けられます)。
Outlookメール送信の実装:最初は「表示して確認」→慣れたら自動送信
Outlookを自動操作する方法は、本文や添付の制御がしやすく、テンプレート化もしやすいです。一方で、環境によってはセキュリティ警告や送信制限が出る場合があります。いきなり自動送信にせず、最初は下書き表示(Display)で確認運用にして、問題がないことを確認してからSendに切り替えるのが無難です。
Outlook送信(サンプル関数)
' Outlookでメールを作成して送信(previewOnly=True のときは表示のみ)
Public Function SendMailWithOutlook(ByVal mailTo As String, ByVal subjectText As String, ByVal bodyText As String, ByVal attachPath As String, ByVal previewOnly As Boolean) As Boolean
On Error GoTo EH
Dim olApp As Object
Dim mailItem As Object
' 既にOutlookが起動していれば掴む(失敗したら新規起動)
On Error Resume Next
Set olApp = GetObject(, "Outlook.Application")
On Error GoTo EH
If olApp Is Nothing Then
Set olApp = CreateObject("Outlook.Application")
End If
Set mailItem = olApp.CreateItem(0) ' 0 = MailItem
With mailItem
.To = mailTo
.Subject = subjectText
.Body = bodyText
.Attachments.Add attachPath
If previewOnly Then
.Display
Else
.Send
End If
End With
SendMailWithOutlook = True
Exit Function
EH:
SendMailWithOutlook = False
End Function
本文をHTMLにしたい場合は .Body ではなく .HTMLBody を使います。署名を自動で入れたい場合はOutlook側設定との兼ね合いもあるので、最初はプレーンテキストで通し、落ち着いたらHTML化する流れが安全です。
一括送信の本体:送信キューを回して、PDF→メール→送信済み更新→ログ
ここがメインです。送信対象クエリ(例:qStatementSendQueue)をRecordsetで回し、1件ずつ処理します。
事前に用意するもの(最低限)
- 送信対象クエリ:qStatementSendQueue(StatementID, CustomerKey, EmailAddress などを返す)
- レポート:rptStatement(明細を出力する)
- 送信状態:T_StatementのSendFlag(Yes/No)やSentAt(日時)
- (推奨)ログテーブル:T_MailSendLog
一括送信プロシージャ(サンプル)
Public Sub SendStatements_Bulk(Optional ByVal previewOnly As Boolean = True)
On Error GoTo EH
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim folderPath As String
Dim pdfPath As String
Dim stmtId As Long
Dim customerKey As Variant
Dim mailTo As String
Dim totalCount As Long
Dim currentIndex As Long
Set db = CurrentDb
folderPath = EnsureFolder(Environ$("TEMP") & "\Statements\")
Set rs = db.OpenRecordset("qStatementSendQueue", dbOpenSnapshot)
' 件数把握(ステータス表示用)
If Not (rs.BOF And rs.EOF) Then
rs.MoveLast
totalCount = rs.RecordCount
rs.MoveFirst
End If
currentIndex = 0
SysCmd acSysCmdSetStatus, "一括送信の準備中..."
Do While Not rs.EOF
currentIndex = currentIndex + 1
SysCmd acSysCmdSetStatus, "処理中 " & currentIndex & "/" & totalCount
stmtId = rs!StatementID
customerKey = rs!CustomerKey
mailTo = Nz(rs!EmailAddress, "")
' メールアドレスが無いものはスキップしてログ
If Len(mailTo) = 0 Then
WriteSendLog stmtId, customerKey, mailTo, "", "SKIP", 0, "メールアドレスが空です"
GoTo NextRecord
End If
' PDFファイル名を一意化(顧客キー/明細ID/日時)
pdfPath = folderPath & "Statement_" & SafeFileToken(customerKey) & "_" & stmtId & "_" & Format(Now, "yyyymmdd_hhnnss") & ".pdf"
' PDF出力
If ExportStatementPdf(customerKey, stmtId, pdfPath) = False Then
WriteSendLog stmtId, customerKey, mailTo, pdfPath, "FAIL", Err.Number, "PDF出力に失敗しました"
GoTo NextRecord
End If
' メール送信(最初は previewOnly=True を推奨)
If SendMailWithOutlook(mailTo, BuildSubject(customerKey, stmtId), BuildBody(customerKey, stmtId), pdfPath, previewOnly) Then
' 送信済み更新(previewOnly の場合は更新しない運用も可)
If previewOnly = False Then
MarkStatementAsSent stmtId
WriteSendLog stmtId, customerKey, mailTo, pdfPath, "SENT", 0, ""
Else
WriteSendLog stmtId, customerKey, mailTo, pdfPath, "PREVIEW", 0, ""
End If
Else
WriteSendLog stmtId, customerKey, mailTo, pdfPath, "FAIL", Err.Number, Err.Description
End If
NextRecord:
rs.MoveNext
DoEvents
Loop
Cleanup:
SysCmd acSysCmdClearStatus
On Error Resume Next
rs.Close
Set rs = Nothing
Set db = Nothing
Exit Sub
EH:
SysCmd acSysCmdClearStatus
MsgBox "一括送信処理でエラーが発生しました: " & Err.Description, vbExclamation
Resume Cleanup
End Sub
補助関数(フォルダー作成・ファイル名整形・件名/本文・送信済み更新)
' フォルダーが無ければ作成して末尾に "\" を付けて返す
Public Function EnsureFolder(ByVal folderPath As String) As String
If Right$(folderPath, 1) <> "\" Then folderPath = folderPath & "\"
If Dir$(folderPath, vbDirectory) = "" Then MkDir folderPath
EnsureFolder = folderPath
End Function
' ファイル名に入れても壊れにくいトークンにする(最低限の置換)
Public Function SafeFileToken(ByVal v As Variant) As String
Dim s As String
s = Nz(v, "")
s = Replace(s, "", "*")
s = Replace(s, "/", "*")
s = Replace(s, ":", "*")
s = Replace(s, "*", "*")
s = Replace(s, "?", "*")
s = Replace(s, """", "*")
s = Replace(s, "<", "*")
s = Replace(s, ">", "*")
s = Replace(s, "|", "_")
SafeFileToken = s
End Function
' 件名の例(必要に応じて顧客名や期間を付与)
Public Function BuildSubject(ByVal customerKey As Variant, ByVal statementId As Long) As String
BuildSubject = "ご利用明細(ステートメント)送付のご連絡"
End Function
' 本文の例(必要に応じて差し込み)
Public Function BuildBody(ByVal customerKey As Variant, ByVal statementId As Long) As String
BuildBody = "お世話になっております。" & vbCrLf & _
"添付にてご利用明細(PDF)をお送りいたしますのでご確認ください。" & vbCrLf & _
"ご不明点がありましたらご連絡ください。" & vbCrLf & vbCrLf & _
"※本メールは送信専用です。"
End Function
' 送信済みフラグ更新(例:T_Statement に SendFlag と SentAt がある想定)
Public Sub MarkStatementAsSent(ByVal statementId As Long)
CurrentDb.Execute "UPDATE T_Statement SET SendFlag=True, SentAt=Now() WHERE StatementID=" & statementId, dbFailOnError
End Sub
上記は「型」です。特に 送信対象クエリ → ループ → PDF → Outlook → 更新/ログ を崩さないと、後で必ず楽になります。
つまずきやすいポイントと対策:ここを押さえると“止まらない一括送信”になる
Where条件のクォート問題(電話番号キーがTextかNumericか)
電話番号をキーにしている場合、Access上はText型で持つことが多いです。その場合、Where条件は シングルクォートが必要 です。逆に、顧客IDが数値(Long)ならシングルクォートは不要です。
| キーの型 | Where条件の例 | 注意点 |
|---|---|---|
| 数値(Long等) | CustomerID=123 | 数値として一致する |
| 文字列(Text:電話番号など) | TelNo=’09012345678′ | 先頭ゼロ・ハイフン・空白の揺れに注意 |
「コピーして貼り付ける電話番号」にハイフンが入るなら、DB側も同じ形式に統一するか、送信側でハイフンを除去して一致させるなど、キーの表現揺れを潰しておくと検索ミスが減ります。
OutputToが不安定に見える問題(レポートの開き方)
PDF出力が「たまに別の人の明細になる」「絞り込みが効いていない」と感じる場合、次の原因が多いです。
- レポートをフィルタ付きで開かずにOutputToしている
- レポート側のRecordSourceが別の条件で固定されている
- フォームのフィルタやTempVarsの値が想定と違う
対策はシンプルで、OpenReport時点でWhereConditionを確実に渡すことです。さらに確実性を上げたいなら、レポートのRecordSourceを「StatementIDで1件に絞れるクエリ」にしておき、WhereConditionはStatementIDだけに寄せるのも有効です。
Outlookのセキュリティ警告・ブロック
Outlook自動送信は、端末のポリシーやセキュリティ製品によって挙動が変わります。現場での落としどころは次の通りです。
- テスト時は .Send ではなく .Display で下書きを出し、人の目で確認して送る
- 本番で自動送信に切り替える場合も、最初は少数件で段階的に
- 社内規定で自動送信が禁止されている場合は、下書き運用を前提にする
大量処理で固まったように見える問題(画面更新・DoEvents・ステータス表示)
90〜100件を回すと、Accessが「応答なし」に見える瞬間が出ることがあります。実際は処理しているだけ、というケースも多いので、次の工夫が効きます。
- SysCmd(acSysCmdSetStatus) で進捗をステータスバーに出す
- ループ内で DoEvents を呼び、UIを固めにくくする
- 不要なフォーム/レポート表示を避け、acHidden で開く
ログ設計:成功/失敗の見える化で“再送が怖くなくなる”
一括送信は「100件中、3件だけ失敗」みたいなことが普通に起きます。そこでログがあると、失敗分だけ拾って再送できます。ログテーブルは次のような構成が扱いやすいです。
| 列名例 | 型 | 用途 |
|---|---|---|
| LogID | AutoNumber | ログ主キー |
| StatementID | Long | どの明細か |
| CustomerKey | Text | どの顧客か(電話番号キー等) |
| EmailAddress | Text | 宛先 |
| PdfPath | Text | 添付ファイルの保存先 |
| Status | Text | SENT / PREVIEW / FAIL / SKIP など |
| ErrorNumber | Long | エラー番号 |
| ErrorDescription | LongText | エラー内容 |
| CreatedAt | Date/Time | 処理日時 |
ログ書き込み用の関数を作っておくと、実装がきれいに保てます。
Public Sub WriteSendLog(ByVal statementId As Long, ByVal customerKey As Variant, ByVal mailTo As String, ByVal pdfPath As String, ByVal status As String, ByVal errNo As Long, ByVal errDesc As String)
Dim sql As String
sql = "INSERT INTO T_MailSendLog (StatementID, CustomerKey, EmailAddress, PdfPath, Status, ErrorNumber, ErrorDescription, CreatedAt) " & _
"VALUES (" & statementId & ", " & SqlQuote(customerKey) & ", " & SqlQuote(mailTo) & ", " & SqlQuote(pdfPath) & ", " & SqlQuote(status) & ", " & errNo & ", " & SqlQuote(errDesc) & ", Now());"
CurrentDb.Execute sql, dbFailOnError
End Sub
これがあるだけで、次のようなクエリが作れるようになります。
- FAILだけ抽出して再送する
- SKIP(メールアドレス無し)だけ抽出してマスタ修正する
- PREVIEWで作った下書きの件数を確認する
「一覧の表示行だけ送りたい」「チェックした行だけ送りたい」を叶える方法
実務では「未送信全部」ではなく、今画面に出ている分だけ、あるいはチェックした分だけ送りたいケースもあります。これもVBAなら対応可能です。
今フォームに表示されているレコードだけを回す(フィルタや並び替えも反映)
連続フォーム/サブフォームの内容をそのまま送信対象にしたい場合、フォームのRecordsetCloneを回す方法が使えます。
' 例:サブフォーム sfrmStatements の表示レコードを回す
Dim rs As DAO.Recordset
Set rs = Me.sfrmStatements.Form.RecordsetClone
If Not (rs.BOF And rs.EOF) Then
rs.MoveFirst
Do While Not rs.EOF
' rs!StatementID などで処理
rs.MoveNext
Loop
End If
「今見えている行」=「今送りたいもの」と一致する運用なら、この方式が直感的です。
チェックボックスで選択送信したい場合(おすすめは一時テーブル方式)
チェックボックスで複数選択して一括送信したい場合、実装は大きく2つあります。
- 一時テーブル(T_SendWork等)に選択したStatementIDを貯め、そこから送信キューを作る
- VBAで選択IDをCollectionに集め、そのまま回す
大量処理・再実行・ログ連携まで考えると、一時テーブル方式が運用に強いです。送信前に「今回の対象一覧」をクエリで確認できるのも利点です。
既存の「1件送信」仕組みを活かすコツ:単発送信関数を“呼び出せる形”に整える
既に「E-mail PDF」ボタンで1件送れる仕組みがあるなら、そこを壊さず、内部処理を関数化して一括送信から呼べる形にしていくのが安全です。よくある改善パターンは次の通りです。
- 現在のボタン処理から「PDF出力」部分を ExportStatementPdf に分離
- 「Outlookメール作成」部分を SendMailWithOutlook に分離
- 単発送信はUI入力(ポップアップ)でキーを受け取って上記関数を呼ぶ
- 一括送信はRecordsetからキーを受け取って上記関数を呼ぶ
こうすると「単発も一括も中身は同じ」になり、修正が1か所で済みます。
一括送信を“止めにくくする”実務的な工夫
運用が回り始めると、想定外のデータや端末差分に必ず当たります。次の工夫を入れておくと、止まりにくくなります。
| 工夫 | 効果 | 実装の方向性 |
|---|---|---|
| スキップ条件を明文化 | メール空などで処理が止まらない | Len(mailTo)=0 ならログして次へ |
| エラーを“1件単位”で握りつぶして次へ | 1件の失敗で全体が止まらない | ループ内で On Error を切り替え、失敗ログ後に継続 |
| 送信済み更新は“成功後”にする | 途中失敗時に取りこぼしが出ない | Send成功→UPDATE→ログ |
| 再実行前提で設計 | やり直しが怖くない | 未送信だけ抽出+ログで失敗抽出 |
| PDF保存先をTEMP配下に寄せる | 権限・パスの問題が減る | Environ(“TEMP”) を基準にする |
特に「送信済み更新のタイミング」と「ログ」は、最初に作り込むほど後が楽です。
既存サンプル(完成品に近いコード)を土台にする考え方
ゼロから組むより、請求書/明細をPDF化してメールするサンプルをベースに改造する方が、安定しやすいケースがあります。探すときは次の観点で選ぶと失敗しにくいです。
- 「複数件をループして処理」している(単発サンプルは多い)
- ログやエラー処理がある(実運用で止まりにくい)
- レポート出力とメール送信が分離されている(改造しやすい)
土台を使う場合でも、最終的に自分のDBのテーブル設計(顧客キー、明細ID、送信フラグ)に沿うように、送信キューとログの構造を合わせるのが肝になります。
まとめ:一括送信はVBAで“型”を作ると、90件でも安定して回せる
Accessの一覧フォームから「PDF作成→メール送信」を複数行まとめて自動化することは可能です。ただし、複数件を順番に処理するにはループと例外処理が不可欠で、マクロだけで完結させるより、VBAで一括送信プロシージャを用意する方が現実的です。
送信対象をクエリで抽出し、Recordsetで回し、レポートを絞り込んでPDF出力し、Outlookで送信し、送信済み更新とログを残す。この型に沿って作ると、途中で止まりにくく、失敗分だけ再送できる運用に乗せられます。既存の単発送信機能を残しつつ「一括送信ボタン」を追加する形で進めるのが、安全でおすすめです。

コメント