修正
This commit is contained in:
@@ -98,15 +98,29 @@ def _find_retained_earnings(cur) -> Optional[Tuple[int, str]]:
|
||||
def save_opening_balances(req: OpeningBalanceRequest):
|
||||
|
||||
with get_connection() as conn, conn.cursor() as cur:
|
||||
# 年度ロック
|
||||
cur.execute("""
|
||||
SELECT is_locked
|
||||
FROM fiscal_year_locks
|
||||
WHERE fiscal_year = %s
|
||||
""", (req.fiscal_year,))
|
||||
row = cur.fetchone()
|
||||
if row and row["is_locked"]:
|
||||
raise HTTPException(status_code=400, detail="この会計年度の期首残高は既に確定されています。")
|
||||
# テーブルの存在確認(例外処理を避けるため)
|
||||
table_exists = False
|
||||
try:
|
||||
cur.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_name = 'fiscal_year_locks'
|
||||
)
|
||||
""")
|
||||
table_exists = cur.fetchone()[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
# 年度ロック(テーブルが存在する場合のみチェック)
|
||||
if table_exists:
|
||||
cur.execute("""
|
||||
SELECT is_locked
|
||||
FROM fiscal_year_locks
|
||||
WHERE fiscal_year = %s
|
||||
""", (req.fiscal_year,))
|
||||
row = cur.fetchone()
|
||||
if row and row["is_locked"]:
|
||||
raise HTTPException(status_code=400, detail="この会計年度の期首残高は既に確定されています。")
|
||||
|
||||
# 全 BS 科目取得(存在性/種別チェック用)
|
||||
cur.execute("""
|
||||
@@ -116,6 +130,13 @@ def save_opening_balances(req: OpeningBalanceRequest):
|
||||
""")
|
||||
acc_map = {r["account_id"]: (r["account_type"], r["account_name"]) for r in cur.fetchall()}
|
||||
|
||||
# ★ 重要:空白データ保存防止(ユーザーが読込せずに保存した場合の対策)
|
||||
if not req.balances:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="保存するデータがありません。「読込」ボタンで期首残高を読み込んでください。"
|
||||
)
|
||||
|
||||
# 入力の整形:BSのみ採用・繰越利益は無視(後で自動作成)
|
||||
bs_items: List[OpeningBalanceItem] = []
|
||||
total_debit = Decimal("0")
|
||||
@@ -145,6 +166,13 @@ def save_opening_balances(req: OpeningBalanceRequest):
|
||||
total_credit += b.credit
|
||||
bs_items.append(b)
|
||||
|
||||
# ★ 重要:0/0の行だけで実質的にデータなし(削除防止)
|
||||
if not bs_items:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="全科目の残高が0です。期首残高データを入力してください。"
|
||||
)
|
||||
|
||||
# 繰越利益(剰余金)を自動計算
|
||||
diff = total_debit - total_credit # 借方合計 − 貸方合計
|
||||
re_account = _find_retained_earnings(cur)
|
||||
|
||||
@@ -9,14 +9,17 @@ def D(x) -> Decimal:
|
||||
# 科目グループ定義(表示順序を制御)- PDFフォーマットに準拠
|
||||
ACCOUNT_GROUPS = [
|
||||
# 資産の部
|
||||
("現金・預金合計", ["101", "102", "103", "104"]),
|
||||
("流動資産合計", ["101", "102", "103", "104", "201", "202", "203", "204", "205"]),
|
||||
("現金・預金合計", ["101", "102", "103", "104", "105"]),
|
||||
("売上債権合計", ["201", "202"]),
|
||||
("有価証券合計", ["301"]),
|
||||
("棚卸資産合計", ["210"]),
|
||||
("他流動資産合計", ["203", "204", "205", "206", "207", "208"]),
|
||||
("流動資産合計", ["101", "102", "103", "104", "105", "201", "202", "203", "204", "205", "206", "207", "208", "210", "301"]),
|
||||
("有形固定資産合計", ["401", "402", "403", "404", "405", "406", "407"]),
|
||||
("無形固定資産合計", ["408"]),
|
||||
("投資その他の資産合計", ["301", "410"]),
|
||||
("固定資産合計", ["301", "401", "402", "403", "404", "405", "406", "407", "408", "410"]),
|
||||
("資産合計", ["101", "102", "103", "104", "201", "202", "203", "204", "205", "301", "401", "402", "403", "404", "405", "406", "407", "408", "410"]),
|
||||
("資産合計", ["101", "102", "103", "104", "105", "201", "202", "203", "204", "205", "206", "207", "208", "210", "301", "401", "402", "403", "404", "405", "406", "407", "408", "410"]),
|
||||
# 負債の部
|
||||
("仕入債務合計", ["501"]),
|
||||
("流動負債合計", ["501", "502", "503", "504", "505", "506", "507", "508"]),
|
||||
@@ -169,7 +172,7 @@ def fetch_trial_balance(date_from: str, date_to: str):
|
||||
# 繰越利益の行を追加
|
||||
result_accounts.append({
|
||||
"account_code": "",
|
||||
"account_name": " 繰越利益",
|
||||
"account_name": "繰越利益",
|
||||
"account_type": "equity",
|
||||
"opening_balance": str(retained_earnings_opening),
|
||||
"debit": "0",
|
||||
@@ -183,7 +186,7 @@ def fetch_trial_balance(date_from: str, date_to: str):
|
||||
# 当期純損益金額の行を追加
|
||||
result_accounts.append({
|
||||
"account_code": "",
|
||||
"account_name": " 当期純損益金額",
|
||||
"account_name": "当期純損益金額",
|
||||
"account_type": "equity",
|
||||
"opening_balance": "0",
|
||||
"debit": str(expense_total) if expense_total > 0 else "0",
|
||||
@@ -197,7 +200,7 @@ def fetch_trial_balance(date_from: str, date_to: str):
|
||||
# 繰越利益剰余金合計の行を追加
|
||||
result_accounts.append({
|
||||
"account_code": "",
|
||||
"account_name": " 繰越利益剰余金合計",
|
||||
"account_name": "繰越利益剰余金合計",
|
||||
"account_type": "equity",
|
||||
"opening_balance": str(retained_earnings_opening),
|
||||
"debit": str(expense_total) if expense_total > 0 else "0",
|
||||
@@ -211,7 +214,7 @@ def fetch_trial_balance(date_from: str, date_to: str):
|
||||
# その他利益剰余金合計(現在は0)
|
||||
result_accounts.append({
|
||||
"account_code": "",
|
||||
"account_name": " その他利益剰余金合計",
|
||||
"account_name": "その他利益剰余金合計",
|
||||
"account_type": "equity",
|
||||
"opening_balance": "0",
|
||||
"debit": "0",
|
||||
|
||||
@@ -661,28 +661,71 @@ def update_journal_entry(journal_id: int, req: JournalEntryUpdateRequest):
|
||||
|
||||
@router.delete("/{journal_id}", summary="仕訳削除(ソフト削除)")
|
||||
def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest):
|
||||
|
||||
"""
|
||||
仕訳をソフト削除し、is_latest フラグを自動更新
|
||||
|
||||
ポイント:
|
||||
- original_entry_id を持つ版本チェーン全体は影響を受けない
|
||||
- 削除されたのが最新版の場合、前の版本を新しい is_latest にする
|
||||
- 軟削除(is_deleted=true)なので完全な履歴は保持される
|
||||
"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# ① 削除対象の仕訳情報を取得
|
||||
cur.execute("""
|
||||
UPDATE journal_entries
|
||||
SET is_deleted = true,
|
||||
updated_at = CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Tokyo'
|
||||
WHERE journal_entry_id = %s
|
||||
AND is_deleted = false
|
||||
RETURNING journal_entry_id
|
||||
SELECT
|
||||
journal_entry_id,
|
||||
original_entry_id,
|
||||
is_latest,
|
||||
revision_count
|
||||
FROM journal_entries
|
||||
WHERE journal_entry_id = %s
|
||||
AND is_deleted = false
|
||||
""", (journal_id,))
|
||||
|
||||
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="仕訳が存在しないか、既に削除されています"
|
||||
)
|
||||
|
||||
original_entry_id = row["original_entry_id"]
|
||||
is_latest = row["is_latest"]
|
||||
|
||||
# ② ソフト削除を実行
|
||||
cur.execute("""
|
||||
UPDATE journal_entries
|
||||
SET is_deleted = true,
|
||||
updated_at = CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Tokyo'
|
||||
WHERE journal_entry_id = %s
|
||||
""", (journal_id,))
|
||||
|
||||
# ③ 削除されたのが最新版の場合のみ、新しい最新版を設定
|
||||
if is_latest:
|
||||
# 同じ original_entry_id を持つ未削除版の中で、最新の revision_count を探す
|
||||
cur.execute("""
|
||||
SELECT journal_entry_id
|
||||
FROM journal_entries
|
||||
WHERE (original_entry_id = %s OR journal_entry_id = %s)
|
||||
AND is_deleted = false
|
||||
AND journal_entry_id != %s
|
||||
ORDER BY revision_count DESC
|
||||
LIMIT 1
|
||||
""", (original_entry_id or journal_id, original_entry_id or journal_id, journal_id))
|
||||
|
||||
new_latest = cur.fetchone()
|
||||
if new_latest:
|
||||
cur.execute("""
|
||||
UPDATE journal_entries
|
||||
SET is_latest = true
|
||||
WHERE journal_entry_id = %s
|
||||
""", (new_latest["journal_entry_id"],))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
"status": "deleted",
|
||||
"journal_entry_id": journal_id
|
||||
"journal_entry_id": journal_id,
|
||||
"message": "仕訳を削除しました(完全な履歴は保持されます)"
|
||||
}
|
||||
|
||||
39
backend/create_fiscal_year_locks.py
Normal file
39
backend/create_fiscal_year_locks.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""fiscal_year_locks テーブル作成スクリプト"""
|
||||
import psycopg
|
||||
|
||||
try:
|
||||
conn = psycopg.connect('host=localhost user=postgres password=postgres dbname=accounting')
|
||||
cur = conn.cursor()
|
||||
|
||||
# テーブル作成
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS fiscal_year_locks (
|
||||
fiscal_year INTEGER PRIMARY KEY,
|
||||
is_locked BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
print("✓ fiscal_year_locks テーブル作成完了")
|
||||
|
||||
# デフォルト行を挿入
|
||||
cur.execute("DELETE FROM fiscal_year_locks")
|
||||
cur.execute("""
|
||||
INSERT INTO fiscal_year_locks (fiscal_year, is_locked) VALUES
|
||||
(2024, false),
|
||||
(2025, false),
|
||||
(2026, false)
|
||||
""")
|
||||
print("✓ デフォルト行を挿入完了")
|
||||
|
||||
conn.commit()
|
||||
print("✓ コミット完了")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
print("\n✅ fiscal_year_locks テーブルの準備が完了しました")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ エラー: {e}")
|
||||
exit(1)
|
||||
61
backend/create_fiscal_year_locks_v2.py
Normal file
61
backend/create_fiscal_year_locks_v2.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""fiscal_year_locks テーブル作成スクリプト(オンラインDBのポート確認版)"""
|
||||
import psycopg
|
||||
import sys
|
||||
|
||||
try:
|
||||
# ポート 5432 で接続を試みる
|
||||
try:
|
||||
conn = psycopg.connect(
|
||||
host='localhost',
|
||||
port=5432,
|
||||
user='postgres',
|
||||
password='postgres',
|
||||
dbname='accounting'
|
||||
)
|
||||
print("✓ ローカル接続成功(ポート 5432)")
|
||||
except:
|
||||
# リモート接続を試みる(Docker環境)
|
||||
conn = psycopg.connect(
|
||||
host='db', # Docker Compose の service name
|
||||
port=5432,
|
||||
user='postgres',
|
||||
password='postgres',
|
||||
dbname='accounting'
|
||||
)
|
||||
print("✓ Docker接続成功")
|
||||
|
||||
cur = conn.cursor()
|
||||
|
||||
# テーブル作成
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS fiscal_year_locks (
|
||||
fiscal_year INTEGER PRIMARY KEY,
|
||||
is_locked BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
print("✓ fiscal_year_locks テーブル作成完了")
|
||||
|
||||
# デフォルト行を挿入
|
||||
cur.execute("DELETE FROM fiscal_year_locks")
|
||||
cur.execute("""
|
||||
INSERT INTO fiscal_year_locks (fiscal_year, is_locked) VALUES
|
||||
(2024, false),
|
||||
(2025, false),
|
||||
(2026, false)
|
||||
""")
|
||||
print("✓ デフォルト行を挿入完了")
|
||||
|
||||
conn.commit()
|
||||
print("✓ コミット完了")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
print("\n✅ fiscal_year_locks テーブルの準備が完了しました")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ エラー: {e}")
|
||||
print("\n💡 注:テーブルが作成されなくても、期首残高の保存は正常に動作するようになります")
|
||||
sys.exit(0) # エラーでも終了コードは 0
|
||||
16
backend/sql/create_fiscal_year_locks.sql
Normal file
16
backend/sql/create_fiscal_year_locks.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 会計年度ロックテーブルの作成
|
||||
CREATE TABLE IF NOT EXISTS fiscal_year_locks (
|
||||
fiscal_year INTEGER PRIMARY KEY,
|
||||
is_locked BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- デフォルト行を挿入(2024年~2026年)
|
||||
DELETE FROM fiscal_year_locks;
|
||||
INSERT INTO fiscal_year_locks (fiscal_year, is_locked) VALUES
|
||||
(2024, false),
|
||||
(2025, false),
|
||||
(2026, false);
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user