CSV取込機能を実装し、Expenseモデルと関連するStore・ExpenseCategoryモデルを追加。CSVパーサーを作成し、出光CSVに対応。明細編集画面のAJAX保存機能を実装し、取込結果を表示する機能を追加。作業ログをdiary.mdに追記。
This commit is contained in:
7
expenses/csv_parsers/__init__.py
Normal file
7
expenses/csv_parsers/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .factory import get_parser
|
||||
from .idemitsu import IdemitsuParser
|
||||
|
||||
__all__ = [
|
||||
'get_parser',
|
||||
'IdemitsuParser',
|
||||
]
|
||||
25
expenses/csv_parsers/base.py
Normal file
25
expenses/csv_parsers/base.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpenseRow:
|
||||
use_date: date
|
||||
description: str
|
||||
amount: int
|
||||
note: str
|
||||
source: str
|
||||
source_hash: str
|
||||
|
||||
|
||||
class CSVParserBase:
|
||||
source: str = ''
|
||||
|
||||
def detect(self, header_line: str) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
def parse(self, lines: Iterable[str]) -> list[ExpenseRow]:
|
||||
raise NotImplementedError
|
||||
26
expenses/csv_parsers/factory.py
Normal file
26
expenses/csv_parsers/factory.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from .base import CSVParserBase
|
||||
from .idemitsu import IdemitsuParser
|
||||
|
||||
|
||||
PARSERS: list[CSVParserBase] = [
|
||||
IdemitsuParser(),
|
||||
]
|
||||
|
||||
|
||||
def detect_parser(lines: Iterable[str]) -> CSVParserBase | None:
|
||||
for line in lines:
|
||||
for parser in PARSERS:
|
||||
if parser.detect(line):
|
||||
return parser
|
||||
return None
|
||||
|
||||
|
||||
def get_parser(lines: Iterable[str]) -> CSVParserBase:
|
||||
parser = detect_parser(lines)
|
||||
if parser is None:
|
||||
raise ValueError('CSVブランドを判定できませんでした。')
|
||||
return parser
|
||||
67
expenses/csv_parsers/idemitsu.py
Normal file
67
expenses/csv_parsers/idemitsu.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from typing import Iterable
|
||||
|
||||
from .base import CSVParserBase, ExpenseRow
|
||||
|
||||
|
||||
class IdemitsuParser(CSVParserBase):
|
||||
source = 'idemitsu'
|
||||
|
||||
def detect(self, header_line: str) -> bool:
|
||||
return '出光' in header_line or 'apollostation' in header_line
|
||||
|
||||
def parse(self, lines: Iterable[str]) -> list[ExpenseRow]:
|
||||
rows: list[ExpenseRow] = []
|
||||
reader = csv.reader(lines)
|
||||
last_row_index: int | None = None
|
||||
for columns in reader:
|
||||
if not columns:
|
||||
continue
|
||||
head = columns[0].strip()
|
||||
if head in ('カード名称', 'お支払日', '今回ご請求額'):
|
||||
continue
|
||||
if head == '利用日':
|
||||
continue
|
||||
if not head:
|
||||
if last_row_index is not None:
|
||||
extra_desc = columns[1].strip() if len(columns) > 1 else ''
|
||||
extra_note = columns[6].strip() if len(columns) > 6 else ''
|
||||
if extra_desc:
|
||||
rows[last_row_index].description += f' {extra_desc}'
|
||||
if extra_note:
|
||||
if rows[last_row_index].note:
|
||||
rows[last_row_index].note += f' {extra_note}'
|
||||
else:
|
||||
rows[last_row_index].note = extra_note
|
||||
continue
|
||||
if len(columns) < 6:
|
||||
continue
|
||||
try:
|
||||
use_date = datetime.strptime(head, '%Y/%m/%d').date()
|
||||
except ValueError:
|
||||
continue
|
||||
description = columns[1].strip()
|
||||
amount_text = columns[5].replace(',', '').strip()
|
||||
note = columns[6].strip() if len(columns) > 6 else ''
|
||||
try:
|
||||
amount = int(amount_text)
|
||||
except ValueError:
|
||||
continue
|
||||
raw = f"{use_date.isoformat()}|{description}|{amount}|{note}|{self.source}"
|
||||
source_hash = hashlib.sha256(raw.encode('utf-8')).hexdigest()
|
||||
rows.append(
|
||||
ExpenseRow(
|
||||
use_date=use_date,
|
||||
description=description,
|
||||
amount=amount,
|
||||
note=note,
|
||||
source=self.source,
|
||||
source_hash=source_hash,
|
||||
)
|
||||
)
|
||||
last_row_index = len(rows) - 1
|
||||
return rows
|
||||
Reference in New Issue
Block a user