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.

331 lines
12 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. # -*- coding: utf-8 -*-
  2. #
  3. #
  4. # Authors: Guewen Baconnier
  5. # Copyright 2015 Camptocamp SA
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as
  9. # published by the Free Software Foundation, either version 3 of the
  10. # License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. #
  21. from lxml import etree
  22. from openerp import models, fields, api, exceptions, _
  23. from openerp.osv.orm import setup_modifiers
  24. class ResPartnerRevision(models.Model):
  25. _name = 'res.partner.revision'
  26. _description = 'Partner Revision'
  27. _order = 'date desc'
  28. _rec_name = 'date'
  29. partner_id = fields.Many2one(comodel_name='res.partner',
  30. string='Partner',
  31. required=True)
  32. change_ids = fields.One2many(comodel_name='res.partner.revision.change',
  33. inverse_name='revision_id',
  34. string='Changes')
  35. date = fields.Datetime(default=fields.Datetime.now)
  36. # TODO: add a revision state, done when all lines are done or
  37. # canceled
  38. note = fields.Text()
  39. @api.multi
  40. def apply(self):
  41. self.mapped('change_ids').apply()
  42. @api.multi
  43. def cancel(self):
  44. self.mapped('change_ids').cancel()
  45. @api.multi
  46. def add_revision(self, record, values):
  47. """ Add a revision on a partner
  48. By default, when a partner is modified by a user or by the
  49. system, the changes are applied and a validated revision is
  50. created. Callers which want to delegate the write of some
  51. fields to the revision must explicitly ask for it by providing a
  52. key ``__revision_rules`` in the environment's context.
  53. Should be called before the execution of ``write`` on the record
  54. so we can keep track of the existing value and also because the
  55. returned values should be used for ``write`` as some of the
  56. values may have been removed.
  57. :param values: the values being written on the partner
  58. :type values: dict
  59. :returns: dict of values that should be wrote on the partner
  60. (fields with a 'Validate' or 'Never' rule are excluded)
  61. """
  62. record.ensure_one()
  63. change_model = self.env['res.partner.revision.change']
  64. write_values = values.copy()
  65. changes = []
  66. rules = self.env['revision.behavior'].get_rules(record._model._name)
  67. for field in values:
  68. rule = rules.get(field)
  69. if not rule:
  70. continue
  71. if field in values:
  72. if not change_model._has_field_changed(record, field,
  73. values[field]):
  74. continue
  75. change, pop_value = change_model._prepare_revision_change(
  76. record, rule, field, values[field]
  77. )
  78. if pop_value:
  79. write_values.pop(field)
  80. changes.append(change)
  81. if changes:
  82. self.env['res.partner.revision'].create({
  83. 'partner_id': record.id,
  84. 'change_ids': [(0, 0, vals) for vals in changes],
  85. 'date': fields.Datetime.now(),
  86. })
  87. return write_values
  88. class ResPartnerRevisionChange(models.Model):
  89. _name = 'res.partner.revision.change'
  90. _description = 'Partner Revision Change'
  91. _rec_name = 'field_id'
  92. revision_id = fields.Many2one(comodel_name='res.partner.revision',
  93. required=True,
  94. string='Revision',
  95. ondelete='cascade')
  96. field_id = fields.Many2one(comodel_name='ir.model.fields',
  97. string='Field',
  98. required=True)
  99. field_type = fields.Selection(related='field_id.ttype',
  100. string='Field Type',
  101. readonly=True)
  102. current_value_display = fields.Char(
  103. string='Current',
  104. compute='_compute_value_display',
  105. )
  106. new_value_display = fields.Char(
  107. string='New',
  108. compute='_compute_value_display',
  109. )
  110. current_value_char = fields.Char(string='Current')
  111. current_value_date = fields.Date(string='Current')
  112. current_value_datetime = fields.Datetime(string='Current')
  113. current_value_float = fields.Float(string='Current')
  114. current_value_integer = fields.Integer(string='Current')
  115. current_value_text = fields.Text(string='Current')
  116. current_value_boolean = fields.Boolean(string='Current')
  117. current_value_reference = fields.Reference(string='Current',
  118. selection='_reference_models')
  119. new_value_char = fields.Char(string='New')
  120. new_value_date = fields.Date(string='New')
  121. new_value_datetime = fields.Datetime(string='New')
  122. new_value_float = fields.Float(string='New')
  123. new_value_integer = fields.Integer(string='New')
  124. new_value_text = fields.Text(string='New')
  125. new_value_boolean = fields.Boolean(string='New')
  126. new_value_reference = fields.Reference(string='New',
  127. selection='_reference_models')
  128. state = fields.Selection(
  129. selection=[('draft', 'Waiting'),
  130. ('done', 'Accepted'),
  131. ('cancel', 'Refused'),
  132. ],
  133. required=True,
  134. default='draft',
  135. )
  136. @api.model
  137. def _reference_models(self):
  138. models = self.env['ir.model'].search([])
  139. return [(model.model, model.name) for model in models]
  140. _suffix_to_types = {
  141. 'char': ('char', 'selection'),
  142. 'date': ('date',),
  143. 'datetime': ('datetime',),
  144. 'float': ('float',),
  145. 'integer': ('integer',),
  146. 'text': ('text',),
  147. 'boolean': ('boolean',),
  148. 'reference': ('many2one',),
  149. }
  150. _type_to_suffix = {ftype: suffix
  151. for suffix, ftypes in _suffix_to_types.iteritems()
  152. for ftype in ftypes}
  153. _current_value_fields = ['current_value_%s' % suffix
  154. for suffix in _suffix_to_types]
  155. _new_value_fields = ['new_value_%s' % suffix
  156. for suffix in _suffix_to_types]
  157. _value_fields = _current_value_fields + _new_value_fields
  158. @api.one
  159. @api.depends(lambda self: self._value_fields)
  160. def _compute_value_display(self):
  161. for prefix in ('current', 'new'):
  162. value = getattr(self, 'get_%s_value' % prefix)()
  163. if self.field_id.ttype == 'many2one' and value:
  164. value = value.display_name
  165. setattr(self, '%s_value_display' % prefix, value)
  166. @api.model
  167. def create(self, vals):
  168. vals = vals.copy()
  169. field = self.env['ir.model.fields'].browse(vals.get('field_id'))
  170. if 'current_value' in vals:
  171. current_value = vals.pop('current_value')
  172. if field:
  173. current_field_name = self.get_field_for_type(field, 'current')
  174. vals[current_field_name] = current_value
  175. if 'new_value' in vals:
  176. new_value = vals.pop('new_value')
  177. if field:
  178. new_field_name = self.get_field_for_type(field, 'new')
  179. vals[new_field_name] = new_value
  180. return super(ResPartnerRevisionChange, self).create(vals)
  181. @api.model
  182. def get_field_for_type(self, field, current_or_new):
  183. assert current_or_new in ('new', 'current')
  184. field_type = self._type_to_suffix.get(field.ttype)
  185. if not field_type:
  186. raise NotImplementedError(
  187. 'field type %s is not supported' % field_type
  188. )
  189. return '%s_value_%s' % (current_or_new, field_type)
  190. @api.multi
  191. def get_current_value(self):
  192. self.ensure_one()
  193. field_name = self.get_field_for_type(self.field_id, 'current')
  194. return self[field_name]
  195. @api.multi
  196. def get_new_value(self):
  197. self.ensure_one()
  198. field_name = self.get_field_for_type(self.field_id, 'new')
  199. return self[field_name]
  200. @api.multi
  201. def apply(self):
  202. # TODO: optimize with 1 write for all fields, group by revision
  203. for change in self:
  204. if change.state in ('cancel', 'done'):
  205. continue
  206. partner = change.revision_id.partner_id
  207. value_for_write = change._convert_value_for_write(
  208. change.get_new_value()
  209. )
  210. partner.write({change.field_id.name: value_for_write})
  211. change.write({'state': 'done'})
  212. @api.multi
  213. def cancel(self):
  214. if any(change.state == 'done' for change in self):
  215. raise exceptions.Warning(
  216. _('This change has already be applied.')
  217. )
  218. self.write({'state': 'cancel'})
  219. @api.model
  220. def _has_field_changed(self, record, field, value):
  221. field_def = record._fields[field]
  222. return field_def.convert_to_write(record[field]) != value
  223. @api.multi
  224. def _convert_value_for_write(self, value):
  225. model = self.env[self.field_id.model_id.model]
  226. model_field_def = model._fields[self.field_id.name]
  227. return model_field_def.convert_to_write(value)
  228. @api.model
  229. def _convert_value_for_revision(self, record, field, value):
  230. field_def = record._fields[field]
  231. if field_def.type == 'many2one':
  232. # store as 'reference'
  233. comodel = field_def.comodel_name
  234. return "%s,%s" % (comodel, value) if value else False
  235. else:
  236. return value
  237. @api.multi
  238. def _prepare_revision_change(self, record, rule, field, value):
  239. """ Prepare data for a revision change
  240. It returns a dict of the values to write on the revision change
  241. and a boolean that indicates if the value should be popped out
  242. of the values to write on the model.
  243. :returns: dict of values, boolean
  244. """
  245. field_def = record._fields[field]
  246. # get a ready to write value for the type of the field,
  247. # for instance takes '.id' from a many2one's record (the
  248. # new value is already a value as expected for the
  249. # write)
  250. current_value = field_def.convert_to_write(record[field])
  251. # get values ready to write as expected by the revision
  252. # (for instance, a many2one is written in a reference
  253. # field)
  254. current_value = self._convert_value_for_revision(record, field,
  255. current_value)
  256. new_value = self._convert_value_for_revision(record, field, value)
  257. change = {
  258. 'current_value': current_value,
  259. 'new_value': new_value,
  260. 'field_id': rule.field_id.id,
  261. }
  262. pop_value = False
  263. if not self.env.context.get('__revision_rules'):
  264. # by default always write on partner
  265. change['state'] = 'done'
  266. elif rule.default_behavior == 'auto':
  267. change['state'] = 'done'
  268. elif rule.default_behavior == 'validate':
  269. change['state'] = 'draft'
  270. pop_value = True # change to apply manually
  271. elif rule.default_behavior == 'never':
  272. change['state'] = 'cancel'
  273. pop_value = True # change never applied
  274. return change, pop_value
  275. def fields_view_get(self, *args, **kwargs):
  276. _super = super(ResPartnerRevisionChange, self)
  277. result = _super.fields_view_get(*args, **kwargs)
  278. if result['type'] != 'form':
  279. return
  280. doc = etree.XML(result['arch'])
  281. for suffix, ftypes in self._suffix_to_types.iteritems():
  282. for prefix in ('current', 'new'):
  283. field_name = '%s_value_%s' % (prefix, suffix)
  284. field_nodes = doc.xpath("//field[@name='%s']" % field_name)
  285. for node in field_nodes:
  286. node.set(
  287. 'attrs',
  288. "{'invisible': "
  289. "[('field_type', 'not in', %s)]}" % (ftypes,)
  290. )
  291. setup_modifiers(node)
  292. result['arch'] = etree.tostring(doc)
  293. return result