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.

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