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.

232 lines
8.9 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  2. import datetime
  3. import logging
  4. import os
  5. import odoo
  6. from odoo import api, fields, models, tools
  7. from odoo.osv import expression
  8. _logger = logging.getLogger(__name__)
  9. try:
  10. # We use a jinja2 sandboxed environment to render mako templates.
  11. # Note that the rendering does not cover all the mako syntax, in particular
  12. # arbitrary Python statements are not accepted, and not all expressions are
  13. # allowed: only "public" attributes (not starting with '_') of objects may
  14. # be accessed.
  15. # This is done on purpose: it prevents incidental or malicious execution of
  16. # Python code that may break the security of the server.
  17. from jinja2.sandbox import SandboxedEnvironment
  18. mako_template_env = SandboxedEnvironment(
  19. variable_start_string="${",
  20. variable_end_string="}",
  21. line_statement_prefix="%",
  22. trim_blocks=True, # do not output newline after blocks
  23. )
  24. mako_template_env.globals.update(
  25. {
  26. "str": str,
  27. "datetime": datetime,
  28. "len": len,
  29. "abs": abs,
  30. "min": min,
  31. "max": max,
  32. "sum": sum,
  33. "filter": filter,
  34. "map": map,
  35. "round": round,
  36. }
  37. )
  38. except ImportError:
  39. _logger.warning("jinja2 not available, templating features will not work!")
  40. class AttachmentSynchronizeTask(models.Model):
  41. _name = "attachment.synchronize.task"
  42. _description = "Attachment synchronize task"
  43. name = fields.Char(required=True)
  44. method_type = fields.Selection(
  45. [("import", "Import Task"), ("export", "Export Task")], required=True
  46. )
  47. pattern = fields.Char(
  48. string="Selection Pattern",
  49. help="Pattern used to select the files to be imported following the 'fnmatch' "
  50. "special characters (e.g. '*.txt' to catch all the text files).\n"
  51. "If empty, import all the files found in 'File Path'.",
  52. )
  53. filepath = fields.Char(
  54. string="File Path", help="Path to imported/exported files in the Backend"
  55. )
  56. backend_id = fields.Many2one("storage.backend", string="Backend")
  57. attachment_ids = fields.One2many("attachment.queue", "task_id", string="Attachment")
  58. move_path = fields.Char(
  59. string="Move Path", help="Imported File will be moved to this path"
  60. )
  61. new_name = fields.Char(
  62. string="New Name",
  63. help="Imported File will be renamed to this name.\n"
  64. "New Name can use 'mako' template where 'obj' is the original file's name.\n"
  65. "For instance : ${obj.name}-${obj.create_date}.csv",
  66. )
  67. after_import = fields.Selection(
  68. selection=[
  69. ("rename", "Rename"),
  70. ("move", "Move"),
  71. ("move_rename", "Move & Rename"),
  72. ("delete", "Delete"),
  73. ],
  74. help="Action after import a file",
  75. )
  76. file_type = fields.Selection(
  77. selection=[],
  78. string="File Type",
  79. help="Used to fill the 'File Type' field in the imported 'Attachments Queues'."
  80. "\nFurther operations will be realized on these Attachments Queues depending "
  81. "on their 'File Type' value.",
  82. )
  83. active = fields.Boolean("Enabled", default=True, old="enabled")
  84. avoid_duplicated_files = fields.Boolean(
  85. string="Avoid importing duplicated files",
  86. help="If checked, a file will not be imported if an Attachment Queue with the "
  87. "same name already exists.",
  88. )
  89. failure_emails = fields.Char(
  90. string="Failure Emails",
  91. help="Used to fill the 'Failure Emails' field in the 'Attachments Queues' "
  92. "related to this task.\nAn alert will be sent to these emails if any operation "
  93. "on these Attachment Queue's file type fails.",
  94. )
  95. count_attachment_failed = fields.Integer(compute="_compute_count_state")
  96. count_attachment_pending = fields.Integer(compute="_compute_count_state")
  97. count_attachment_done = fields.Integer(compute="_compute_count_state")
  98. def _compute_count_state(self):
  99. for record in self:
  100. for state in ["failed", "pending", "done"]:
  101. record["count_attachment_{}".format(state)] = \
  102. len(record.attachment_ids.filtered(lambda r: r.state == state))
  103. def _prepare_attachment_vals(self, data, filename):
  104. self.ensure_one()
  105. vals = {
  106. "name": filename,
  107. "datas": data,
  108. "datas_fname": filename,
  109. "task_id": self.id,
  110. "file_type": self.file_type or False,
  111. }
  112. return vals
  113. @api.model
  114. def _template_render(self, template, record):
  115. try:
  116. template = mako_template_env.from_string(tools.ustr(template))
  117. except Exception:
  118. _logger.exception("Failed to load template '{}'".format(template))
  119. variables = {"obj": record}
  120. try:
  121. render_result = template.render(variables)
  122. except Exception:
  123. _logger.exception(
  124. "Failed to render template '{}'' using values '{}'".format(
  125. template, variables
  126. )
  127. )
  128. render_result = u""
  129. if render_result == u"False":
  130. render_result = u""
  131. return render_result
  132. @api.model
  133. def run_task_import_scheduler(self, domain=None):
  134. if domain is None:
  135. domain = []
  136. domain = expression.AND(
  137. [domain, [("method_type", "=", "import")]]
  138. )
  139. for task in self.search(domain):
  140. task.run_import()
  141. def run(self):
  142. for record in self:
  143. method = "run_{}".format(record.method_type)
  144. if not hasattr(self, method):
  145. raise NotImplemented
  146. else:
  147. getattr(record, method)()
  148. def run_import(self):
  149. self.ensure_one()
  150. attach_obj = self.env["attachment.queue"]
  151. backend = self.backend_id
  152. filepath = self.filepath or ""
  153. filenames = backend._list(relative_path=filepath, pattern=self.pattern)
  154. if self.avoid_duplicated_files:
  155. filenames = self._file_to_import(filenames)
  156. total_import = 0
  157. for file_name in filenames:
  158. with api.Environment.manage():
  159. with odoo.registry(self.env.cr.dbname).cursor() as new_cr:
  160. new_env = api.Environment(new_cr, self.env.uid, self.env.context)
  161. try:
  162. full_absolute_path = os.path.join(filepath, file_name)
  163. data = backend._get_b64_data(full_absolute_path)
  164. attach_vals = self._prepare_attachment_vals(data, file_name)
  165. attachment = attach_obj.with_env(new_env).create(attach_vals)
  166. new_full_path = False
  167. if self.after_import == "rename":
  168. new_name = self._template_render(self.new_name, attachment)
  169. new_full_path = os.path.join(filepath, new_name)
  170. elif self.after_import == "move":
  171. new_full_path = os.path.join(self.move_path, file_name)
  172. elif self.after_import == "move_rename":
  173. new_name = self._template_render(self.new_name, attachment)
  174. new_full_path = os.path.join(self.move_path, new_name)
  175. if new_full_path:
  176. backend._add_b64_data(new_full_path, data)
  177. if self.after_import in (
  178. "delete",
  179. "rename",
  180. "move",
  181. "move_rename",
  182. ):
  183. backend._delete(full_absolute_path)
  184. total_import += 1
  185. except Exception as e:
  186. new_env.cr.rollback()
  187. raise e
  188. else:
  189. new_env.cr.commit()
  190. _logger.info("Run import complete! Imported {0} files".format(total_import))
  191. def _file_to_import(self, filenames):
  192. imported = (
  193. self.env["attachment.queue"]
  194. .search([("name", "in", filenames)])
  195. .mapped("name")
  196. )
  197. return list(set(filenames) - set(imported))
  198. def run_export(self):
  199. for task in self:
  200. task.attachment_ids.filtered(lambda a: a.state == "pending").run()
  201. def button_duplicate_record(self):
  202. # due to orm limitation method call from ui should not have params
  203. # so we need to define this method to be able to copy
  204. # if we do not do this the context will be injected in default params
  205. # in V14 maybe we can call copy directly
  206. self.copy()
  207. def copy(self, default=None):
  208. if default is None:
  209. default = {}
  210. if "active" not in default:
  211. default["active"] = False
  212. return super().copy(default=default)