# 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に変更。作業ルール⑥違反のため修正 |