From c6dd8469b9bbcc3e99719cdbc956c04f00e270e9 Mon Sep 17 00:00:00 2001 From: han0 Date: Wed, 18 Nov 2020 17:21:20 +0800 Subject: [PATCH] feat: sheet --- nc_http/tests/sheet.py | 30 ++++++ nc_http/utils/sheet/__init__.py | 0 nc_http/utils/sheet/paper_size.py | 6 ++ nc_http/utils/sheet/sample.py | 87 +++++++++++++++++ nc_http/utils/sheet/sheet.py | 155 ++++++++++++++++++++++++++++++ 5 files changed, 278 insertions(+) create mode 100644 nc_http/tests/sheet.py create mode 100644 nc_http/utils/sheet/__init__.py create mode 100644 nc_http/utils/sheet/paper_size.py create mode 100644 nc_http/utils/sheet/sample.py create mode 100644 nc_http/utils/sheet/sheet.py diff --git a/nc_http/tests/sheet.py b/nc_http/tests/sheet.py new file mode 100644 index 0000000..dfbaab0 --- /dev/null +++ b/nc_http/tests/sheet.py @@ -0,0 +1,30 @@ +import unittest + +from nc_http.utils.sheet.sample import SampleSheet + + +class SheetTestCase(unittest.TestCase): + test_data = [ + { + 'admin_unit_official_name': '测试地区 1', + 'count_valid_situation': 34, + 'count_valid_situation_per': 0.32, + 'count_valid_situation_yoy': 0.34, + 'count_valid_situation_mom': 0.88, + }, + { + 'admin_unit_official_name': '测试地区 2', + 'count_valid_situation': 56, + 'count_valid_situation_per': 0.62, + 'count_valid_situation_yoy': 0.87, + 'count_valid_situation_mom': 0.42, + }, + ] + + def test_create(self): + sheet = SampleSheet(self.test_data, year='2018') + sheet.create('sample.xlsx') + + +if __name__ == '__main__': + unittest.main() diff --git a/nc_http/utils/sheet/__init__.py b/nc_http/utils/sheet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nc_http/utils/sheet/paper_size.py b/nc_http/utils/sheet/paper_size.py new file mode 100644 index 0000000..e904bca --- /dev/null +++ b/nc_http/utils/sheet/paper_size.py @@ -0,0 +1,6 @@ +class PaperSize: + A3 = 8 + A4 = 9 + A5 = 11 + B4 = 12 + B5 = 13 diff --git a/nc_http/utils/sheet/sample.py b/nc_http/utils/sheet/sample.py new file mode 100644 index 0000000..635238c --- /dev/null +++ b/nc_http/utils/sheet/sample.py @@ -0,0 +1,87 @@ +from nc_http.utils.sheet.sheet import Sheet + + +class SampleSheet(Sheet): + """ + 样例表格 + """ + + all_fields = [ + 'admin_unit_official_name', + 'count_valid_situation', + 'count_valid_situation_per', + 'count_valid_situation_yoy', + 'count_valid_situation_mom', + ] + style_formats = { + 'title': { + 'font': 'Arial Unicode MS', 'font_size': 20, 'bold': False, + 'align': 'center', 'valign': 'vcenter', + }, + 'header': { + 'font': 'Arial Unicode MS', 'font_size': 11, 'bold': False, + 'align': 'center', 'valign': 'vcenter', 'border': True, 'text_wrap': False, + }, + 'body': { + 'font': 'Arial Unicode MS', 'font_size': 11, 'bold': False, + 'align': 'center', 'valign': 'vcenter', 'border': True, 'text_wrap': True, + }, + 'remark': { + 'font': 'Arial Unicode MS', 'font_size': 10, 'bold': False, + 'align': 'center', 'valign': 'vcenter', + 'bottom': 1, 'top_color': 'white', 'top': 1 + }, + 'remark_year': { + 'font': 'Arial Unicode MS', 'font_size': 8, 'bold': False, + 'align': 'center', 'valign': 'vcenter', + 'bottom': 1, 'top_color': 'white', 'top': 1, + 'num_format': 'yyyy"年"' + }, + } + round_digits = 4 + row_stretches = [ + [0, 30], + ] + col_stretches = [ + [0, 0, 29, {'text_wrap': True}], + [1, 1, 20], + [2, 2, 20, {'num_format': '0.00%'}], + [3, 3, 20, {'num_format': '0.00%'}], + [4, 4, 20, {'num_format': '0.00%'}], + ] + + def set_sheet(self): + self.title = '全市管辖单位样例数据统计表' + self.headers = [ + ['', '', '', ], + ['', '', '', ], + ['地区项目', '总数', '占比情况', '同比情况', '环比情况', ], + ] + self.merges = [ + [0, 0, 0, 4, self.title, self.style_formats['title']], + [1, 0, 1, 4, '{} 年'.format(self.meta['year']), self.style_formats['remark']], + ] + + def clean_data(self): + return [self.flat_row(item) for item in self.origin_data] + + +if __name__ == '__main__': + test_data = [ + { + 'admin_unit_official_name': '测试地区 1', + 'count_valid_situation': 34, + 'count_valid_situation_per': 0.32, + 'count_valid_situation_yoy': 0.34, + 'count_valid_situation_mom': 0.88, + }, + { + 'admin_unit_official_name': '测试地区 2', + 'count_valid_situation': 56, + 'count_valid_situation_per': 0.62, + 'count_valid_situation_yoy': 0.87, + 'count_valid_situation_mom': 0.42, + }, + ] + sheet = SampleSheet(test_data, year='2018') + sheet.create('sample.xlsx') diff --git a/nc_http/utils/sheet/sheet.py b/nc_http/utils/sheet/sheet.py new file mode 100644 index 0000000..095cef8 --- /dev/null +++ b/nc_http/utils/sheet/sheet.py @@ -0,0 +1,155 @@ +import os +import subprocess + +from nc_http.core.excel.excel_writer import ExcelWriter +from nc_http.utils.sheet.paper_size import PaperSize + + +class Sheet: + origin_data = [] # 源统计数据 + data = [] # 处理后的统计数据 + year = '' # 表数据代表年份 + title = '' # 表标题 + headers = [] # 表头指定 + merges = [] # 单元格合并指定 + row_stretches = [] # 行高指定 + col_stretches = [] # 列宽指定 + filename = '' # 文件名 + pdf_filename = '' # 文件名 + paper = PaperSize.A3 # 纸张大小 + + round_digits = 1 # 小数保留位数 + subtotal_fields = [] # 需要进行小计累加的字段 + all_fields = [] # 所有字段 + + style_formats = {} # 表头、表体样式 + + def __init__(self, data, title=None, headers=None, merges=None, row_stretches=None, col_stretches=None, **meta): + self.meta = {} + self.origin_data = data + if title: + self.title = title + if headers: + self.headers = headers + if merges: + self.merges = merges + if row_stretches: + self.row_stretches = row_stretches + if col_stretches: + self.col_stretches = col_stretches + if meta: + self.meta = meta + + self.set_sheet() + self.data = self.clean_data() + # self.filename = self.create_filename() + # self.pdf_filename = self.create_filename(suffix='pdf') + + def set_sheet(self): + """ + 处理相关动态配置 + :return: + """ + pass + + def clean_data(self): + """ + 数据预清洗 + :return: + """ + return list(self.data) + + def create(self, file_path): + """ + 创建 excel 文件 + :param file_path: + :return: + """ + return ExcelWriter.create_excel( + file_path, self.data, + headers=self.headers, + merges=self.merges, + row_stretches=self.row_stretches, + col_stretches=self.col_stretches, + paper=self.paper, + style_formats=self.style_formats, + ) + + def pack(self): + """ + 创建 excel 文件句柄 + :return: + """ + return ExcelWriter.pack_excel( + self.data, + headers=self.headers, + merges=self.merges, + row_stretches=self.row_stretches, + col_stretches=self.col_stretches, + paper=self.paper, + style_formats=self.style_formats, + ) + + @classmethod + def _init_subtotal(cls, subtotal, fields=None): + """ + 小计初始化 + :param subtotal: + :param fields: + :return: + """ + fields = fields or cls.subtotal_fields + for field in fields: + if subtotal.get(field) is None: + subtotal[field] = 0 + + @classmethod + def _add_subtotal(cls, subtotal, item, fields): + """ + 小计累加 + :param subtotal: dict 小计 + :param item: dict 统计项 + :param fields: list 需要小计字段 + :return: + """ + for field in fields: + if subtotal.get(field) is None: + subtotal[field] = 0 + # subtotal.setdefault(field, 0) + subtotal[field] += round((item[field] or 0), cls.round_digits) + # 小计小数点后三位舍入(四舍六入五成双) 若遇到数据舍入错误建议牺牲性能更换为 Decimal 运算 + # subtotal[field] = round(subtotal[field], cls.round_digits) + return subtotal + + def create_filename(self, suffix='xlsx'): + """ + 创建表格文件名 + :param suffix: + :return: + """ + filename = '{}.{}'.format(self.title, suffix) + return os.sep.join([self.meta['year'], '表', filename]) + + def flat_row(self, item): + return [round(item[k], self.round_digits) if isinstance(item.get(k), float) else item.get(k, '') for k in + self.all_fields] + + @staticmethod + def create_pdf(sheet_file, path=None): + """ + 创建 PDF 文件 (依赖 soffice) + :param sheet_file: + :param path: + :return: + """ + path = path or os.sep.join(sheet_file.split(os.sep)[:-1]) + command = "soffice --headless --convert-to pdf {} --outdir {}".format(sheet_file, path) + try: + p = subprocess.call(command, shell=True) + print(p) + assert p == 0, '转格式失败...' + except Exception as e: + print('create_pdf_file | command: {}'.format(command)) + # Logger.warning('create_pdf_file | command: {}'.format(command)) + raise e + return sheet_file.replace('.xlsx', '.pdf')