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.

491 lines
19 KiB

  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 itertools import groupby
  22. from lxml import etree
  23. from operator import attrgetter
  24. from openerp import models, fields, api, exceptions, _
  25. from openerp.osv.orm import setup_modifiers
  26. # sentinel object to be sure that no empty value was passed to
  27. # ResPartnerRevisionChange._value_for_revision
  28. _NO_VALUE = object()
  29. class ResPartnerRevision(models.Model):
  30. _name = 'res.partner.revision'
  31. _description = 'Partner Revision'
  32. _order = 'date desc'
  33. _rec_name = 'date'
  34. partner_id = fields.Many2one(comodel_name='res.partner',
  35. string='Partner',
  36. select=True,
  37. required=True,
  38. readonly=True)
  39. change_ids = fields.One2many(comodel_name='res.partner.revision.change',
  40. inverse_name='revision_id',
  41. string='Changes',
  42. readonly=True)
  43. date = fields.Datetime(default=fields.Datetime.now,
  44. select=True,
  45. readonly=True)
  46. state = fields.Selection(
  47. compute='_compute_state',
  48. selection=[('draft', 'Pending'),
  49. ('done', 'Done')],
  50. string='State',
  51. store=True,
  52. )
  53. note = fields.Text()
  54. @api.one
  55. @api.depends('change_ids', 'change_ids.state')
  56. def _compute_state(self):
  57. if all(change.state in ('done', 'cancel') for change
  58. in self.mapped('change_ids')):
  59. self.state = 'done'
  60. else:
  61. self.state = 'draft'
  62. @api.multi
  63. def apply(self):
  64. self.mapped('change_ids').apply()
  65. @api.multi
  66. def cancel(self):
  67. self.mapped('change_ids').cancel()
  68. @api.multi
  69. def add_revision(self, record, values):
  70. """ Add a revision on a partner
  71. By default, when a partner is modified by a user or by the
  72. system, the changes are applied and a validated revision is
  73. created. Callers which want to delegate the write of some
  74. fields to the revision must explicitly ask for it by providing a
  75. key ``__revision_rules`` in the environment's context.
  76. Should be called before the execution of ``write`` on the record
  77. so we can keep track of the existing value and also because the
  78. returned values should be used for ``write`` as some of the
  79. values may have been removed.
  80. :param values: the values being written on the partner
  81. :type values: dict
  82. :returns: dict of values that should be wrote on the partner
  83. (fields with a 'Validate' or 'Never' rule are excluded)
  84. """
  85. record.ensure_one()
  86. change_model = self.env['res.partner.revision.change']
  87. write_values = values.copy()
  88. changes = []
  89. rules = self.env['revision.field.rule'].get_rules(record._model._name)
  90. for field in values:
  91. rule = rules.get(field)
  92. if not rule:
  93. continue
  94. if field in values:
  95. if not change_model._has_field_changed(record, field,
  96. values[field]):
  97. continue
  98. change, pop_value = change_model._prepare_revision_change(
  99. record, rule, field, values[field]
  100. )
  101. if pop_value:
  102. write_values.pop(field)
  103. changes.append(change)
  104. if changes:
  105. self.env['res.partner.revision'].create({
  106. 'partner_id': record.id,
  107. 'change_ids': [(0, 0, vals) for vals in changes],
  108. 'date': fields.Datetime.now(),
  109. })
  110. return write_values
  111. class ResPartnerRevisionChange(models.Model):
  112. """ Store the change of one field for one revision on one partner
  113. This model is composed of 3 sets of fields:
  114. * 'origin'
  115. * 'old'
  116. * 'new'
  117. The 'new' fields contain the value that needs to be validated.
  118. The 'old' field copies the actual value of the partner when the
  119. change is either applied either canceled. This field is used as a storage
  120. place but never shown by itself.
  121. The 'origin' fields is a related field towards the actual values of
  122. the partner until the change is either applied either canceled, past
  123. that it shows the 'old' value.
  124. The reason behind this is that the values may change on a partner between
  125. the moment when the revision is created and when it is applied.
  126. On the views, we show the origin fields which represent the actual
  127. partner values or the old values and we show the new fields.
  128. The 'origin' and 'new_value_display' are displayed on
  129. the tree view where we need a unique of field, the other fields are
  130. displayed on the form view so we benefit from their widgets.
  131. """
  132. _name = 'res.partner.revision.change'
  133. _description = 'Partner Revision Change'
  134. _rec_name = 'field_id'
  135. revision_id = fields.Many2one(comodel_name='res.partner.revision',
  136. required=True,
  137. string='Revision',
  138. ondelete='cascade',
  139. readonly=True)
  140. field_id = fields.Many2one(comodel_name='ir.model.fields',
  141. string='Field',
  142. required=True,
  143. readonly=True)
  144. field_type = fields.Selection(related='field_id.ttype',
  145. string='Field Type',
  146. readonly=True)
  147. origin_value_display = fields.Char(
  148. string='Previous',
  149. compute='_compute_value_display',
  150. )
  151. new_value_display = fields.Char(
  152. string='New',
  153. compute='_compute_value_display',
  154. )
  155. # Fields showing the origin partner's value or the 'old' value if
  156. # the change is applied or canceled.
  157. origin_value_char = fields.Char(compute='_compute_origin_values',
  158. string='Previous',
  159. readonly=True)
  160. origin_value_date = fields.Date(compute='_compute_origin_values',
  161. string='Previous',
  162. readonly=True)
  163. origin_value_datetime = fields.Datetime(compute='_compute_origin_values',
  164. string='Previous',
  165. readonly=True)
  166. origin_value_float = fields.Float(compute='_compute_origin_values',
  167. string='Previous',
  168. readonly=True)
  169. origin_value_integer = fields.Integer(compute='_compute_origin_values',
  170. string='Previous',
  171. readonly=True)
  172. origin_value_text = fields.Text(compute='_compute_origin_values',
  173. string='Previous',
  174. readonly=True)
  175. origin_value_boolean = fields.Boolean(compute='_compute_origin_values',
  176. string='Previous',
  177. readonly=True)
  178. origin_value_reference = fields.Reference(
  179. compute='_compute_origin_values',
  180. string='Previous',
  181. selection='_reference_models',
  182. readonly=True,
  183. )
  184. # Fields storing the previous partner's values (saved when the
  185. # revision is applied)
  186. old_value_char = fields.Char(string='Old',
  187. readonly=True)
  188. old_value_date = fields.Date(string='Old',
  189. readonly=True)
  190. old_value_datetime = fields.Datetime(string='Old',
  191. readonly=True)
  192. old_value_float = fields.Float(string='Old',
  193. readonly=True)
  194. old_value_integer = fields.Integer(string='Old',
  195. readonly=True)
  196. old_value_text = fields.Text(string='Old',
  197. readonly=True)
  198. old_value_boolean = fields.Boolean(string='Old',
  199. readonly=True)
  200. old_value_reference = fields.Reference(string='Old',
  201. selection='_reference_models',
  202. readonly=True)
  203. # Fields storing the value applied on the partner
  204. new_value_char = fields.Char(string='New',
  205. readonly=True)
  206. new_value_date = fields.Date(string='New',
  207. readonly=True)
  208. new_value_datetime = fields.Datetime(string='New',
  209. readonly=True)
  210. new_value_float = fields.Float(string='New',
  211. readonly=True)
  212. new_value_integer = fields.Integer(string='New',
  213. readonly=True)
  214. new_value_text = fields.Text(string='New',
  215. readonly=True)
  216. new_value_boolean = fields.Boolean(string='New',
  217. readonly=True)
  218. new_value_reference = fields.Reference(string='New',
  219. selection='_reference_models',
  220. readonly=True)
  221. state = fields.Selection(
  222. selection=[('draft', 'Pending'),
  223. ('done', 'Accepted'),
  224. ('cancel', 'Refused'),
  225. ],
  226. required=True,
  227. default='draft',
  228. readonly=True,
  229. )
  230. @api.model
  231. def _reference_models(self):
  232. models = self.env['ir.model'].search([])
  233. return [(model.model, model.name) for model in models]
  234. _suffix_to_types = {
  235. 'char': ('char', 'selection'),
  236. 'date': ('date',),
  237. 'datetime': ('datetime',),
  238. 'float': ('float',),
  239. 'integer': ('integer',),
  240. 'text': ('text',),
  241. 'boolean': ('boolean',),
  242. 'reference': ('many2one',),
  243. }
  244. _type_to_suffix = {ftype: suffix
  245. for suffix, ftypes in _suffix_to_types.iteritems()
  246. for ftype in ftypes}
  247. _origin_value_fields = ['origin_value_%s' % suffix
  248. for suffix in _suffix_to_types]
  249. _old_value_fields = ['old_value_%s' % suffix
  250. for suffix in _suffix_to_types]
  251. _new_value_fields = ['new_value_%s' % suffix
  252. for suffix in _suffix_to_types]
  253. _value_fields = (_origin_value_fields +
  254. _old_value_fields +
  255. _new_value_fields)
  256. @api.one
  257. @api.depends('revision_id.partner_id.*')
  258. def _compute_origin_values(self):
  259. field_name = self.get_field_for_type(self.field_id, 'origin')
  260. if self.state == 'draft':
  261. value = self.revision_id.partner_id[self.field_id.name]
  262. else:
  263. old_field = self.get_field_for_type(self.field_id, 'old')
  264. value = self[old_field]
  265. setattr(self, field_name, value)
  266. @api.one
  267. @api.depends(lambda self: self._value_fields)
  268. def _compute_value_display(self):
  269. for prefix in ('origin', 'new'):
  270. value = getattr(self, 'get_%s_value' % prefix)()
  271. if self.field_id.ttype == 'many2one' and value:
  272. value = value.display_name
  273. setattr(self, '%s_value_display' % prefix, value)
  274. @api.model
  275. def get_field_for_type(self, field, prefix):
  276. assert prefix in ('origin', 'old', 'new')
  277. field_type = self._type_to_suffix.get(field.ttype)
  278. if not field_type:
  279. raise NotImplementedError(
  280. 'field type %s is not supported' % field_type
  281. )
  282. return '%s_value_%s' % (prefix, field_type)
  283. @api.multi
  284. def get_origin_value(self):
  285. self.ensure_one()
  286. field_name = self.get_field_for_type(self.field_id, 'origin')
  287. return self[field_name]
  288. @api.multi
  289. def get_new_value(self):
  290. self.ensure_one()
  291. field_name = self.get_field_for_type(self.field_id, 'new')
  292. return self[field_name]
  293. @api.multi
  294. def set_old_value(self):
  295. """ Copy the value of the partner to the 'old' field """
  296. for change in self:
  297. # copy the existing partner's value for the history
  298. old_value_for_write = self._value_for_revision(
  299. change.revision_id.partner_id,
  300. change.field_id.name
  301. )
  302. old_field_name = self.get_field_for_type(change.field_id, 'old')
  303. change.write({old_field_name: old_value_for_write})
  304. @api.multi
  305. def apply(self):
  306. """ Apply the change on the revision's partner
  307. It is optimized thus that it makes only one write on the partner
  308. per revision if many changes are applied at once.
  309. """
  310. changes_ok = self.browse()
  311. key = attrgetter('revision_id')
  312. for revision, changes in groupby(self.sorted(key=key), key=key):
  313. values = {}
  314. partner = revision.partner_id
  315. for change in changes:
  316. if change.state in ('cancel', 'done'):
  317. continue
  318. field = change.field_id
  319. value_for_write = change._convert_value_for_write(
  320. change.get_new_value()
  321. )
  322. values[field.name] = value_for_write
  323. change.set_old_value()
  324. changes_ok |= change
  325. if not values:
  326. continue
  327. previous_revisions = self.env['res.partner.revision'].search(
  328. [('date', '<', revision.date),
  329. ('state', '=', 'draft'),
  330. ('partner_id', '=', revision.partner_id.id),
  331. ],
  332. limit=1,
  333. )
  334. if previous_revisions:
  335. raise exceptions.Warning(
  336. _('This change cannot be applied because a previous '
  337. 'revision for the same partner is pending.\n'
  338. 'Apply all the anterior revisions before applying '
  339. 'this one.')
  340. )
  341. partner.with_context(__no_revision=True).write(values)
  342. changes_ok.write({'state': 'done'})
  343. @api.multi
  344. def cancel(self):
  345. """ Reject the change """
  346. if any(change.state == 'done' for change in self):
  347. raise exceptions.Warning(
  348. _('This change has already be applied.')
  349. )
  350. self.set_old_value()
  351. self.write({'state': 'cancel'})
  352. @api.model
  353. def _has_field_changed(self, record, field, value):
  354. field_def = record._fields[field]
  355. return field_def.convert_to_write(record[field]) != value
  356. @api.multi
  357. def _convert_value_for_write(self, value):
  358. model = self.env[self.field_id.model_id.model]
  359. model_field_def = model._fields[self.field_id.name]
  360. return model_field_def.convert_to_write(value)
  361. @api.model
  362. def _value_for_revision(self, record, field_name, value=_NO_VALUE):
  363. """ Return a value from the record ready to write in a revision field
  364. :param record: modified record
  365. :param field_name: name of the modified field
  366. :param value: if no value is given, it is read from the record
  367. """
  368. field_def = record._fields[field_name]
  369. if value is _NO_VALUE:
  370. # when the value is read from the record, we need to prepare
  371. # it for the write (e.g. extract .id from a many2one record)
  372. value = field_def.convert_to_write(record[field_name])
  373. if field_def.type == 'many2one':
  374. # store as 'reference'
  375. comodel = field_def.comodel_name
  376. return "%s,%s" % (comodel, value) if value else False
  377. else:
  378. return value
  379. @api.multi
  380. def _prepare_revision_change(self, record, rule, field_name, value):
  381. """ Prepare data for a revision change
  382. It returns a dict of the values to write on the revision change
  383. and a boolean that indicates if the value should be popped out
  384. of the values to write on the model.
  385. :returns: dict of values, boolean
  386. """
  387. new_field_name = self.get_field_for_type(rule.field_id, 'new')
  388. new_value = self._value_for_revision(record, field_name, value=value)
  389. change = {
  390. new_field_name: new_value,
  391. 'field_id': rule.field_id.id,
  392. }
  393. pop_value = False
  394. if (not self.env.context.get('__revision_rules') or
  395. rule.action == 'auto'):
  396. change['state'] = 'done'
  397. elif rule.action == 'validate':
  398. change['state'] = 'draft'
  399. pop_value = True # change to apply manually
  400. elif rule.action == 'never':
  401. change['state'] = 'cancel'
  402. pop_value = True # change never applied
  403. if change['state'] in ('cancel', 'done'):
  404. # Normally the 'old' value is set when we use the 'apply'
  405. # button, but since we short circuit the 'apply', we
  406. # directly set the 'old' value here
  407. old_field_name = self.get_field_for_type(rule.field_id, 'old')
  408. # get values ready to write as expected by the revision
  409. # (for instance, a many2one is written in a reference
  410. # field)
  411. origin_value = self._value_for_revision(record, field_name)
  412. change[old_field_name] = origin_value
  413. return change, pop_value
  414. def fields_view_get(self, *args, **kwargs):
  415. _super = super(ResPartnerRevisionChange, self)
  416. result = _super.fields_view_get(*args, **kwargs)
  417. if result['type'] != 'form':
  418. return
  419. doc = etree.XML(result['arch'])
  420. for suffix, ftypes in self._suffix_to_types.iteritems():
  421. for prefix in ('origin', 'old', 'new'):
  422. field_name = '%s_value_%s' % (prefix, suffix)
  423. field_nodes = doc.xpath("//field[@name='%s']" % field_name)
  424. for node in field_nodes:
  425. node.set(
  426. 'attrs',
  427. "{'invisible': "
  428. "[('field_type', 'not in', %s)]}" % (ftypes,)
  429. )
  430. setup_modifiers(node)
  431. result['arch'] = etree.tostring(doc)
  432. return result