helpdesk_template.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  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 HelpdeskTemplate(models.Model):
  8. _name = 'helpdesk.template'
  9. _description = 'Helpdesk Template'
  10. _order = 'name'
  11. name = fields.Char(
  12. string='Name',
  13. required=True,
  14. translate=True,
  15. help="Name of the template"
  16. )
  17. description = fields.Text(
  18. string='Description',
  19. translate=True,
  20. help="Description of the template"
  21. )
  22. active = fields.Boolean(
  23. string='Active',
  24. default=True,
  25. help="If unchecked, this template will be hidden and won't be available"
  26. )
  27. field_ids = fields.One2many(
  28. 'helpdesk.template.field',
  29. 'template_id',
  30. string='Fields',
  31. copy=True,
  32. help="Fields included in this template"
  33. )
  34. @api.model
  35. def default_get(self, fields_list):
  36. """Set default required fields when creating a new template"""
  37. res = super().default_get(fields_list)
  38. # Only set defaults if creating a new record (not editing existing)
  39. if 'field_ids' in fields_list and not self.env.context.get('default_field_ids'):
  40. # Get the required fields from form builder (same as website_helpdesk_form_editor.js)
  41. required_field_names = ['partner_name', 'partner_email', 'name', 'description']
  42. # Get field records
  43. ticket_model = self.env['ir.model'].search([('model', '=', 'helpdesk.ticket')], limit=1)
  44. if ticket_model:
  45. # Find the field records
  46. required_fields = self.env['ir.model.fields'].search([
  47. ('model_id', '=', ticket_model.id),
  48. ('name', 'in', required_field_names),
  49. ('website_form_blacklisted', '=', False)
  50. ])
  51. # Create field mapping
  52. field_map = {f.name: f for f in required_fields}
  53. # Prepare default field_ids
  54. field_ids_commands = []
  55. sequence = 10
  56. # Add partner_name (required: true, sequence 10)
  57. if 'partner_name' in field_map:
  58. field_ids_commands.append((0, 0, {
  59. 'field_id': field_map['partner_name'].id,
  60. 'required': True,
  61. 'sequence': sequence
  62. }))
  63. sequence += 10
  64. # Add partner_email (required: true, sequence 20)
  65. if 'partner_email' in field_map:
  66. field_ids_commands.append((0, 0, {
  67. 'field_id': field_map['partner_email'].id,
  68. 'required': True,
  69. 'sequence': sequence
  70. }))
  71. sequence += 10
  72. # Add name (modelRequired: true, sequence 30) - required by model
  73. # Note: model_required will be set automatically in create() based on field.required
  74. if 'name' in field_map:
  75. name_field = field_map['name']
  76. field_ids_commands.append((0, 0, {
  77. 'field_id': name_field.id,
  78. 'required': True, # Mark as required since it's modelRequired
  79. 'model_required': name_field.required, # Auto-detect from field definition
  80. 'sequence': sequence
  81. }))
  82. sequence += 10
  83. # Add description (required: true, sequence 40)
  84. if 'description' in field_map:
  85. field_ids_commands.append((0, 0, {
  86. 'field_id': field_map['description'].id,
  87. 'required': True,
  88. 'sequence': sequence
  89. }))
  90. sequence += 10
  91. if field_ids_commands:
  92. res['field_ids'] = field_ids_commands
  93. _logger.info(f"Setting default required fields for new template: {[cmd[2]['field_id'] for cmd in field_ids_commands]}")
  94. return res
  95. @api.model_create_multi
  96. def create(self, vals_list):
  97. """Override create to automatically add required fields from form builder"""
  98. # Get the required fields from form builder (same as website_helpdesk_form_editor.js)
  99. # These are the fields that are always required in the form builder:
  100. # - partner_name (required: true)
  101. # - partner_email (required: true)
  102. # - name (modelRequired: true) - required by the model
  103. # - description (required: true)
  104. required_field_names = ['partner_name', 'partner_email', 'name', 'description']
  105. # Get field records
  106. ticket_model = self.env['ir.model'].search([('model', '=', 'helpdesk.ticket')], limit=1)
  107. if not ticket_model:
  108. return super().create(vals_list)
  109. # Find the field records
  110. required_fields = self.env['ir.model.fields'].search([
  111. ('model_id', '=', ticket_model.id),
  112. ('name', 'in', required_field_names),
  113. ('website_form_blacklisted', '=', False)
  114. ])
  115. # Create field mapping
  116. field_map = {f.name: f for f in required_fields}
  117. # Prepare default field_ids for each template
  118. for vals in vals_list:
  119. if 'field_ids' not in vals or not vals.get('field_ids'):
  120. # Only add default fields if no fields are provided
  121. field_ids_commands = []
  122. sequence = 10
  123. # Add partner_name (required: true, sequence 10)
  124. if 'partner_name' in field_map:
  125. field_ids_commands.append((0, 0, {
  126. 'field_id': field_map['partner_name'].id,
  127. 'required': True,
  128. 'sequence': sequence
  129. }))
  130. sequence += 10
  131. # Add partner_email (required: true, sequence 20)
  132. if 'partner_email' in field_map:
  133. field_ids_commands.append((0, 0, {
  134. 'field_id': field_map['partner_email'].id,
  135. 'required': True,
  136. 'sequence': sequence
  137. }))
  138. sequence += 10
  139. # Add name (modelRequired: true, sequence 30) - required by model
  140. # Note: model_required will be set automatically in create() based on field.required
  141. if 'name' in field_map:
  142. name_field = field_map['name']
  143. field_ids_commands.append((0, 0, {
  144. 'field_id': name_field.id,
  145. 'required': True, # Mark as required since it's modelRequired
  146. 'model_required': name_field.required, # Auto-detect from field definition
  147. 'sequence': sequence
  148. }))
  149. sequence += 10
  150. # Add description (required: true, sequence 40)
  151. if 'description' in field_map:
  152. field_ids_commands.append((0, 0, {
  153. 'field_id': field_map['description'].id,
  154. 'required': True,
  155. 'sequence': sequence
  156. }))
  157. sequence += 10
  158. if field_ids_commands:
  159. vals['field_ids'] = field_ids_commands
  160. _logger.info(f"Adding default required fields to new template: {[cmd[2]['field_id'] for cmd in field_ids_commands]}")
  161. return super().create(vals_list)
  162. def write(self, vals):
  163. """Override write to regenerate forms in all teams using this template"""
  164. result = super().write(vals)
  165. # If template fields or active status changed, regenerate forms in all teams using this template
  166. # Note: field_ids changes are handled by helpdesk.template.field create/write/unlink methods
  167. # but we also check here in case field_ids is directly modified
  168. if 'field_ids' in vals or 'active' in vals:
  169. # Find all teams using this template
  170. teams = self.env['helpdesk.team'].search([
  171. ('template_id', 'in', self.ids),
  172. ('use_website_helpdesk_form', '=', True)
  173. ])
  174. # Regenerate form XML for each team
  175. for team in teams:
  176. # Ensure view exists before regenerating
  177. if not team.website_form_view_id:
  178. team._ensure_submit_form_view()
  179. # Regenerate or restore form based on template status
  180. if team.website_form_view_id:
  181. try:
  182. if team.template_id.active:
  183. team._regenerate_form_from_template()
  184. _logger.info(f"Regenerated form for team {team.id} after template {team.template_id.id} change")
  185. else:
  186. # If template is deactivated, restore default form
  187. team._restore_default_form()
  188. _logger.info(f"Restored default form for team {team.id} after template {team.template_id.id} deactivation")
  189. except Exception as e:
  190. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  191. return result
  192. class HelpdeskTemplateField(models.Model):
  193. _name = 'helpdesk.template.field'
  194. _description = 'Helpdesk Template Field'
  195. _order = 'sequence, id'
  196. template_id = fields.Many2one(
  197. 'helpdesk.template',
  198. string='Template',
  199. required=False, # Made optional to support workflow_template_id
  200. ondelete='cascade',
  201. index=True,
  202. help="Legacy template (deprecated - use workflow_template_id instead)"
  203. )
  204. workflow_template_id = fields.Many2one(
  205. 'helpdesk.workflow.template',
  206. string='Workflow Template',
  207. required=False, # Made optional to support template_id (legacy)
  208. ondelete='cascade',
  209. index=True,
  210. help="Workflow template containing this field"
  211. )
  212. field_id = fields.Many2one(
  213. 'ir.model.fields',
  214. string='Field',
  215. required=True,
  216. domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]",
  217. ondelete='cascade',
  218. help="Field from helpdesk.ticket model"
  219. )
  220. field_name = fields.Char(
  221. related='field_id.name',
  222. string='Field Name',
  223. store=True,
  224. readonly=True
  225. )
  226. field_type = fields.Selection(
  227. related='field_id.ttype',
  228. string='Field Type',
  229. readonly=True
  230. )
  231. label_custom = fields.Char(
  232. string='Custom Label',
  233. help="Custom label for the field in the form. If empty, uses the field's default label."
  234. )
  235. placeholder = fields.Text(
  236. string='Placeholder',
  237. help="Placeholder text shown when field is empty"
  238. )
  239. default_value = fields.Char(
  240. string='Default Value',
  241. help="Default value for the field"
  242. )
  243. help_text = fields.Html(
  244. string='Help Text',
  245. help="Help text/description shown below the field (supports HTML formatting)"
  246. )
  247. widget = fields.Selection(
  248. [
  249. ('default', 'Default'),
  250. ('radio', 'Radio Buttons'),
  251. ('checkbox', 'Checkboxes'),
  252. ],
  253. string='Widget',
  254. default='default',
  255. help="Widget to use for selection/many2one fields. Default uses dropdown select."
  256. )
  257. selection_type = fields.Selection(
  258. [
  259. ('dropdown', 'Dropdown List'),
  260. ('radio', 'Radio'),
  261. ],
  262. string='Selection Type',
  263. default='dropdown',
  264. help="Display type for selection and many2one fields. Dropdown List shows a select dropdown, Radio shows radio buttons. Same as Odoo formbuilder."
  265. )
  266. selection_options = fields.Text(
  267. string='Selection Options',
  268. help="For selection fields (not relations): JSON array of [value, label] pairs. Example: [['option1', 'Option 1'], ['option2', 'Option 2']]"
  269. )
  270. rows = fields.Integer(
  271. string='Height (Rows)',
  272. default=3,
  273. help="Number of rows for textarea fields. Default is 3."
  274. )
  275. input_type = fields.Selection(
  276. [
  277. ('text', 'Text'),
  278. ('email', 'Email'),
  279. ('tel', 'Telephone'),
  280. ('url', 'Url'),
  281. ],
  282. string='Input Type',
  283. default='text',
  284. help="Input type for text fields. Determines the HTML input type attribute."
  285. )
  286. sequence = fields.Integer(
  287. string='Sequence',
  288. default=10,
  289. help="Order in which fields are displayed"
  290. )
  291. required = fields.Boolean(
  292. string='Required',
  293. default=False,
  294. help="Make this field required in addition to its base configuration"
  295. )
  296. model_required = fields.Boolean(
  297. string='Model Required',
  298. default=False,
  299. readonly=True,
  300. help="This field is mandatory for the model and cannot be removed"
  301. )
  302. # Visibility conditions
  303. visibility_dependency = fields.Many2one(
  304. 'ir.model.fields',
  305. string='Visibility Dependency',
  306. domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]",
  307. help="Field on which visibility depends"
  308. )
  309. visibility_condition = fields.Char(
  310. string='Visibility Condition Value',
  311. help="Value to compare against the dependency field (for text, number, date, etc.)"
  312. )
  313. visibility_comparator = fields.Selection(
  314. [
  315. # Basic comparators
  316. ('equal', 'Is equal to'),
  317. ('!equal', 'Is not equal to'),
  318. ('contains', 'Contains'),
  319. ('!contains', "Doesn't contain"),
  320. ('set', 'Is set'),
  321. ('!set', 'Is not set'),
  322. # Numeric comparators
  323. ('greater', 'Is greater than'),
  324. ('less', 'Is less than'),
  325. ('greater or equal', 'Is greater than or equal to'),
  326. ('less or equal', 'Is less than or equal to'),
  327. # Date/Datetime comparators
  328. ('dateEqual', 'Is equal to (date)'),
  329. ('date!equal', 'Is not equal to (date)'),
  330. ('after', 'Is after'),
  331. ('before', 'Is before'),
  332. ('equal or after', 'Is after or equal to'),
  333. ('equal or before', 'Is before or equal to'),
  334. ('between', 'Is between (included)'),
  335. ('!between', 'Is not between (excluded)'),
  336. # Selection/Many2one comparators
  337. ('selected', 'Is equal to (selected)'),
  338. ('!selected', 'Is not equal to (not selected)'),
  339. # File comparators
  340. ('fileSet', 'Is set (file)'),
  341. ('!fileSet', 'Is not set (file)'),
  342. ],
  343. string='Visibility Comparator',
  344. default='equal',
  345. help="Comparison operator for visibility condition"
  346. )
  347. # Computed field to determine dependency field type
  348. visibility_dependency_type = fields.Char(
  349. string='Dependency Field Type',
  350. compute='_compute_visibility_dependency_type',
  351. store=False,
  352. help="Type of the visibility dependency field"
  353. )
  354. # Field for many2one dependency - store ID as Integer (not Many2one to avoid model validation)
  355. # The widget will handle the dynamic model change and display
  356. visibility_condition_m2o_id = fields.Integer(
  357. string='Visibility Condition (Many2one ID)',
  358. help="ID of the selected record when dependency is a many2one field (model stored separately)"
  359. )
  360. visibility_condition_m2o_model = fields.Char(
  361. string='M2O Model',
  362. related='visibility_dependency.relation',
  363. store=False,
  364. readonly=True,
  365. help="Model name for the many2one condition"
  366. )
  367. # Field for selection dependency - computed selection options
  368. visibility_condition_selection = fields.Selection(
  369. selection='_get_visibility_condition_selection_options',
  370. string='Visibility Condition (Selection)',
  371. help="Selected value when dependency is a selection field"
  372. )
  373. # Field for range conditions (between/!between) - second value for date/datetime ranges
  374. visibility_between = fields.Char(
  375. string='Visibility Between (End Value)',
  376. help="Second value for 'between' and '!between' comparators (for date/datetime ranges)"
  377. )
  378. def _get_visibility_condition_selection_options(self):
  379. """Return selection options based on visibility_dependency field"""
  380. # Handle empty recordset (when called from fields_get)
  381. if not self:
  382. return []
  383. # Handle multiple records (shouldn't happen, but be safe)
  384. if len(self) > 1:
  385. return []
  386. record = self[0] if self else None
  387. if not record or not record.visibility_dependency or record.visibility_dependency.ttype != 'selection':
  388. return []
  389. # Get selection options from ir.model.fields.selection
  390. selection_records = self.env['ir.model.fields.selection'].search([
  391. ('field_id', '=', record.visibility_dependency.id)
  392. ], order='sequence, id')
  393. if selection_records:
  394. return [(sel.value, sel.name) for sel in selection_records]
  395. # Fallback: try to get from field definition (for old-style selection)
  396. try:
  397. model = self.env[record.visibility_dependency.model]
  398. field = model._fields.get(record.visibility_dependency.name)
  399. if field and hasattr(field, 'selection') and field.selection:
  400. if callable(field.selection):
  401. return field.selection(model)
  402. return field.selection
  403. except:
  404. pass
  405. return []
  406. @api.depends('visibility_dependency')
  407. def _compute_visibility_dependency_type(self):
  408. """Compute the type of the visibility dependency field"""
  409. for record in self:
  410. if record.visibility_dependency:
  411. record.visibility_dependency_type = record.visibility_dependency.ttype
  412. else:
  413. record.visibility_dependency_type = False
  414. @api.onchange('visibility_condition_m2o_id', 'visibility_dependency')
  415. def _onchange_visibility_condition_m2o_id(self):
  416. """Sync many2one ID to visibility_condition"""
  417. if self.visibility_dependency and self.visibility_dependency.ttype == 'many2one':
  418. if self.visibility_condition_m2o_id:
  419. self.visibility_condition = str(self.visibility_condition_m2o_id)
  420. else:
  421. self.visibility_condition = False
  422. @api.onchange('visibility_condition_selection')
  423. def _onchange_visibility_condition_selection(self):
  424. """Sync selection value to visibility_condition"""
  425. if self.visibility_condition_selection:
  426. self.visibility_condition = self.visibility_condition_selection
  427. @api.onchange('visibility_dependency')
  428. def _onchange_visibility_dependency(self):
  429. """Clear condition values when dependency changes"""
  430. if not self.visibility_dependency:
  431. self.visibility_condition = False
  432. self.visibility_condition_m2o_id = False
  433. self.visibility_condition_selection = False
  434. elif self.visibility_dependency.ttype not in ['many2one', 'selection']:
  435. self.visibility_condition_m2o_id = False
  436. self.visibility_condition_selection = False
  437. elif self.visibility_dependency.ttype == 'many2one':
  438. # Load current value into m2o_id if exists
  439. if self.visibility_condition and self.visibility_condition.isdigit():
  440. try:
  441. model_name = self.visibility_dependency.relation
  442. if model_name:
  443. model = self.env[model_name]
  444. record = model.browse(int(self.visibility_condition))
  445. if record.exists():
  446. # Store the ID - the widget will handle the model change
  447. self.visibility_condition_m2o_id = int(self.visibility_condition)
  448. else:
  449. self.visibility_condition_m2o_id = False
  450. else:
  451. self.visibility_condition_m2o_id = False
  452. except:
  453. self.visibility_condition_m2o_id = False
  454. elif self.visibility_dependency.ttype == 'selection':
  455. # Load current value into selection if exists
  456. if self.visibility_condition:
  457. self.visibility_condition_selection = self.visibility_condition
  458. @api.onchange('visibility_comparator')
  459. def _onchange_visibility_comparator(self):
  460. """Clear visibility_between when comparator changes away from between/!between"""
  461. if self.visibility_comparator not in ['between', '!between']:
  462. self.visibility_between = False
  463. _sql_constraints = [
  464. ('unique_template_field', 'unique(template_id, field_id)',
  465. 'A field can only be added once to a template'),
  466. ('unique_workflow_template_field', 'unique(workflow_template_id, field_id)',
  467. 'A field can only be added once to a workflow template'),
  468. # Note: CHECK constraint added via pre-migration script to avoid issues during module update
  469. # The constraint is: (template_id IS NOT NULL AND workflow_template_id IS NULL) OR (template_id IS NULL AND workflow_template_id IS NOT NULL)
  470. ]
  471. @api.model
  472. def _register_hook(self):
  473. """Register label_custom field in ir.model.fields if it doesn't exist"""
  474. super()._register_hook()
  475. try:
  476. model = self.env['ir.model'].search([('model', '=', 'helpdesk.template.field')], limit=1)
  477. if model:
  478. field_model = self.env['ir.model.fields']
  479. existing_field = field_model.search([
  480. ('model_id', '=', model.id),
  481. ('name', '=', 'label_custom')
  482. ], limit=1)
  483. if not existing_field:
  484. field_model.create({
  485. 'model_id': model.id,
  486. 'name': 'label_custom',
  487. 'field_description': 'Custom Label',
  488. 'ttype': 'char',
  489. 'state': 'manual',
  490. 'required': False,
  491. 'readonly': False,
  492. 'store': True,
  493. })
  494. _logger.info("Campo label_custom registrado en _register_hook")
  495. except Exception as e:
  496. _logger.error(f"Error registrando label_custom en _register_hook: {str(e)}", exc_info=True)
  497. @api.model
  498. def _migrate_label_custom_field(self):
  499. """
  500. Migration method to ensure label_custom field exists in database.
  501. This method should be called after module update to fix any missing field issues.
  502. """
  503. try:
  504. # Check if column exists in database
  505. self.env.cr.execute("""
  506. SELECT column_name
  507. FROM information_schema.columns
  508. WHERE table_name = 'helpdesk_template_field'
  509. AND column_name = 'label_custom'
  510. """)
  511. column_exists = self.env.cr.fetchone()
  512. if not column_exists:
  513. _logger.warning("Column 'label_custom' does not exist. Adding it...")
  514. # Add column manually if it doesn't exist
  515. self.env.cr.execute("""
  516. ALTER TABLE helpdesk_template_field
  517. ADD COLUMN label_custom VARCHAR
  518. """)
  519. self.env.cr.commit()
  520. _logger.info("Column 'label_custom' added successfully")
  521. else:
  522. _logger.info("Column 'label_custom' already exists")
  523. # Update ir.model.fields to ensure field is registered
  524. field_model = self.env['ir.model.fields']
  525. model_id = self.env['ir.model'].search([('model', '=', 'helpdesk.template.field')], limit=1)
  526. if model_id:
  527. existing_field = field_model.search([
  528. ('model_id', '=', model_id.id),
  529. ('name', '=', 'label_custom')
  530. ], limit=1)
  531. if not existing_field:
  532. _logger.warning("Field 'label_custom' not found in ir.model.fields. Creating it...")
  533. field_model.create({
  534. 'model_id': model_id.id,
  535. 'name': 'label_custom',
  536. 'field_description': 'Custom Label',
  537. 'ttype': 'char',
  538. 'state': 'manual',
  539. })
  540. _logger.info("Field 'label_custom' registered in ir.model.fields")
  541. else:
  542. _logger.info("Field 'label_custom' already registered in ir.model.fields")
  543. # Clear cache to ensure changes are reflected
  544. self.env.registry.clear_cache()
  545. except Exception as e:
  546. _logger.error(f"Error in _migrate_label_custom_field: {str(e)}", exc_info=True)
  547. # Don't raise to avoid breaking module update
  548. @api.model_create_multi
  549. def create(self, vals_list):
  550. """Override create to mark model required fields and regenerate forms when template field is added"""
  551. # Mark model required fields automatically based on field definition
  552. for vals in vals_list:
  553. if 'field_id' in vals and vals['field_id']:
  554. field = self.env['ir.model.fields'].browse(vals['field_id'])
  555. # Check if field is required at model level (not just in form)
  556. # A field is model required if:
  557. # 1. It's in the helpdesk.ticket model
  558. # 2. It has required=True in ir.model.fields (mandatory at model level)
  559. # 3. It's not blacklisted for website forms
  560. if (field.model == 'helpdesk.ticket' and
  561. field.required and
  562. not field.website_form_blacklisted):
  563. vals['model_required'] = True
  564. _logger.info(f"Auto-marked field {field.name} as model_required (required at model level)")
  565. fields_created = super().create(vals_list)
  566. # Get unique templates that were modified (both legacy and workflow)
  567. templates = fields_created.mapped('template_id')
  568. workflow_templates = fields_created.mapped('workflow_template_id')
  569. # Regenerate forms in all teams using legacy templates
  570. for template in templates:
  571. if not template:
  572. continue
  573. teams = self.env['helpdesk.team'].search([
  574. ('template_id', '=', template.id),
  575. ('use_website_helpdesk_form', '=', True)
  576. ])
  577. for team in teams:
  578. # Ensure view exists before regenerating
  579. if not team.website_form_view_id:
  580. team._ensure_submit_form_view()
  581. # Regenerate form if view exists
  582. if team.website_form_view_id:
  583. try:
  584. team._regenerate_form_from_template()
  585. _logger.info(f"Regenerated form for team {team.id} after adding field to template {template.id}")
  586. except Exception as e:
  587. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  588. # Regenerate forms in all teams using workflow templates
  589. for workflow_template in workflow_templates:
  590. if not workflow_template:
  591. continue
  592. teams = self.env['helpdesk.team'].search([
  593. ('workflow_template_id', '=', workflow_template.id),
  594. ('use_website_helpdesk_form', '=', True)
  595. ])
  596. for team in teams:
  597. # Ensure view exists before regenerating
  598. if not team.website_form_view_id:
  599. team._ensure_submit_form_view()
  600. # Regenerate form if view exists
  601. if team.website_form_view_id:
  602. try:
  603. team._regenerate_form_from_template()
  604. _logger.info(f"Regenerated form for team {team.id} after adding field to workflow template {workflow_template.id}")
  605. except Exception as e:
  606. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  607. return fields_created
  608. def write(self, vals):
  609. """Override write to mark model required fields and regenerate forms when template field is modified"""
  610. # Mark/unmark model_required automatically based on field definition
  611. if 'field_id' in vals and vals['field_id']:
  612. field = self.env['ir.model.fields'].browse(vals['field_id'])
  613. # Check if field is required at model level
  614. if (field.model == 'helpdesk.ticket' and
  615. field.required and
  616. not field.website_form_blacklisted):
  617. vals['model_required'] = True
  618. _logger.info(f"Auto-marked field {field.name} as model_required (required at model level)")
  619. else:
  620. # Field is not model required, unmark it
  621. vals['model_required'] = False
  622. elif 'field_id' in vals and not vals['field_id']:
  623. # Field_id is being cleared, unmark model_required
  624. vals['model_required'] = False
  625. result = super().write(vals)
  626. # If any field configuration changed, regenerate forms
  627. if any(key in vals for key in ['field_id', 'sequence', 'required', 'visibility_dependency',
  628. 'visibility_condition', 'visibility_comparator', 'label_custom',
  629. 'model_required', 'placeholder', 'default_value', 'help_text',
  630. 'widget', 'selection_options', 'rows', 'input_type', 'selection_type',
  631. 'template_id', 'workflow_template_id']):
  632. # Get unique templates that were modified (both legacy and workflow)
  633. templates = self.mapped('template_id')
  634. workflow_templates = self.mapped('workflow_template_id')
  635. # Regenerate forms in all teams using legacy templates
  636. for template in templates:
  637. if not template:
  638. continue
  639. teams = self.env['helpdesk.team'].search([
  640. ('template_id', '=', template.id),
  641. ('use_website_helpdesk_form', '=', True)
  642. ])
  643. for team in teams:
  644. # Ensure view exists before regenerating
  645. if not team.website_form_view_id:
  646. team._ensure_submit_form_view()
  647. # Regenerate form if view exists
  648. if team.website_form_view_id:
  649. try:
  650. team._regenerate_form_from_template()
  651. _logger.info(f"Regenerated form for team {team.id} after modifying field in template {template.id}")
  652. except Exception as e:
  653. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  654. # Regenerate forms in all teams using workflow templates
  655. for workflow_template in workflow_templates:
  656. if not workflow_template:
  657. continue
  658. teams = self.env['helpdesk.team'].search([
  659. ('workflow_template_id', '=', workflow_template.id),
  660. ('use_website_helpdesk_form', '=', True)
  661. ])
  662. for team in teams:
  663. # Ensure view exists before regenerating
  664. if not team.website_form_view_id:
  665. team._ensure_submit_form_view()
  666. # Regenerate form if view exists
  667. if team.website_form_view_id:
  668. try:
  669. team._regenerate_form_from_template()
  670. _logger.info(f"Regenerated form for team {team.id} after modifying field in workflow template {workflow_template.id}")
  671. except Exception as e:
  672. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  673. return result
  674. def unlink(self):
  675. """Override unlink to prevent deletion of model required fields and regenerate forms"""
  676. # Prevent deletion of model required fields
  677. model_required_fields = self.filtered('model_required')
  678. if model_required_fields:
  679. field_names = [f.field_id.name if f.field_id else 'Unknown' for f in model_required_fields]
  680. raise UserError(
  681. _("Cannot delete model required field(s): %s. This field is mandatory for the model and cannot be removed. "
  682. "Try hiding it with the 'Visibility' option instead and add it a default value.")
  683. % ', '.join(field_names)
  684. )
  685. # Get templates before deletion (both legacy and workflow)
  686. templates = self.mapped('template_id')
  687. workflow_templates = self.mapped('workflow_template_id')
  688. result = super().unlink()
  689. # Regenerate forms in all teams using legacy templates
  690. for template in templates:
  691. if not template:
  692. continue
  693. teams = self.env['helpdesk.team'].search([
  694. ('template_id', '=', template.id),
  695. ('use_website_helpdesk_form', '=', True)
  696. ])
  697. for team in teams:
  698. # Ensure view exists before regenerating
  699. if not team.website_form_view_id:
  700. team._ensure_submit_form_view()
  701. # Regenerate form if view exists
  702. if team.website_form_view_id:
  703. try:
  704. team._regenerate_form_from_template()
  705. _logger.info(f"Regenerated form for team {team.id} after removing field from template {template.id}")
  706. except Exception as e:
  707. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  708. # Regenerate forms in all teams using workflow templates
  709. for workflow_template in workflow_templates:
  710. if not workflow_template:
  711. continue
  712. teams = self.env['helpdesk.team'].search([
  713. ('workflow_template_id', '=', workflow_template.id),
  714. ('use_website_helpdesk_form', '=', True)
  715. ])
  716. for team in teams:
  717. # Ensure view exists before regenerating
  718. if not team.website_form_view_id:
  719. team._ensure_submit_form_view()
  720. # Regenerate form if view exists
  721. if team.website_form_view_id:
  722. try:
  723. team._regenerate_form_from_template()
  724. _logger.info(f"Regenerated form for team {team.id} after removing field from workflow template {workflow_template.id}")
  725. except Exception as e:
  726. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  727. return result