|
- # © 2016 cole
- # Copyright 2016 上海开阖软件有限公司 (http://www.osbzr.com)
- # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-
- from docxtpl import DocxTemplate
-
- import docx
-
- import jinja2
- from datetime import datetime
- from reportlab.graphics.barcode import createBarcodeDrawing
-
- """
- 使用一个独立的文件来封装需要支持图片等功能,避免污染report_docx.py
- """
-
-
- def calc_length(s):
- """
- 把字符串,数字类型的参数转化为docx的长度对象,如:
- 12 => Pt(12)
- '12' => Pt(12)
- '12pt' => Pt(12) 单位为point
- '12cm' => Cm(12) 单位为厘米
- '12mm' => Mm(12) 单位为毫米
- '12inchs' => Inchs(12) 单位为英寸
- '12emu' => Emu(12)
- '12twips' => Twips(12)
- """
- if not isinstance(s, str):
- # 默认为像素
- return docx.shared.Pt(s)
-
- if s.endswith('cm'):
- return docx.shared.Cm(float(s[:-2]))
- elif s.endswith('mm'):
- return docx.shared.Mm(float(s[:-2]))
- elif s.endswith('inchs'):
- return docx.shared.Inches(float(s[:-5]))
- elif s.endswith('pt') or s.endswith('px'):
- return docx.shared.Pt(float(s[:-2]))
- elif s.endswith('emu'):
- return docx.shared.Emu(float(s[:-3]))
- elif s.endswith('twips'):
- return docx.shared.Twips(float(s[:-5]))
- else:
- # 默认为像素
- return docx.shared.Pt(float(s))
-
-
- def calc_alignment(s):
- """
- 把字符串转换为对齐的常量
- """
- A = docx.enum.text.WD_ALIGN_PARAGRAPH
- if s == 'center':
- return A.CENTER
- elif s == 'left':
- return A.LEFT
- elif s == 'right':
- return A.RIGHT
- else:
- return A.LEFT
-
- @jinja2.pass_context
- def rmb_format(ctx, data):
- """
- 将数值按位数分开
- """
- value = round(data,2)
- if abs(value) < 0.01:
- # 值为0的不输出,即返回12个空格
- return ['' for i in range(12)]
- # 先将数字转为字符,去掉小数点,然后和12个空格拼成列表,取最后12个元素返回
- return (['' for i in range(12)] + list(('%0.2f' % value).replace('.', '')))[-12:]
-
- @jinja2.pass_context
- def rmb_upper(ctx, data, field):
- """
- 人民币大写
- 来自:http://topic.csdn.net/u/20091129/20/b778a93d-9f8f-4829-9297-d05b08a23f80.html
- 传入浮点类型的值返回 unicode 字符串
- :param 传入阿拉伯数字
- :return 返回值是对应阿拉伯数字的绝对值的中文数字
- """
- rmbmap = ["零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"]
- unit = ["分", "角", "元", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿",
- "拾", "佰", "仟", "万", "拾", "佰", "仟", "兆"]
- value = round(sum(getattr(d, field) for d in data), 2)
- # 冲红负数处理
- xflag = 0
- if value < 0:
- xflag = value
- value = abs(value)
- # 先把value 数字进行格式化保留两位小数,转成字符串然后去除小数点
- nums = list(map(int, list(str('%0.2f' % value).replace('.', ''))))
- words = []
- zflag = 0 # 标记连续0次数,以删除万字,或适时插入零字
- start = len(nums) - 3
- for i in range(start, -3, -1): # 使i对应实际位数,负数为角分
- # 大部分情况对应数字不等于零 或者是刚开始循环
- if 0 != nums[start - i] or len(words) == 0:
- if zflag:
- words.append(rmbmap[0])
- zflag = 0
- words.append(rmbmap[nums[start - i]]) # 数字对应的中文字符
- words.append(unit[i + 2]) # 列表此位置的单位
- # 控制‘万/元’ 万和元比较特殊,如2拾万和2拾1万 无论有没有这个1 万字是必须的
- elif 0 == i or (0 == i % 4 and zflag < 3):
- # 上面那种情况定义了 2拾1万 的显示 这个是特殊对待的 2拾万(一类)的显示
- words.append(unit[i + 2])
- # 元(控制条件为 0 == i )和万(控制条为(0 == i % 4 and zflag < 3))的情况的处理是一样的
- zflag = 0
- else:
- zflag += 1
- if words[-1] != unit[0]: # 结尾非‘分’补整字 最小单位 如果最后一个字符不是最小单位(分)则要加一个整字
- words.append("整")
- if xflag < 0: # 如果为负数则要在数字前面加上‘负’字
- words.insert(0, "负")
- return ''.join(words)
-
-
- @jinja2.pass_context
- def total(ctx, data, field):
- return round(sum(getattr(d, field) for d in data), 2)
-
-
- @jinja2.pass_context
- def picture(ctx, data, width=None, height=None, align=None):
- """
- 把图片的二进制数据(使用了base64编码)转化为一个docx.Document对象
-
- data:图片的二进制数据(使用了base64编码)
- word例:{{p line.goods_id.image | picture(width=’6cm’)}}
- width:图片的宽度,可以为:'12cm','12mm','12pt' 等,参考前面的 calc_length()
- height:图片的长度,如果没有设置,根据长度自动缩放
- align:图片的位置,'left','center','right'
- """
-
- if not data:
- return None
-
- # 转化为file-like对象
- # 在python2.7中,bytes==str,可以直接使用
- # 在python3.5中,bytes和str是不同的类型,需要使用base64这个库
-
- # data使用了base64编码,所以这里需要解码
- import base64
- data = base64.b64decode(data)
-
- import io
- data = io.BytesIO(data)
-
- tpl = ctx['tpl']
- doc = tpl.new_subdoc()
-
- if width:
- width = calc_length(width)
- if height:
- height = calc_length(height)
-
- p = doc.add_paragraph()
- p.alignment = calc_alignment(align)
- p.add_run().add_picture(data, width=width, height=height)
- return doc
-
-
- @jinja2.pass_context
- def barcode(ctx, data, barcode_type, width=300, height=70,
- humanreadable=0, quiet=1, align=None):
- '''生成条形码、二维码
-
- 在 word 中用法:
- {{p obj.name|barcode('QR',250,250)}}
-
- (生成 barcode 二进制后的代码,可编码成base64,再调用 picture,只是多了一次编解码过程,略慢)
- :param barcode_type:
- Accepted types: 'Codabar', 'Code11', 'Code128',
- 'EAN13', 'EAN8', 'Extended39',
- 'Extended93', 'FIM', 'I2of5',
- 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93',
- 'UPCA', 'USPS_4State'
- :param humanreadable:
- Accepted values: 0 (default) or 1. 1 will insert the readable value
- at the bottom of the output image
- '''
- value = data
- if barcode_type == 'UPCA' and len(value) in (11, 12, 13):
- barcode_type = 'EAN13'
- if len(value) in (11, 12):
- value = '0%s' % value
- try:
- width, height, humanreadable, quiet = int(width), int(
- height), bool(int(humanreadable)), bool(int(quiet))
- # for `QR` type, `quiet` is not supported. And is simply ignored.
- # But we can use `barBorder` to get a similar behaviour.
- bar_border = 4
- if barcode_type == 'QR' and quiet:
- bar_border = 0
-
- barcode_img = createBarcodeDrawing(
- barcode_type, value=value, format='png',
- width=width, height=height,
- humanReadable=humanreadable, quiet=quiet, barBorder=bar_border
- )
-
- import base64
- barcode_img = base64.b64encode(barcode_img.asString('png'))
-
- return picture(ctx, barcode_img, width, height)
- except (ValueError, AttributeError):
- if barcode_type == 'Code128':
- raise ValueError("Cannot convert into barcode.")
- else:
- return barcode(ctx, data, 'Code128', width=width, height=height,
- humanreadable=humanreadable, quiet=quiet)
-
-
- def get_env():
- """
- 创建一个jinja的enviroment,然后添加一个过滤器
- """
- jinja_env = jinja2.Environment()
- jinja_env.filters['picture'] = picture
- jinja_env.filters['total'] = total
- jinja_env.filters['rmb_upper'] = rmb_upper
- jinja_env.filters['rmb_format'] = rmb_format
- jinja_env.filters['barcode'] = barcode
- jinja_env.globals['time'] = datetime.now()
- return jinja_env
-
-
- def test():
- """
- 演示了如何使用,可以直接执行该文件,但是需要使用自己写的docx模版,和图片
- """
- tpl = DocxTemplate("tpls/test_tpl.docx")
- # 读取图片的数据且使用base64编码
- data = open('tpls/python_logo.png', 'rb').read().encode('base64')
- obj = {'logo': data}
- # 需要添加模版对象
- ctx = {'obj': obj, 'tpl': tpl}
- jinja_env = get_env()
- tpl.render(ctx, jinja_env)
-
- tpl.save('tpls/test.docx')
-
-
- def main():
- test()
-
-
- if __name__ == '__main__':
- main()
|