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.

528 lines
20 KiB

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