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.

222 lines
8.8 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. for name in record._cache:
  76. if name in models.LOG_ACCESS_COLUMNS:
  77. continue
  78. v = record[name]
  79. f = record._fields[name]
  80. if f.type == "many2one" and isinstance(v.id, models.NewId):
  81. continue
  82. vals[name] = self._convert_to_onchange(record, f, v)
  83. if not record.id:
  84. result.append((0, 0, vals))
  85. elif vals:
  86. result.append((1, record.id, vals))
  87. else:
  88. result.append((4, record.id))
  89. return result
  90. else:
  91. return field.convert_to_onchange(value, record, [field.name])
  92. def play_onchanges(self, values, onchange_fields=None):
  93. """
  94. Play the onchange methods defined on the current record and return the
  95. changed values.
  96. The record is not modified by the onchange.
  97. The intend of this method is to provide a way to get on the server side
  98. the values returned by the onchange methods when called by the UI.
  99. This method is useful in B2B contexts where records are created or
  100. modified from server side.
  101. The returned values are those changed by the execution of onchange
  102. methods registered for the onchange_fields according to the provided
  103. values. As consequence, the result will not contain the modifications
  104. that could occurs by the execution of compute methods registered for
  105. the same onchange_fields.
  106. It's on purpose that we avoid to trigger the compute methods for the
  107. onchange_fields. These compute methods will be triggered when calling
  108. the create or write method. In this way we avoid to compute useless
  109. information.
  110. :param values: dict of input value that
  111. :param onchange_fields: fields for which onchange methods will be
  112. played. If not provided, the list of field is based on the values keys.
  113. Order in onchange_fields is very important as onchanges methods will
  114. be played in that order.
  115. :return: changed values
  116. This method reimplement the onchange method to be able to work on the
  117. current recordset if provided.
  118. """
  119. env = self.env
  120. if self:
  121. self.ensure_one()
  122. if not onchange_fields:
  123. onchange_fields = values.keys()
  124. elif not isinstance(onchange_fields, list):
  125. onchange_fields = [onchange_fields]
  126. if not onchange_fields:
  127. onchange_fields = values.keys()
  128. # filter out keys in field_onchange that do not refer to actual fields
  129. names = [n for n in onchange_fields if n in self._fields]
  130. # create a new record with values, and attach ``self`` to it
  131. with env.do_in_onchange():
  132. # keep a copy of the original record.
  133. # attach ``self`` with a different context (for cache consistency)
  134. origin = self.with_context(__onchange=True)
  135. origin_dirty = set(self._get_dirty())
  136. fields.copy_cache(self, origin.env)
  137. if self:
  138. record = self
  139. record.update(values)
  140. else:
  141. # initialize with default values, they may be used in onchange
  142. new_values = self.default_get(self._fields.keys())
  143. new_values.update(values)
  144. record = self.new(new_values)
  145. values = {name: record[name] for name in record._cache}
  146. record._origin = origin
  147. # determine which field(s) should be triggered an onchange
  148. todo = list(names) or list(values)
  149. done = set()
  150. # dummy assignment: trigger invalidations on the record
  151. with env.do_in_onchange():
  152. for name in todo:
  153. if name == "id":
  154. continue
  155. value = record[name]
  156. field = self._fields[name]
  157. if field.type == "many2one" and field.delegate and not value:
  158. # do not nullify all fields of parent record for new
  159. # records
  160. continue
  161. record[name] = value
  162. dirty = set()
  163. # process names in order (or the keys of values if no name given)
  164. while todo:
  165. name = todo.pop(0)
  166. if name in done:
  167. continue
  168. done.add(name)
  169. with env.do_in_onchange():
  170. # apply field-specific onchange methods
  171. record._onchange_eval(name, "1", {})
  172. # determine which fields have been modified
  173. dirties = self._compute_onchange_dirty(origin, record, name)
  174. dirty |= set(dirties)
  175. todo.extend(dirties)
  176. # prepare the result to return a dictionary with the new values for
  177. # the dirty fields
  178. result = {}
  179. for name in dirty:
  180. field = self._fields[name]
  181. value = record[name]
  182. if field.type == "many2one" and isinstance(value.id, models.NewId):
  183. continue
  184. result[name] = self._convert_to_onchange(record, field, value)
  185. # reset dirty values into the current record
  186. if self:
  187. to_reset = dirty | set(values.keys())
  188. with env.do_in_onchange():
  189. for name in to_reset:
  190. original = origin[name]
  191. new = self[name]
  192. if original == new:
  193. continue
  194. self[name] = origin[name]
  195. env.dirty[record].discard(name)
  196. env.dirty[record].update(origin_dirty)
  197. return result