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.

402 lines
15 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. class ResPartnerRevision(models.Model):
  27. _name = 'res.partner.revision'
  28. _description = 'Partner Revision'
  29. _order = 'date desc'
  30. _rec_name = 'date'
  31. partner_id = fields.Many2one(comodel_name='res.partner',
  32. string='Partner',
  33. select=True,
  34. required=True,
  35. readonly=True)
  36. change_ids = fields.One2many(comodel_name='res.partner.revision.change',
  37. inverse_name='revision_id',
  38. string='Changes',
  39. readonly=True)
  40. date = fields.Datetime(default=fields.Datetime.now,
  41. select=True,
  42. readonly=True)
  43. state = fields.Selection(
  44. compute='_compute_state',
  45. selection=[('draft', 'Pending'),
  46. ('done', 'Done')],
  47. string='State',
  48. store=True,
  49. )
  50. note = fields.Text()
  51. @api.one
  52. @api.depends('change_ids', 'change_ids.state')
  53. def _compute_state(self):
  54. if all(change.state in ('done', 'cancel') for change
  55. in self.mapped('change_ids')):
  56. self.state = 'done'
  57. else:
  58. self.state = 'draft'
  59. @api.multi
  60. def apply(self):
  61. self.mapped('change_ids').apply()
  62. @api.multi
  63. def cancel(self):
  64. self.mapped('change_ids').cancel()
  65. @api.multi
  66. def add_revision(self, record, values):
  67. """ Add a revision on a partner
  68. By default, when a partner is modified by a user or by the
  69. system, the changes are applied and a validated revision is
  70. created. Callers which want to delegate the write of some
  71. fields to the revision must explicitly ask for it by providing a
  72. key ``__revision_rules`` in the environment's context.
  73. Should be called before the execution of ``write`` on the record
  74. so we can keep track of the existing value and also because the
  75. returned values should be used for ``write`` as some of the
  76. values may have been removed.
  77. :param values: the values being written on the partner
  78. :type values: dict
  79. :returns: dict of values that should be wrote on the partner
  80. (fields with a 'Validate' or 'Never' rule are excluded)
  81. """
  82. record.ensure_one()
  83. change_model = self.env['res.partner.revision.change']
  84. write_values = values.copy()
  85. changes = []
  86. rules = self.env['revision.field.rule'].get_rules(record._model._name)
  87. for field in values:
  88. rule = rules.get(field)
  89. if not rule:
  90. continue
  91. if field in values:
  92. if not change_model._has_field_changed(record, field,
  93. values[field]):
  94. continue
  95. change, pop_value = change_model._prepare_revision_change(
  96. record, rule, field, values[field]
  97. )
  98. if pop_value:
  99. write_values.pop(field)
  100. changes.append(change)
  101. if changes:
  102. self.env['res.partner.revision'].create({
  103. 'partner_id': record.id,
  104. 'change_ids': [(0, 0, vals) for vals in changes],
  105. 'date': fields.Datetime.now(),
  106. })
  107. return write_values
  108. class ResPartnerRevisionChange(models.Model):
  109. _name = 'res.partner.revision.change'
  110. _description = 'Partner Revision Change'
  111. _rec_name = 'field_id'
  112. revision_id = fields.Many2one(comodel_name='res.partner.revision',
  113. required=True,
  114. string='Revision',
  115. ondelete='cascade',
  116. readonly=True)
  117. field_id = fields.Many2one(comodel_name='ir.model.fields',
  118. string='Field',
  119. required=True,
  120. readonly=True)
  121. field_type = fields.Selection(related='field_id.ttype',
  122. string='Field Type',
  123. readonly=True)
  124. current_value_display = fields.Char(
  125. string='Current',
  126. compute='_compute_value_display',
  127. )
  128. new_value_display = fields.Char(
  129. string='New',
  130. compute='_compute_value_display',
  131. )
  132. current_value_char = fields.Char(string='Current',
  133. readonly=True)
  134. current_value_date = fields.Date(string='Current',
  135. readonly=True)
  136. current_value_datetime = fields.Datetime(string='Current',
  137. readonly=True)
  138. current_value_float = fields.Float(string='Current',
  139. readonly=True)
  140. current_value_integer = fields.Integer(string='Current',
  141. readonly=True)
  142. current_value_text = fields.Text(string='Current',
  143. readonly=True)
  144. current_value_boolean = fields.Boolean(string='Current',
  145. readonly=True)
  146. current_value_reference = fields.Reference(string='Current',
  147. selection='_reference_models',
  148. readonly=True)
  149. new_value_char = fields.Char(string='New',
  150. readonly=True)
  151. new_value_date = fields.Date(string='New',
  152. readonly=True)
  153. new_value_datetime = fields.Datetime(string='New',
  154. readonly=True)
  155. new_value_float = fields.Float(string='New',
  156. readonly=True)
  157. new_value_integer = fields.Integer(string='New',
  158. readonly=True)
  159. new_value_text = fields.Text(string='New',
  160. readonly=True)
  161. new_value_boolean = fields.Boolean(string='New',
  162. readonly=True)
  163. new_value_reference = fields.Reference(string='New',
  164. selection='_reference_models',
  165. readonly=True)
  166. state = fields.Selection(
  167. selection=[('draft', 'Pending'),
  168. ('done', 'Accepted'),
  169. ('cancel', 'Refused'),
  170. ],
  171. required=True,
  172. default='draft',
  173. readonly=True,
  174. )
  175. @api.model
  176. def _reference_models(self):
  177. models = self.env['ir.model'].search([])
  178. return [(model.model, model.name) for model in models]
  179. _suffix_to_types = {
  180. 'char': ('char', 'selection'),
  181. 'date': ('date',),
  182. 'datetime': ('datetime',),
  183. 'float': ('float',),
  184. 'integer': ('integer',),
  185. 'text': ('text',),
  186. 'boolean': ('boolean',),
  187. 'reference': ('many2one',),
  188. }
  189. _type_to_suffix = {ftype: suffix
  190. for suffix, ftypes in _suffix_to_types.iteritems()
  191. for ftype in ftypes}
  192. _current_value_fields = ['current_value_%s' % suffix
  193. for suffix in _suffix_to_types]
  194. _new_value_fields = ['new_value_%s' % suffix
  195. for suffix in _suffix_to_types]
  196. _value_fields = _current_value_fields + _new_value_fields
  197. @api.one
  198. @api.depends(lambda self: self._value_fields)
  199. def _compute_value_display(self):
  200. for prefix in ('current', 'new'):
  201. value = getattr(self, 'get_%s_value' % prefix)()
  202. if self.field_id.ttype == 'many2one' and value:
  203. value = value.display_name
  204. setattr(self, '%s_value_display' % prefix, value)
  205. @api.model
  206. def create(self, vals):
  207. vals = vals.copy()
  208. field = self.env['ir.model.fields'].browse(vals.get('field_id'))
  209. if 'current_value' in vals:
  210. current_value = vals.pop('current_value')
  211. if field:
  212. current_field_name = self.get_field_for_type(field, 'current')
  213. vals[current_field_name] = current_value
  214. if 'new_value' in vals:
  215. new_value = vals.pop('new_value')
  216. if field:
  217. new_field_name = self.get_field_for_type(field, 'new')
  218. vals[new_field_name] = new_value
  219. return super(ResPartnerRevisionChange, self).create(vals)
  220. @api.model
  221. def get_field_for_type(self, field, current_or_new):
  222. assert current_or_new in ('new', 'current')
  223. field_type = self._type_to_suffix.get(field.ttype)
  224. if not field_type:
  225. raise NotImplementedError(
  226. 'field type %s is not supported' % field_type
  227. )
  228. return '%s_value_%s' % (current_or_new, field_type)
  229. @api.multi
  230. def get_current_value(self):
  231. self.ensure_one()
  232. field_name = self.get_field_for_type(self.field_id, 'current')
  233. return self[field_name]
  234. @api.multi
  235. def get_new_value(self):
  236. self.ensure_one()
  237. field_name = self.get_field_for_type(self.field_id, 'new')
  238. return self[field_name]
  239. @api.multi
  240. def apply(self):
  241. """ Apply the change on the revision's partner
  242. It is optimized thus that it makes only one write on the partner
  243. per revision if many changes are applied at once.
  244. """
  245. changes_ok = self.browse()
  246. key = attrgetter('revision_id')
  247. for revision, changes in groupby(self.sorted(key=key), key=key):
  248. values = {}
  249. partner = revision.partner_id
  250. for change in changes:
  251. if change.state in ('cancel', 'done'):
  252. continue
  253. value_for_write = change._convert_value_for_write(
  254. change.get_new_value()
  255. )
  256. values[change.field_id.name] = value_for_write
  257. changes_ok |= change
  258. if not values:
  259. continue
  260. previous_revisions = self.env['res.partner.revision'].search(
  261. [('date', '<', revision.date),
  262. ('state', '=', 'draft'),
  263. ('partner_id', '=', revision.partner_id.id),
  264. ],
  265. limit=1,
  266. )
  267. if previous_revisions:
  268. raise exceptions.Warning(
  269. _('This change cannot be applied because a previous '
  270. 'revision for the same partner is pending.\n'
  271. 'Apply all the anterior revisions before applying '
  272. 'this one.')
  273. )
  274. partner.write(values)
  275. changes_ok.write({'state': 'done'})
  276. @api.multi
  277. def cancel(self):
  278. """ Reject the change """
  279. if any(change.state == 'done' for change in self):
  280. raise exceptions.Warning(
  281. _('This change has already be applied.')
  282. )
  283. self.write({'state': 'cancel'})
  284. @api.model
  285. def _has_field_changed(self, record, field, value):
  286. field_def = record._fields[field]
  287. return field_def.convert_to_write(record[field]) != value
  288. @api.multi
  289. def _convert_value_for_write(self, value):
  290. model = self.env[self.field_id.model_id.model]
  291. model_field_def = model._fields[self.field_id.name]
  292. return model_field_def.convert_to_write(value)
  293. @api.model
  294. def _convert_value_for_revision(self, record, field, value):
  295. field_def = record._fields[field]
  296. if field_def.type == 'many2one':
  297. # store as 'reference'
  298. comodel = field_def.comodel_name
  299. return "%s,%s" % (comodel, value) if value else False
  300. else:
  301. return value
  302. @api.multi
  303. def _prepare_revision_change(self, record, rule, field, value):
  304. """ Prepare data for a revision change
  305. It returns a dict of the values to write on the revision change
  306. and a boolean that indicates if the value should be popped out
  307. of the values to write on the model.
  308. :returns: dict of values, boolean
  309. """
  310. field_def = record._fields[field]
  311. # get a ready to write value for the type of the field,
  312. # for instance takes '.id' from a many2one's record (the
  313. # new value is already a value as expected for the
  314. # write)
  315. current_value = field_def.convert_to_write(record[field])
  316. # get values ready to write as expected by the revision
  317. # (for instance, a many2one is written in a reference
  318. # field)
  319. current_value = self._convert_value_for_revision(record, field,
  320. current_value)
  321. new_value = self._convert_value_for_revision(record, field, value)
  322. change = {
  323. 'current_value': current_value,
  324. 'new_value': new_value,
  325. 'field_id': rule.field_id.id,
  326. }
  327. pop_value = False
  328. if not self.env.context.get('__revision_rules'):
  329. # by default always write on partner
  330. change['state'] = 'done'
  331. elif rule.action == 'auto':
  332. change['state'] = 'done'
  333. elif rule.action == 'validate':
  334. change['state'] = 'draft'
  335. pop_value = True # change to apply manually
  336. elif rule.action == 'never':
  337. change['state'] = 'cancel'
  338. pop_value = True # change never applied
  339. return change, pop_value
  340. def fields_view_get(self, *args, **kwargs):
  341. _super = super(ResPartnerRevisionChange, self)
  342. result = _super.fields_view_get(*args, **kwargs)
  343. if result['type'] != 'form':
  344. return
  345. doc = etree.XML(result['arch'])
  346. for suffix, ftypes in self._suffix_to_types.iteritems():
  347. for prefix in ('current', 'new'):
  348. field_name = '%s_value_%s' % (prefix, suffix)
  349. field_nodes = doc.xpath("//field[@name='%s']" % field_name)
  350. for node in field_nodes:
  351. node.set(
  352. 'attrs',
  353. "{'invisible': "
  354. "[('field_type', 'not in', %s)]}" % (ftypes,)
  355. )
  356. setup_modifiers(node)
  357. result['arch'] = etree.tostring(doc)
  358. return result