helpdesk_template.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  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=True,
  200. ondelete='cascade',
  201. index=True
  202. )
  203. field_id = fields.Many2one(
  204. 'ir.model.fields',
  205. string='Field',
  206. required=True,
  207. domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]",
  208. ondelete='cascade',
  209. help="Field from helpdesk.ticket model"
  210. )
  211. field_name = fields.Char(
  212. related='field_id.name',
  213. string='Field Name',
  214. store=True,
  215. readonly=True
  216. )
  217. field_type = fields.Selection(
  218. related='field_id.ttype',
  219. string='Field Type',
  220. readonly=True
  221. )
  222. label_custom = fields.Char(
  223. string='Custom Label',
  224. help="Custom label for the field in the form. If empty, uses the field's default label."
  225. )
  226. placeholder = fields.Text(
  227. string='Placeholder',
  228. help="Placeholder text shown when field is empty"
  229. )
  230. default_value = fields.Char(
  231. string='Default Value',
  232. help="Default value for the field"
  233. )
  234. help_text = fields.Html(
  235. string='Help Text',
  236. help="Help text/description shown below the field (supports HTML formatting)"
  237. )
  238. widget = fields.Selection(
  239. [
  240. ('default', 'Default'),
  241. ('radio', 'Radio Buttons'),
  242. ('checkbox', 'Checkboxes'),
  243. ],
  244. string='Widget',
  245. default='default',
  246. help="Widget to use for selection/many2one fields. Default uses dropdown select."
  247. )
  248. selection_type = fields.Selection(
  249. [
  250. ('dropdown', 'Dropdown List'),
  251. ('radio', 'Radio'),
  252. ],
  253. string='Selection Type',
  254. default='dropdown',
  255. help="Display type for selection and many2one fields. Dropdown List shows a select dropdown, Radio shows radio buttons. Same as Odoo formbuilder."
  256. )
  257. selection_options = fields.Text(
  258. string='Selection Options',
  259. help="For selection fields (not relations): JSON array of [value, label] pairs. Example: [['option1', 'Option 1'], ['option2', 'Option 2']]"
  260. )
  261. rows = fields.Integer(
  262. string='Height (Rows)',
  263. default=3,
  264. help="Number of rows for textarea fields. Default is 3."
  265. )
  266. input_type = fields.Selection(
  267. [
  268. ('text', 'Text'),
  269. ('email', 'Email'),
  270. ('tel', 'Telephone'),
  271. ('url', 'Url'),
  272. ],
  273. string='Input Type',
  274. default='text',
  275. help="Input type for text fields. Determines the HTML input type attribute."
  276. )
  277. sequence = fields.Integer(
  278. string='Sequence',
  279. default=10,
  280. help="Order in which fields are displayed"
  281. )
  282. required = fields.Boolean(
  283. string='Required',
  284. default=False,
  285. help="Make this field required in addition to its base configuration"
  286. )
  287. model_required = fields.Boolean(
  288. string='Model Required',
  289. default=False,
  290. readonly=True,
  291. help="This field is mandatory for the model and cannot be removed"
  292. )
  293. # Visibility conditions
  294. visibility_dependency = fields.Many2one(
  295. 'ir.model.fields',
  296. string='Visibility Dependency',
  297. domain="[('model', '=', 'helpdesk.ticket'), ('website_form_blacklisted', '=', False)]",
  298. help="Field on which visibility depends"
  299. )
  300. visibility_condition = fields.Char(
  301. string='Visibility Condition Value',
  302. help="Value to compare against the dependency field (for text, number, date, etc.)"
  303. )
  304. visibility_comparator = fields.Selection(
  305. [
  306. # Basic comparators
  307. ('equal', 'Is equal to'),
  308. ('!equal', 'Is not equal to'),
  309. ('contains', 'Contains'),
  310. ('!contains', "Doesn't contain"),
  311. ('set', 'Is set'),
  312. ('!set', 'Is not set'),
  313. # Numeric comparators
  314. ('greater', 'Is greater than'),
  315. ('less', 'Is less than'),
  316. ('greater or equal', 'Is greater than or equal to'),
  317. ('less or equal', 'Is less than or equal to'),
  318. # Date/Datetime comparators
  319. ('dateEqual', 'Is equal to (date)'),
  320. ('date!equal', 'Is not equal to (date)'),
  321. ('after', 'Is after'),
  322. ('before', 'Is before'),
  323. ('equal or after', 'Is after or equal to'),
  324. ('equal or before', 'Is before or equal to'),
  325. ('between', 'Is between (included)'),
  326. ('!between', 'Is not between (excluded)'),
  327. # Selection/Many2one comparators
  328. ('selected', 'Is equal to (selected)'),
  329. ('!selected', 'Is not equal to (not selected)'),
  330. # File comparators
  331. ('fileSet', 'Is set (file)'),
  332. ('!fileSet', 'Is not set (file)'),
  333. ],
  334. string='Visibility Comparator',
  335. default='equal',
  336. help="Comparison operator for visibility condition"
  337. )
  338. # Computed field to determine dependency field type
  339. visibility_dependency_type = fields.Char(
  340. string='Dependency Field Type',
  341. compute='_compute_visibility_dependency_type',
  342. store=False,
  343. help="Type of the visibility dependency field"
  344. )
  345. # Field for many2one dependency - store ID as Integer (not Many2one to avoid model validation)
  346. # The widget will handle the dynamic model change and display
  347. visibility_condition_m2o_id = fields.Integer(
  348. string='Visibility Condition (Many2one ID)',
  349. help="ID of the selected record when dependency is a many2one field (model stored separately)"
  350. )
  351. visibility_condition_m2o_model = fields.Char(
  352. string='M2O Model',
  353. related='visibility_dependency.relation',
  354. store=False,
  355. readonly=True,
  356. help="Model name for the many2one condition"
  357. )
  358. # Field for selection dependency - computed selection options
  359. visibility_condition_selection = fields.Selection(
  360. selection='_get_visibility_condition_selection_options',
  361. string='Visibility Condition (Selection)',
  362. help="Selected value when dependency is a selection field"
  363. )
  364. # Field for range conditions (between/!between) - second value for date/datetime ranges
  365. visibility_between = fields.Char(
  366. string='Visibility Between (End Value)',
  367. help="Second value for 'between' and '!between' comparators (for date/datetime ranges)"
  368. )
  369. def _get_visibility_condition_selection_options(self):
  370. """Return selection options based on visibility_dependency field"""
  371. # Handle empty recordset (when called from fields_get)
  372. if not self:
  373. return []
  374. # Handle multiple records (shouldn't happen, but be safe)
  375. if len(self) > 1:
  376. return []
  377. record = self[0] if self else None
  378. if not record or not record.visibility_dependency or record.visibility_dependency.ttype != 'selection':
  379. return []
  380. # Get selection options from ir.model.fields.selection
  381. selection_records = self.env['ir.model.fields.selection'].search([
  382. ('field_id', '=', record.visibility_dependency.id)
  383. ], order='sequence, id')
  384. if selection_records:
  385. return [(sel.value, sel.name) for sel in selection_records]
  386. # Fallback: try to get from field definition (for old-style selection)
  387. try:
  388. model = self.env[record.visibility_dependency.model]
  389. field = model._fields.get(record.visibility_dependency.name)
  390. if field and hasattr(field, 'selection') and field.selection:
  391. if callable(field.selection):
  392. return field.selection(model)
  393. return field.selection
  394. except:
  395. pass
  396. return []
  397. @api.depends('visibility_dependency')
  398. def _compute_visibility_dependency_type(self):
  399. """Compute the type of the visibility dependency field"""
  400. for record in self:
  401. if record.visibility_dependency:
  402. record.visibility_dependency_type = record.visibility_dependency.ttype
  403. else:
  404. record.visibility_dependency_type = False
  405. @api.onchange('visibility_condition_m2o_id', 'visibility_dependency')
  406. def _onchange_visibility_condition_m2o_id(self):
  407. """Sync many2one ID to visibility_condition"""
  408. if self.visibility_dependency and self.visibility_dependency.ttype == 'many2one':
  409. if self.visibility_condition_m2o_id:
  410. self.visibility_condition = str(self.visibility_condition_m2o_id)
  411. else:
  412. self.visibility_condition = False
  413. @api.onchange('visibility_condition_selection')
  414. def _onchange_visibility_condition_selection(self):
  415. """Sync selection value to visibility_condition"""
  416. if self.visibility_condition_selection:
  417. self.visibility_condition = self.visibility_condition_selection
  418. @api.onchange('visibility_dependency')
  419. def _onchange_visibility_dependency(self):
  420. """Clear condition values when dependency changes"""
  421. if not self.visibility_dependency:
  422. self.visibility_condition = False
  423. self.visibility_condition_m2o_id = False
  424. self.visibility_condition_selection = False
  425. elif self.visibility_dependency.ttype not in ['many2one', 'selection']:
  426. self.visibility_condition_m2o_id = False
  427. self.visibility_condition_selection = False
  428. elif self.visibility_dependency.ttype == 'many2one':
  429. # Load current value into m2o_id if exists
  430. if self.visibility_condition and self.visibility_condition.isdigit():
  431. try:
  432. model_name = self.visibility_dependency.relation
  433. if model_name:
  434. model = self.env[model_name]
  435. record = model.browse(int(self.visibility_condition))
  436. if record.exists():
  437. # Store the ID - the widget will handle the model change
  438. self.visibility_condition_m2o_id = int(self.visibility_condition)
  439. else:
  440. self.visibility_condition_m2o_id = False
  441. else:
  442. self.visibility_condition_m2o_id = False
  443. except:
  444. self.visibility_condition_m2o_id = False
  445. elif self.visibility_dependency.ttype == 'selection':
  446. # Load current value into selection if exists
  447. if self.visibility_condition:
  448. self.visibility_condition_selection = self.visibility_condition
  449. @api.onchange('visibility_comparator')
  450. def _onchange_visibility_comparator(self):
  451. """Clear visibility_between when comparator changes away from between/!between"""
  452. if self.visibility_comparator not in ['between', '!between']:
  453. self.visibility_between = False
  454. _sql_constraints = [
  455. ('unique_template_field', 'unique(template_id, field_id)',
  456. 'A field can only be added once to a template')
  457. ]
  458. @api.model
  459. def _register_hook(self):
  460. """Register label_custom field in ir.model.fields if it doesn't exist"""
  461. super()._register_hook()
  462. try:
  463. model = self.env['ir.model'].search([('model', '=', 'helpdesk.template.field')], limit=1)
  464. if model:
  465. field_model = self.env['ir.model.fields']
  466. existing_field = field_model.search([
  467. ('model_id', '=', model.id),
  468. ('name', '=', 'label_custom')
  469. ], limit=1)
  470. if not existing_field:
  471. field_model.create({
  472. 'model_id': model.id,
  473. 'name': 'label_custom',
  474. 'field_description': 'Custom Label',
  475. 'ttype': 'char',
  476. 'state': 'manual',
  477. 'required': False,
  478. 'readonly': False,
  479. 'store': True,
  480. })
  481. _logger.info("Campo label_custom registrado en _register_hook")
  482. except Exception as e:
  483. _logger.error(f"Error registrando label_custom en _register_hook: {str(e)}", exc_info=True)
  484. @api.model
  485. def _migrate_label_custom_field(self):
  486. """
  487. Migration method to ensure label_custom field exists in database.
  488. This method should be called after module update to fix any missing field issues.
  489. """
  490. try:
  491. # Check if column exists in database
  492. self.env.cr.execute("""
  493. SELECT column_name
  494. FROM information_schema.columns
  495. WHERE table_name = 'helpdesk_template_field'
  496. AND column_name = 'label_custom'
  497. """)
  498. column_exists = self.env.cr.fetchone()
  499. if not column_exists:
  500. _logger.warning("Column 'label_custom' does not exist. Adding it...")
  501. # Add column manually if it doesn't exist
  502. self.env.cr.execute("""
  503. ALTER TABLE helpdesk_template_field
  504. ADD COLUMN label_custom VARCHAR
  505. """)
  506. self.env.cr.commit()
  507. _logger.info("Column 'label_custom' added successfully")
  508. else:
  509. _logger.info("Column 'label_custom' already exists")
  510. # Update ir.model.fields to ensure field is registered
  511. field_model = self.env['ir.model.fields']
  512. model_id = self.env['ir.model'].search([('model', '=', 'helpdesk.template.field')], limit=1)
  513. if model_id:
  514. existing_field = field_model.search([
  515. ('model_id', '=', model_id.id),
  516. ('name', '=', 'label_custom')
  517. ], limit=1)
  518. if not existing_field:
  519. _logger.warning("Field 'label_custom' not found in ir.model.fields. Creating it...")
  520. field_model.create({
  521. 'model_id': model_id.id,
  522. 'name': 'label_custom',
  523. 'field_description': 'Custom Label',
  524. 'ttype': 'char',
  525. 'state': 'manual',
  526. })
  527. _logger.info("Field 'label_custom' registered in ir.model.fields")
  528. else:
  529. _logger.info("Field 'label_custom' already registered in ir.model.fields")
  530. # Clear cache to ensure changes are reflected
  531. self.env.registry.clear_cache()
  532. except Exception as e:
  533. _logger.error(f"Error in _migrate_label_custom_field: {str(e)}", exc_info=True)
  534. # Don't raise to avoid breaking module update
  535. @api.model_create_multi
  536. def create(self, vals_list):
  537. """Override create to mark model required fields and regenerate forms when template field is added"""
  538. # Mark model required fields automatically based on field definition
  539. for vals in vals_list:
  540. if 'field_id' in vals and vals['field_id']:
  541. field = self.env['ir.model.fields'].browse(vals['field_id'])
  542. # Check if field is required at model level (not just in form)
  543. # A field is model required if:
  544. # 1. It's in the helpdesk.ticket model
  545. # 2. It has required=True in ir.model.fields (mandatory at model level)
  546. # 3. It's not blacklisted for website forms
  547. if (field.model == 'helpdesk.ticket' and
  548. field.required and
  549. not field.website_form_blacklisted):
  550. vals['model_required'] = True
  551. _logger.info(f"Auto-marked field {field.name} as model_required (required at model level)")
  552. fields_created = super().create(vals_list)
  553. # Get unique templates that were modified
  554. templates = fields_created.mapped('template_id')
  555. # Regenerate forms in all teams using these templates
  556. for template in templates:
  557. if not template:
  558. continue
  559. teams = self.env['helpdesk.team'].search([
  560. ('template_id', '=', template.id),
  561. ('use_website_helpdesk_form', '=', True)
  562. ])
  563. for team in teams:
  564. # Ensure view exists before regenerating
  565. if not team.website_form_view_id:
  566. team._ensure_submit_form_view()
  567. # Regenerate form if view exists
  568. if team.website_form_view_id:
  569. try:
  570. team._regenerate_form_from_template()
  571. _logger.info(f"Regenerated form for team {team.id} after adding field to template {template.id}")
  572. except Exception as e:
  573. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  574. return fields_created
  575. def write(self, vals):
  576. """Override write to mark model required fields and regenerate forms when template field is modified"""
  577. # Mark/unmark model_required automatically based on field definition
  578. if 'field_id' in vals and vals['field_id']:
  579. field = self.env['ir.model.fields'].browse(vals['field_id'])
  580. # Check if field is required at model level
  581. if (field.model == 'helpdesk.ticket' and
  582. field.required and
  583. not field.website_form_blacklisted):
  584. vals['model_required'] = True
  585. _logger.info(f"Auto-marked field {field.name} as model_required (required at model level)")
  586. else:
  587. # Field is not model required, unmark it
  588. vals['model_required'] = False
  589. elif 'field_id' in vals and not vals['field_id']:
  590. # Field_id is being cleared, unmark model_required
  591. vals['model_required'] = False
  592. result = super().write(vals)
  593. # If any field configuration changed, regenerate forms
  594. if any(key in vals for key in ['field_id', 'sequence', 'required', 'visibility_dependency',
  595. 'visibility_condition', 'visibility_comparator', 'label_custom',
  596. 'model_required', 'placeholder', 'default_value', 'help_text',
  597. 'widget', 'selection_options', 'rows', 'input_type', 'selection_type']):
  598. # Get unique templates that were modified
  599. templates = self.mapped('template_id')
  600. # Regenerate forms in all teams using these templates
  601. for template in templates:
  602. if not template:
  603. continue
  604. teams = self.env['helpdesk.team'].search([
  605. ('template_id', '=', template.id),
  606. ('use_website_helpdesk_form', '=', True)
  607. ])
  608. for team in teams:
  609. # Ensure view exists before regenerating
  610. if not team.website_form_view_id:
  611. team._ensure_submit_form_view()
  612. # Regenerate form if view exists
  613. if team.website_form_view_id:
  614. try:
  615. team._regenerate_form_from_template()
  616. _logger.info(f"Regenerated form for team {team.id} after modifying field in template {template.id}")
  617. except Exception as e:
  618. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  619. return result
  620. def unlink(self):
  621. """Override unlink to prevent deletion of model required fields and regenerate forms"""
  622. # Prevent deletion of model required fields
  623. model_required_fields = self.filtered('model_required')
  624. if model_required_fields:
  625. field_names = [f.field_id.name if f.field_id else 'Unknown' for f in model_required_fields]
  626. raise UserError(
  627. _("Cannot delete model required field(s): %s. This field is mandatory for the model and cannot be removed. "
  628. "Try hiding it with the 'Visibility' option instead and add it a default value.")
  629. % ', '.join(field_names)
  630. )
  631. # Get templates before deletion
  632. templates = self.mapped('template_id')
  633. result = super().unlink()
  634. # Regenerate forms in all teams using these templates
  635. for template in templates:
  636. if not template:
  637. continue
  638. teams = self.env['helpdesk.team'].search([
  639. ('template_id', '=', template.id),
  640. ('use_website_helpdesk_form', '=', True)
  641. ])
  642. for team in teams:
  643. # Ensure view exists before regenerating
  644. if not team.website_form_view_id:
  645. team._ensure_submit_form_view()
  646. # Regenerate form if view exists
  647. if team.website_form_view_id:
  648. try:
  649. team._regenerate_form_from_template()
  650. _logger.info(f"Regenerated form for team {team.id} after removing field from template {template.id}")
  651. except Exception as e:
  652. _logger.error(f"Error regenerating form for team {team.id}: {e}", exc_info=True)
  653. return result