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.

237 lines
9.5 KiB

  1. # -*- coding: utf-8 -*-
  2. # © 2016-2017 Akretion (http://www.akretion.com)
  3. # © 2016-2017 Camptocamp (http://www.camptocamp.com/)
  4. # © 2019 ACSONE SA/NV
  5. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  6. from odoo import api, models, fields
  7. class Base(models.AbstractModel):
  8. _inherit = "base"
  9. @api.model
  10. def _compute_onchange_dirty(
  11. self, original_record, modified_record, fieldname_onchange=None
  12. ):
  13. """
  14. Return the list of dirty fields. (designed to be called by
  15. play_onchanges)
  16. The list of dirty fields is computed from the list marked as dirty
  17. on the record. Form this list, we remove the fields for which the value
  18. into the original record is the same as the one into the current record
  19. :param original_record:
  20. :return: changed values
  21. """
  22. dirties = []
  23. if fieldname_onchange:
  24. for field_name, field in modified_record._fields.items():
  25. # special case. We consider that a related field is modified
  26. # if a modified field is in the first position of the path
  27. # to traverse to get the value.
  28. if field.related and field.related[0].startswith(
  29. fieldname_onchange
  30. ):
  31. dirties.append(field_name)
  32. for field_name in modified_record._get_dirty():
  33. original_value = original_record[field_name]
  34. new_value = modified_record[field_name]
  35. if original_value == new_value:
  36. continue
  37. dirties.append(field_name)
  38. for field_name, field in modified_record._fields.items():
  39. new_value = modified_record[field_name]
  40. if field.type not in ("one2many", "many2many"):
  41. continue
  42. # if the field is a one2many or many2many, check that any
  43. # item is a new Id
  44. if models.NewId in [type(i.id) for i in new_value]:
  45. dirties.append(field_name)
  46. continue
  47. # if the field is a one2many or many2many, check if any item
  48. # is dirty
  49. for r in new_value:
  50. if r._get_dirty():
  51. ori = [
  52. r1
  53. for r1 in original_record[field_name]
  54. if r1.id == r.id
  55. ][0]
  56. # if fieldname_onchange is None avoid recurssion...
  57. if fieldname_onchange and self._compute_onchange_dirty(
  58. ori, r
  59. ):
  60. dirties.append(field_name)
  61. break
  62. return dirties
  63. def _convert_to_onchange(self, record, field, value):
  64. if field.type == "many2one":
  65. # for many2one, we keep the id and don't call the
  66. # convert_on_change to avoid the call to name_get by the
  67. # convert_to_onchange
  68. if value.id:
  69. return value.id
  70. return False
  71. elif field.type in ("one2many", "many2many"):
  72. result = [(5,)]
  73. for record in value:
  74. vals = {}
  75. # We have to check first if the record already exists
  76. # (only in case of M2M).
  77. if field.type == "many2many" and not isinstance(
  78. record.id, models.NewId):
  79. result.append((4, record.id))
  80. continue
  81. for name in record._cache:
  82. if name in models.LOG_ACCESS_COLUMNS:
  83. continue
  84. v = record[name]
  85. f = record._fields[name]
  86. if f.type == "many2one" and isinstance(v.id, models.NewId):
  87. continue
  88. vals[name] = self._convert_to_onchange(record, f, v)
  89. if not record.id:
  90. result.append((0, 0, vals))
  91. elif vals:
  92. result.append((1, record.id, vals))
  93. else:
  94. result.append((4, record.id))
  95. return result
  96. else:
  97. return field.convert_to_onchange(value, record, [field.name])
  98. def play_onchanges(self, values, onchange_fields=None):
  99. """
  100. Play the onchange methods defined on the current record and return the
  101. changed values.
  102. The record is not modified by the onchange.
  103. The intend of this method is to provide a way to get on the server side
  104. the values returned by the onchange methods when called by the UI.
  105. This method is useful in B2B contexts where records are created or
  106. modified from server side.
  107. The returned values are those changed by the execution of onchange
  108. methods registered for the onchange_fields according to the provided
  109. values. As consequence, the result will not contain the modifications
  110. that could occurs by the execution of compute methods registered for
  111. the same onchange_fields.
  112. It's on purpose that we avoid to trigger the compute methods for the
  113. onchange_fields. These compute methods will be triggered when calling
  114. the create or write method. In this way we avoid to compute useless
  115. information.
  116. :param values: dict of input value that
  117. :param onchange_fields: fields for which onchange methods will be
  118. played. If not provided, the list of field is based on the values keys.
  119. Order in onchange_fields is very important as onchanges methods will
  120. be played in that order.
  121. :return: changed values
  122. This method reimplement the onchange method to be able to work on the
  123. current recordset if provided.
  124. """
  125. updated_values = values.copy()
  126. env = self.env
  127. if self:
  128. self.ensure_one()
  129. if not onchange_fields:
  130. onchange_fields = values.keys()
  131. elif not isinstance(onchange_fields, list):
  132. onchange_fields = [onchange_fields]
  133. if not onchange_fields:
  134. onchange_fields = values.keys()
  135. # filter out keys in field_onchange that do not refer to actual fields
  136. names = [n for n in onchange_fields if n in self._fields]
  137. # create a new record with values, and attach ``self`` to it
  138. with env.do_in_onchange():
  139. # keep a copy of the original record.
  140. # attach ``self`` with a different context (for cache consistency)
  141. origin = self.with_context(__onchange=True)
  142. origin_dirty = set(self._get_dirty())
  143. fields.copy_cache(self, origin.env)
  144. if self:
  145. record = self
  146. record.update(values)
  147. else:
  148. # initialize with default values, they may be used in onchange
  149. new_values = self.default_get(self._fields.keys())
  150. new_values.update(values)
  151. record = self.new(new_values)
  152. values = {name: record[name] for name in record._cache}
  153. record._origin = origin
  154. # determine which field(s) should be triggered an onchange
  155. todo = list(names) or list(values)
  156. done = set()
  157. # dummy assignment: trigger invalidations on the record
  158. with env.do_in_onchange():
  159. for name in todo:
  160. if name == "id":
  161. continue
  162. value = record[name]
  163. field = self._fields[name]
  164. if field.type == "many2one" and field.delegate and not value:
  165. # do not nullify all fields of parent record for new
  166. # records
  167. continue
  168. record[name] = value
  169. dirty = set()
  170. # process names in order (or the keys of values if no name given)
  171. while todo:
  172. name = todo.pop(0)
  173. if name in done:
  174. continue
  175. done.add(name)
  176. with env.do_in_onchange():
  177. # apply field-specific onchange methods
  178. record._onchange_eval(name, "1", {})
  179. # determine which fields have been modified
  180. dirties = self._compute_onchange_dirty(origin, record, name)
  181. dirty |= set(dirties)
  182. todo.extend(dirties)
  183. # preserve values to update since these are the one selected
  184. # by the user.
  185. for f in dirties:
  186. field = self._fields[f]
  187. if (f in updated_values and
  188. field.type not in ("one2many", "many2many")):
  189. record[f] = values[f]
  190. # prepare the result to return a dictionary with the new values for
  191. # the dirty fields
  192. result = {}
  193. for name in dirty:
  194. field = self._fields[name]
  195. value = record[name]
  196. if field.type == "many2one" and isinstance(value.id, models.NewId):
  197. continue
  198. result[name] = self._convert_to_onchange(record, field, value)
  199. # reset dirty values into the current record
  200. if self:
  201. to_reset = dirty | set(values.keys())
  202. with env.do_in_onchange():
  203. for name in to_reset:
  204. original = origin[name]
  205. new = self[name]
  206. if original == new:
  207. continue
  208. self[name] = origin[name]
  209. env.dirty[record].discard(name)
  210. env.dirty[record].update(origin_dirty)
  211. return result