データベースルールにowner_typeとis_canceledを追加し、is_businessを廃止。Expenseモデルと関連するマイグレーションを実施。明細編集UIをowner_type選択に更新し、取り消し済みの経費を一覧から除外。作業ログをdiary.mdに追記。
This commit is contained in:
@@ -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に対してマイグレーションを実行
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
56
expenses/migrations/0002_owner_type_cancel.py
Normal file
56
expenses/migrations/0002_owner_type_cancel.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
仕様書.md
3
仕様書.md
@@ -57,12 +57,13 @@
|
|||||||
* amount(金額)
|
* amount(金額)
|
||||||
* store_id(店舗区分、NULL可)
|
* store_id(店舗区分、NULL可)
|
||||||
* expense_category_id(経費区分、NULL可)
|
* expense_category_id(経費区分、NULL可)
|
||||||
* is_business(会社経費 / 家計フラグ、NULL可)
|
* owner_type(company/personal/pending)
|
||||||
* note(備考)
|
* note(備考)
|
||||||
* source(CSV種別:idemitsu 等)
|
* source(CSV種別: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)
|
||||||
|
|||||||
Reference in New Issue
Block a user