一个openpyxl操作Excel进行单元格合并、边框、字体大小、货币格式、公式的实例

272 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

需求背景:根据前端传来的数据,生成固定格式的Excel表,保存到服务器,并返回下载链接

依赖包版本:

[tool.poetry.dependencies]
python = "^3.10"
fastapi = "^0.75.2"
uvicorn = "^0.17.6"
xlrd = "^2.0.1"
xlwt = "^1.3.0"
python-dotenv = "^0.20.0"
openpyxl = "^3.0.9"
aioredis = {extras = ["hiredis"], version = "^2.0.1"}
httpx = "^0.22.0"
gunicorn = "^20.1.0"
loguru = "^0.6.0"

关键模块代码如下:

from datetime import datetime
from enum import Enum, IntEnum
from pathlib import Path
from typing import List, Optional, Tuple, Union

from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, Side

from settings import BASE_DIR, MEDIA_ROOT

MAI_TOU_ICON = "⬆️"
MONEY_FMT = "¥#,##0.00"
CENTER_ALIGN = Alignment(horizontal="center", vertical="center")


class ColorEnum(str, Enum):
    red = "FF0000"


class CollumnEnum(IntEnum):
    xuhao = 1
    customer = 2
    factory = 3
    size = 4
    juan = 5
    auto_jian = 6
    price = 7
    total = 8
    pack = 9
    mai_tou = 10


def thin_border() -> Border:
    thin = Side(border_style="thin", color="000000")
    return Border(top=thin, left=thin, right=thin, bottom=thin)


def write_order_num(ws, value: str) -> None:
    cell = ws["B1"]
    cell.value = value
    cell.font = Font(size=36, underline="singleAccounting")
    cell.alignment = CENTER_ALIGN


def write_special(ws, value: str) -> None:
    cell = ws["A2"]
    cell.value = value
    # cell = ws.cell(2, 1, value)
    cell.font = Font(color=ColorEnum.red)
    cell.alignment = Alignment(vertical="center")


def write_date(ws, value: str) -> None:
    cell = ws.cell(1, 10, value)
    cell.alignment = Alignment(horizontal="right")
    cell.font = Font(size=16)


def fill_row_one_and_row_two(ws, order_num, special, now) -> Border:
    ws.merge_cells("B1:I1")
    ws.merge_cells("A2:J2")
    ws.row_dimensions[1].height = 51.2
    ws.row_dimensions[2].height = 29.1
    col_widths = [7, 14.15, 18.14, 26.71, 12.92, 12.92, 11.86, 13.48, 39.73, 35.86]
    first_ord = ord("A")
    for col_ord, width in enumerate(col_widths, first_ord):
        ws.column_dimensions[chr(col_ord)].width = width
    write_order_num(ws, order_num)
    write_special(ws, special)

    date = f"日期:{now:%m.%d} "
    write_date(ws, date)

    border = thin_border()
    cell_range = ws["A1":"J2"]  # type:ignore
    for row in cell_range:
        for cell in row:
            cell.border = border
    return border


def fill_title(ws, border) -> None:
    ws.row_dimensions[4].height = 44
    titles = "序号 客户款号 工厂款号 尺寸 卷数 件数 单价 总金额 包装要求⚠️  唛头"
    for col, t in enumerate(titles.split(), 1):
        cell = ws.cell(4, col, t)
        cell.border = border
        cell.alignment = CENTER_ALIGN
        cell.font = Font(size=16)


def fill_body(ws, border, lines) -> int:
    start_row = 5
    row_height = 124.5
    body_font = Font(size=12)
    mai_font = Font(size=36)
    mai_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
    for row, data in enumerate(lines, start_row):
        ws.row_dimensions[row].height = row_height
        col_value = {c + 1: v for _, c, v in data}
        if size := col_value.get(CollumnEnum.size):
            if isinstance(size, int) or size.isdigit():
                col_value[CollumnEnum.size] = f"0.45*{size}米/卷"
        col_value[CollumnEnum.total] = f"=E{row}*G{row}"
        if (v := col_value[CollumnEnum.mai_tou]).startswith(MAI_TOU_ICON):
            v = v.replace(MAI_TOU_ICON, "").strip()
        col_value[CollumnEnum.mai_tou] = MAI_TOU_ICON + "\n" + v
        for col in CollumnEnum:
            cell = ws.cell(row, col, col_value.get(col, ""))
            cell.border = border
            cell.alignment = CENTER_ALIGN
            cell.font = body_font
        ws[f"G{row}"].number_format = ws[f"H{row}"].number_format = MONEY_FMT
        ws[f"J{row}"].font = mai_font
        ws[f"J{row}"].alignment = mai_align
    return row


def fill_tail(ws, border, row) -> None:
    formula = "=SUM({0}5:INDEX({0}:{0},ROW()-1))"
    col_value = {"B": "合计", "E": None, "H": None}
    tail_font = Font(size=12)
    ws.row_dimensions[row].height = 24
    for col in "ABCDEFGHIJ":
        if (value := col_value.get(col, "")) is None:
            value = formula.format(col)
        cell = ws[f"{col}{row}"]
        cell.value = value
        cell.border = border
        cell.alignment = CENTER_ALIGN
        cell.font = tail_font
    ws[f"H{row}"].number_format = MONEY_FMT


def run(
    lines: List[List[Tuple[int, int, str]]],
    fname: Optional[Union[Path, str]] = None,
    order_num: Optional[str] = None,
    special="特殊事项:  纸管+纸箱+唛头和货号",
) -> str:
    now = datetime.now()
    wb = Workbook()
    ws = wb.active
    border = fill_row_one_and_row_two(ws, order_num, special, now)
    ws.row_dimensions[3].height = 16.8
    fill_title(ws, border)
    last_row = fill_body(ws, border, lines)
    fill_tail(ws, border, last_row + 1)

    if fname is None:
        fname = MEDIA_ROOT / f"order_{order_num}_{now:%Y%m%d%H%M%S}.xls"
    wb.save(fname)
    return str(fname).replace(str(BASE_DIR) + "/", "")  # 返回相对路径


def main():
    from data_sample import datas

    num = len(list(MEDIA_ROOT.glob("*"))) + 1
    fn = run(datas, None, f"305-{num}")
    print(fn)


if __name__ == "__main__":
    main()