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.

471 lines
17 KiB

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