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.

485 lines
18 KiB

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