helpdesk_workflow_template_field.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import logging
  4. from odoo import api, fields, models, _
  5. from odoo.exceptions import UserError
  6. _logger = logging.getLogger(__name__)
  7. class HelpdeskWorkflowTemplateField(models.Model):
  8. """
  9. Form field configuration for workflow templates.
  10. Migrated from helpdesk.template.field to consolidate templates and workflows.
  11. """
  12. _name = 'helpdesk.workflow.template.field'
  13. _description = 'Workflow Template Form Field'
  14. _order = 'sequence, id'
  15. workflow_template_id = fields.Many2one(
  16. 'helpdesk.workflow.template',
  17. string='Workflow Template',
  18. required=True,
  19. ondelete='cascade',
  20. index=True
  21. )
  22. field_id = fields.Many2one(
  23. 'ir.model.fields',
  24. string='Field',
  25. required=True,
  26. domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]",
  27. ondelete='cascade',
  28. help="Field from helpdesk.ticket model"
  29. )
  30. field_name = fields.Char(
  31. related='field_id.name',
  32. string='Field Name',
  33. store=True,
  34. readonly=True
  35. )
  36. field_type = fields.Selection(
  37. related='field_id.ttype',
  38. string='Field Type',
  39. readonly=True
  40. )
  41. label_custom = fields.Char(
  42. string='Custom Label',
  43. help="Custom label for the field in the form. If empty, uses the field's default label."
  44. )
  45. placeholder = fields.Text(
  46. string='Placeholder',
  47. help="Placeholder text shown when field is empty"
  48. )
  49. default_value = fields.Char(
  50. string='Default Value',
  51. help="Default value for the field"
  52. )
  53. help_text = fields.Html(
  54. string='Help Text',
  55. help="Help text/description shown below the field (supports HTML formatting)"
  56. )
  57. widget = fields.Selection(
  58. [
  59. ('default', 'Default'),
  60. ('radio', 'Radio Buttons'),
  61. ('checkbox', 'Checkboxes'),
  62. ],
  63. string='Widget',
  64. default='default',
  65. help="Widget to use for selection/many2one fields. Default uses dropdown select."
  66. )
  67. selection_type = fields.Selection(
  68. [
  69. ('dropdown', 'Dropdown List'),
  70. ('radio', 'Radio'),
  71. ],
  72. string='Selection Type',
  73. default='dropdown',
  74. help="Display type for selection and many2one fields."
  75. )
  76. selection_options = fields.Text(
  77. string='Selection Options',
  78. help="For selection fields (not relations): JSON array of [value, label] pairs."
  79. )
  80. rows = fields.Integer(
  81. string='Height (Rows)',
  82. default=3,
  83. help="Number of rows for textarea fields. Default is 3."
  84. )
  85. input_type = fields.Selection(
  86. [
  87. ('text', 'Text'),
  88. ('email', 'Email'),
  89. ('tel', 'Telephone'),
  90. ('url', 'Url'),
  91. ],
  92. string='Input Type',
  93. default='text',
  94. help="Input type for text fields. Determines the HTML input type attribute."
  95. )
  96. sequence = fields.Integer(
  97. string='Sequence',
  98. default=10,
  99. help="Order in which fields are displayed"
  100. )
  101. required = fields.Boolean(
  102. string='Required',
  103. default=False,
  104. help="Make this field required in addition to its base configuration"
  105. )
  106. model_required = fields.Boolean(
  107. string='Model Required',
  108. default=False,
  109. readonly=True,
  110. help="This field is mandatory for the model and cannot be removed"
  111. )
  112. # Visibility conditions
  113. visibility_dependency = fields.Many2one(
  114. 'ir.model.fields',
  115. string='Visibility Dependency',
  116. domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]",
  117. help="Field on which visibility depends"
  118. )
  119. visibility_condition = fields.Char(
  120. string='Visibility Condition Value',
  121. help="Value to compare against the dependency field"
  122. )
  123. visibility_comparator = fields.Selection(
  124. [
  125. # Basic comparators
  126. ('equal', 'Is equal to'),
  127. ('!equal', 'Is not equal to'),
  128. ('contains', 'Contains'),
  129. ('!contains', "Doesn't contain"),
  130. ('set', 'Is set'),
  131. ('!set', 'Is not set'),
  132. # Numeric comparators
  133. ('greater', 'Is greater than'),
  134. ('less', 'Is less than'),
  135. ('greater or equal', 'Is greater than or equal to'),
  136. ('less or equal', 'Is less than or equal to'),
  137. # Date/Datetime comparators
  138. ('dateEqual', 'Is equal to (date)'),
  139. ('date!equal', 'Is not equal to (date)'),
  140. ('after', 'Is after'),
  141. ('before', 'Is before'),
  142. ('equal or after', 'Is after or equal to'),
  143. ('equal or before', 'Is before or equal to'),
  144. ('between', 'Is between (included)'),
  145. ('!between', 'Is not between (excluded)'),
  146. # Selection/Many2one comparators
  147. ('selected', 'Is equal to (selected)'),
  148. ('!selected', 'Is not equal to (not selected)'),
  149. # File comparators
  150. ('fileSet', 'Is set (file)'),
  151. ('!fileSet', 'Is not set (file)'),
  152. ],
  153. string='Visibility Comparator',
  154. default='equal',
  155. help="Comparison operator for visibility condition"
  156. )
  157. # Computed field for dependency field type
  158. visibility_dependency_type = fields.Char(
  159. string='Dependency Field Type',
  160. compute='_compute_visibility_dependency_type',
  161. store=False,
  162. help="Type of the visibility dependency field"
  163. )
  164. # Field for many2one dependency
  165. visibility_condition_m2o_id = fields.Integer(
  166. string='Visibility Condition (Many2one ID)',
  167. help="ID of the selected record when dependency is a many2one field"
  168. )
  169. visibility_condition_m2o_model = fields.Char(
  170. string='M2O Model',
  171. related='visibility_dependency.relation',
  172. store=False,
  173. readonly=True,
  174. help="Model name for the many2one condition"
  175. )
  176. # Field for selection dependency
  177. visibility_condition_selection = fields.Selection(
  178. selection='_get_visibility_condition_selection_options',
  179. string='Visibility Condition (Selection)',
  180. help="Selected value when dependency is a selection field"
  181. )
  182. # Field for range conditions
  183. visibility_between = fields.Char(
  184. string='Visibility Between (End Value)',
  185. help="Second value for 'between' and '!between' comparators"
  186. )
  187. def _get_visibility_condition_selection_options(self):
  188. """Return selection options based on visibility_dependency field"""
  189. if not self:
  190. return []
  191. if len(self) > 1:
  192. return []
  193. record = self[0] if self else None
  194. if not record or not record.visibility_dependency or record.visibility_dependency.ttype != 'selection':
  195. return []
  196. # Get selection options from ir.model.fields.selection
  197. selection_records = self.env['ir.model.fields.selection'].search([
  198. ('field_id', '=', record.visibility_dependency.id)
  199. ], order='sequence, id')
  200. if selection_records:
  201. return [(sel.value, sel.name) for sel in selection_records]
  202. # Fallback: try to get from field definition
  203. try:
  204. model = self.env[record.visibility_dependency.model]
  205. field = model._fields.get(record.visibility_dependency.name)
  206. if field and hasattr(field, 'selection') and field.selection:
  207. if callable(field.selection):
  208. return field.selection(model)
  209. return field.selection
  210. except:
  211. pass
  212. return []
  213. @api.depends('visibility_dependency')
  214. def _compute_visibility_dependency_type(self):
  215. """Compute the type of the visibility dependency field"""
  216. for record in self:
  217. if record.visibility_dependency:
  218. record.visibility_dependency_type = record.visibility_dependency.ttype
  219. else:
  220. record.visibility_dependency_type = False
  221. @api.onchange('visibility_condition_m2o_id', 'visibility_dependency')
  222. def _onchange_visibility_condition_m2o_id(self):
  223. """Sync many2one ID to visibility_condition"""
  224. if self.visibility_dependency and self.visibility_dependency.ttype == 'many2one':
  225. if self.visibility_condition_m2o_id:
  226. self.visibility_condition = str(self.visibility_condition_m2o_id)
  227. else:
  228. self.visibility_condition = False
  229. @api.onchange('visibility_condition_selection')
  230. def _onchange_visibility_condition_selection(self):
  231. """Sync selection value to visibility_condition"""
  232. if self.visibility_condition_selection:
  233. self.visibility_condition = self.visibility_condition_selection
  234. @api.onchange('visibility_dependency')
  235. def _onchange_visibility_dependency(self):
  236. """Clear condition values when dependency changes"""
  237. if not self.visibility_dependency:
  238. self.visibility_condition = False
  239. self.visibility_condition_m2o_id = False
  240. self.visibility_condition_selection = False
  241. elif self.visibility_dependency.ttype not in ['many2one', 'selection']:
  242. self.visibility_condition_m2o_id = False
  243. self.visibility_condition_selection = False
  244. elif self.visibility_dependency.ttype == 'many2one':
  245. if self.visibility_condition and self.visibility_condition.isdigit():
  246. try:
  247. model_name = self.visibility_dependency.relation
  248. if model_name:
  249. model = self.env[model_name]
  250. record = model.browse(int(self.visibility_condition))
  251. if record.exists():
  252. self.visibility_condition_m2o_id = int(self.visibility_condition)
  253. else:
  254. self.visibility_condition_m2o_id = False
  255. else:
  256. self.visibility_condition_m2o_id = False
  257. except:
  258. self.visibility_condition_m2o_id = False
  259. elif self.visibility_dependency.ttype == 'selection':
  260. if self.visibility_condition:
  261. self.visibility_condition_selection = self.visibility_condition
  262. @api.onchange('visibility_comparator')
  263. def _onchange_visibility_comparator(self):
  264. """Clear visibility_between when comparator changes away from between/!between"""
  265. if self.visibility_comparator not in ['between', '!between']:
  266. self.visibility_between = False
  267. _sql_constraints = [
  268. ('unique_workflow_template_field', 'unique(workflow_template_id, field_id)',
  269. 'A field can only be added once to a workflow template')
  270. ]
  271. @api.model_create_multi
  272. def create(self, vals_list):
  273. """Override create to mark model required fields and regenerate forms"""
  274. for vals in vals_list:
  275. if 'field_id' in vals and vals['field_id']:
  276. field = self.env['ir.model.fields'].browse(vals['field_id'])
  277. if (field.model == 'helpdesk.ticket' and
  278. field.required and
  279. not field.website_form_blacklisted):
  280. vals['model_required'] = True
  281. _logger.info(f"Auto-marked field {field.name} as model_required")
  282. fields_created = super().create(vals_list)
  283. # Regenerate forms in all teams using these workflow templates
  284. workflow_templates = fields_created.mapped('workflow_template_id')
  285. self._regenerate_team_forms(workflow_templates)
  286. return fields_created
  287. def write(self, vals):
  288. """Override write to mark model required fields and regenerate forms"""
  289. if 'field_id' in vals and vals['field_id']:
  290. field = self.env['ir.model.fields'].browse(vals['field_id'])
  291. if (field.model == 'helpdesk.ticket' and
  292. field.required and
  293. not field.website_form_blacklisted):
  294. vals['model_required'] = True
  295. else:
  296. vals['model_required'] = False
  297. elif 'field_id' in vals and not vals['field_id']:
  298. vals['model_required'] = False
  299. result = super().write(vals)
  300. # If any field configuration changed, regenerate forms
  301. regenerate_keys = ['field_id', 'sequence', 'required', 'visibility_dependency',
  302. 'visibility_condition', 'visibility_comparator', 'label_custom',
  303. 'model_required', 'placeholder', 'default_value', 'help_text',
  304. 'widget', 'selection_options', 'rows', 'input_type', 'selection_type']
  305. if any(key in vals for key in regenerate_keys):
  306. workflow_templates = self.mapped('workflow_template_id')
  307. self._regenerate_team_forms(workflow_templates)
  308. return result
  309. def unlink(self):
  310. """Prevent deletion of model required fields and regenerate forms"""
  311. model_required_fields = self.filtered('model_required')
  312. if model_required_fields:
  313. field_names = [f.field_id.name if f.field_id else 'Unknown' for f in model_required_fields]
  314. raise UserError(
  315. _("Cannot delete model required field(s): %s. This field is mandatory for the model and cannot be removed. "
  316. "Try hiding it with the 'Visibility' option instead and add it a default value.")
  317. % ', '.join(field_names)
  318. )
  319. workflow_templates = self.mapped('workflow_template_id')
  320. result = super().unlink()
  321. self._regenerate_team_forms(workflow_templates)
  322. return result
  323. def _regenerate_team_forms(self, workflow_templates):
  324. """Helper to regenerate forms in teams using these workflow templates"""
  325. for wf_template in workflow_templates:
  326. if not wf_template:
  327. continue
  328. teams = self.env['helpdesk.team'].search([
  329. ('workflow_template_id', '=', wf_template.id),
  330. ('use_website_helpdesk_form', '=', True)
  331. ])
  332. for team in teams:
  333. if not team.website_form_view_id:
  334. team._ensure_submit_form_view()
  335. if team.website_form_view_id:
  336. try:
  337. team._regenerate_form_from_template()
  338. _logger.info(f"Regenerated form for team {team.id} after workflow template field change")
  339. except Exception as e:
  340. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)