読み込み中
豆知識
現在の位置づけ: ✅ CLAIMS ACCRUAL SHIPPED + TRAVEL/PAYMENT INHERITANCE SHIPPED (Wave 9, 2026-04-23) · ✅ FE force-regenerate wired (Wave 9.2, 2026-04-24)
優先度: High
Date: 2026-01-30 (original) · Updated 2026-04-24 (see § 2026-04 Amendment, § Reference Implementation (2026-04-23), and § Wave 9.2 — FE wiring for force=true)
Related plan: auto_journal_entry_creation_fedf17a1.plan.md
Related audit: audit-reports/db-schema-audit-2026-04-23.md
travel and payment documents do not create new journal entries. Instead, on publish, the system reads document_relationships, finds the linked source document (a claims for travel, or claims / salary_slip for payment), pulls its metadata.journalEntry, and copies it onto the current document as metadata.linkedJournalEntry. The FE then renders a "紐付仕訳 JE-…" chip that deep-links into /tax-journal?jeId=….
| Concern | Direct accrual JE | Inheritance |
|---|---|---|
| Source data quality | travel.content.expenses.items[] is freeform JSON (no document_expenses rows in production) |
Inherits already-validated JE from claims |
| Double-booking risk | Travel expenses are usually already captured in a claims doc. Posting twice would inflate 役員借入金 | Zero new JE rows — impossible to double-book |
| Payment timing | payment is a clearing instruction, not an accrual event. The actual clearing JE is created by reconciliation-link.service on bank-feed match |
Inheritance gives the UI a JE link without competing with the bank-feed JE |
| FY2025 safety | Any new historical JE risks invalidating the e-Tax filing | Zero historical writes — verified byte-identical |
20260315_出張報告書_東京ビジネス会合
│ document_relationships.relationship_type = 'reimbursed_by'
▼
20260301-31_立替経費申請書_高原オフィス備品_JPY98300
│ metadata.journalEntry.id = 1823fbc6-...
│ metadata.journalEntry.entryNumber = JE-20260423-CL001
│ metadata.journalEntry.totalAmount = 98300
▼
(on travel publish) journal.service.generateForTravelDocument
│ reads document_relationships
│ finds linked claims, copies its metadata.journalEntry
▼
20260315_出張報告書.metadata.linkedJournalEntry = {
id: '1823fbc6-...',
entryNumber: 'JE-20260423-CL001',
sourceDocumentId: '<claims-doc-id>',
sourceDocumentTitle: '20260301-31_立替経費申請書_高原オフィス備品_JPY98300',
inheritedAt: '2026-04-23T...'
}
The FE <JournalEntryChip linkedJournalEntry={…}/> renders as: 紐付仕訳 JE-20260423-CL001 → (muted style, opens /tax-journal?jeId=1823fbc6-…).
payment documents look at both claims and salary_slip linked sources, preferring the most recent JE. If the linked claims doc has JE-20260423-CL001 and a sibling salary doc has JE-20260425-PS001, payment inherits whichever has the larger entryNumber lexicographic value (i.e. the most recent).
Both generators are no-ops on re-publish:
metadata.linkedJournalEntry.id already equals the source's metadata.journalEntry.id, the generator returns early.salary_slip generator's existing filter was widened to reference_type IN ('pay_slip', 'salary_slip') so legacy pay_slip rows in production data don't cause duplicate JE creation.backend: src/services/journal.service.ts — generateForTravelDocument(), generateForPaymentDocument(), widened reference_type filter in generateForSalarySlipDocument().backend: src/services/document.service.ts — confirmCreateDocument() enqueues GENERATE_DRAFT_JOURNAL for TRAVEL and PAYMENT document types.frontend: src/components/ui/journal-entry-chip.tsx — canonical primitive that handles both journalEntry (own JE) and linkedJournalEntry (inherited).frontend: src/components/ui/document-pipeline-ribbon.tsx — generalised ribbon with documentType prop ("claims" | "travel" | "payment" | …).frontend: src/features/travel-expense/components/travel-expenses.tsx — renders <DocumentPipelineRibbon documentType="travel" /> at top.frontend: src/features/document/components/react-renderers/payment-statement-react-renderer.tsx — renders <DocumentPipelineRibbon documentType="payment" /> at top.追加のマイグレーションと出張・支払書類の公開の前後で、仕訳の件数と借方合計が変わらない(バイト単位で同一)ことを確認しています。
On 2026-04-23 we cut over to J-GAAP accrual for claims documents. The first accrual JE under the new logic is:
| Field | Value |
|---|---|
| Entry number | JE-20260423-CL001 |
| Entry date | 2026-03-30 (max expense_date of the 8 lines) |
| Reference type | claims |
| Reference document | 20260301-31_立替経費申請書_高原オフィス備品_JPY98300 |
| Status | posted |
| Total Dr | ¥98,300 (= 16 lines: 8 × expense + 8 × 仮払消費税) |
| Total Cr | ¥98,300 (= 8 lines × 役員借入金 238) |
| Linked docs | → 20260425_給与明細書_経費精算_JPY98300 → 20260425_支払明細書_経費精算_JPY98300 |
The JE appears in 仕訳帳 (tax-journal) as a single composite row:
2026/03/30 消耗品費・仮払消費税 (16) ¥98,300 役員借入金 (8) ¥98,300
立替経費計上: 20260301-31_立替経費申請書_高原オフィス備品_JPY98300
転記済 JE-20260423-CL001
Per-line breakdown (Dr side):
| Account | Amount | Category |
|---|---|---|
| 消耗品費 (510) | ¥16,791 | 折りたたみかご・ヨガマット・ジョイントマット |
| 備品費 (515) | ¥69,101 | プライオボックス・バーベル・ミラー・電子レンジ |
| 通信費 (508) | ¥11,800 | Apple Developer Program |
| 仮払消費税 (131) | ¥9,770 | 10% × 上記合計 |
| Total Dr | ¥98,300 |
Cr side: 役員借入金 (238) × 8 lines = ¥98,300 (one line per expense_date bucket, pointing to the source expense.id via reference_id).
Before enhancement (pre-2026-04-23 baseline): FY2025 第1期 (2025/06/06–2026/02/15) = 158 JEs, ¥98,289,899 total Dr, all created_at ≤ 2026-04-14.
After enhancement: identical — no new rows with created_at ≥ 2026-04-23 in the FY2025 date range. The cut-over JE (1823fbc6-…) has entry_date = 2026-03-30 which is in FY2026 第2期 (2026-02-16 → 2027-02-15), so FY2025 remains byte-identical with what was e-Taxed on 2026-04-15.
backend: src/services/journal.service.ts — generateForClaimsDocument() rewritten to credit 役員借入金 (238), use per-line expense_date, and enforce assertPeriodNotLocked() for each distinct date. Cross-path dedup via reference_document_id + reference_type='claims'. v6.1 (2026-04-23): after JE creation, the service now writes documents.metadata.journalEntryId and documents.metadata.journalEntryNumber back-references (mirrors the sales_invoice.journal_entry_id pattern) so the FE 仕訳チップ renders instantly on publish.backend: src/services/document.service.ts — getDocuments() and getDocumentById() now enrich responses with content.linkedDocuments (from document_relationships), metadata.isSettled (from document_transaction_links), metadata.journalEntry (bulk lookup by journal_entries.reference_document_id), and metadata.expenseAttachmentsResolved (file-id → {fileName, fileUrl, fileType} per expense line). These fix the UI bugs where 立替経費 showed 未清算, linkage badges were grey, and the 添付ファイル modal was empty.backend: src/common/prompts/journal-entry-generator.ts — bank-feed reimbursement branch now writes Dr 役員借入金 / Cr 普通預金 (clearing) instead of re-posting the expense.backend: src/services/reconciliation-link.service.ts — v6.1 (2026-04-23): createJournalEntryForLink() now detects when the linked document is a claims type and uses Dr 役員借入金 (238) / Cr 普通預金 (102) for the clearing JE, matching the accrual booked at publish time. Legacy employee reimbursements without a claims link fall back to Dr 立替金 (1350) / Cr 普通預金 (1100) for backward compatibility.backend: src/services/file-document-matcher.service.ts (new) + POST /api/file-upload/bulk-attach-to-document — heuristically binds bulk-uploaded PDFs to expense lines via filename matching (amount in _JPY(\d+), date in YYYYMMDD, vendor substring). Match result is persisted on documents.metadata.expenseAttachments = { [expenseId]: fileId[] } and resolved to full file records on subsequent getDocumentById()./?tab=accounting&flowTab=accounting — new claim appears in "最近" with lock icon, workflow chain shows 経費申請 → 給与処理 (¥98,300) → 支払明細書./view/claims/<id> — basicInfo, 8 expense lines, 証憑紐付け badge, ロック中 badge, connected workflow panel. v6.1 (2026-04-23): page header now shows a green 仕訳 JE-20260423-CL001 · ¥98,300 → chip that deep-links to /tax-journal?jeId=<uuid>, and linked-doc chips for 給与明細 / 支払明細. A top ClaimPipelineRibbon visualises the 5-step flow (申請書 → 仕訳 → 給与明細 → 支払明細 → 銀行取引). The right-side reimbursement dock's step 5 銀行取引 no longer hides for claims-only views — it renders a 待機中 placeholder with an optional N件の候補を表示 button driven by useCandidateBankTransactions. A new 証憑をまとめてアップロード header button posts to POST /api/file-upload/bulk-attach-to-document and the returned matches immediately light up each row's paperclip icon./tax-journal?jeId=<uuid> — new deep-link support. Accepts ?jeId= or ?highlight= to auto-scroll and highlight the entry row for ~3s before cleaning up the URL param./tax-journal — JE-20260423-CL001 visible in 2026年度, 借方合計 = 貸方合計 ¥14,801,841, 未紐づけ reduced by 1.This specification details the backend enhancements needed to automatically create journal entries when documents are confirmed (status changed to "published"). This ensures compliance with Japanese GAAP 発生主義 (accrual basis) accounting principles.
sales_invoice) auto-JE is SHIPPED — sale-invoice.service.ts → generateJournalEntry() writes Dr 売掛金 / Cr 売上高 + 仮受消費税 on invoice publish.salary_slip) auto-JE is SHIPPED — pay-slip.service.ts.fixed-asset.service.ts, depreciation.job.ts.claims — 立替経費申請書): NOT SHIPPED. No JE is created when a claims document is published.travel — 出張報告書): NOT SHIPPED.payment — 支払明細書): NOT SHIPPED.The current bank-feed AI creates cash-basis JEs on the payment date, which violates 発生主義 (mandatory for 法人 under J-GAAP). This spec's Phase 2 closes that gap.
Use 役員借入金 (code 238), NOT 未払金.
Research (Firecrawl, 2026-04-23):
The 未払金 account is the correct liability for employees (not 役員) and for supplier invoices paid on credit. For a 合同会社 with a single 代表社員 paying out-of-pocket, the canonical liability is 役員借入金, which:
For each line in a published claims document (per-line JE, or one composite JE with one debit line per expense):
entry_date = expense line date (content.expenses[i].date)
reference_type = 'claims'
reference_document_id = claims doc id
source_type = 'document'
description = `立替経費: ${doc.document_name} #${line.index}`
Dr (line) {expense account from AI extraction, e.g. 旅費交通費 504, 消耗品費 510} net_amount
Dr (line) 仮払消費税 (131) tax_amount (if line has qualified invoice)
Cr 役員借入金 (238) gross_amount
If the chart of accounts has duplicate codes for the same name (see the schema audit), the service must resolve to the lower-numeric (3-digit) canonical code for now, and a follow-up housekeeping migration will consolidate.
When the bank-feed AI sees a withdrawal that is matched (via document_transaction_links) to a published claims / payment / salary_slip document, it must NOT post the expense to P/L. Instead:
Dr 役員借入金 (238) gross_amount
Cr 普通預金 (110x) gross_amount
This clears the 役員借入金 balance that was accrued on publish. The bank-feed AI prompt in src/common/prompts/journal-entry-generator.ts already has a reimbursement branch (line 221-228) that writes Dr 旅費交通費等 / Cr 未払金 — this is wrong and must be changed to read the linked document's existing accrual and write the clearing entry instead.
The original spec offered two options. As of 2026-04-23 we choose Option B (service hook on document confirm) because:
sale-invoice.service.ts → generateJournalEntry()pg_notify + listener adds a moving part that can silently drop eventsclaims documentsThe 64 already-published claims documents will NOT be backfilled. Rationale:
New claims from 2026-04-23 onward use accrual. The spec owner (税理士 if engaged) must sign off on this cut-over line.
journal_entries.reference_document_id = <claims_doc_id>
AND journal_entries.reference_type = 'claims'
AND journal_entries.deleted_at IS NULL
If a row matches, skip creation and return the existing entry_id. The existing sale-invoice.service.ts uses the same pattern via sales_invoice.journal_entry_id.
The frontend now calls POST /journal-entries/draft/bulk after batch document creation in the expense wizard. However, this only works for:
Add a PostgreSQL trigger on the documents table that fires when status changes to 'published'.
-- Function to auto-create journal entry on document publish
CREATE OR REPLACE FUNCTION auto_create_journal_entry()
RETURNS TRIGGER AS $$
DECLARE
v_supporting_types TEXT[] := ARRAY['claims', 'travel', 'payment', 'salary_slip', 'sales_invoice'];
BEGIN
-- Only process if status changed to 'published'
IF (TG_OP = 'UPDATE' AND
NEW.status = 'published' AND
OLD.status != 'published' AND
NEW.document_type = ANY(v_supporting_types)) THEN
-- Call the existing journal entry creation service
-- This should be implemented as a service call, not inline SQL
PERFORM pg_notify('auto_journal_entry', json_build_object(
'document_id', NEW.id,
'document_type', NEW.document_type,
'user_id', NEW.user_id
)::text);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger
CREATE TRIGGER trg_auto_create_journal_entry
AFTER UPDATE ON documents
FOR EACH ROW
EXECUTE FUNCTION auto_create_journal_entry();
Create a background service that listens to the auto_journal_entry notification channel and processes journal entry creation:
// Pseudo-code for backend service
class AutoJournalEntryService {
async processAutoJournalEntry(
notification: AutoJournalNotification
): Promise<void> {
const { document_id, document_type, user_id } = notification
// Skip if journal entry already exists
const existingEntry =
await this.journalEntryRepository.findByDocumentId(document_id)
if (existingEntry) {
logger.info(`Journal entry already exists for document ${document_id}`)
return
}
// Create journal entry based on document type
await this.createJournalEntryForDocument(
document_id,
document_type,
user_id
)
}
}
Enhance existing endpoints to automatically create journal entries:
PATCH /documents/:id/status// Enhanced endpoint
async updateDocumentStatus(documentId: string, newStatus: string): Promise<Document> {
const document = await this.documentRepository.findById(documentId);
// Update status
const updatedDocument = await this.documentRepository.updateStatus(documentId, newStatus);
// Auto-create journal entry if transitioning to 'published'
if (newStatus === 'published' && document.status !== 'published') {
await this.autoJournalEntryService.createForDocument(documentId);
}
return updatedDocument;
}
POST /documents/batch// Enhanced batch creation endpoint
async createBatchDocuments(request: BatchCreateRequest): Promise<BatchCreateResponse> {
const createdDocuments = await this.documentRepository.batchCreate(request.documents);
// Auto-create journal entries for all created documents
const journalResults = await Promise.allSettled(
createdDocuments
.filter(doc => JOURNAL_SUPPORTING_TYPES.includes(doc.documentType))
.map(doc => this.autoJournalEntryService.createForDocument(doc.id))
);
return {
createdDocuments,
journalEntryResults: journalResults,
};
}
| Document Type | Debit Account | Credit Account | Notes |
|---|---|---|---|
claims (立替経費申請書) |
費用 + 仮払消費税 | 役員借入金 | Director expense claim |
payment (支払明細書) |
役員借入金 | 未払金 | Reimbursement statement |
travel (出張報告書) |
旅費交通費 + 仮払消費税 | 役員借入金 | Travel report |
sales_invoice (請求書) |
売掛金 | 売上高 + 仮受消費税 | Sales invoice |
salary_slip (給与明細書) |
給与 + 法定福利費 | 未払金 | Salary slip |
Reference the existing AccrualFirstAccountingService in the frontend for the business logic:
src/services/accrual-first-accounting-service.tsThis service already implements the correct J-GAAP journal entry creation rules.
PATCH /documents/:id/status Responseinterface UpdateStatusResponse {
document: Document
journalEntry?: {
id: string
created: boolean
error?: string
}
}
POST /documents/batch Responseinterface BatchCreateResponse {
createdDocuments: Document[]
linkedRelationships: DocumentRelationship[]
// NEW: Journal sync results
journalEntries?: {
created: number
failed: number
failedDocumentIds: string[]
}
}
POST /documents/:id/confirmDedicated endpoint for document confirmation with auto journal creation:
// Request
POST /documents/:id/confirm
// Response
{
"success": true,
"data": {
"document": { /* Document object */ },
"journalEntry": {
"id": "uuid",
"status": "created",
"debitTotal": 11000,
"creditTotal": 11000
}
}
}
Phase 1 (Current): Frontend auto-sync in expense wizard
Phase 2 (This Spec): Backend auto-creation
Phase 3 (Future): Bank transaction settlement
| Failure | Handling |
|---|---|
| Missing account mapping | Log warning, mark document for manual review |
| Database error | Retry with exponential backoff (3 attempts) |
| Invalid document data | Skip journal creation, notify user |
src/services/accrual-first-accounting-service.tsPOST /journal-entries/draft/bulkforce=trueThe Wave 9.1 BE work added RegenerateJournalEntryBody { force?: boolean } to POST /api/journal-entries/regenerate/:document_id and the matching journal.service.ts#regenerateJournalEntry({ force }) flow that flips the existing JE to status='reversed' (with reversed_at, reversed_by, reversal_reason) before posting a fresh one. The FE side was missing.
Wave 9.2 closes the loop:
JournalApis.regenerateEntry(documentId, { force }) (in src/api/tax-journal.ts) now POSTs { force } as the request body. force defaults to false, preserving the original 409-on-existing-JE behaviour for callers that omit it.useRegenerateJournalEntryMutation (in src/features/journal/queries.ts) wraps the API call and invalidates documents.full(documentId), documents.detail(documentId), journal.all, and taxJournal.all on success so the chip / dock / 仕訳帳 all refresh atomically.<ForceRegenerateButton> in claims.tsx only renders next to the chip when Math.abs(calculatedSummary.totalAmount - journalEntry.totalDebit) >= 1, and prompts the user with a window.confirm that explicitly states the original JE will be marked status='reversed' (audit trail preserved). See src/features/expenses/components/claims.tsx.The BE schema and behaviour are unchanged from Wave 9.1; this amendment is purely about FE wiring + cache invalidations.
metadata.journalEntry nested shape (2026-04-24 afternoon)The Playwright walkthrough surfaced that the FE <JournalEntryChip /> and the new <ForceRegenerateButton /> both read documentMetadata.journalEntry.{id, entryNumber, totalDebit, status} (nested), but journal.service.ts#generateForClaimsDocument only wrote the flat back-compat keys metadata.journalEntryId + metadata.journalEntryNumber. Result: every auto-JE on a claims doc was correctly persisted to journal_entries, but the FE chip and the divergence-aware force-regenerate button silently rendered nothing — making both Wave 9.2 features invisible in production.
generateForClaimsDocument (around line 470 in journal.service.ts) now writes both shapes inside the same Prisma transaction:
metadata: {
...existingMetadata,
// Flat keys — kept for back-compat with older readers (dock badges, document list)
journalEntryId: entry.id,
journalEntryNumber: entry.entry_number,
// Nested object — read by JournalEntryChip + ForceRegenerateButton (Wave 9.2)
journalEntry: {
id: entry.id,
entryNumber: entry.entry_number,
totalDebit: totalDebit.toNumber(),
status: entry.status
}
}
Pre-existing rows can be backfilled with the SQL snippet in docs/HANDOVER-2026-04-24-accrual-claims-pipeline.md § "Mid-walkthrough fix #3".
読み込み中
豆知識