GoodERP
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

257 lines
8.6KB

  1. # Copyright 2016 上海开阖软件有限公司 (http://www.osbzr.com)
  2. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  3. import random
  4. from docxtpl import DocxTemplate
  5. from datetime import datetime, timedelta
  6. import pytz
  7. import tempfile
  8. import os
  9. import subprocess
  10. import re
  11. import platform
  12. from odoo import models
  13. from odoo import fields
  14. from odoo.exceptions import UserError
  15. from odoo.tools import misc
  16. # try:
  17. # # pip install docx2pdf
  18. # from docx2pdf import convert
  19. # except ImportError:
  20. # convert = None
  21. # try:
  22. # # pip install pywin32
  23. # import pythoncom
  24. # except ImportError:
  25. # pythoncom = None
  26. class DataModelProxy(object):
  27. '''使用一个代理类,来转发 model 的属性,用来消除掉属性值为 False 的情况
  28. 且支持 selection 字段取到实际的显示值
  29. '''
  30. DEFAULT_TZ = 'Asia/Shanghai'
  31. def __init__(self, data):
  32. self.data = data
  33. def _compute_by_selection(self, field, temp):
  34. if field and field.type == 'selection':
  35. # _description_selection 会将标签翻译到对应语言
  36. selection = field._description_selection(self.data.env)
  37. try:
  38. return [value for _, value in selection if _ == temp][0]
  39. except KeyError:
  40. temp = ''
  41. return temp
  42. def _compute_by_datetime(self, field, temp):
  43. if field and field.type == 'datetime' and temp:
  44. tz = pytz.timezone(
  45. self.data.env.context.get('tz') or self.DEFAULT_TZ)
  46. temp_date = fields.Datetime.from_string(temp) + tz._utcoffset
  47. temp = fields.Datetime.to_string(temp_date)
  48. return temp
  49. def _compute_temp_false(self, field, temp):
  50. if not temp:
  51. if field and field.type in ('integer', 'float'):
  52. return 0
  53. if field.type == 'float' and int(temp) == temp:
  54. temp = int(temp)
  55. return temp or ''
  56. def __getattr__(self, key):
  57. if not self.data:
  58. return ""
  59. # 支持 dict 类型的报表数据源,并支持使用 "." 操作符获取属性值
  60. if isinstance(self.data, dict):
  61. value = self.data.get(key, '')
  62. if isinstance(value, (dict, models.Model, models.TransientModel)):
  63. value = DataModelProxy(value)
  64. return value
  65. temp = getattr(self.data, key)
  66. field = self.data._fields.get(key)
  67. if isinstance(temp, str) and (
  68. '&' in temp or '<' in temp or '>' in temp):
  69. temp = temp.replace('&', '&amp;').replace(
  70. '<', '&lt;').replace('>', '&gt;')
  71. # 增加支持 models.TransientModel 数据源
  72. if isinstance(temp, (models.Model, models.TransientModel)):
  73. return DataModelProxy(temp)
  74. # 允许从 method 中获得数据
  75. if callable(temp):
  76. return temp
  77. temp = self._compute_by_selection(field, temp)
  78. temp = self._compute_by_datetime(field, temp)
  79. return self._compute_temp_false(field, temp)
  80. def __getitem__(self, index):
  81. '''支持列表取值'''
  82. if isinstance(self.data, dict):
  83. return DataModelProxy(dict([list(self.data.items())[index]]))
  84. return DataModelProxy(self.data[index])
  85. def __iter__(self):
  86. '''支持迭代器行为'''
  87. return IterDataModelProxy(self.data)
  88. def __len__(self):
  89. '''支持返回长度'''
  90. if isinstance(self.data, dict):
  91. return len(self.data.items())
  92. return len(self.data)
  93. def __str__(self):
  94. '''支持直接在word 上写 many2one 字段'''
  95. name = ''
  96. if isinstance(self.data, dict):
  97. val = list(self.data.values())
  98. if len(val) > 0:
  99. name = str(val[0])
  100. return name
  101. if self.data and self.data.display_name:
  102. name = self.data.display_name
  103. if '&' in self.data.display_name:
  104. name = name.replace('&', '&amp;')
  105. if '<' in self.data.display_name:
  106. name = name.replace('<', '&lt;')
  107. if '>' in self.data.display_name:
  108. name = name.replace('>', '&gt;')
  109. return name
  110. class IterDataModelProxy(object):
  111. '''迭代器类,用 next 函数支持 for in 操作'''
  112. def __init__(self, data):
  113. self.data = data
  114. if isinstance(self.data, dict):
  115. self.length = len(list(data.items()))
  116. else:
  117. self.length = len(data)
  118. self.current = 0
  119. def __next__(self):
  120. if self.current >= self.length:
  121. raise StopIteration()
  122. if isinstance(self.data, dict):
  123. temp = DataModelProxy(
  124. dict([list(self.data.items())[self.current]]))
  125. else:
  126. temp = DataModelProxy(self.data[self.current])
  127. self.current += 1
  128. return temp
  129. class ReportDocx(models.TransientModel):
  130. _name = 'gooderp.report.docx'
  131. _description = 'docx report'
  132. ir_actions_report_id = fields.Many2one(
  133. comodel_name="ir.actions.report",
  134. required=True
  135. )
  136. def generate_temp_file(self, tempname, suffix='docx'):
  137. return os.path.join(tempname, 'temp_%s_%s.%s' %
  138. (os.getpid(), random.randint(1, 10000), suffix))
  139. def create_report(self, res_ids, data):
  140. # 如果提供了 res_ids(报表model的IDS)则优先使用此数据,
  141. # data 提供额外的筛选条件,在 res_ids为空的情况下,使用
  142. # data 提供的筛选条件生成自定义数据,通过调用 model 的
  143. # get_report_data 获得 dict 格式数据
  144. report_data = DataModelProxy(self.get_docx_data(
  145. self.ir_actions_report_id, res_ids, data))
  146. tempname = tempfile.mkdtemp()
  147. temp_out_file = self.generate_temp_file(tempname)
  148. doc = DocxTemplate(misc.file_open(
  149. self.ir_actions_report_id.template_file).name)
  150. # 2016-11-2 支持了图片
  151. # 1.导入依赖,python3语法
  152. from . import report_helper
  153. # 2. 需要添加一个"tpl"属性获得模版对象
  154. doc.render({'obj': report_data, 'tpl': doc}, report_helper.get_env())
  155. doc.save(temp_out_file)
  156. if self.ir_actions_report_id.output_type == 'pdf':
  157. temp_file = self.render_to_pdf(temp_out_file)
  158. else:
  159. temp_file = temp_out_file
  160. report_stream = ''
  161. with open(temp_file, 'rb') as input_stream:
  162. report_stream = input_stream.read()
  163. os.remove(temp_file)
  164. return report_stream, self.ir_actions_report_id.output_type
  165. def render_to_pdf(self, temp_file):
  166. folder = tempfile.mkdtemp()
  167. args = ['libreoffice', '--headless', '--convert-to',
  168. 'pdf', '--outdir', folder, temp_file]
  169. # if platform.system() == "Windows":
  170. # # 线程初始化
  171. # pythoncom.CoInitialize()
  172. # filename = temp_file.split('.')[0]+'.pdf'
  173. # convert(temp_file, filename)
  174. # # 释放资源
  175. # pythoncom.CoUninitialize()
  176. # return filename
  177. if platform.system() == "Windows":
  178. args = ["soffice",'--headless', '--convert-to',
  179. 'pdf', '--outdir', folder, temp_file]
  180. process = subprocess.run(
  181. args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  182. filename = re.search('-> (.*?) using filter', process.stdout.decode())
  183. if filename is None:
  184. raise UserError(process.stdout.decode())
  185. else:
  186. return filename.group(1)
  187. def get_docx_data(self, report, res_ids, data=None):
  188. # 打印时, 在消息处显示打印人
  189. # 2019.10.28 信莱德软件,并不是每个
  190. # report.model 都继续自 mail.thread,
  191. # 所以这里不能强求 message_post 执行成功
  192. try:
  193. message = str((datetime.now() + timedelta(hours=8)).strftime(
  194. '%Y-%m-%d %H:%M:%S')) + ' ' + self.env.user.name + u' 打印了该单据'
  195. for record in self.env.get(report.model).browse(res_ids):
  196. record.message_post(body=message)
  197. except Exception as e:
  198. pass
  199. res = self.env.get(report.model).sudo().browse(res_ids)
  200. if len(res) == 0:
  201. data = data and dict(data) or {}
  202. report_func = data.get('report_function', 'get_report_data')
  203. func = getattr(self.env.get(report.model), report_func)
  204. if callable(func):
  205. res = func(data=data)
  206. return res
  207. def _save_file(self, folder_name, file):
  208. out_stream = open(folder_name, 'wb')
  209. try:
  210. out_stream.writelines(file)
  211. finally:
  212. out_stream.close()
上海开阖软件有限公司 沪ICP备12045867号-1