Files
mia-accounting/src/accounting/report/utils/csv_export.py

110 lines
3.3 KiB
Python

# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The utilities to export the report as CSV for download.
"""
import csv
import datetime as dt
from abc import ABC, abstractmethod
from decimal import Decimal
from io import StringIO
from urllib.parse import quote
from flask import Response
from accounting.report.period import Period
class BaseCSVRow(ABC):
"""The base CSV row."""
@property
@abstractmethod
def values(self) -> list[str | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
"""Exports the data rows as a CSV file for download.
:param filename: The download file name.
:param rows: The data rows.
:return: The response for download the CSV file.
"""
with StringIO() as fp:
writer = csv.writer(fp)
writer.writerows([x.values for x in rows])
fp.seek(0)
response: Response = Response(fp.read(), mimetype="text/csv")
response.headers["Content-Disposition"] \
= f"attachment; filename={quote(filename)}"
return response
def period_spec(period: Period) -> str:
"""Constructs the period specification to be used in the filename.
:param period: The period.
:return: The period specification to be used in the filename.
"""
start: str | None = __get_start_str(period.start)
end: str | None = __get_end_str(period.end)
if period.start is None and period.end is None:
return "all-time"
if start == end:
return start
if period.start is None:
return f"until-{end}"
if period.end is None:
return f"since-{start}"
return f"{start}-{end}"
def __get_start_str(start: dt.date | None) -> str | None:
"""Returns the string representation of the start date.
:param start: The start date.
:return: The string representation of the start date, or None if the start
date is None.
"""
if start is None:
return None
if start.month == 1 and start.day == 1:
return str(start.year)
if start.day == 1:
return start.strftime("%Y%m")
return start.strftime("%Y%m%d")
def __get_end_str(end: dt.date | None) -> str | None:
"""Returns the string representation of the end date.
:param end: The end date.
:return: The string representation of the end date, or None if the end
date is None.
"""
if end is None:
return None
if end.month == 12 and end.day == 31:
return str(end.year)
if (end + dt.timedelta(days=1)).day == 1:
return end.strftime("%Y%m")
return end.strftime("%Y%m%d")