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.

470 lines
17 KiB

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