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.

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