読み込み中
豆知識
現在の位置づけ: Design Proposal
優先度: Medium
Date: 2026-01-30
Related Plan: auto_journal_entry_creation_fedf17a1.plan.md (Phase 3)
This specification outlines the design for automatically creating settlement journal entries when bank transactions are matched to outstanding payables. This completes the Japanese GAAP 発生主義 (accrual basis) workflow by handling the 決済 (settlement) phase.
1. Expense Claim (経費申請) → Journal Entry: Dr. 費用 | Cr. 役員借入金
2. Payment Statement (支払報告) → Journal Entry: Dr. 役員借入金 | Cr. 未払金
3. Bank Transaction (銀行出金) → Journal Entry: Dr. 未払金 | Cr. 普通預金 ← THIS PHASE
The third step (bank transaction settlement) is currently manual and should be automated when:
Import → AI Categorization → Manual Review → Create Transaction Record
// When bank transaction is matched to outstanding payable
interface BankSettlementEntry {
transactionId: string
linkedDocumentId: string
journalEntry: {
debit: {
accountCode: string // e.g., "2150" (役員借入金) or "2130" (未払金)
accountName: string
amount: number
}
credit: {
accountCode: "1110" // 普通預金
accountName: "普通預金"
amount: number
}
}
}
| Original Liability | Settlement Entry |
|---|---|
| 役員借入金 (Director Loan) | Dr. 役員借入金 | Cr. 普通預金 |
| 未払金 (Accounts Payable) | Dr. 未払金 | Cr. 普通預金 |
| 立替金(従業員) (Employee Advance) | Dr. 立替金(従業員) | Cr. 普通預金 |
POST /bank-transactions/:id/settle
// Request Body
{
"linkedDocumentId": "uuid", // Optional: Link to source document
"liabilityAccountCode": "2150", // Account to debit
"note": "経費精算 - 出張旅費" // Optional: Description
}
// Response
{
"success": true,
"data": {
"settlementId": "uuid",
"journalEntry": {
"id": "uuid",
"transactionDate": "2026-01-30",
"debitAccount": "2150",
"creditAccount": "1110",
"amount": 15000
},
"linkedDocument": {
"id": "uuid",
"documentName": "2026年1月出張経費精算書"
}
}
}
// GET /bank-transactions/:id
{
"id": "uuid",
"transactionDate": "2026-01-30",
"amount": -15000, // Negative = withdrawal
"description": "フリコミ タナカタロウ",
"category": "expense_reimbursement",
"categoryConfidence": 0.85,
// NEW: Settlement info
"settlement": {
"status": "pending" | "settled" | "partial",
"matchedLiabilities": [
{
"accountCode": "2150",
"accountName": "役員借入金",
"outstandingAmount": 15000,
"linkedDocumentId": "uuid",
"matchScore": 0.92 // AI-calculated match confidence
}
],
"journalEntryId": null // Set when settled
}
}
POST /bank-transactions/batch-settle
// Request Body
{
"settlements": [
{
"transactionId": "uuid-1",
"liabilityAccountCode": "2150",
"amount": 15000,
"linkedDocumentId": "uuid-doc-1"
},
{
"transactionId": "uuid-2",
"liabilityAccountCode": "2130",
"amount": 8000
}
]
}
// Response
{
"success": true,
"data": {
"settled": 2,
"failed": 0,
"journalEntries": [
{ "transactionId": "uuid-1", "journalEntryId": "uuid-je-1" },
{ "transactionId": "uuid-2", "journalEntryId": "uuid-je-2" }
]
}
}
-- Find outstanding liabilities that may match a bank transaction
WITH outstanding_payables AS (
SELECT
jel.account_code,
jel.account_name,
SUM(CASE WHEN jel.side = 'credit' THEN jel.amount ELSE -jel.amount END) as balance,
je.source_document_id,
d.document_name,
d.created_at
FROM journal_entry_lines jel
JOIN journal_entries je ON je.id = jel.journal_entry_id
LEFT JOIN documents d ON d.id = je.source_document_id
WHERE jel.account_code IN ('2150', '2130', '2160') -- Liability accounts
AND je.user_id = :user_id
GROUP BY jel.account_code, jel.account_name, je.source_document_id, d.document_name, d.created_at
HAVING SUM(CASE WHEN jel.side = 'credit' THEN jel.amount ELSE -jel.amount END) > 0
)
SELECT * FROM outstanding_payables
ORDER BY balance DESC;
| Factor | Weight | Description |
|---|---|---|
| Amount match | 40% | Exact or close amount match |
| Date proximity | 25% | Bank date close to document date |
| Description match | 20% | Keywords in transaction description |
| Document type | 15% | Matching expense categories |
function calculateMatchScore(
transaction: BankTransaction,
liability: OutstandingLiability
): number {
let score = 0
// Amount match (40%)
const amountDiff = Math.abs(transaction.amount + liability.balance)
const amountRatio =
1 - amountDiff / Math.max(Math.abs(transaction.amount), liability.balance)
score += Math.max(0, amountRatio) * 0.4
// Date proximity (25%)
const daysDiff = Math.abs(
differenceInDays(transaction.date, liability.documentDate)
)
const dateScore = Math.max(0, 1 - daysDiff / 30) // 30-day window
score += dateScore * 0.25
// Description match (20%)
const descriptionScore = calculateDescriptionMatch(
transaction.description,
liability.documentName
)
score += descriptionScore * 0.2
// Document type match (15%)
const typeScore = isExpenseCategory(transaction.category) ? 1 : 0
score += typeScore * 0.15
return score
}
Add a "Settlement" action to matched transactions:
// In bank transaction list item
{
transaction.settlement?.matchedLiabilities.length > 0 && (
<Button
variant="outline-emerald"
size="sm"
onClick={() =>
handleSettle(
transaction.id,
transaction.settlement.matchedLiabilities[0]
)
}
>
<CheckCircle2 className="w-4 h-4 mr-1" />
決済確認
</Button>
)
}
When linking a bank transaction to a document:
async function linkTransactionToDocument(
transactionId: string,
documentId: string
): Promise<void> {
// 1. Create the link
await bankTransactionApi.linkDocument(transactionId, documentId)
// 2. Check for outstanding liability on the document
const liability = await journalApi.getOutstandingLiability(documentId)
// 3. If liability exists and amounts match, offer to settle
if (liability && Math.abs(transaction.amount) === liability.amount) {
const shouldSettle = await confirmDialog({
title: "自動決済",
message: `この取引で「${liability.accountName}」¥${liability.amount.toLocaleString()}を決済しますか?`,
confirmText: "決済する",
cancelText: "後で確認",
})
if (shouldSettle) {
await bankTransactionApi.settle(transactionId, {
linkedDocumentId: documentId,
liabilityAccountCode: liability.accountCode,
})
}
}
}
ALTER TABLE bank_transactions ADD COLUMN IF NOT EXISTS settlement_status VARCHAR(20) DEFAULT 'pending';
ALTER TABLE bank_transactions ADD COLUMN IF NOT EXISTS settlement_journal_entry_id UUID REFERENCES journal_entries(id);
ALTER TABLE bank_transactions ADD COLUMN IF NOT EXISTS linked_document_id UUID REFERENCES documents(id);
-- Index for settlement queries
CREATE INDEX idx_bank_transactions_settlement ON bank_transactions(settlement_status, user_id);
CREATE TABLE IF NOT EXISTS settlement_audit_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
bank_transaction_id UUID NOT NULL REFERENCES bank_transactions(id),
journal_entry_id UUID NOT NULL REFERENCES journal_entries(id),
linked_document_id UUID REFERENCES documents(id),
liability_account_code VARCHAR(10) NOT NULL,
settlement_amount DECIMAL(15, 2) NOT NULL,
settled_by UUID NOT NULL,
settled_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
notes TEXT
);
// 400 Bad Request
{
"error": "INVALID_SETTLEMENT",
"message": "取引金額が未払残高を超えています",
"details": {
"transactionAmount": 20000,
"outstandingBalance": 15000
}
}
// 409 Conflict
{
"error": "ALREADY_SETTLED",
"message": "この取引は既に決済済みです",
"journalEntryId": "uuid"
}
/bank-transactions/journal-entries/documents/:id/linkssrc/services/accrual-first-accounting-service.ts読み込み中
豆知識