您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

350 行
10 KiB

  1. # Copyright (c) 2012 Web Notes Technologies Pvt Ltd (http://erpnext.com)
  2. #
  3. # MIT License (MIT)
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a
  6. # copy of this software and associated documentation files (the "Software"),
  7. # to deal in the Software without restriction, including without limitation
  8. # the rights to use, copy, modify, merge, publish, distribute, sublicense,
  9. # and/or sell copies of the Software, and to permit persons to whom the
  10. # Software is furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  16. # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  17. # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  18. # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  19. # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
  20. # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  21. #
  22. # Tree (Hierarchical) Nested Set Model (nsm)
  23. #
  24. # To use the nested set model,
  25. # use the following pattern
  26. # 1. name your parent field as "parent_item_group" if not have a property nsm_parent_field as your field name in the document class
  27. # 2. have a field called "old_parent" in your fields list - this identifies whether the parent has been changed
  28. # 3. call update_nsm(doc_obj) in the on_upate method
  29. # ------------------------------------------
  30. from __future__ import unicode_literals
  31. # uncomment below lines for testing
  32. # import sys, unittest
  33. # sys.path.extend([".", "erpnext", "lib/py"])
  34. import webnotes, unittest
  35. from webnotes import msgprint
  36. from webnotes.model.doclist import DocList
  37. from webnotes.model.doc import Document
  38. class TestNSM(unittest.TestCase):
  39. def setUp(self):
  40. webnotes.conn.sql("delete from `tabItem Group`")
  41. self.data = [
  42. ["t1", None, 1, 20],
  43. ["c0", "t1", 2, 3],
  44. ["c1", "t1", 4, 11],
  45. ["gc1", "c1", 5, 6],
  46. ["gc2", "c1", 7, 8],
  47. ["gc3", "c1", 9, 10],
  48. ["c2", "t1", 12, 17],
  49. ["gc4", "c2", 13, 14],
  50. ["gc5", "c2", 15, 16],
  51. ["c3", "t1", 18, 19]
  52. ]
  53. for d in self.data:
  54. self.__dict__[d[0]] = DocList([Document(fielddata = {
  55. "doctype": "Item Group", "item_group_name": d[0], "parent_item_group": d[1],
  56. "__islocal": 1
  57. })])
  58. self.save_all()
  59. self.reload_all()
  60. def save_all(self):
  61. for d in self.data:
  62. self.__dict__[d[0]].save()
  63. def reload_all(self, data=None):
  64. for d in data or self.data:
  65. self.__dict__[d[0]].load_from_db()
  66. def test_basic_tree(self, data=None):
  67. for d in data or self.data:
  68. self.assertEquals(self.__dict__[d[0]].doc.lft, d[2])
  69. self.assertEquals(self.__dict__[d[0]].doc.rgt, d[3])
  70. def test_move_group(self):
  71. self.c1.doc.parent_item_group = 'c2'
  72. self.c1.save()
  73. self.reload_all()
  74. new_tree = [
  75. ["t1", None, 1, 20],
  76. ["c0", "t1", 2, 3],
  77. ["c2", "t1", 4, 17],
  78. ["gc4", "c2", 5, 6],
  79. ["gc5", "c2", 7, 8],
  80. ["c1", "t1", 9, 16],
  81. ["gc1", "c1", 10, 11],
  82. ["gc2", "c1", 12, 13],
  83. ["gc3", "c1", 14, 15],
  84. ["c3", "t1", 18, 19]
  85. ]
  86. self.test_basic_tree(new_tree)
  87. # Move back
  88. self.c1.doc.parent_item_group = 'gc4'
  89. self.c1.save()
  90. self.reload_all()
  91. new_tree = [
  92. ["t1", None, 1, 20],
  93. ["c0", "t1", 2, 3],
  94. ["c2", "t1", 4, 17],
  95. ["gc4", "c2", 5, 14],
  96. ["c1", "t1", 6, 13],
  97. ["gc1", "c1", 7, 8],
  98. ["gc2", "c1", 9, 10],
  99. ["gc3", "c1", 11, 12],
  100. ["gc5", "c2", 15, 16],
  101. ["c3", "t1", 18, 19]
  102. ]
  103. self.test_basic_tree(new_tree)
  104. # Move to root
  105. self.c1.doc.parent_item_group = ''
  106. self.c1.save()
  107. self.reload_all()
  108. new_tree = [
  109. ["t1", None, 1, 12],
  110. ["c0", "t1", 2, 3],
  111. ["c2", "t1", 4, 9],
  112. ["gc4", "c2", 5, 6],
  113. ["gc5", "c2", 7, 8],
  114. ["c3", "t1", 10, 11],
  115. ["c1", "t1", 13, 20],
  116. ["gc1", "c1", 14, 15],
  117. ["gc2", "c1", 16, 17],
  118. ["gc3", "c1", 18, 19],
  119. ]
  120. self.test_basic_tree(new_tree)
  121. # move leaf
  122. self.gc3.doc.parent_item_group = 'c2'
  123. self.gc3.save()
  124. self.reload_all()
  125. new_tree = [
  126. ["t1", None, 1, 14],
  127. ["c0", "t1", 2, 3],
  128. ["c2", "t1", 4, 11],
  129. ["gc4", "c2", 5, 6],
  130. ["gc5", "c2", 7, 8],
  131. ["gc3", "c1", 9, 10],
  132. ["c3", "t1", 12, 13],
  133. ["c1", "t1", 15, 20],
  134. ["gc1", "c1", 16, 17],
  135. ["gc2", "c1", 18, 19],
  136. ]
  137. self.test_basic_tree(new_tree)
  138. # delete leaf
  139. from webnotes.model import delete_doc
  140. delete_doc(self.gc2.doc.doctype, self.gc2.doc.name)
  141. new_tree = [
  142. ["t1", None, 1, 14],
  143. ["c0", "t1", 2, 3],
  144. ["c2", "t1", 4, 11],
  145. ["gc4", "c2", 5, 6],
  146. ["gc5", "c2", 7, 8],
  147. ["gc3", "c1", 9, 10],
  148. ["c3", "t1", 12, 13],
  149. ["c1", "t1", 15, 18],
  150. ["gc1", "c1", 16, 17],
  151. ]
  152. del self.__dict__["gc2"]
  153. self.reload_all(new_tree)
  154. self.test_basic_tree(new_tree)
  155. # for testing
  156. # for d in new_tree:
  157. # doc = self.__dict__[d[0]].doc
  158. # print doc.name, doc.lft, doc.rgt
  159. def tearDown(self):
  160. webnotes.conn.rollback()
  161. # called in the on_update method
  162. def update_nsm(doc_obj):
  163. # get fields, data from the DocType
  164. pf, opf = 'parent_node', 'old_parent'
  165. if str(doc_obj.__class__)=='webnotes.model.doc.Document':
  166. # passed as a Document object
  167. d = doc_obj
  168. else:
  169. # passed as a DocType object
  170. d = doc_obj.doc
  171. if hasattr(doc_obj,'nsm_parent_field'):
  172. pf = doc_obj.nsm_parent_field
  173. if hasattr(doc_obj,'nsm_oldparent_field'):
  174. opf = doc_obj.nsm_oldparent_field
  175. p, op = d.fields.get(pf, ''), d.fields.get(opf, '')
  176. # has parent changed (?) or parent is None (root)
  177. if not d.lft and not d.rgt:
  178. update_add_node(d.doctype, d.name, p or '', pf)
  179. elif op != p:
  180. update_move_node(d, pf)
  181. # set old parent
  182. webnotes.conn.set(d, opf, p or '')
  183. # reload
  184. d._loadfromdb()
  185. def update_move_node(doc, parent_field):
  186. # move to dark side
  187. parent = doc.fields.get(parent_field)
  188. webnotes.conn.sql("""update `tab%s` set lft = -lft, rgt = -rgt
  189. where lft >= %s and rgt <= %s"""% (doc.doctype, '%s', '%s'), (doc.lft, doc.rgt))
  190. # shift left
  191. diff = doc.rgt - doc.lft + 1
  192. webnotes.conn.sql("""update `tab%s` set lft = lft -%s, rgt = rgt - %s
  193. where lft > %s"""% (doc.doctype, '%s', '%s', '%s'), (diff, diff, doc.rgt))
  194. # shift left rgts of ancestors whose only rgts must shift
  195. webnotes.conn.sql("""update `tab%s` set rgt = rgt - %s
  196. where lft < %s and rgt > %s"""% (doc.doctype, '%s', '%s', '%s'),
  197. (diff, doc.lft, doc.rgt))
  198. if parent:
  199. new_parent = webnotes.conn.sql("""select lft, rgt from `tab%s`
  200. where name = %s""" % (doc.doctype, '%s'), parent, as_dict=1)[0]
  201. # set parent lft, rgt
  202. webnotes.conn.sql("""update `tab%s` set rgt = rgt + %s
  203. where name = %s"""% (doc.doctype, '%s', '%s'), (diff, parent))
  204. # shift right at new parent
  205. webnotes.conn.sql("""update `tab%s` set lft = lft + %s, rgt = rgt + %s
  206. where lft > %s""" % (doc.doctype, '%s', '%s', '%s'),
  207. (diff, diff, new_parent["rgt"]))
  208. # shift right rgts of ancestors whose only rgts must shift
  209. webnotes.conn.sql("""update `tab%s` set rgt = rgt + %s
  210. where lft < %s and rgt > %s""" % (doc.doctype, '%s', '%s', '%s'),
  211. (diff, new_parent["lft"], new_parent["rgt"]))
  212. new_diff = new_parent["rgt"] - doc.lft
  213. else:
  214. # new root
  215. max_rgt = webnotes.conn.sql("""select max(rgt) from `tab%s`""" % doc.doctype)[0][0]
  216. new_diff = max_rgt + 1 - doc.lft
  217. # bring back from dark side
  218. webnotes.conn.sql("""update `tab%s` set lft = -lft + %s, rgt = -rgt + %s
  219. where lft < 0"""% (doc.doctype, '%s', '%s'), (new_diff, new_diff))
  220. def rebuild_tree(doctype, parent_field):
  221. """
  222. call rebuild_node for all root nodes
  223. """
  224. # get all roots
  225. right = 1
  226. result = webnotes.conn.sql("SELECT name FROM `tab%s` WHERE `%s`='' or `%s` IS NULL ORDER BY name ASC" % (doctype, parent_field, parent_field))
  227. for r in result:
  228. right = rebuild_node(doctype, r[0], right, parent_field)
  229. webnotes.conn.sql("commit")
  230. webnotes.conn.sql("start transaction")
  231. def rebuild_node(doctype, parent, left, parent_field, cnt = 0):
  232. """
  233. reset lft, rgt and recursive call for all children
  234. """
  235. from webnotes.utils import now
  236. n = now()
  237. # the right value of this node is the left value + 1
  238. right = left+1
  239. # get all children of this node
  240. result = webnotes.conn.sql("SELECT name FROM `tab%s` WHERE `%s`='%s'" % (doctype, parent_field, parent))
  241. for r in result:
  242. right = rebuild_node(doctype, r[0], right, parent_field, cnt)
  243. # we've got the left value, and now that we've processed
  244. # the children of this node we also know the right value
  245. webnotes.conn.sql("UPDATE `tab%s` SET lft=%s, rgt=%s, modified='%s' WHERE name='%s'" % (doctype,left,right,n,parent))
  246. # commit after every 100
  247. cnt += 1
  248. if cnt % 100 == 0:
  249. cnt = 0
  250. webnotes.conn.sql("commit")
  251. webnotes.conn.sql("start transaction")
  252. #return the right value of this node + 1
  253. return right+1
  254. def update_add_node(doctype, name, parent, parent_field):
  255. """
  256. insert a new node
  257. """
  258. from webnotes.utils import now
  259. n = now()
  260. # get the last sibling of the parent
  261. if parent:
  262. right = webnotes.conn.sql("select rgt from `tab%s` where name='%s'" % (doctype, parent))[0][0]
  263. else: # root
  264. right = webnotes.conn.sql("select ifnull(max(rgt),0)+1 from `tab%s` where ifnull(`%s`,'') =''" % (doctype, parent_field))[0][0]
  265. right = right or 1
  266. # update all on the right
  267. webnotes.conn.sql("update `tab%s` set rgt = rgt+2, modified='%s' where rgt >= %s" %(doctype,n,right))
  268. webnotes.conn.sql("update `tab%s` set lft = lft+2, modified='%s' where lft >= %s" %(doctype,n,right))
  269. # update index of new node
  270. if webnotes.conn.sql("select * from `tab%s` where lft=%s or rgt=%s"% (doctype, right, right+1)):
  271. webnotes.msgprint("Nested set error. Please send mail to support")
  272. raise Exception
  273. webnotes.conn.sql("update `tab%s` set lft=%s, rgt=%s, modified='%s' where name='%s'" % (doctype,right,right+1,n,name))
  274. return right
  275. class DocTypeNestedSet:
  276. def on_update(self):
  277. update_nsm(self)
  278. def on_trash(self):
  279. self.doc.fields[self.nsm_parent_field] = ""
  280. update_nsm(self)
  281. def validate_root_details(self, root, parent_field):
  282. #does not exists parent
  283. if self.doc.name==root and self.doc.fields.get(parent_field):
  284. msgprint("You can not assign parent for root: %s" % (root, ), raise_exception=1)
  285. elif self.doc.name!=root and not self.doc.parent_account:
  286. msgprint("Parent is mandatory for %s" % (self.doc.name, ), raise_exception=1)
  287. # if __name__=="__main__":
  288. # webnotes.connect()
  289. # unittest.main()