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.

248 lines
8.3KB

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