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.

486 lines
18 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2017 Therp BV <http://therp.nl>
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. # pylint: disable=method-required-super
  5. """Abstract model to show each relation from two sides."""
  6. import collections
  7. import logging
  8. from psycopg2.extensions import AsIs
  9. from openerp import _, api, fields, models
  10. from openerp.exceptions import ValidationError
  11. from openerp.tools import drop_view_if_exists
  12. _logger = logging.getLogger(__name__)
  13. _last_key_offset = -1
  14. _specification_register = collections.OrderedDict()
  15. def register_select_specification(base_name, is_inverse, select_sql):
  16. """Register SELECT clause for rows to be included in view.
  17. Each SELECT clause must contain a first column in the form:
  18. '<base_keyfield> * %%(padding)s + %(key_offset),'
  19. The %%(padding)s will be used as a parameter for the
  20. cursor.execute function, the %(key_offset) will be replaced
  21. by dictionary replacement.
  22. The columns for each SELECT clause must be ordered as follows:
  23. id - specified as defined above:
  24. res_model: model._name of the underlying table,
  25. res_id: base key in the underlying table,
  26. this_partner_id: must refer to a partner
  27. other_partner_id: must refer to a related partner
  28. type_id: refers to res.partner.relation.type,
  29. date_start: start date of relation if relevant or NULL,
  30. date_end: end date of relation if relevant or NULL,
  31. %(is_inverse)s as is_inverse: used to determine type_selection_id,
  32. """
  33. global _last_key_offset
  34. key_name = base_name + (is_inverse and '_inverse' or '')
  35. assert key_name not in _specification_register
  36. assert '%%(padding)s' in select_sql
  37. assert '%(key_offset)s' in select_sql
  38. assert '%(is_inverse)s' in select_sql
  39. _last_key_offset += 1
  40. _specification_register[key_name] = dict(
  41. base_name=base_name,
  42. is_inverse=is_inverse,
  43. key_offset=_last_key_offset,
  44. select_sql=select_sql % {
  45. 'key_offset': _last_key_offset,
  46. 'is_inverse': is_inverse})
  47. def get_select_specification(base_name, is_inverse):
  48. key_name = base_name + (is_inverse and '_inverse' or '')
  49. return _specification_register[key_name]
  50. # Register relations
  51. register_select_specification(
  52. base_name='relation',
  53. is_inverse=False,
  54. select_sql="""\
  55. SELECT
  56. (rel.id * %%(padding)s) + %(key_offset)s AS id,
  57. 'res.partner.relation' AS res_model,
  58. rel.id AS res_id,
  59. rel.left_partner_id AS this_partner_id,
  60. rel.right_partner_id AS other_partner_id,
  61. rel.type_id,
  62. rel.date_start,
  63. rel.date_end,
  64. %(is_inverse)s as is_inverse
  65. FROM res_partner_relation rel
  66. """)
  67. # Register inverse relations
  68. register_select_specification(
  69. base_name='relation',
  70. is_inverse=True,
  71. select_sql="""\
  72. SELECT
  73. (rel.id * %%(padding)s) + %(key_offset)s AS id,
  74. 'res.partner.relation',
  75. rel.id,
  76. rel.right_partner_id,
  77. rel.left_partner_id,
  78. rel.type_id,
  79. rel.date_start,
  80. rel.date_end,
  81. %(is_inverse)s as is_inverse
  82. FROM res_partner_relation rel
  83. """)
  84. class ResPartnerRelationAll(models.AbstractModel):
  85. """Abstract model to show each relation from two sides."""
  86. _auto = False
  87. _log_access = False
  88. _name = 'res.partner.relation.all'
  89. _description = 'All (non-inverse + inverse) relations between partners'
  90. _order = \
  91. 'this_partner_id, type_selection_id, date_end desc, date_start desc'
  92. res_model = fields.Char(
  93. string='Resource Model',
  94. readonly=True,
  95. required=True,
  96. help="The database object this relation is based on.")
  97. res_id = fields.Integer(
  98. string='Resource ID',
  99. readonly=True,
  100. required=True,
  101. help="The id of the object in the model this relation is based on.")
  102. this_partner_id = fields.Many2one(
  103. comodel_name='res.partner',
  104. string='One Partner',
  105. required=True)
  106. other_partner_id = fields.Many2one(
  107. comodel_name='res.partner',
  108. string='Other Partner',
  109. required=True)
  110. type_id = fields.Many2one(
  111. comodel_name='res.partner.relation.type',
  112. string='Underlying Relation Type',
  113. readonly=True,
  114. required=True)
  115. date_start = fields.Date('Starting date')
  116. date_end = fields.Date('Ending date')
  117. is_inverse = fields.Boolean(
  118. string="Is reverse type?",
  119. readonly=True,
  120. help="Inverse relations are from right to left partner.")
  121. type_selection_id = fields.Many2one(
  122. comodel_name='res.partner.relation.type.selection',
  123. string='Relation Type',
  124. required=True)
  125. active = fields.Boolean(
  126. string='Active',
  127. readonly=True,
  128. help="Records with date_end in the past are inactive")
  129. any_partner_id = fields.Many2many(
  130. comodel_name='res.partner',
  131. string='Partner',
  132. compute=lambda self: None,
  133. search='_search_any_partner_id')
  134. def _get_active_selects(self):
  135. """Return selects actually to be used.
  136. Selects are registered from all modules PRESENT. But should only be
  137. used to build view if module actually INSTALLED.
  138. """
  139. return ['relation', 'relation_inverse']
  140. def _get_statement(self):
  141. """Allow other modules to add to statement."""
  142. active_selects = self._get_active_selects()
  143. union_select = ' UNION '.join(
  144. [_specification_register[key]['select_sql']
  145. for key in active_selects])
  146. return """\
  147. CREATE OR REPLACE VIEW %%(table)s AS
  148. WITH base_selection AS (%(union_select)s)
  149. SELECT
  150. bas.*,
  151. CASE
  152. WHEN NOT bas.is_inverse OR typ.is_symmetric
  153. THEN bas.type_id * 2
  154. ELSE (bas.type_id * 2) + 1
  155. END as type_selection_id,
  156. (bas.date_end IS NULL OR bas.date_end >= current_date) AS active
  157. %%(additional_view_fields)s
  158. FROM base_selection bas
  159. JOIN res_partner_relation_type typ ON (bas.type_id = typ.id)
  160. %%(additional_tables)s
  161. """ % {'union_select': union_select}
  162. def _get_padding(self):
  163. """Utility function to define padding in one place."""
  164. return 100
  165. def _get_additional_view_fields(self):
  166. """Allow inherit models to add fields to view.
  167. If fields are added, the resulting string must have each field
  168. prepended by a comma, like so:
  169. return ', typ.allow_self, typ.left_partner_category'
  170. """
  171. return ''
  172. def _get_additional_tables(self):
  173. """Allow inherit models to add tables (JOIN's) to view.
  174. Example:
  175. return 'JOIN type_extention ext ON (bas.type_id = ext.id)'
  176. """
  177. return ''
  178. @api.model_cr_context
  179. def _auto_init(self):
  180. cr = self._cr
  181. drop_view_if_exists(cr, self._table)
  182. cr.execute(
  183. self._get_statement(),
  184. {'table': AsIs(self._table),
  185. 'padding': self._get_padding(),
  186. 'additional_view_fields':
  187. AsIs(self._get_additional_view_fields()),
  188. 'additional_tables':
  189. AsIs(self._get_additional_tables())})
  190. return super(ResPartnerRelationAll, self)._auto_init()
  191. @api.model
  192. def _search_any_partner_id(self, operator, value):
  193. """Search relation with partner, no matter on which side."""
  194. # pylint: disable=no-self-use
  195. return [
  196. '|',
  197. ('this_partner_id', operator, value),
  198. ('other_partner_id', operator, value)]
  199. @api.multi
  200. def name_get(self):
  201. return {
  202. this.id: '%s %s %s' % (
  203. this.this_partner_id.name,
  204. this.type_selection_id.display_name,
  205. this.other_partner_id.name,
  206. ) for this in self}
  207. @api.onchange('type_selection_id')
  208. def onchange_type_selection_id(self):
  209. """Add domain on partners according to category and contact_type."""
  210. def check_partner_domain(partner, partner_domain, side):
  211. """Check wether partner_domain results in empty selection
  212. for partner, or wrong selection of partner already selected.
  213. """
  214. warning = {}
  215. if partner:
  216. test_domain = [('id', '=', partner.id)] + partner_domain
  217. else:
  218. test_domain = partner_domain
  219. partner_model = self.env['res.partner']
  220. partners_found = partner_model.search(test_domain, limit=1)
  221. if not partners_found:
  222. warning['title'] = _('Error!')
  223. if partner:
  224. warning['message'] = (
  225. _('%s partner incompatible with relation type.') %
  226. side.title())
  227. else:
  228. warning['message'] = (
  229. _('No %s partner available for relation type.') %
  230. side)
  231. return warning
  232. this_partner_domain = []
  233. other_partner_domain = []
  234. if self.type_selection_id.contact_type_this:
  235. this_partner_domain.append((
  236. 'is_company', '=',
  237. self.type_selection_id.contact_type_this == 'c'))
  238. if self.type_selection_id.partner_category_this:
  239. this_partner_domain.append((
  240. 'category_id', 'in',
  241. self.type_selection_id.partner_category_this.ids))
  242. if self.type_selection_id.contact_type_other:
  243. other_partner_domain.append((
  244. 'is_company', '=',
  245. self.type_selection_id.contact_type_other == 'c'))
  246. if self.type_selection_id.partner_category_other:
  247. other_partner_domain.append((
  248. 'category_id', 'in',
  249. self.type_selection_id.partner_category_other.ids))
  250. result = {'domain': {
  251. 'this_partner_id': this_partner_domain,
  252. 'other_partner_id': other_partner_domain}}
  253. # Check wether domain results in no choice or wrong choice of partners:
  254. warning = {}
  255. partner_model = self.env['res.partner']
  256. if this_partner_domain:
  257. this_partner = False
  258. if bool(self.this_partner_id.id):
  259. this_partner = self.this_partner_id
  260. else:
  261. this_partner_id = \
  262. 'default_this_partner_id' in self.env.context and \
  263. self.env.context['default_this_partner_id'] or \
  264. 'active_id' in self.env.context and \
  265. self.env.context['active_id'] or \
  266. False
  267. if this_partner_id:
  268. this_partner = partner_model.browse(this_partner_id)
  269. warning = check_partner_domain(
  270. this_partner, this_partner_domain, _('this'))
  271. if not warning and other_partner_domain:
  272. warning = check_partner_domain(
  273. self.other_partner_id, other_partner_domain, _('other'))
  274. if warning:
  275. result['warning'] = warning
  276. return result
  277. @api.onchange(
  278. 'this_partner_id',
  279. 'other_partner_id')
  280. def onchange_partner_id(self):
  281. """Set domain on type_selection_id based on partner(s) selected."""
  282. def check_type_selection_domain(type_selection_domain):
  283. """If type_selection_id already selected, check wether it
  284. is compatible with the computed type_selection_domain. An empty
  285. selection can practically only occur in a practically empty
  286. database, and will not lead to problems. Therefore not tested.
  287. """
  288. warning = {}
  289. if not (type_selection_domain and self.type_selection_id):
  290. return warning
  291. test_domain = (
  292. [('id', '=', self.type_selection_id.id)] +
  293. type_selection_domain)
  294. type_model = self.env['res.partner.relation.type.selection']
  295. types_found = type_model.search(test_domain, limit=1)
  296. if not types_found:
  297. warning['title'] = _('Error!')
  298. warning['message'] = _(
  299. 'Relation type incompatible with selected partner(s).')
  300. return warning
  301. type_selection_domain = []
  302. if self.this_partner_id:
  303. type_selection_domain += [
  304. '|',
  305. ('contact_type_this', '=', False),
  306. ('contact_type_this', '=',
  307. self.this_partner_id.get_partner_type()),
  308. '|',
  309. ('partner_category_this', '=', False),
  310. ('partner_category_this', 'in',
  311. self.this_partner_id.category_id.ids)]
  312. if self.other_partner_id:
  313. type_selection_domain += [
  314. '|',
  315. ('contact_type_other', '=', False),
  316. ('contact_type_other', '=',
  317. self.other_partner_id.get_partner_type()),
  318. '|',
  319. ('partner_category_other', '=', False),
  320. ('partner_category_other', 'in',
  321. self.other_partner_id.category_id.ids)]
  322. result = {'domain': {
  323. 'type_selection_id': type_selection_domain}}
  324. # Check wether domain results in no choice or wrong choice for
  325. # type_selection_id:
  326. warning = check_type_selection_domain(type_selection_domain)
  327. if warning:
  328. result['warning'] = warning
  329. return result
  330. @api.model
  331. def _correct_vals(self, vals, type_selection):
  332. """Fill left and right partner from this and other partner."""
  333. vals = vals.copy()
  334. if 'type_selection_id' in vals:
  335. vals['type_id'] = type_selection.type_id.id
  336. if type_selection.is_inverse:
  337. if 'this_partner_id' in vals:
  338. vals['right_partner_id'] = vals['this_partner_id']
  339. if 'other_partner_id' in vals:
  340. vals['left_partner_id'] = vals['other_partner_id']
  341. else:
  342. if 'this_partner_id' in vals:
  343. vals['left_partner_id'] = vals['this_partner_id']
  344. if 'other_partner_id' in vals:
  345. vals['right_partner_id'] = vals['other_partner_id']
  346. # Delete values not in underlying table:
  347. for key in (
  348. 'this_partner_id',
  349. 'type_selection_id',
  350. 'other_partner_id',
  351. 'is_inverse'):
  352. if key in vals:
  353. del vals[key]
  354. return vals
  355. @api.multi
  356. def get_base_resource(self):
  357. """Get base resource from res_model and res_id."""
  358. self.ensure_one()
  359. base_model = self.env[self.res_model]
  360. return base_model.browse([self.res_id])
  361. @api.multi
  362. def write_resource(self, base_resource, vals):
  363. """write handled by base resource."""
  364. self.ensure_one()
  365. # write for models other then res.partner.relation SHOULD
  366. # be handled in inherited models:
  367. relation_model = self.env['res.partner.relation']
  368. assert self.res_model == relation_model._name
  369. base_resource.write(vals)
  370. @api.model
  371. def _get_type_selection_from_vals(self, vals):
  372. """Get type_selection_id straight from vals or compute from type_id.
  373. """
  374. type_selection_id = vals.get('type_selection_id', False)
  375. if not type_selection_id:
  376. type_id = vals.get('type_id', False)
  377. if type_id:
  378. is_inverse = vals.get('is_inverse')
  379. type_selection_id = type_id * 2 + (is_inverse and 1 or 0)
  380. return type_selection_id and self.type_selection_id.browse(
  381. type_selection_id) or False
  382. @api.multi
  383. def write(self, vals):
  384. """For model 'res.partner.relation' call write on underlying model.
  385. """
  386. new_type_selection = self._get_type_selection_from_vals(vals)
  387. for rec in self:
  388. type_selection = new_type_selection or rec.type_selection_id
  389. vals = rec._correct_vals(vals, type_selection)
  390. base_resource = rec.get_base_resource()
  391. rec.write_resource(base_resource, vals)
  392. # Invalidate cache to make res.partner.relation.all reflect changes
  393. # in underlying res.partner.relation:
  394. self.env.invalidate_all()
  395. return True
  396. @api.model
  397. def _compute_base_name(self, type_selection):
  398. """This will be overridden for each inherit model."""
  399. return 'relation'
  400. @api.model
  401. def _compute_id(self, base_resource, type_selection):
  402. """Compute id. Allow for enhancements in inherit model."""
  403. base_name = self._compute_base_name(type_selection)
  404. key_offset = get_select_specification(
  405. base_name, type_selection.is_inverse)['key_offset']
  406. return base_resource.id * self._get_padding() + key_offset
  407. @api.model
  408. def create_resource(self, vals, type_selection):
  409. relation_model = self.env['res.partner.relation']
  410. return relation_model.create(vals)
  411. @api.model
  412. def create(self, vals):
  413. """Divert non-problematic creates to underlying table.
  414. Create a res.partner.relation but return the converted id.
  415. """
  416. type_selection = self._get_type_selection_from_vals(vals)
  417. if not type_selection: # Should not happen
  418. raise ValidationError(
  419. _('No relation type specified in vals: %s.') % vals)
  420. vals = self._correct_vals(vals, type_selection)
  421. base_resource = self.create_resource(vals, type_selection)
  422. res_id = self._compute_id(base_resource, type_selection)
  423. return self.browse(res_id)
  424. @api.multi
  425. def unlink_resource(self, base_resource):
  426. """Delegate unlink to underlying model."""
  427. self.ensure_one()
  428. # unlink for models other then res.partner.relation SHOULD
  429. # be handled in inherited models:
  430. relation_model = self.env['res.partner.relation']
  431. assert self.res_model == relation_model._name
  432. base_resource.unlink()
  433. @api.multi
  434. def unlink(self):
  435. """For model 'res.partner.relation' call unlink on underlying model.
  436. """
  437. for rec in self:
  438. base_resource = rec.get_base_resource()
  439. rec.unlink_resource(base_resource)
  440. return True