💴

確定申告シミュレーターを作りました。

2024/02/25に公開

確定申告の季節がやってきました。
この機に税金の仕組みを学びなおそうと思って、自分用の簡単なシミュレーションプログラムを作ってみました。
この手のシミュレーションはいろんなサイトで確認できましたが、自分で作ることで理解が深められるので作ってみました。

プログラムの概要

  • HTML&JavaScriptのシンプルなプログラムです。

  • 必要項目を入力すると税額などが自動計算されます。

    • ※確定申告書のすべての項目をカバーしていません。基本的には自分が必要な項目のみとしています。(今後時間があれば拡張したいと思います)
  • 計算結果をローカルストレージに保存します。

  • 保存した複数の結果を一覧で比較できます。

    • ※経費や控除額によって納税額がどれくらい変動するかも把握したいのでこの機能を作りました。
  • バリデーションは実装していません(今後時間があれば実装したいと思います)

  • ベース部分は生成AIチャットサービスのPhindに要件を伝えて作ってもらいました。

(注)このプログラムの計算結果の正確性については保証できません。(最低限の検証しかしていません。クラウドの確定申告サービスでの私の確定申告の結果と同じであることは確認しました)

ソースコード

非常に長いので下記をクリックして表示してください。
※きれいではないです。(時間があればきれいにしたいと思います)

入力ページ(input_page.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>確定申告シミュレーター</title>
<style>
    body {
        background-color: lightyellow;
        margin: 10px
    }
    .salary-section {
        margin-bottom: 1em;
    }
    .salary-section .add-button {
        margin-top: 0.5em;
    }
    .sub-total-amount {
        font-weight: 200;
    }
    .label-sub-total-amount {
        font-weight: bold;
        font-size: 14pt;
    }
    .label-total-amount {
        font-weight: bold;
        font-size: 16pt;
    }
    .input-title-text {
        width: 20em;
    }
    .input-text {
        width: 10em;
    }
    .input-amount {
        width: 5em;
    }
    .display-amount {
        background-color: lightyellow;
        width: 5em;
        border: 0px solid #ccc;
        font-size: 12pt;
    }
    .display-sub-total-amount {
        background-color: lightyellow;
        width: 6em;
        border: 0px solid #ccc;
        font-weight: bold;
        font-size: 14pt;
    }
    .display-total-amount {
        background-color: lightyellow;
        width: 6em;
        border: 0px solid #ccc;
        font-weight: bold;
        font-size: 16pt;
    }
    .annotation-link {
        color: blue;
        font-size: 9pt;
    }
</style>
</head>
<body onload="updateTotal()">
<h1>確定申告シミュレーター</h1>
<form>
<a href="result_page.html">計算結果ページ</a><br>
<label for="title">ラベル</label>
<input type="text" class="input-title-text" id="title" name="title" size="20">
<hr>
<div class="contents">
    <h2>収入金額等</h2>
    <h3>事業</h3>
    <label for="businessIncome">㋐営業等:</label>
    <input type="text" class="input-amount" id="businessIncome" name="businessIncome" oninput="validateNumber(this)">

    <h3>給与</h3>
    <div class="salary-section">
        <div class="salary-row">
        <label for="companyName">会社名:</label>
        <input type="text" class="input-text" id="companyName" name="companyName">
        <label for="paymentAmount">㋔支払金額:</label>
        <input type="text" class="input-amount" id="paymentAmount" name="paymentAmount" oninput="validateNumber(this)">
        <label for="withholdingTax">[a]源泉徴収税額:</label>
        <input type="text" class="input-amount" id="withholdingTax" name="withholdingTax" oninput="validateNumber(this)">
        <label for="socialInsurance">[b]社会保険料の金額:</label>
        <input type="text" class="input-amount" id="socialInsurance" name="socialInsurance" oninput="validateNumber(this)">
        </div>
    </div>
    <button type="button" class="add-button"></button>
    <p class="sub-total-amount">給与支払合計金額[㋔の合計]:<input type="text" class="display-amount" id="totalSalary" name="totalSalary" readonly></p>

    <p class="label-sub-total-amount">[A]収入合計金額:<input type="text" class="display-sub-total-amount" id="totalIncome" name="totalIncome" readonly></p>

    <hr>
    <h2>所得金額等</h2>
    <label for="expenseAmount">[B]経費:</label>
    <input type="text" class="input-amount" id="expenseAmount" name="expenseAmount" oninput="validateNumber(this)"> 
    調整:<input type="text" class="input-amount" id="expenseAmountAdjust" name="expenseAmount" oninput="validateNumberAndMinus(this)">
    ※調整を入力したら自動的に経費に反映されます(マイナスも入力できます)<br>
    <label for="expenseCategory">(58)青色申告特別控除額:</label>
    <select id="expenseCategory" name="expenseCategory">
        <option value="0">-</option>
        <option value="100000">10万</option>
        <option value="550000">55万</option>
        <option value="650000">65万</option>
    </select>
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/2072.htm" target="_blank">※参考(国税庁のページ)</a>
    <h3>事業所得</h3>
    <label for="businessIncomeProfit">①営業等の所得[㋐-B-(58)]:</label>
    <input type="text" class="display-amount" id="businessIncomeProfit" name="businessIncomeProfit" readonly><br>
    <h3>給与所得</h3>
    <p class="sub-total-amount">⑥給与(控除後の金額):<input type="text" class="display-amount" id="salaryAfterDeductionAmount" readonly>
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1410.htm" target="_blank">※参考(国税庁のページ)</a></p>
    <p class="label-sub-total-amount">⑫所得金額[① + ⑥]:<input type="text" class="display-sub-total-amount" id="totalOperatingProfit" name="totalOperatingProfit" readonly></p>

    <hr>
    <h2>所得から差し引かれる金額(控除)</h2>
    <label>社会保険料控除 </label>
    <input type="text" class="display-amount" id="totalSocialInsurance" name="totalSocialInsurance" readonly>
    <label for="nationalHealthInsurance"> (国民健康保険料:</label>
    <input type="text" class="input-amount" id="nationalHealthInsurance" name="nationalHealthInsurance" oninput="validateNumber(this)">
    <label for="nationalPension">国民年金:</label>
    <input type="text" class="input-amount" id="nationalPension" name="nationalPension" oninput="validateNumber(this)">
    <label for="nationalPension">社会保険料控除(給与)[bの合計]:</label>
    <input type="text" class="display-amount" id="totalSalarySocialInsurance" name="totalSalarySocialInsurance" oninput="validateNumber(this)"><a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1130.htm" target="_blank">※参考(国税庁のページ)</a><br>
    <label for="smallBusinessMutualAidPremiumDeduction">小規模企業共済等掛金控除:</label>
    <input type="text" class="input-amount" id="smallBusinessMutualAidPremiumDeduction" name="smallBusinessMutualAidPremiumDeduction" oninput="validateNumber(this)">
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1135.htm" target="_blank">※参考(国税庁のページ)</a><br>
    <label>生命保険料控除 </label>
    <label for="newLifeInsurance">(新)生命保険料控除:</label>
    <input type="text" class="input-amount" id="newLifeInsurance" name="newLifeInsurance" oninput="validateNumber(this)">
    <label for="careInsurance">介護保険料控除:</label>
    <input type="text" class="input-amount" class="input-amount" id="careInsurance" name="careInsurance" oninput="validateNumber(this)">
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1140.htm" target="_blank">※参考(国税庁のページ)</a></p>
    <label for="basicDeduction">基礎控除:</label>
    <input type="text" id="displayBasicDeduction" name="displayBasicDeduction" class="display-amount" readonly>
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1199.htm" target="_blank">※参考(国税庁のページ)</a><br>
    <input type="hidden" id="basicDeduction" name="basicDeduction" value="480000">
    <label for="medicalExpense">医療費控除:</label>
    <input type="text" class="input-amount" id="medicalExpense" name="medicalExpense" oninput="validateNumber(this)">
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1120.htm" target="_blank">※参考(国税庁のページ)</a><br>
    <label for="donation">寄附金控除:</label>
    <input type="text" class="input-amount" id="donation" name="donation" oninput="validateNumber(this)">
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1150.htm" target="_blank">※参考(国税庁のページ)</a>

    <p class="label-sub-total-amount">㉙控除合計金額:<input type="text" class="display-sub-total-amount" id="totalDeduction" name="totalDeduction" readonly></p>

    <hr>
    <h2>税金の計算</h2>
    <p class="label-sub-total-amount">㉚課税される所得金額[⑫ - ㉙]:<input type="text" class="display-sub-total-amount" id="taxableIncomeAmount" name="taxableIncomeAmount" readonly></p>
    <p class="label-sub-total-amount">㉛上の㉚に対する税額:<input type="text" class="display-sub-total-amount" id="tax" name="tax" readonly>
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/2260.htm" target="_blank">※参考(国税庁のページ)</a></p>
    <p class="label-sub-total-amount">㊹復興特別所得税額:<input type="text" class="display-sub-total-amount" id="reconstructionSpecialIncomeTaxAmount" name="reconstructionSpecialIncomeTaxAmount" readonly>
    <a class="annotation-link" href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/gensen/2507.htm" target="_blank">※参考(国税庁のページ)</a></p>
    <p class="label-sub-total-amount">㊺所得税及び復興特別所得税の額:<input type="text" class="display-sub-total-amount" id="tax2" name="tax2" readonly></p>
    <p class="label-sub-total-amount">㊽源泉徴収税額[aの合計]:<input type="text" class="display-sub-total-amount" id="totalWithholdingTax" name="totalWithholdingTax" readonly></p>
    <p class="label-sub-total-amount">㊾申告納税額:<input type="text" class="display-sub-total-amount" id="finalTax" name="finalTax" readonly></p>
    <hr>
    <button type="button" id="saveButton">記録する(上書き)</button>
    <button type="button" id="addButton">記録する(追記)</button>
    </form>
</div>
</body>
<script>
    // 給与欄追加
    function addSalaryField() {
        const salarySection = document.querySelector('.salary-section');
        const salaryField = document.createElement('div');
        salaryField.innerHTML = `
            <div class="salary-row">
                <label for="companyName">会社名:</label>
                <input type="text" class="input-text" id="companyName" name="companyName">
                <label for="paymentAmount">㋔支払金額:</label>
                <input type="text" class="input-amount" id="paymentAmount" name="paymentAmount" oninput="validateNumber(this)">
                <label for="withholdingTax">[a]源泉徴収税額:</label>
                <input type="text" class="input-amount" id="withholdingTax" name="withholdingTax" oninput="validateNumber(this)">
                <label for="socialInsurance">[b]社会保険料の金額:</label>
                <input type="text" class="input-amount" id="socialInsurance" name="socialInsurance" oninput="validateNumber(this)">
            </div>
        `;
        salarySection.appendChild(salaryField);
        document.querySelectorAll('.salary-section input[name="paymentAmount"], .salary-section input[name="withholdingTax"], .salary-section input[name="socialInsurance"]').forEach(input => {
            input.addEventListener('change', updateTotal);
        });
    }

    // 入力制限(数字のみ入力を受け付ける)
    function validateNumber(input) {
        input.value = input.value.replace(/[^0-9]/g, '');
    }

    // 入力制限(数字とマイナスのみ入力を受け付ける)
    function validateNumberAndMinus(input) {
        input.value = input.value.replace(/[^0-9-]/g, '');
    }

    function clearExpenseAmountAdjust() {
        document.getElementById('expenseAmountAdjust').value = null
    }

    // 自動計算
    function updateTotal() {
        // 収入
        let totalIncome = 0;
        let totalBusinessIncome = 0;
        const businessIncomeAmount = document.getElementById('businessIncome');
        totalBusinessIncome += parseFloat(businessIncomeAmount.value) || 0;
        totalIncome += totalBusinessIncome;

        // 給与
        const paymentAmounts = document.querySelectorAll('.salary-section input[name="paymentAmount"]');
        let salaryTotal = 0;
        paymentAmounts.forEach(input => {
            salaryTotal += parseFloat(input.value) ||     0;
        });
        totalIncome += salaryTotal;
        const salaryAfterDeductionAmount = calculateSalaryAfterDeductionAmount(salaryTotal)
        document.getElementById('totalSalary').value = formatNumber(salaryTotal);
        document.getElementById('salaryAfterDeductionAmount').value = formatNumber(salaryAfterDeductionAmount);
        document.getElementById('totalIncome').value = formatNumber(totalIncome);

        // 経費
        const expenseAmount = document.getElementById('expenseAmount');
        const expenseAmountAdjust = document.getElementById('expenseAmountAdjust');
        const adjustedExpenseAmount = parseFloat(expenseAmount.value) + (parseFloat(expenseAmountAdjust.value) || 0);
        expenseAmount.value = adjustedExpenseAmount || null;
        let expenseTotal = 0;
        expenseTotal += parseFloat(expenseAmount.value) || 0;
        const expenseCategoryAmount = document.getElementById('expenseCategory');
        expenseTotal += parseFloat(expenseCategoryAmount.value) || 0;

        let businessIncomeProfit = parseFloat(businessIncomeAmount.value) || 0;
        businessIncomeProfit -= expenseTotal;
        document.getElementById('businessIncomeProfit').value = formatNumber(businessIncomeProfit);
        // document.getElementById('totalExpense').value = formatNumber(expenseTotal);

        const totalOperatingProfit = (totalBusinessIncome - expenseTotal) + salaryAfterDeductionAmount;
        document.getElementById('totalOperatingProfit').value = formatNumber(totalOperatingProfit);

        // 控除
        let totalDeduction = 0;
        let totalSocialInsurance = 0;
        let totalSalarySocialInsurance = 0;
        const salarySocialInsuranceAmounts = document.querySelectorAll('.salary-section input[name="socialInsurance"]');
        salarySocialInsuranceAmounts.forEach(input => {
            totalSalarySocialInsurance += parseFloat(input.value) || 0;
        });
        document.getElementById('totalSalarySocialInsurance').value = formatNumber(totalSalarySocialInsurance);
        totalSocialInsurance += totalSalarySocialInsurance;
        const nationalHealthInsurance = document.getElementById('nationalHealthInsurance');
        totalSocialInsurance += parseFloat(nationalHealthInsurance.value) || 0;
        const nationalPension = document.getElementById('nationalPension');
        totalSocialInsurance += parseFloat(nationalPension.value) || 0;
        document.getElementById('totalSocialInsurance').value = formatNumber(totalSocialInsurance);
        totalDeduction += totalSocialInsurance;

        const smallBusinessMutualAidPremiumDeduction = document.getElementById('smallBusinessMutualAidPremiumDeduction');
        totalDeduction += parseFloat(smallBusinessMutualAidPremiumDeduction.value) || 0;

        const newLifeInsurance = document.getElementById('newLifeInsurance');
        totalDeduction += parseFloat(newLifeInsurance.value) || 0;
        const careInsurance = document.getElementById('careInsurance');
        totalDeduction += parseFloat(careInsurance.value) || 0;

        const basicDeduction = calculateBasicDeduction(totalOperatingProfit);
        document.getElementById('basicDeduction').value = basicDeduction;
        document.getElementById('displayBasicDeduction').value = formatNumber(basicDeduction);
        totalDeduction += basicDeduction;
        const medicalExpense = document.getElementById('medicalExpense');
        totalDeduction += parseFloat(medicalExpense.value) || 0;
        const donation = document.getElementById('donation');
        totalDeduction += parseFloat(donation.value) || 0;
        document.getElementById('totalDeduction').value = formatNumber(totalDeduction);

        // 税額
        // const taxableIncomeAmount = Math.floor((totalIncome - expenseTotal - totalDeduction) / 1000) * 1000;
        const beforeTaxableIncomeAmount = totalOperatingProfit - totalDeduction
        const taxableIncomeAmount = Math.floor(beforeTaxableIncomeAmount / 1000) * 1000;
        document.getElementById('taxableIncomeAmount').value = formatNumber(taxableIncomeAmount);
        const tax = calculateTax(taxableIncomeAmount);
        document.getElementById('tax').value = formatNumber(tax);

        const reconstructionSpecialIncomeTaxAmount = calculateReconstructionSpecialIncomeTaxAmount(tax);
        document.getElementById('reconstructionSpecialIncomeTaxAmount').value = formatNumber(reconstructionSpecialIncomeTaxAmount);
        
        const tax2 = tax + reconstructionSpecialIncomeTaxAmount;
        document.getElementById('tax2').value = formatNumber(tax2);

        const totalWithholdingTax = calculateTotalWithholdingTax();
        document.getElementById('totalWithholdingTax').value = formatNumber(totalWithholdingTax);

        const finalTax = Math.floor((tax2 - totalWithholdingTax) / 100) * 100;
        document.getElementById('finalTax').value = formatNumber(finalTax);
    }

    // 給与控除額計算
    // https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1410.htm
    function calculateSalaryAfterDeductionAmount(salary) {
        let result;
        if (salary <= 550999) {
            result = 0;
        } else if (salary >= 551000 && salary <= 1618999) {
            result = salary - 550000;
        } else if (salary >= 1619000 && salary <= 1619999) {
            result = 1069000;
        } else if (salary >= 1620000 && salary <= 1621999) {
            result = 1070000;
        } else if (salary >= 1622000 && salary <= 1623999) {
            result = 1072000;
        } else if (salary >= 1624000 && salary <= 1627999) {
            result = 1074000;
        } else if (salary >= 1628000 && salary <= 1799999) {
            const A = Math.floor(salary / 4000) * 1000;
            result = A * 2.4 + 100000;
        } else if (salary >= 1800000 && salary <= 3599999) {
            const A = Math.floor(salary / 4000) * 1000;
            result = A * 2.8 - 80000;
        } else if (salary >= 3600000 && salary <= 6599999) {
            const A = Math.floor(salary / 4000) * 1000;
            result = A * 3.2 - 440000;
        } else if (salary >= 6600000 && salary <= 8499999) {
            result = salary * 0.9 - 1100000;
        } else if (salary >= 8500000) {
            result = salary - 1950000;
        }
        return result;
    }    

    // 基礎控除の計算
    // https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1199.htm
    function calculateBasicDeduction(totalIncome) {
        if (totalIncome <= 24000000) {
            return 480000;
        } else if (totalIncome > 24000000 && totalIncome <= 24500000) {
            return 320000;
        } else if (totalIncome > 24500000 && totalIncome <= 25000000) {
            return 160000;
        } else {
            return 0;
        }
    }

    // 所得税の税額計算
    // https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/2260.htm
    function calculateTax(taxableIncomeAmount) {
        let tax;
        if (taxableIncomeAmount >= 1000 && taxableIncomeAmount <= 1949000) {
            tax = taxableIncomeAmount * 0.05;
        } else if (taxableIncomeAmount >= 1950000 && taxableIncomeAmount <= 3299000) {
            tax = taxableIncomeAmount * 0.10 - 97500;
        } else if (taxableIncomeAmount >= 3300000 && taxableIncomeAmount <= 6949000) {
            tax = taxableIncomeAmount * 0.20 - 427500;
        } else if (taxableIncomeAmount >= 6950000 && taxableIncomeAmount <= 8999000) {
            tax = taxableIncomeAmount * 0.23 - 636000;
        } else if (taxableIncomeAmount >= 9000000 && taxableIncomeAmount <= 17999000) {
            tax = taxableIncomeAmount * 0.33 - 1536000;
        } else if (taxableIncomeAmount >= 18000000 && taxableIncomeAmount <= 39999000) {
            tax = taxableIncomeAmount * 0.40 - 2796000;
        } else if (taxableIncomeAmount >= 40000000) {
            tax = taxableIncomeAmount * 0.45 - 4796000;
        }
        return tax;
    }

    // 復興特別所得税額の税額計算
    function calculateReconstructionSpecialIncomeTaxAmount(tax) {
        let reconstructionSpecialIncomeTaxAmount = 0;
        return Math.floor(tax * 0.021);
    }

    // 源泉徴収額の税額計算
    function calculateTotalWithholdingTax() {
        let totalWithholdingTax = 0;
        const salaryWithholdingTax = document.querySelectorAll('.salary-section input[name="withholdingTax"]');
        salaryWithholdingTax.forEach(input => {
            totalWithholdingTax += parseFloat(input.value) || 0;
        });
        return totalWithholdingTax;
    }
    
    // 初期表示
    function setDefaultValuesFromLocalStorage() {
        // ローカルストレージからデータを取得
        const salaryData = localStorage.getItem('finalTaxData');
        if (!salaryData) {
            console.log('No data found in local storage.');
            document.getElementById('saveButton').disabled = true;
            return;
        }

        // URLからindexパラメータを取得
        const urlParams = new URLSearchParams(window.location.search);
        const index = urlParams.get('index');

        // indexが指定されていない場合は何もしない
        if (!index) {
            console.log('No index parameter found in URL.');
            return;
        }

        // JSONデータを解析
        const data = JSON.parse(salaryData);
        // 配列のpの値を取得
        const lastItem = data[index - 1];

        // 入力フォームの各フィールドに最後に記録された値を設定
        document.getElementById('title').value = lastItem.title || '';
        document.getElementById('businessIncome').value = lastItem.businessIncome || '';
        document.getElementById('expenseAmount').value = lastItem.expenseAmount || '';
        document.getElementById('expenseCategory').value = lastItem.expenseCategory || '';
        document.getElementById('nationalHealthInsurance').value = lastItem.nationalHealthInsurance || '';
        document.getElementById('nationalPension').value = lastItem.nationalPension || '';
        document.getElementById('smallBusinessMutualAidPremiumDeduction').value = lastItem.smallBusinessMutualAidPremiumDeduction || '';
        document.getElementById('newLifeInsurance').value = lastItem.newLifeInsurance || '';
        document.getElementById('basicDeduction').value = lastItem.basicDeduction || '';
        document.getElementById('careInsurance').value = lastItem.careInsurance || '';
        document.getElementById('medicalExpense').value = lastItem.medicalExpense || '';
        document.getElementById('donation').value = lastItem.donation || '';

        document.getElementById('companyName').value = lastItem.donation || '';
        document.getElementById('paymentAmount').value = lastItem.donation || '';
        document.getElementById('withholdingTax').value = lastItem.donation || '';
        document.getElementById('socialInsurance').value = lastItem.donation || '';

        const salaries = lastItem.salaries;

        salaries.forEach((salary, index) => {
            const newRow = document.querySelector('.salary-section .salary-row:last-child');

            newRow.querySelector('input[name="companyName"]').value = salary.companyName;
            newRow.querySelector('input[name="paymentAmount"]').value = salary.paymentAmount;
            newRow.querySelector('input[name="withholdingTax"]').value = salary.withholdingTax;
            newRow.querySelector('input[name="socialInsurance"]').value = salary.socialInsurance;
            if (index < salaries.length - 1) {
                addSalaryField();
            }
        });
    }

    // 数字をカンマ区切り
    function formatNumber(num) {
        return num ? num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,') : '';
    }

    // 入力データを取得
    function getInputData() {
        const formData = new FormData(document.querySelector('form'));
        const data = {
            title: formData.get('title'),
            businessIncome: formData.get('businessIncome'),
            businessCategory: formData.get('businessCategory'),
            totalIncome: formData.get('totalIncome'),
            salaries: [],
            expenseAmount: formData.get('expenseAmount'),
            expenseCategory: formData.get('expenseCategory'),
            // totalExpense: formData.get('totalExpense'),
            businessIncomeProfit: formData.get('businessIncomeProfit'),
            salaryAfterDeductionAmount: formData.get('salaryAfterDeductionAmount'),
            totalOperatingProfit: formData.get('totalOperatingProfit'),
            totalSocialInsurance: formData.get('totalSocialInsurance'),
            nationalHealthInsurance: formData.get('nationalHealthInsurance'),
            nationalPension: formData.get('nationalPension'),
            totalSalarySocialInsurance: formData.get('totalSalarySocialInsurance'),
            smallBusinessMutualAidPremiumDeduction: formData.get('smallBusinessMutualAidPremiumDeduction'),
            newLifeInsurance: formData.get('newLifeInsurance'),
            careInsurance: formData.get('careInsurance'),
            basicDeduction: formData.get('basicDeduction'),
            medicalExpense: formData.get('medicalExpense'),
            donation: formData.get('donation'),
            totalDeduction: formData.get('totalDeduction'),
            taxableIncomeAmount: formData.get('taxableIncomeAmount'),
            tax: formData.get('tax'),
            reconstructionSpecialIncomeTaxAmount: formData.get('reconstructionSpecialIncomeTaxAmount'),
            tax2: formData.get('tax2'),
            totalWithholdingTax: formData.get('totalWithholdingTax'),
            finalTax: formData.get('finalTax'),
            // finalTax2: formData.get('finalTax2')
        };

        const salarySections = document.querySelectorAll('.salary-row');
            salarySections.forEach(section => {
            const companyName = section.querySelector('input[name="companyName"]').value;
            const paymentAmount = section.querySelector('input[name="paymentAmount"]').value;
            const withholdingTax = section.querySelector('input[name="withholdingTax"]').value;
            const socialInsurance = section.querySelector('input[name="socialInsurance"]').value;

            data.salaries.push({
                companyName,
                paymentAmount,
                withholdingTax,
                socialInsurance
            });
        });
        return data;
    }

    // データ記録(上書き)
    function saveData() {
        // URLからindexパラメータを取得
        const urlParams = new URLSearchParams(window.location.search);
        const index = urlParams.get('index');

        //  ローカルストレージから既存のデータを取得
        const existingData = localStorage.getItem('finalTaxData');
        let finalTaxData = existingData ? JSON.parse(existingData) : [];

        // index番目の配列の値を書き換え
        if (index && finalTaxData[index - 1]) {
            finalTaxData[index -1] = getInputData();
            // データをローカルストレージに保存
            localStorage.setItem('finalTaxData', JSON.stringify(finalTaxData));
        } else {
            console.log('Invalid index or no data found at index index in local storage.');
            addData();
        }
        window.location.href = "result_page.html";
    }

    // データ追記
    function addData() {

        data = getInputData();

        // ローカルストレージから既存のデータを取得
        const existingData = localStorage.getItem('finalTaxData');
        let finalTaxData = existingData ? JSON.parse(existingData) : [];

        // 新しいデータを配列に追加
        finalTaxData.push(data);

        // データをローカルストレージに保存
        localStorage.setItem('finalTaxData', JSON.stringify(finalTaxData));

        // POSTリクエストを送信するためのコードをここに追加します。
        //    例: fetch('https://example.com/api/submit', {
        //             method: 'POST',
        //             headers: {
        //                 'Content-Type': 'application/json'
        //             },
        //             body: jsonData
        //         })
        //         .then(response => response.json())
        //         .then(data => console.log(data))
        //         .catch(error => console.error('Error:', error));
        window.location.href = "result_page.html";
    }

    document.addEventListener('DOMContentLoaded', () => {
        document.querySelector('.add-button').addEventListener('click', addSalaryField);
        document.querySelectorAll('.salary-section input[name="paymentAmount"], .salary-section input[name="withholdingTax"], .salary-section input[name="socialInsurance"]').forEach(input => {
            input.addEventListener('change', updateTotal);
        });

        document.getElementById('businessIncome').addEventListener('change', updateTotal);
        document.getElementById('expenseAmount').addEventListener('change', updateTotal);
        document.getElementById('expenseAmountAdjust').addEventListener('change', updateTotal);
        document.getElementById('expenseAmountAdjust').addEventListener('change', clearExpenseAmountAdjust);
        document.getElementById('expenseCategory').addEventListener('change', updateTotal);

        document.getElementById('nationalHealthInsurance').addEventListener('change', updateTotal);
        document.getElementById('nationalPension').addEventListener('change', updateTotal);
        document.getElementById('smallBusinessMutualAidPremiumDeduction').addEventListener('change', updateTotal);
        document.getElementById('newLifeInsurance').addEventListener('change', updateTotal);
        document.getElementById('careInsurance').addEventListener('change', updateTotal);
        document.getElementById('basicDeduction').addEventListener('change', updateTotal);
        document.getElementById('medicalExpense').addEventListener('change', updateTotal);
        document.getElementById('donation').addEventListener('change', updateTotal);

        document.getElementById('saveButton').addEventListener('click', saveData);
        document.getElementById('addButton').addEventListener('click', addData);
        setDefaultValuesFromLocalStorage();
    });
</script>
</html>

※入力ページの修正履歴[1]

計算結果ページ(result_page.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>計算結果</title>
<style>
    body {
        background-color: lightyellow;
        margin: 10px
    }
    table {
        border-collapse: collapse;
        width: 100%;
    }
    th, td {
        border: 1px solid black;
        padding: 8px;
        text-align: left;
    }
    th {
        background-color: #f2f2f2;
    }
    .accordion {
        cursor: pointer;
        padding: 8px;
        width: 100%;
        border: none;
        text-align: left;
        outline: none;
        transition:   0.4s;
    }
    .accordion:hover {
        background-color: #ddd;
    }
    .accordion-panel {
        padding: 0 18px;
        display: none;
        font-size: 8pt;
        overflow: hidden;
        background-color: #f1f1f1;
    }
    .accordion-detail {
        font-size: 8pt;
    }
</style>
</head>
<body>
    <h1>計算結果</h1>
    <a href="input_page.html">入力フォームに戻る</a>
    <button id="copyButton">計算結果JSONをコピー</button>
    <!--<button type="button" onclick="localStorage.clear();">全データクリア</button>-->
    <table id="dataTable">
        <thead>
            <tr>
                <th>削除</th>
                <th>編集</th>
                <th>ラベル</th>
                <th>収入</th>
                <th>[B]経費</th>
                <th>青色申告特別控除</th>
                <th>⑫所得</th>
                <th>㉙所得から差し引かれる金額(控除)</th>
                <th>㉚課税所得[⑫ - ㉙]</th>
                <th>㉛㉚に対する税額</th>
                <th>㊹復興特別所得税額</th>
                <th>㊺所得税及び復興特別所得税の額</th>
                <th>㊽源泉徴収税額</th>
                <th>㊾申告納税額</th>
            </tr>
        </thead>
        <tbody>
            <!-- データはJavaScriptで動的に追加されます -->
        </tbody>
    </table>
</body>
<script>
    // ローカルストレージからJSONデータを取得
    const jsonData = JSON.parse(localStorage.getItem('finalTaxData'));

    //   データが存在する場合、テーブルに追加
    if (jsonData) {
        // jsonData.forEach(data => addDataToTable(data));
        addDataToTable(jsonData);
    } else {
        console.log('ローカルストレージにデータが存在しません。');
    }

    document.getElementById('copyButton').addEventListener('click', function() {
        // ローカルストレージからJSONデータを取得
        const jsonData = localStorage.getItem('finalTaxData');
        if (jsonData) {
            // データをクリップボードにコピー
            navigator.clipboard.writeText(jsonData).then(function() {
                alert('JSONデータがクリップボードにコピーされました。');
            }).catch(function(err) {
                console.error('クリップボードへのコピーに失敗しました: ', err);
            });
        } else {
            alert('ローカルストレージにデータが存在しません。');
        }
    });

    // アコーディオンを開閉する関数
    function toggleAccordion(btn, panel) {
        if (panel.style.display === "block") {
            panel.style.display = "none";
            btn.innerHTML = "内訳を表示";
        } else {
            panel.style.display = "block";
            btn.innerHTML = "内訳を隠す";
        }
    }

    //   テーブルにデータを追加する関数
    function addDataToTable(data) {
        let index = 0;
        data.forEach((item, index) => {
            index++;
            const tableBody = document.getElementById('dataTable').getElementsByTagName('tbody')[0];
            const row = tableBody.insertRow();

            // 削除ボタンを追加
            const deleteButton = document.createElement('button');
            deleteButton.textContent = '削除';
            deleteButton.className = 'delete-button';
            deleteButton.addEventListener('click', () => {
                //  ローカルストレージからデータを削除
                data.splice(index - 1, 1);
                localStorage.setItem('finalTaxData', JSON.stringify(data));
                //  テーブルから行を削除
                tableBody.removeChild(row);
            });
            const deleteCell = document.createElement('td');
            deleteCell.appendChild(deleteButton);
            row.appendChild(deleteCell);

            //  編集ボタンを追加
            const editButton = document.createElement('button');
            editButton.textContent = '編集';
            editButton.className = 'edit-button';
            editButton.addEventListener('click', () => {
                // GETパラメータ p  に行番号を設定して input_page.html  に遷移
                window.location.href = `input_page.html?index=${index}`;
            });
            const editCell = document.createElement('td');
            editCell.appendChild(editButton);
            row.appendChild(editCell);

            // 1.ラベル
            const titleCell = row.insertCell();
            titleCell.textContent = item.title;

            // 2.収入と内訳
            const totalIncomeCell = row.insertCell();
            totalIncomeCell.textContent = item.totalIncome;
            const incomeAccordionBtn = document.createElement('button');
            incomeAccordionBtn.className = 'accordion';
            incomeAccordionBtn.innerHTML = '内訳を表示';
            incomeAccordionBtn.onclick = function() { toggleAccordion(this, incomeDetailsPanel); };
            totalIncomeCell.appendChild(incomeAccordionBtn);
            const incomeDetailsPanel = document.createElement('div');
            incomeDetailsPanel.className = 'accordion-panel';
            totalIncomeCell.appendChild(incomeDetailsPanel);

            // 収入の内訳をアコーディオンに追加
            const incomeDetail = document.createElement('p');
            incomeDetail.classList.add('accordion-detail');
            incomeDetail.textContent = `事業収入: ${formatNumber(item.businessIncome)}`;
            incomeDetailsPanel.appendChild(incomeDetail.cloneNode(true));
            let existSalaryPaymentAmount = false
            item.salaries.forEach(salary => {
                if (salary.paymentAmount) {
                    existSalaryPaymentAmount = true
                }
            });
            if (existSalaryPaymentAmount) {
                incomeDetail.textContent = `給与`;
                incomeDetailsPanel.appendChild(incomeDetail.cloneNode(true));
                item.salaries.forEach(salary => {
                    // 給与の内訳をアコーディオンに追加
                    incomeDetail.textContent = `${salary.companyName}: ${formatNumber(salary.paymentAmount)}`;
                    incomeDetailsPanel.appendChild(incomeDetail.cloneNode(true));
                });
            }

            // 3.経費と内訳
            const expenseAmountCell = row.insertCell();
            expenseAmountCell.textContent = formatNumber(item.expenseAmount);

            // 4.青色申告特別控除と内訳
            const expenseCategoryCell = row.insertCell();
            expenseCategoryCell.textContent = formatNumber(item.expenseCategory);

            // 5.所得
            const totalOperatingProfitCell = row.insertCell();
            totalOperatingProfitCell.textContent = item.totalOperatingProfit;

            // 6.控除
            const deductionCell = row.insertCell();
            deductionCell.textContent = item.totalDeduction;
            const deductionAccordionBtn = document.createElement('button');
            deductionAccordionBtn.className = 'accordion';
            deductionAccordionBtn.innerHTML = '内訳を表示';
            deductionAccordionBtn.onclick = function() { toggleAccordion(this, deductionDetailsPanel); };
            deductionCell.appendChild(deductionAccordionBtn);
            const deductionDetailsPanel = document.createElement('div');
            deductionDetailsPanel.className = 'accordion-panel';
            deductionCell.appendChild(deductionDetailsPanel);

            // 控除の内訳をアコーディオンに追加
            const deductionDetail = document.createElement('p');
            deductionDetail.classList.add('accordion-detail');
            deductionDetail.textContent = `国民健康保険料: ${formatNumber(item.nationalHealthInsurance)}`;
            deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));
            deductionDetail.textContent = `国民年金: ${formatNumber(item.nationalPension)}`;
            deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));
            let existSalarySocialInsurance = false
            item.salaries.forEach(salary => {
                if (salary.socialInsurance) {
                    existSalarySocialInsurance = true
                }
            });
            if (existSalarySocialInsurance) {
                deductionDetail.textContent = `社会保険料控除(給与)`;
                deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));
                item.salaries.forEach(salary => {
                    deductionDetail.textContent = `${salary.companyName}: ${formatNumber(salary.socialInsurance)}`;
                    deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));
                });
            }
            if (item.smallBusinessMutualAidPremiumDeduction) {
                deductionDetail.textContent = `小規模企業共済等掛金控除: ${formatNumber(item.smallBusinessMutualAidPremiumDeduction)}`;
                deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));
            }
            deductionDetail.textContent = `(新)生命保険料控除: ${formatNumber(item.newLifeInsurance)}`;
            deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));
            deductionDetail.textContent = `介護保険料控除: ${formatNumber(item.careInsurance)}`;
            deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));
            deductionDetail.textContent = `基礎控除: ${formatNumber(item.basicDeduction)}`;
            deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));
            deductionDetail.textContent = `寄附金控除: ${formatNumber(item.donation)}`;
            deductionDetailsPanel.appendChild(deductionDetail.cloneNode(true));

            // 6.㉚課税所得[⑫ - ㉙]
            const taxableIncomeAmountCell = row.insertCell();
            taxableIncomeAmountCell.textContent = item.taxableIncomeAmount;

            // 7.㉛上の㉚に対する税額
            const taxCell = row.insertCell();
            taxCell.textContent = item.tax;

            // 8.㊹復興特別所得税額[㊸×2.1%]
            const reconstructionSpecialIncomeTaxAmountCell = row.insertCell();
            reconstructionSpecialIncomeTaxAmountCell.textContent = item.reconstructionSpecialIncomeTaxAmount;

            // 9.㊺所得税及び復興特別所得税の額[㊸+㊹]
            const tax2Cell = row.insertCell();
            tax2Cell.textContent = item.tax2;

            // 10.㊽源泉徴収税額
            const withholdingTaxCell = row.insertCell();
            withholdingTaxCell.textContent = item.totalWithholdingTax;
            let existWithholdingTax = false
            item.salaries.forEach(salary => {
                if (salary.withholdingTaxDetailsPanel) {
                    existWithholdingTax = true
                }
            });
            if (existWithholdingTax) {
                const withholdingTaxAccordionBtn = document.createElement('button');
                withholdingTaxAccordionBtn.className = 'accordion';
                withholdingTaxAccordionBtn.innerHTML = '内訳を表示';
                withholdingTaxAccordionBtn.onclick = function() { toggleAccordion(this, withholdingTaxDetailsPanel); };
                withholdingTaxCell.appendChild(withholdingTaxAccordionBtn);
                const withholdingTaxDetailsPanel = document.createElement('div');
                withholdingTaxDetailsPanel.className = 'accordion-panel';
                withholdingTaxCell.appendChild(withholdingTaxDetailsPanel);

                // 源泉徴収税額の内訳をアコーディオンに追加
                const withholdingTaxDetail = document.createElement('p');
                withholdingTaxDetail.classList.add('accordion-detail');
                item.salaries.forEach(salary => {
                    withholdingTaxDetail.textContent = `${salary.companyName}: ${formatNumber(salary.withholdingTax)}`;
                    withholdingTaxDetailsPanel.appendChild(withholdingTaxDetail.cloneNode(true));
                });
            }

            // 11.㊾申告納税額[㊺-㊽]
            const finalTaxCell = row.insertCell();
            finalTaxCell.textContent = item.finalTax;
            tableBody.appendChild(row);
        });

    }

    // 数字をカンマ区切り
    function formatNumber(num) {
        return num ? num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,') : '';
    }
</script>
</html>

※計算結果ページの修正履歴[2]

使い方

上記のソースコードをローカルに保存して、input_page.htmlを開くだけで使えます。
ファイル名はinput_page.htmlとresult_page.htmlにして保存してください。

入力ページ(input_page.html)

入力すると自動計算されます。
「記録する(上書き)」を押下するとローカルストレージに上書き更新され、計算結果ページに遷移します。
「記録する(追記)」を押下するとローカルストレージに追記され、計算結果ページに遷移します。

計算結果ページ(result_page.html)

計算結果の一覧が表示されます。
編集ボタンを押下すると選択した行の内容が表示されます。

今後やりたいこと

適格請求書発行事業者になったので消費税申告のシミュレーターも作りたいと検討しています。
税計算は複雑なのでDDDの手法で複雑さに対処したいと考えています。
学習中のTypeScriptでリファクタリングしたいと考えています。

さいごに

税計算は複雑で面倒くさいですが、税への関心がある確定申告のタイミングを利用して勢いで作りました。
作るために国税庁のサイトなどを見て理解が深まったのでよかったと思います。
アウトプットすることのメリットですね。

この記事がわずかでも参考になれば幸いです。
最後までお読みいただき、ありがとうございました。

脚注
  1. 入力ページの修正履歴
    2024/3/6 軽微な修正をしました。
    2024/3/12 入力ページに「経費調整」追加、「小規模企業共済等掛金控除」追加、軽微な修正をしました。 ↩︎

  2. 計算結果ページの修正履歴
    2024/3/6 軽微な修正をしました。
    2024/3/12 計算結果ページに「計算結果JSONをコピー」ボタン追加、軽微な修正をしました。 ↩︎

Discussion