# accounting_core.js 仕様書 **ファイルパス**: `/backoffice/common/js/accounting_core.js` **作成日**: 2026-03-18 **バージョン**: 1.1 **ステータス**: ⬜ 未実装(本仕様書に基づき実装すること) --- ## 1. 概要 ### 1.1 目的 各会計HTML(`pl.html` `bs.html` `trial.html` `tax.html` `corporate.html` 等)に分散している集計ロジックを一本化する共通エンジン。 各HTMLは表示のみを担当し、集計・計算はすべてこのファイルに委譲する。 ### 1.2 読み込み方法 ```html ``` ※ パスは呼び出し元の階層に応じて調整する。 | 呼び出し元 | パス | |-----------|------| | `common/accounting/*.html` | `../js/accounting_core.js` | | `private/accounting/*.html` | `../../common/js/accounting_core.js` | | `corporate/accounting/*.html` | `../../common/js/accounting_core.js` | ### 1.3 依存関係 - 外部ライブラリ不要(バニラJS) - マスタ参照先: `https://docs.scsps.jp/master/accounts.json`(必ずこのURLを使う。`/master/accounts.json` は使わない) - データ保存先: サーバーAPI(`load_journals.php` / `save_journals.php`) --- ## 2. データ構造 ### 2.1 仕訳データ(サーバーAPI) **取得**: `load_journals.php?company=[会社コード]&year=[年]` **保存**: `save_journals.php`(POST: `{ company, year, journals }`) **1件の形式**: ```json { "id": "1234567890.123", "date": "2024-03-12", "company": "SPS", "entries": [ { "account": "現金", "debit": 11000, "credit": 0, "tax_type": "10%" }, { "account": "売上(収入)", "debit": 0, "credit": 10000, "tax_type": "10%" }, { "account": "仮受消費税等", "debit": 0, "credit": 1000, "tax_type": "10%" } ], "description": "売上 A社", "case_id": "", "locked": false } ``` ### 2.2 勘定科目マスタ(fetch) `https://docs.scsps.jp/master/accounts.json` から取得。 ```json { "accounts": [ { "code": "現金", "name": "現金", "type": "asset", "tax": "0%", "for": ["SPS", "SCSPS"], "active": true } ] } ``` | `type` 値 | 区分 | |-----------|------| | `asset` | 資産 | | `liability` | 負債 | | `equity` | 純資産 | | `income` | 収益(売上) | | `expense` | 費用(経費) | --- ## 3. 関数仕様 ### 3.1 データ取得系 --- #### `getJournals(company, year)` サーバーAPIから仕訳データを取得する。非同期関数(Promise)。 | 引数 | 型 | 説明 | |------|----|------| | `company` | string | `"SPS"` または `"SCSPS"` | | `year` | number | 西暦年(例: `2024`) | **戻り値**: `Promise>`(該当データがなければ空配列 `[]`) ```javascript // 使用例 const journals = await getJournals("SPS", 2024); ``` **内部処理**: 1. `/master/api/load_journals.php?company=SPS&year=2024` をfetch 2. レスポンスの `journals` 配列を取得(失敗時は `[]` を返す) 3. `status !== 'pending'` でフィルタして返す --- #### `getAccounts(company)` マスタから会社別勘定科目リストを取得する。非同期関数(Promise)。 | 引数 | 型 | 説明 | |------|----|------| | `company` | string | `"SPS"` または `"SCSPS"` | **戻り値**: `Promise>` ```javascript // 使用例 const accounts = await getAccounts("SPS"); ``` **内部処理**: 1. `https://docs.scsps.jp/master/accounts.json` を fetch 2. `active: true` かつ `for` に `company` が含まれるものを返す 3. fetch 失敗時はコンソールエラーを出力し `[]` を返す **キャッシュ**: 同一セッション内では fetch を1回に抑えるため、モジュール変数 `_accountsCache` に保持する。 --- ### 3.2 集計系 --- #### `calcPL(journals, accounts)` 損益計算書の数値を返す。 | 引数 | 型 | 説明 | |------|----|------| | `journals` | Array | `getJournals()` の戻り値 | | `accounts` | Array | `getAccounts()` の戻り値 | **戻り値**: ```javascript { income: { total: 1000000, // 収益合計(税込) breakdown: [ { account: "売上(収入)", amount: 1000000 } ] }, expense: { total: 300000, // 費用合計(税込) breakdown: [ { account: "旅費交通費", amount: 50000 }, { account: "消耗品費", amount: 10000 } // ... ] }, netIncome: 700000 // 当期純利益(income.total - expense.total) } ``` **集計ルール**: - `type: "income"` の科目 → 貸方金額(`credit`)の合計を収益とする - `type: "expense"` の科目 → 借方金額(`debit`)の合計を費用とする - 金額はすべて税込 - 科目マスタに存在しない科目は「未分類」としてまとめる --- #### `calcBS(journals, accounts)` 貸借対照表の数値を返す。 | 引数 | 型 | 説明 | |------|----|------| | `journals` | Array | `getJournals()` の戻り値 | | `accounts` | Array | `getAccounts()` の戻り値 | **戻り値**: ```javascript { assets: { total: 500000, breakdown: [ { account: "現金", amount: 200000 }, { account: "その他の預金", amount: 300000 } ] }, liabilities: { total: 100000, breakdown: [ { account: "仮受消費税等", amount: 100000 } ] }, equity: { total: 400000, breakdown: [ { account: "事業主借", amount: 400000 } ] } } ``` **集計ルール**: - `type: "asset"` → 借方合計 - 貸方合計 - `type: "liability"` → 貸方合計 - 借方合計 - `type: "equity"` → 貸方合計 - 借方合計 - 貸借バランスチェック: `assets.total === liabilities.total + equity.total` が成立するか検証し、不一致の場合はコンソール警告を出す --- #### `calcTrial(journals, accounts)` 試算表(残高試算表)の数値を返す。 | 引数 | 型 | 説明 | |------|----|------| | `journals` | Array | `getJournals()` の戻り値 | | `accounts` | Array | `getAccounts()` の戻り値 | **戻り値**: ```javascript [ { account: "現金", type: "asset", totalDebit: 500000, totalCredit: 100000, balance: 400000 // asset/expense: debit - credit, liability/equity/income: credit - debit } // ... ] ``` --- #### `calcTax(journals, accounts)` 確定申告(個人事業)の事業所得を返す。 | 引数 | 型 | 説明 | |------|----|------| | `journals` | Array | `getJournals()` の戻り値 | | `accounts` | Array | `getAccounts()` の戻り値 | **戻り値**: ```javascript { revenue: 1000000, // 売上金額(税抜) expenses: { total: 300000, breakdown: [ { account: "旅費交通費", amount: 50000 } ] }, businessIncome: 700000 // 事業所得(revenue - expenses.total) } ``` **注意**: - 確定申告の売上・経費は**税抜**で計算する(`taxBreakdown()` で分解) - `仮受消費税等` と `仮払消費税等` は集計対象外 --- #### `calcConsumptionTax(journals)` 消費税申告の課税売上・課税仕入を返す。 | 引数 | 型 | 説明 | |------|----|------| | `journals` | Array | `getJournals()` の戻り値 | **戻り値**: ```javascript { taxable_sales: { rate10: { base: 900000, tax: 90000 }, // 10%課税売上 rate8: { base: 0, tax: 0 }, // 8%課税売上(軽減税率) exempt: 0 // 非課税売上 }, taxable_purchases: { rate10: { base: 200000, tax: 20000 }, rate8: { base: 0, tax: 0 }, exempt: 0 }, // 簡易課税用(みなし仕入率は呼び出し元で適用) taxable_sales_total: 900000 } ``` **集計ルール**: - `tax_type: "10%"` → 10%区分に加算 - `tax_type: "8%"` → 8%区分に加算 - `tax_type: "非課税"` → `exempt` に加算 - `仮受消費税等` の科目を売上消費税、`仮払消費税等` を仕入消費税として集計 --- ### 3.3 ユーティリティ系 --- #### `taxBreakdown(amount, rate)` 税込金額を本体価格と消費税額に分解する。 | 引数 | 型 | 説明 | |------|----|------| | `amount` | number | 税込金額(整数) | | `rate` | string | `"10%"` / `"8%"` / `"非課税"` | **戻り値**: ```javascript { base: 9091, tax: 909 } // 10%の場合 { base: 9259, tax: 741 } // 8%の場合 { base: amount, tax: 0 } // 非課税の場合 ``` **計算方法(端数処理: 切り捨て)**: - 10%: `base = Math.floor(amount / 1.1)`, `tax = amount - base` - 8%: `base = Math.floor(amount / 1.08)`, `tax = amount - base` --- #### `formatAmount(n)` 数値を日本円の表示形式に変換する。 | 引数 | 型 | 説明 | |------|----|------| | `n` | number | 整数(負数も可) | **戻り値**: string ```javascript formatAmount(1234567) // → "¥1,234,567" formatAmount(-50000) // → "¥-50,000" formatAmount(0) // → "¥0" ``` --- #### `toWareki(year)` 西暦年を和暦文字列に変換する。 | 引数 | 型 | 説明 | |------|----|------| | `year` | number | 西暦年(例: `2024`) | **戻り値**: string ```javascript toWareki(2024) // → "令和6年" toWareki(2019) // → "令和元年" toWareki(2018) // → "平成30年" ``` **対応範囲**: | 元号 | 開始 | |------|------| | 令和 | 2019年5月1日~ | | 平成 | 1989年1月8日~ | ※ 年だけの変換のため月日は考慮しない。2019年は「令和元年」として扱う。 --- #### `buildPeriods(company)` 会社・現在年に応じた期間選択肢を動的生成する。 | 引数 | 型 | 説明 | |------|----|------| | `company` | string | `"SPS"` または `"SCSPS"` | **戻り値**: `Array` **SPS(個人事業)の場合**: ```javascript [ { label: "令和7年(2025)", year: 2025, start: "2025-01-01", end: "2025-12-31" }, { label: "令和6年(2024)", year: 2024, start: "2024-01-01", end: "2024-12-31" }, { label: "令和5年(2023)", year: 2023, start: "2023-01-01", end: "2023-12-31" } // 現在年から遡って5年分を生成 ] ``` **SCSPS(法人)の場合**: ```javascript [ { label: "第5期", period: 5, start: "2024-07-01", end: "2025-06-30" }, { label: "第4期", period: 4, start: "2023-07-01", end: "2024-06-30" }, { label: "第3期", period: 3, start: "2022-07-01", end: "2023-06-30" } // 第1期: 令和元年11月1日(2019-11-01)から ] ``` **法人の期番号計算**: - 第1期: 2019-11-01 ~ 2020-06-30 - 第2期以降: 7月1日 ~ 翌年6月30日 - 現在日付から「進行中の期」を判定し、それ以前の全期を生成 --- #### `lockJournals(company, year)` 指定会社・年の全仕訳データに `locked: true` をセットする。 | 引数 | 型 | 説明 | |------|----|------| | `company` | string | `"SPS"` または `"SCSPS"` | | `year` | number | 西暦年 | **戻り値**: `Promise<{ count: number }>`(ロックしたデータ件数) **注意**: ロック後はデータを `save_journals.php` 経由でサーバーに保存する。 --- #### `unlockJournals(company, year)` 指定会社・年の全仕訳データに `locked: false` をセットする(修正申告用)。 | 引数 | 型 | 説明 | |------|----|------| | `company` | string | `"SPS"` または `"SCSPS"` | | `year` | number | 西暦年 | **戻り値**: `Promise<{ count: number }>`(アンロックしたデータ件数) **注意**: アンロック後はデータを `save_journals.php` 経由でサーバーに保存する。 --- ## 4. エラーハンドリング方針 | 状況 | 対応 | |------|------| | `load_journals.php` のfetch失敗・データなし | 空配列 `[]` を返す(エラーにしない) | | マスタ fetch 失敗 | コンソールエラー、空配列 `[]` を返す。呼び出し元でフォールバック表示すること | | 仕訳データの JSON パース失敗 | コンソールエラー、空配列 `[]` を返す | | 貸借不一致 | `calcBS()` でコンソール警告のみ(エラーはスローしない) | | 未知の tax_type | `taxBreakdown()` は `{ base: amount, tax: 0 }` を返す | --- ## 5. UIルール(表示に使う際の注意) 本ファイル自体はUIを持たないが、呼び出し元HTMLは以下のUIガイドライン(`03_ui_design/01_design_guideline.md`)に従うこと。 - ボタン通常時: `#000080`(ネイビー)/ 白文字 - ホバー時のみ色変化(通常 → `#0000a0`、削除 → `#ff4444`、警告 → `#ffaa00`) - **グリーン(`#00cc88` 等)はボタン・背景に使用禁止** - 状態テキストの色: 正常 `#4466cc` / 警告 `#ffaa00` / エラー `#ff4444` - アイコン不使用(テキストのみ) - ボタン・入力フィールドの高さ: 38px --- ## 6. 実装スケルトン ```javascript /** * accounting_core.js * SPS会計システム 共通集計エンジン * * @version 1.1 * @see /docs/07_accounting/10_accounting_core_spec.md */ 'use strict'; // ---- キャッシュ ---- let _accountsCache = null; // ---- データ取得 ---- async function getJournals(company, year) { try { const res = await fetch(`/master/api/load_journals.php?company=${company}&year=${year}`); const data = await res.json(); const journals = data.journals || []; return journals.filter(j => j.status !== 'pending'); } catch (e) { console.error(`[accounting_core] getJournals: fetch error (${company}/${year})`, e); return []; } } async function getAccounts(company) { if (!_accountsCache) { try { const res = await fetch('https://docs.scsps.jp/master/accounts.json'); const data = await res.json(); _accountsCache = data.accounts || []; } catch (e) { console.error('[accounting_core] getAccounts: fetch error', e); return []; } } return _accountsCache.filter(a => a.active && a.for.includes(company)); } // ---- 集計 ---- function calcPL(journals, accounts) { /* TODO */ } function calcBS(journals, accounts) { /* TODO */ } function calcTrial(journals, accounts) { /* TODO */ } function calcTax(journals, accounts) { /* TODO */ } function calcConsumptionTax(journals) { /* TODO */ } // ---- ユーティリティ ---- function taxBreakdown(amount, rate) { if (rate === '10%') { const base = Math.floor(amount / 1.1); return { base, tax: amount - base }; } if (rate === '8%') { const base = Math.floor(amount / 1.08); return { base, tax: amount - base }; } return { base: amount, tax: 0 }; } function formatAmount(n) { return '¥' + Math.abs(n).toLocaleString('ja-JP') * (n < 0 ? -1 : 1); } function toWareki(year) { if (year >= 2019) return `令和${year === 2019 ? '元' : year - 2018}年`; if (year >= 1989) return `平成${year === 1989 ? '元' : year - 1988}年`; return `${year}年`; } function buildPeriods(company) { /* TODO */ } async function lockJournals(company, year) { const journals = await getJournals(company, year); const locked = journals.map(j => ({ ...j, locked: true })); await fetch('/master/api/save_journals.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ company, year, journals: locked }) }); return { count: locked.length }; } async function unlockJournals(company, year) { const journals = await getJournals(company, year); const unlocked = journals.map(j => ({ ...j, locked: false })); await fetch('/master/api/save_journals.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ company, year, journals: unlocked }) }); return { count: unlocked.length }; } ``` --- ## 7. 呼び出し例(各HTMLでの使い方) ### pl.html(損益計算書) ```javascript (async () => { const company = 'SPS'; const year = 2024; const journals = await getJournals(company, year); const accounts = await getAccounts(company); const pl = calcPL(journals, accounts); document.getElementById('total-income').textContent = formatAmount(pl.income.total); document.getElementById('total-expense').textContent = formatAmount(pl.expense.total); document.getElementById('net-income').textContent = formatAmount(pl.netIncome); })(); ``` ### tax.html(確定申告) ```javascript (async () => { const journals = await getJournals('SPS', 2024); const accounts = await getAccounts('SPS'); const tax = calcTax(journals, accounts); document.getElementById('revenue').textContent = formatAmount(tax.revenue); document.getElementById('business-income').textContent = formatAmount(tax.businessIncome); })(); ``` --- ## 8. 既知の問題点と修正が必要なもの | No. | 問題 | 対応 | |----|------|------| | 1 | 各HTMLに個別集計ロジックがある | 本ファイル実装後、各HTMLから集計コードを削除 | | 2 | `fetch('/master/accounts.json')` になっているHTMLがある | `getAccounts()` に置き換えることで自動解決 | | 3 | `corporate.html` の期番号が2020年第1期前提 | `buildPeriods('SCSPS')` で令和元年11月設立(2019年第1期)を正しく処理 | | 4 | `consumption_tax.html` が未完成 | `calcConsumptionTax()` 実装後に接続 | | 5 | PDF出力が未実装 | 本ファイルのスコープ外(別途実装) | --- ## 9. 改訂履歴 | 日付 | 版 | 内容 | |------|----|------| | 2026-03-18 | 1.0 | 初版作成(引き継ぎメモ・設計書4点をもとに作成) | | 2026-04-01 | 1.1 | 会計担当(11代目):1.3節・2.1節・3.1節・lockJournals/unlockJournals・4節・6節のlocalStorageをサーバーAPIに変更。作業ルール⑥違反のため修正 |