From 7ae367cd66709796d1492824f38ec0192162b57d Mon Sep 17 00:00:00 2001 From: president Date: Sun, 21 Dec 2025 16:36:39 +0900 Subject: [PATCH] =?UTF-8?q?=E6=9C=88=E6=AC=A1=E3=83=AC=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=88=E6=A9=9F=E8=83=BD=E3=82=92=E5=AE=9F=E8=A3=85=E3=81=97?= =?UTF-8?q?=E3=80=81=E7=B5=8C=E8=B2=BB=E3=81=AE=E5=8F=96=E3=82=8A=E6=B6=88?= =?UTF-8?q?=E3=81=97=E3=83=BB=E5=BE=A9=E5=B8=B0=E6=A9=9F=E8=83=BD=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=80=82CSV=E5=8F=96=E8=BE=BC=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=AB=E3=83=89=E3=83=A9=E3=83=83=E3=82=B0=EF=BC=86?= =?UTF-8?q?=E3=83=89=E3=83=AD=E3=83=83=E3=83=97=E3=82=A8=E3=83=AA=E3=82=A2?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85=E3=81=97=E3=80=81=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82=E9=87=91=E9=A1=8D?= =?UTF-8?q?=E3=81=AE=E3=82=AB=E3=83=B3=E3=83=9E=E5=8C=BA=E5=88=87=E3=82=8A?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=82=92=E5=85=A8=E4=BD=93=E3=81=AB=E9=81=A9?= =?UTF-8?q?=E7=94=A8=E3=80=82favicon=E3=81=A8apple-touch-icon=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81404=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=82=92=E5=9B=9E=E9=81=BF=E3=80=82=E4=BD=9C=E6=A5=AD?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=82=92diary.md=E3=81=AB=E8=BF=BD=E8=A8=98?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/diary.md | 9 ++ config/settings.py | 1 + config/urls.py | 8 + database.rules | 2 + expenses/csv_parsers/base.py | 2 +- expenses/services.py | 76 +++++++++ expenses/urls.py | 1 + expenses/views.py | 86 +++++++++-- templates/base.html | 7 + templates/expenses/csv_upload.html | 69 ++++++++- templates/expenses/expense_list.html | 73 ++++++++- templates/expenses/monthly_report.html | 170 ++++++++++++++++++++- templates/expenses/monthly_report_pdf.html | 96 +++++++++++- testdata/IDEMITSU_2510.csv | 24 +++ testdata/IDEMITSU_2511.csv | 27 ++++ testdata/IDEMITSU_2512.csv | 27 ++++ testdata/IDEMITSU_2601.csv | 25 +++ 17 files changed, 682 insertions(+), 21 deletions(-) create mode 100644 testdata/IDEMITSU_2510.csv create mode 100644 testdata/IDEMITSU_2511.csv create mode 100644 testdata/IDEMITSU_2512.csv create mode 100644 testdata/IDEMITSU_2601.csv diff --git a/Doc/diary.md b/Doc/diary.md index 20bde23..062c709 100644 --- a/Doc/diary.md +++ b/Doc/diary.md @@ -22,3 +22,12 @@ - 0002_owner_type_cancel マイグレーションを追加し既存データを移行 - 仕様書のDB項目を owner_type / is_canceled に更新 - リモートDBに対してマイグレーションを実行 +- 取消/復帰APIと一覧の取消ボタン・表示切替トグルを追加 +- 取消済み行の打ち消し線表示を追加 +- CSV取込のD&Dエリアを実装しファイル名表示を追加 +- CSVブランド判定失敗時のエラーメッセージ表示を追加 +- CSVパーサの行結合で落ちる不具合を修正(ExpenseRowのfrozen解除) +- 月次レポートの集計ロジックと画面表示を実装(店舗/経費区分/区分/未分類) +- 月次レポートに明細行一覧を追加(各集計の下に日付/利用先/金額/備考) +- 金額のカンマ区切りと右寄せを全体に適用(humanize + amountクラス) +- favicon/apple-touch-iconの404回避と簡易アイコンを追加 diff --git a/config/settings.py b/config/settings.py index 394ae2e..4a638b2 100644 --- a/config/settings.py +++ b/config/settings.py @@ -35,6 +35,7 @@ INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', + 'django.contrib.humanize', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', diff --git a/config/urls.py b/config/urls.py index b7c2e81..4f49178 100644 --- a/config/urls.py +++ b/config/urls.py @@ -15,9 +15,17 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin +from django.http import HttpResponse from django.urls import include, path + +def empty_icon(request): + return HttpResponse(status=204) + urlpatterns = [ path('admin/', admin.site.urls), + path('favicon.ico', empty_icon), + path('apple-touch-icon.png', empty_icon), + path('apple-touch-icon-precomposed.png', empty_icon), path('', include('expenses.urls')), ] diff --git a/database.rules b/database.rules index 9eda544..a3b8185 100644 --- a/database.rules +++ b/database.rules @@ -33,3 +33,5 @@ root@x85-131-243-202:~# sudo -u postgres psql -c "\du" 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 + +DJANGO_DEBUG=true 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 diff --git a/expenses/csv_parsers/base.py b/expenses/csv_parsers/base.py index e9a83f7..5897d3e 100644 --- a/expenses/csv_parsers/base.py +++ b/expenses/csv_parsers/base.py @@ -5,7 +5,7 @@ from datetime import date from typing import Iterable -@dataclass(frozen=True) +@dataclass class ExpenseRow: use_date: date description: str diff --git a/expenses/services.py b/expenses/services.py index 2724b28..803c313 100644 --- a/expenses/services.py +++ b/expenses/services.py @@ -1,7 +1,12 @@ from __future__ import annotations +from collections import defaultdict +from dataclasses import dataclass +from datetime import date from typing import Iterable +from django.db.models import Count, Sum + from .csv_parsers import get_parser from .models import Expense @@ -34,3 +39,74 @@ def import_csv_lines(lines: Iterable[str]) -> CSVImportResult: else: duplicated += 1 return CSVImportResult(imported=imported, duplicated=duplicated) + + +@dataclass +class MonthlyReportResult: + target_month: date + total_amount: int + store_totals: list[dict] + category_totals: list[dict] + owner_totals: list[dict] + unclassified_counts: dict + + +def build_monthly_report(start_date: date, end_date: date) -> MonthlyReportResult: + queryset = Expense.objects.filter( + is_canceled=False, + use_date__gte=start_date, + use_date__lt=end_date, + ) + detail_queryset = ( + queryset.select_related('store', 'expense_category') + .order_by('use_date', 'id') + ) + store_details = defaultdict(list) + category_details = defaultdict(list) + owner_details = defaultdict(list) + for expense in detail_queryset: + detail = { + 'use_date': expense.use_date, + 'description': expense.description, + 'amount': expense.amount, + 'note': expense.note, + } + store_details[expense.store_id].append(detail) + category_details[expense.expense_category_id].append(detail) + owner_details[expense.owner_type].append(detail) + total_amount = queryset.aggregate(total=Sum('amount'))['total'] or 0 + store_totals = list( + queryset.values('store_id', 'store__name') + .annotate(total=Sum('amount'), count=Count('id')) + .order_by('store__name', 'store_id') + ) + for row in store_totals: + row['details'] = store_details.get(row['store_id'], []) + category_totals = list( + queryset.values('expense_category_id', 'expense_category__name') + .annotate(total=Sum('amount'), count=Count('id')) + .order_by('expense_category__name', 'expense_category_id') + ) + for row in category_totals: + row['details'] = category_details.get(row['expense_category_id'], []) + owner_totals = list( + queryset.values('owner_type') + .annotate(total=Sum('amount'), count=Count('id')) + .order_by('owner_type') + ) + for row in owner_totals: + row['details'] = owner_details.get(row['owner_type'], []) + unclassified_counts = { + 'store_missing': queryset.filter(store__isnull=True).count(), + 'category_missing': queryset.filter(expense_category__isnull=True).count(), + 'owner_pending': queryset.filter(owner_type='pending').count(), + 'total': queryset.count(), + } + return MonthlyReportResult( + target_month=start_date, + total_amount=total_amount, + store_totals=store_totals, + category_totals=category_totals, + owner_totals=owner_totals, + unclassified_counts=unclassified_counts, + ) diff --git a/expenses/urls.py b/expenses/urls.py index bff496f..cf498b3 100644 --- a/expenses/urls.py +++ b/expenses/urls.py @@ -7,6 +7,7 @@ urlpatterns = [ path('', views.csv_upload, name='csv_upload'), path('expenses/', views.expense_list, name='expense_list'), path('expenses//update/', views.expense_update, name='expense_update'), + path('expenses//cancel/', views.expense_cancel, name='expense_cancel'), path('reports/monthly/', views.monthly_report, name='monthly_report'), path('reports/monthly/pdf/', views.monthly_report_pdf, name='monthly_report_pdf'), ] diff --git a/expenses/views.py b/expenses/views.py index 26369ac..0d4f011 100644 --- a/expenses/views.py +++ b/expenses/views.py @@ -1,5 +1,6 @@ import io import json +from datetime import date, datetime from urllib.parse import urlencode import chardet @@ -8,7 +9,7 @@ from django.shortcuts import redirect from django.shortcuts import get_object_or_404, render from django.views.decorators.http import require_POST -from .services import import_csv_lines +from .services import build_monthly_report, import_csv_lines from .models import Expense, ExpenseCategory, Store @@ -21,7 +22,11 @@ def csv_upload(request): encoding = detected.get('encoding') or 'utf-8' text = raw.decode(encoding, errors='ignore') lines = io.StringIO(text) - result = import_csv_lines(lines) + try: + result = import_csv_lines(lines) + except ValueError as exc: + context['error_message'] = str(exc) + return render(request, 'expenses/csv_upload.html', context) query = urlencode( {'imported': result.imported, 'duplicated': result.duplicated, 'encoding': encoding} ) @@ -30,11 +35,11 @@ def csv_upload(request): def expense_list(request): - expenses = ( - Expense.objects.filter(is_canceled=False) - .select_related('store', 'expense_category') - .order_by('-use_date', '-id')[:200] - ) + include_canceled = request.GET.get('include_canceled') == '1' + queryset = Expense.objects.select_related('store', 'expense_category') + if not include_canceled: + queryset = queryset.filter(is_canceled=False) + expenses = queryset.order_by('-use_date', '-id')[:200] stores = Store.objects.filter(is_active=True).order_by('name') categories = ExpenseCategory.objects.filter(is_active=True).order_by('name') return render( @@ -47,6 +52,7 @@ def expense_list(request): 'imported': request.GET.get('imported'), 'duplicated': request.GET.get('duplicated'), 'encoding': request.GET.get('encoding'), + 'include_canceled': include_canceled, }, ) @@ -104,8 +110,70 @@ def expense_update(request, expense_id: int): def monthly_report(request): - return render(request, 'expenses/monthly_report.html') + error_message = None + target_month = request.GET.get('target_month') + if target_month: + try: + start_date = datetime.strptime(target_month, '%Y-%m').date().replace(day=1) + except ValueError: + error_message = '年月の形式が不正です。' + start_date = date.today().replace(day=1) + else: + start_date = date.today().replace(day=1) + if start_date.month == 12: + end_date = start_date.replace(year=start_date.year + 1, month=1) + else: + end_date = start_date.replace(month=start_date.month + 1) + report = build_monthly_report(start_date, end_date) + return render( + request, + 'expenses/monthly_report.html', + { + 'report': report, + 'target_month': start_date.strftime('%Y-%m'), + 'error_message': error_message, + }, + ) def monthly_report_pdf(request): - return render(request, 'expenses/monthly_report_pdf.html') + target_month = request.GET.get('target_month') + if target_month: + try: + start_date = datetime.strptime(target_month, '%Y-%m').date().replace(day=1) + except ValueError: + start_date = date.today().replace(day=1) + else: + start_date = date.today().replace(day=1) + if start_date.month == 12: + end_date = start_date.replace(year=start_date.year + 1, month=1) + else: + end_date = start_date.replace(month=start_date.month + 1) + report = build_monthly_report(start_date, end_date) + return render( + request, + 'expenses/monthly_report_pdf.html', + { + 'report': report, + 'target_month': start_date.strftime('%Y-%m'), + }, + ) + + +@require_POST +def expense_cancel(request, expense_id: int): + try: + payload = json.loads(request.body.decode('utf-8')) + except json.JSONDecodeError: + return JsonResponse({'status': 'error', 'message': 'JSON形式が不正です。'}, status=400) + if not isinstance(payload, dict): + return JsonResponse({'status': 'error', 'message': 'JSONはオブジェクトで送信してください。'}, status=400) + if 'is_canceled' not in payload: + return JsonResponse({'status': 'error', 'message': 'is_canceledが必要です。'}, status=400) + if not isinstance(payload['is_canceled'], bool): + return JsonResponse({'status': 'error', 'message': 'is_canceledの型が不正です。'}, status=400) + expense = get_object_or_404(Expense, pk=expense_id) + expense.is_canceled = payload['is_canceled'] + expense.human_confirmed = True + expense.save(update_fields=['is_canceled', 'human_confirmed', 'updated_at']) + return JsonResponse({'status': 'ok'}) diff --git a/templates/base.html b/templates/base.html index 89cad92..a396286 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,6 +4,13 @@ {% block title %}Card Data Sorting{% endblock %} + + +
diff --git a/templates/expenses/csv_upload.html b/templates/expenses/csv_upload.html index d5ab24b..4180a23 100644 --- a/templates/expenses/csv_upload.html +++ b/templates/expenses/csv_upload.html @@ -5,13 +5,76 @@ {% block content %}

CSV取込

+ {% if error_message %} +

{{ error_message }}

+ {% endif %} +
-

ここにDrag & Dropエリアを配置予定。

-
+ {% csrf_token %} - +
+
{% endblock %} diff --git a/templates/expenses/expense_list.html b/templates/expenses/expense_list.html index bde39c1..3b1aefe 100644 --- a/templates/expenses/expense_list.html +++ b/templates/expenses/expense_list.html @@ -1,15 +1,26 @@ {% extends 'base.html' %} +{% load humanize %} {% block title %}明細編集{% endblock %} {% block content %}

明細編集

+ {% if imported %}

取込件数: {{ imported }} / 重複件数: {{ duplicated }} / 文字コード: {{ encoding }}

{% endif %}
{% csrf_token %} +
@@ -17,6 +28,7 @@ + @@ -25,10 +37,15 @@ {% for expense in expenses %} - + - + + {% empty %} - + {% endfor %} @@ -96,6 +113,21 @@ return response.json(); } + async function setCanceled(expenseId, isCanceled) { + const response = await fetch(`/expenses/${expenseId}/cancel/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + body: JSON.stringify({ is_canceled: isCanceled }), + }); + if (!response.ok) { + throw new Error('取消更新に失敗しました'); + } + return response.json(); + } + function coerceValue(field, value) { return value; } @@ -117,6 +149,41 @@ } }); }); + + document.querySelectorAll('.js-expense-cancel').forEach((button) => { + button.addEventListener('click', async (event) => { + const row = event.target.closest('tr'); + if (!row) { + return; + } + const expenseId = row.dataset.expenseId; + const isCanceled = event.target.textContent.trim() === '取消'; + try { + await setCanceled(expenseId, isCanceled); + if (isCanceled && !document.querySelector('#js-include-canceled')?.checked) { + row.remove(); + return; + } + row.classList.toggle('is-canceled', isCanceled); + event.target.textContent = isCanceled ? '復帰' : '取消'; + } catch (error) { + alert(error.message); + } + }); + }); + + const includeToggle = document.querySelector('#js-include-canceled'); + if (includeToggle) { + includeToggle.addEventListener('change', (event) => { + const url = new URL(window.location.href); + if (event.target.checked) { + url.searchParams.set('include_canceled', '1'); + } else { + url.searchParams.delete('include_canceled'); + } + window.location.href = url.toString(); + }); + } {% endblock %} diff --git a/templates/expenses/monthly_report.html b/templates/expenses/monthly_report.html index 51e2a60..10532d8 100644 --- a/templates/expenses/monthly_report.html +++ b/templates/expenses/monthly_report.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load humanize %} {% block title %}月次レポート{% endblock %} @@ -8,20 +9,181 @@ + {% if error_message %} +

{{ error_message }}

+ {% endif %} +
+

合計

+

{{ report.total_amount|intcomma }} 円

+

店舗別合計

-

集計結果をここに表示。

+
利用日 利用先 金額取消 店舗区分 経費区分 区分
{{ expense.use_date }} {{ expense.description }}{{ expense.amount }}{{ expense.amount|intcomma }} + +
データがありません。データがありません。
+ + + + + + + + + {% for row in report.store_totals %} + + + + + + {% empty %} + + + + {% endfor %} + +
店舗件数金額
{{ row.store__name|default:"未設定" }}{{ row.count }}{{ row.total|intcomma }}
対象データがありません。
+ {% for row in report.store_totals %} +

{{ row.store__name|default:"未設定" }} の明細

+ + + + + + + + + + + {% for detail in row.details %} + + + + + + + {% empty %} + + + + {% endfor %} + +
日付利用先金額備考
{{ detail.use_date }}{{ detail.description }}{{ detail.amount|intcomma }}{{ detail.note }}
対象データがありません。
+ {% endfor %}

経費区分別合計

-

集計結果をここに表示。

+ + + + + + + + + + {% for row in report.category_totals %} + + + + + + {% empty %} + + + + {% endfor %} + +
経費区分件数金額
{{ row.expense_category__name|default:"未設定" }}{{ row.count }}{{ row.total|intcomma }}
対象データがありません。
+ {% for row in report.category_totals %} +

{{ row.expense_category__name|default:"未設定" }} の明細

+ + + + + + + + + + + {% for detail in row.details %} + + + + + + + {% empty %} + + + + {% endfor %} + +
日付利用先金額備考
{{ detail.use_date }}{{ detail.description }}{{ detail.amount|intcomma }}{{ detail.note }}
対象データがありません。
+ {% endfor %}
-

未分類件数の警告をここに表示。

+

区分別合計

+ + + + + + + + + + {% for row in report.owner_totals %} + + + + + + {% empty %} + + + + {% endfor %} + +
区分件数金額
{{ row.owner_type }}{{ row.count }}{{ row.total|intcomma }}
対象データがありません。
+ {% for row in report.owner_totals %} +

{{ row.owner_type }} の明細

+ + + + + + + + + + + {% for detail in row.details %} + + + + + + + {% empty %} + + + + {% endfor %} + +
日付利用先金額備考
{{ detail.use_date }}{{ detail.description }}{{ detail.amount|intcomma }}{{ detail.note }}
対象データがありません。
+ {% endfor %} +
+
+

未分類件数

+
    +
  • 店舗未設定: {{ report.unclassified_counts.store_missing }}
  • +
  • 経費区分未設定: {{ report.unclassified_counts.category_missing }}
  • +
  • 区分未設定: {{ report.unclassified_counts.owner_pending }}
  • +
  • 対象総件数: {{ report.unclassified_counts.total }}
  • +
{% endblock %} diff --git a/templates/expenses/monthly_report_pdf.html b/templates/expenses/monthly_report_pdf.html index b47ef1f..54f0019 100644 --- a/templates/expenses/monthly_report_pdf.html +++ b/templates/expenses/monthly_report_pdf.html @@ -1,3 +1,4 @@ +{% load humanize %} @@ -8,10 +9,103 @@ h1, h2 { margin: 0 0 8px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #333; padding: 6px; } + .amount { text-align: right; }

月次レポート

-

PDF出力用テンプレートです。

+

対象年月: {{ target_month }}

+

合計

+

{{ report.total_amount|intcomma }} 円

+

店舗別合計

+ + + + + + + + + + {% for row in report.store_totals %} + + + + + + {% empty %} + + + + {% endfor %} + +
店舗件数金額
{{ row.store__name|default:"未設定" }}{{ row.count }}{{ row.total|intcomma }}
対象データがありません。
+

経費区分別合計

+ + + + + + + + + + {% for row in report.category_totals %} + + + + + + {% empty %} + + + + {% endfor %} + +
経費区分件数金額
{{ row.expense_category__name|default:"未設定" }}{{ row.count }}{{ row.total|intcomma }}
対象データがありません。
+

区分別合計

+ + + + + + + + + + {% for row in report.owner_totals %} + + + + + + {% empty %} + + + + {% endfor %} + +
区分件数金額
{{ row.owner_type }}{{ row.count }}{{ row.total|intcomma }}
対象データがありません。
+

未分類件数

+ + + + + + + + + + + + + + + + + + + +
店舗未設定{{ report.unclassified_counts.store_missing }}
経費区分未設定{{ report.unclassified_counts.category_missing }}
区分未設定{{ report.unclassified_counts.owner_pending }}
対象総件数{{ report.unclassified_counts.total }}
diff --git a/testdata/IDEMITSU_2510.csv b/testdata/IDEMITSU_2510.csv new file mode 100644 index 0000000..65c55d2 --- /dev/null +++ b/testdata/IDEMITSU_2510.csv @@ -0,0 +1,24 @@ +J[h,oS[hJ[h +x,2025/10/07 +񂲐z,0000056846 + +p,pXyяi,{lEƑ敪,x敪,O敪,pz,l +2025/08/09,dsbҌ߁@iO}ވ]j,,P,,890, +2025/08/10,`ookd@bnl@ahkk,,P,,150, +2025/08/12,`ookd@bnl@ahkk,,P,,480, +2025/08/14,dsb{@@iވ]쁨]ʓj,,P,,1530, +2025/08/20,vm@^LJe,,P,,4927, +2025/08/21,cbl,,P,,2199, +2025/08/22,dsb{@@ir`X鐲j,,P,,1780, +2025/08/24,`adl`v~A,,P,,1080, +2025/08/26,nodm`h@bg`sfos@rtarbq,,P,,3369,nʉ݊zFQQDOO@trc +,ir`m@eq`mbhrbnj,,,,,~Z[gFW^QV@PTRDPRUS +2025/08/28,P|YfL,,P,,19950, +2025/08/31,Rp[X,,P,,1652, +2025/08/31,Rp[X,,P,,1144, +2025/09/01,GbNXT|o|^l,,P,,2200, +2025/09/01,OD@hCT[rX,,P,,270, +2025/09/01,`ookd@bnl@ahkk,,P,,1200, +2025/09/05,R|N@I@yC,,P,,100, +2025/09/08,OD^T|o|,,P,,2612, +2025/09/10,AbgEjteB,,P,,11313, diff --git a/testdata/IDEMITSU_2511.csv b/testdata/IDEMITSU_2511.csv new file mode 100644 index 0000000..841e7cf --- /dev/null +++ b/testdata/IDEMITSU_2511.csv @@ -0,0 +1,27 @@ +J[h,oS[hJ[h +x,2025/11/07 +񂲐z,0000061542 + +p,pXyяi,{lEƑ敪,x敪,O敪,pz,l +2025/09/10,`ookd@bnl@ahkk,,P,,150, +2025/09/10,cn,,P,,1423, +2025/09/12,`ookd@bnl@ahkk,,P,,480, +2025/09/14,Rp[X,,P,,2490, +2025/09/16,cbl,,P,,3058, +2025/09/17,`l`ynmDbnDio,,P,,3992, +2025/09/18,Rp[X,,P,,2476, +2025/09/21,cbl,,P,,5026, +2025/09/24,`adl`v~A,,P,,1080, +2025/09/26,nodm`h@bg`sfos@rtarbq,,P,,3419,nʉ݊zFQQDOO@trc +,ir`m@eq`mbhrbnj,,,,,~Z[gFX^QV@PTTDSOXP +2025/09/27,vm@^LJe,,P,,2133, +2025/09/29,Rp[X,,P,,2050, +2025/09/30,Rp[X,,P,,5440, +2025/10/01,GbNXT|o|^l,,P,,2200, +2025/10/01,OD@hCT[rX,,P,,270, +2025/10/01,vm@^LJe,,P,,2485, +2025/10/01,[XgAiaaj,,P,,660, +2025/10/01,`ookd@bnl@ahkk,,P,,1200, +2025/10/03,Rp[X,,P,,6160, +2025/10/06,cbl,,P,,12738, +2025/10/08,OD^T|o|,,P,,2612, diff --git a/testdata/IDEMITSU_2512.csv b/testdata/IDEMITSU_2512.csv new file mode 100644 index 0000000..64512dd --- /dev/null +++ b/testdata/IDEMITSU_2512.csv @@ -0,0 +1,27 @@ +J[h,oS[hJ[h +x,2025/12/08 +񂲐z,0000109353 + +p,pXyяi,{lEƑ敪,x敪,O敪,pz,l +2025/10/28,L@@@@@@@@@@,,P,,24794,ʁFQOO +2025/10/10,`ookd@bnl@ahkk,,P,,150, +2025/10/12,Rp[X,,P,,3530, +2025/10/12,`ookd@bnl@ahkk,,P,,480, +2025/10/14,GbNXT|o|^l,,P,,1190, +2025/10/20,Rp[X,,P,,4174, +2025/10/20,Rp[X,,P,,1680, +2025/10/21,b@i@r,,P,,38456, +2025/10/23,R|N@I@yC,,P,,100, +2025/10/24,`adl`v~A,,P,,1080, +2025/10/26,nodm`h@bg`sfos@rtarbq,,P,,3488,nʉ݊zFQQDOO@trc +,ir`m@eq`mbhrbnj,,,,,~Z[gFPO^QV@PTWDTSTT +2025/10/28,cbl,,P,,4158, +2025/10/29,cn,,P,,2154, +2025/11/01,GbNXT|o|^l,,P,,2200, +2025/11/01,OD@hCT[rX,,P,,270, +2025/11/01,`ookd@bnl@ahkk,,P,,1200, +2025/11/04,R|N@I@yC,,P,,130, +2025/11/06,cbl,,P,,5236, +2025/11/07,OD^T|o|,,P,,2612, +2025/11/09,Rp[X,,P,,1480, +2025/11/10,AbgEjteB,,P,,10791, diff --git a/testdata/IDEMITSU_2601.csv b/testdata/IDEMITSU_2601.csv new file mode 100644 index 0000000..c9e5953 --- /dev/null +++ b/testdata/IDEMITSU_2601.csv @@ -0,0 +1,25 @@ +J[h,oS[hJ[h +x,2026/01/07 +񂲐z,0000132408 + +p,pXyяi,{lEƑ敪,x敪,O敪,pz,l +2025/11/26,L@@@@@@@@@@,,P,,23926,ʁFPXR +2025/11/10,`ookd@bnl@ahkk,,P,,150, +2025/11/12,`ookd@bnl@ahkk,,P,,480, +2025/11/13,vm@^LJe,,P,,18128, +2025/11/15,Kl嗤PCwOX,,P,,57000, +2025/11/24,`adl`v~A,,P,,1080, +2025/11/26,nodm`h@bg`sfos@rtarbq,,P,,3568,nʉ݊zFQQDOO@trc +,ir`m@eq`mbhrbnj,,,,,~Z[gFPP^QV@PUQDPWPX +2025/11/26,cn,,P,,2774, +2025/11/28,`vC,,P,,2950, +2025/11/28,`ookd@bnl@ahkk,,P,,1350, +2025/11/28,zNVbveeVȂX,,P,,3065, +2025/11/30,`l`ynmDbnDio,,P,,1577, +2025/11/30,`l`ynmDbnDio,,P,,4971, +2025/12/01,GbNXT|o|^l,,P,,2200, +2025/12/01,OD@hCT[rX,,P,,272, +2025/12/01,`ookd@bnl@ahkk,,P,,1200, +2025/12/03,`ookd@bnl@ahkk,,P,,480, +2025/12/08,OD^T|o|,,P,,2612, +2025/12/10,AbgEjteB,,P,,4625,