データベースルールにowner_typeとis_canceledを追加し、is_businessを廃止。Expenseモデルと関連するマイグレーションを実施。明細編集UIをowner_type選択に更新し、取り消し済みの経費を一覧から除外。作業ログをdiary.mdに追記。

This commit is contained in:
president
2025-12-21 11:56:38 +09:00
parent 02d8cd6a30
commit d301ddcbfb
7 changed files with 100 additions and 48 deletions

View File

@@ -17,3 +17,8 @@
- CSV取込後に明細編集画面へリダイレクトし、取込結果を一覧画面に表示 - CSV取込後に明細編集画面へリダイレクトし、取込結果を一覧画面に表示
- 出光CSVパーサに備考/続行行の結合と外貨備考の取り込みを追加 - 出光CSVパーサに備考/続行行の結合と外貨備考の取り込みを追加
- source_hashに備考を含め、noteをExpenseへ保存 - source_hashに備考を含め、noteをExpenseへ保存
- database.rules に合わせて owner_type と is_canceled を導入し is_business を廃止
- 明細編集UI/APIを owner_type 選択に更新、取り消し済みは一覧から除外
- 0002_owner_type_cancel マイグレーションを追加し既存データを移行
- 仕様書のDB項目を owner_type / is_canceled に更新
- リモートDBに対してマイグレーションを実行

View File

@@ -30,3 +30,6 @@ root@x85-131-243-202:~# sudo -u postgres psql -c "\du"
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS postgres | Superuser, Create role, Create DB, Replication, Bypass RLS
president | president |
source .venv/bin/activate
DB_NAME=accounting DB_USER=account_user DB_PASSWORD=account_Hideyukey-1234 DB_HOST=labo.sunamura-llc.com DB_PORT=5432 .venv/bin/python manage.py runserver

View File

@@ -0,0 +1,56 @@
# Generated by Django 4.2.27 on 2025-12-20 00:00
from django.db import migrations, models
def map_is_business_to_owner_type(apps, schema_editor):
Expense = apps.get_model('expenses', 'Expense')
for expense in Expense.objects.all():
if expense.is_business is True:
expense.owner_type = 'company'
elif expense.is_business is False:
expense.owner_type = 'personal'
else:
expense.owner_type = 'pending'
expense.save(update_fields=['owner_type'])
def reverse_owner_type_to_is_business(apps, schema_editor):
Expense = apps.get_model('expenses', 'Expense')
for expense in Expense.objects.all():
if expense.owner_type == 'company':
expense.is_business = True
elif expense.owner_type == 'personal':
expense.is_business = False
else:
expense.is_business = None
expense.save(update_fields=['is_business'])
class Migration(migrations.Migration):
dependencies = [
('expenses', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='expense',
name='owner_type',
field=models.CharField(
choices=[('company', 'Company'), ('personal', 'Personal'), ('pending', 'Pending')],
default='pending',
max_length=20,
),
),
migrations.AddField(
model_name='expense',
name='is_canceled',
field=models.BooleanField(default=False),
),
migrations.RunPython(map_is_business_to_owner_type, reverse_owner_type_to_is_business),
migrations.RemoveField(
model_name='expense',
name='is_business',
),
]

View File

@@ -21,6 +21,11 @@ class Expense(models.Model):
SOURCE_CHOICES = [ SOURCE_CHOICES = [
('idemitsu', 'Idemitsu'), ('idemitsu', 'Idemitsu'),
] ]
OWNER_TYPE_CHOICES = [
('company', 'Company'),
('personal', 'Personal'),
('pending', 'Pending'),
]
use_date = models.DateField() use_date = models.DateField()
description = models.CharField(max_length=255) description = models.CharField(max_length=255)
@@ -29,12 +34,13 @@ class Expense(models.Model):
expense_category = models.ForeignKey( expense_category = models.ForeignKey(
ExpenseCategory, null=True, blank=True, on_delete=models.SET_NULL ExpenseCategory, null=True, blank=True, on_delete=models.SET_NULL
) )
is_business = models.BooleanField(null=True, blank=True) owner_type = models.CharField(max_length=20, choices=OWNER_TYPE_CHOICES, default='pending')
note = models.TextField(blank=True) note = models.TextField(blank=True)
source = models.CharField(max_length=50, choices=SOURCE_CHOICES) source = models.CharField(max_length=50, choices=SOURCE_CHOICES)
source_hash = models.CharField(max_length=64) source_hash = models.CharField(max_length=64)
ai_score = models.FloatField(null=True, blank=True) ai_score = models.FloatField(null=True, blank=True)
human_confirmed = models.BooleanField(default=False) human_confirmed = models.BooleanField(default=False)
is_canceled = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)

View File

@@ -30,7 +30,11 @@ def csv_upload(request):
def expense_list(request): def expense_list(request):
expenses = Expense.objects.select_related('store', 'expense_category').order_by('-use_date', '-id')[:200] expenses = (
Expense.objects.filter(is_canceled=False)
.select_related('store', 'expense_category')
.order_by('-use_date', '-id')[:200]
)
stores = Store.objects.filter(is_active=True).order_by('name') stores = Store.objects.filter(is_active=True).order_by('name')
categories = ExpenseCategory.objects.filter(is_active=True).order_by('name') categories = ExpenseCategory.objects.filter(is_active=True).order_by('name')
return render( return render(
@@ -57,10 +61,13 @@ def expense_update(request, expense_id: int):
return JsonResponse({'status': 'error', 'message': 'JSONはオブジェクトで送信してください。'}, status=400) return JsonResponse({'status': 'error', 'message': 'JSONはオブジェクトで送信してください。'}, status=400)
expense = get_object_or_404(Expense, pk=expense_id) expense = get_object_or_404(Expense, pk=expense_id)
fields = {} fields = {}
for key in ('store_id', 'expense_category_id', 'is_business', 'note'): for key in ('store_id', 'expense_category_id', 'owner_type', 'note'):
if key in payload: if key in payload:
value = payload[key] value = payload[key]
if value in ('', None): if value in ('', None):
if key == 'owner_type':
fields[key] = 'pending'
else:
fields[key] = None fields[key] = None
else: else:
fields[key] = value fields[key] = value
@@ -78,9 +85,11 @@ def expense_update(request, expense_id: int):
return JsonResponse( return JsonResponse(
{'status': 'error', 'message': 'expense_category_idが不正です。'}, status=400 {'status': 'error', 'message': 'expense_category_idが不正です。'}, status=400
) )
if 'is_business' in fields and fields['is_business'] is not None: if 'owner_type' in fields and fields['owner_type'] is not None:
if not isinstance(fields['is_business'], bool): if not isinstance(fields['owner_type'], str):
return JsonResponse({'status': 'error', 'message': 'is_businessの型が不正です。'}, status=400) return JsonResponse({'status': 'error', 'message': 'owner_typeの型が不正です。'}, status=400)
if fields['owner_type'] not in {'company', 'personal', 'pending'}:
return JsonResponse({'status': 'error', 'message': 'owner_typeが不正です。'}, status=400)
if 'note' in fields and fields['note'] is not None: if 'note' in fields and fields['note'] is not None:
if not isinstance(fields['note'], str): if not isinstance(fields['note'], str):
return JsonResponse({'status': 'error', 'message': 'noteの型が不正です。'}, status=400) return JsonResponse({'status': 'error', 'message': 'noteの型が不正です。'}, status=400)

View File

@@ -19,7 +19,7 @@
<th>金額</th> <th>金額</th>
<th>店舗区分</th> <th>店舗区分</th>
<th>経費区分</th> <th>経費区分</th>
<th>会社/家計</th> <th>区分</th>
<th>備考</th> <th>備考</th>
</tr> </tr>
</thead> </thead>
@@ -50,39 +50,17 @@
</select> </select>
</td> </td>
<td> <td>
<label> <select class="js-expense-field" data-field="owner_type">
<input <option value="pending" {% if expense.owner_type == "pending" %}selected{% endif %}>
class="js-expense-field"
data-field="is_business"
type="radio"
name="is_business_{{ expense.id }}"
value="true"
{% if expense.is_business is True %}checked{% endif %}
>
会社
</label>
<label>
<input
class="js-expense-field"
data-field="is_business"
type="radio"
name="is_business_{{ expense.id }}"
value="false"
{% if expense.is_business is False %}checked{% endif %}
>
家計
</label>
<label>
<input
class="js-expense-field"
data-field="is_business"
type="radio"
name="is_business_{{ expense.id }}"
value=""
{% if expense.is_business is None %}checked{% endif %}
>
未設定 未設定
</label> </option>
<option value="company" {% if expense.owner_type == "company" %}selected{% endif %}>
会社
</option>
<option value="personal" {% if expense.owner_type == "personal" %}selected{% endif %}>
家計
</option>
</select>
</td> </td>
<td> <td>
<input <input
@@ -119,12 +97,6 @@
} }
function coerceValue(field, value) { function coerceValue(field, value) {
if (field === 'is_business') {
if (value === '') {
return null;
}
return value === 'true';
}
return value; return value;
} }

View File

@@ -57,12 +57,13 @@
* amount金額 * amount金額
* store_id店舗区分、NULL可 * store_id店舗区分、NULL可
* expense_category_id経費区分、NULL可 * expense_category_id経費区分、NULL可
* is_business会社経費 / 家計フラグ、NULL可 * owner_typecompany/personal/pending
* note備考 * note備考
* sourceCSV種別idemitsu 等) * sourceCSV種別idemitsu 等)
* source_hash行ハッシュ * source_hash行ハッシュ
* ai_score将来用、NULL可 * ai_score将来用、NULL可
* human_confirmed人間確定フラグ * human_confirmed人間確定フラグ
* is_canceled取り消しフラグ
* created_at / updated_at * created_at / updated_at
UNIQUE(source, source_hash) UNIQUE(source, source_hash)