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 line
14 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. from __future__ import unicode_literals
  23. """
  24. Transactions are defined as collection of classes, a Bean represents collection of Document
  25. objects for a transaction with main and children.
  26. Group actions like save, etc are performed on doclists
  27. """
  28. import webnotes
  29. from webnotes import _, msgprint
  30. from webnotes.utils import cint, cstr, flt
  31. from webnotes.model.doc import Document
  32. class DocstatusTransitionError(webnotes.ValidationError): pass
  33. class BeanPermissionError(webnotes.ValidationError): pass
  34. class TimestampMismatchError(webnotes.ValidationError): pass
  35. class Bean:
  36. """
  37. Collection of Documents with one parent and multiple children
  38. """
  39. def __init__(self, dt=None, dn=None):
  40. self.obj = None
  41. self.ignore_permissions = False
  42. self.ignore_children_type = []
  43. self.ignore_check_links = False
  44. self.ignore_validate = False
  45. self.ignore_fields = False
  46. self.ignore_mandatory = False
  47. if isinstance(dt, basestring) and not dn:
  48. dn = dt
  49. if dt and dn:
  50. self.load_from_db(dt, dn)
  51. elif isinstance(dt, list):
  52. self.set_doclist(dt)
  53. elif isinstance(dt, dict):
  54. self.set_doclist([dt])
  55. def load_from_db(self, dt=None, dn=None, prefix='tab'):
  56. """
  57. Load doclist from dt
  58. """
  59. from webnotes.model.doc import getchildren
  60. if not dt: dt = self.doc.doctype
  61. if not dn: dn = self.doc.name
  62. doc = Document(dt, dn, prefix=prefix)
  63. # get all children types
  64. tablefields = webnotes.model.meta.get_table_fields(dt)
  65. # load chilren
  66. doclist = webnotes.doclist([doc,])
  67. for t in tablefields:
  68. doclist += getchildren(doc.name, t[0], t[1], dt, prefix=prefix)
  69. self.set_doclist(doclist)
  70. if dt == dn:
  71. self.convert_type(self.doc)
  72. def __iter__(self):
  73. return self.doclist.__iter__()
  74. @property
  75. def meta(self):
  76. if not hasattr(self, "_meta"):
  77. self._meta = webnotes.get_doctype(self.doc.doctype)
  78. return self._meta
  79. def from_compressed(self, data, docname):
  80. from webnotes.model.utils import expand
  81. self.set_doclist(expand(data))
  82. def set_doclist(self, doclist):
  83. for i, d in enumerate(doclist):
  84. if isinstance(d, dict):
  85. doclist[i] = Document(fielddata=d)
  86. self.doclist = webnotes.doclist(doclist)
  87. self.doc = self.doclist[0]
  88. if self.obj:
  89. self.obj.doclist = self.doclist
  90. self.obj.doc = self.doc
  91. def make_controller(self):
  92. if self.obj:
  93. # update doclist before running any method
  94. self.obj.doclist = self.doclist
  95. return self.obj
  96. self.obj = webnotes.get_obj(doc=self.doc, doclist=self.doclist)
  97. self.obj.bean = self
  98. self.controller = self.obj
  99. return self.obj
  100. def to_dict(self):
  101. return [d.fields for d in self.doclist]
  102. def check_if_latest(self, method="save"):
  103. from webnotes.model.meta import is_single
  104. conflict = False
  105. if not cint(self.doc.fields.get('__islocal')):
  106. if is_single(self.doc.doctype):
  107. modified = webnotes.conn.get_value(self.doc.doctype, self.doc.name, "modified")
  108. if isinstance(modified, list):
  109. modified = modified[0]
  110. if cstr(modified) and cstr(modified) != cstr(self.doc.modified):
  111. conflict = True
  112. else:
  113. tmp = webnotes.conn.sql("""select modified, docstatus from `tab%s`
  114. where name="%s" for update"""
  115. % (self.doc.doctype, self.doc.name), as_dict=True)
  116. if not tmp:
  117. webnotes.msgprint("""This record does not exist. Please refresh.""", raise_exception=1)
  118. modified = cstr(tmp[0].modified)
  119. if modified and modified != cstr(self.doc.modified):
  120. conflict = True
  121. self.check_docstatus_transition(tmp[0].docstatus, method)
  122. if conflict:
  123. webnotes.msgprint(_("Error: Document has been modified after you have opened it") \
  124. + (" (%s, %s). " % (modified, self.doc.modified)) \
  125. + _("Please refresh to get the latest document."), raise_exception=TimestampMismatchError)
  126. def check_docstatus_transition(self, db_docstatus, method):
  127. valid = {
  128. "save": [0,0],
  129. "submit": [0,1],
  130. "cancel": [1,2],
  131. "update_after_submit": [1,1]
  132. }
  133. labels = {
  134. 0: _("Draft"),
  135. 1: _("Submitted"),
  136. 2: _("Cancelled")
  137. }
  138. if not hasattr(self, "to_docstatus"):
  139. self.to_docstatus = 0
  140. if method != "runserverobj" and [db_docstatus, self.to_docstatus] != valid[method]:
  141. webnotes.msgprint(_("Cannot change from") + ": " + labels[db_docstatus] + " > " + \
  142. labels[self.to_docstatus], raise_exception=DocstatusTransitionError)
  143. def check_links(self):
  144. if self.ignore_check_links:
  145. return
  146. ref, err_list = {}, []
  147. for d in self.doclist:
  148. if not ref.get(d.doctype):
  149. ref[d.doctype] = d.make_link_list()
  150. err_list += d.validate_links(ref[d.doctype])
  151. if err_list:
  152. webnotes.msgprint("""[Link Validation] Could not find the following values: %s.
  153. Please correct and resave. Document Not Saved.""" % ', '.join(err_list), raise_exception=1)
  154. def update_timestamps_and_docstatus(self):
  155. from webnotes.utils import now
  156. ts = now()
  157. user = webnotes.__dict__.get('session', {}).get('user') or 'Administrator'
  158. for d in self.doclist:
  159. if self.doc.fields.get('__islocal'):
  160. if not d.owner:
  161. d.owner = user
  162. if not d.creation:
  163. d.creation = ts
  164. d.modified_by = user
  165. d.modified = ts
  166. if d.docstatus != 2 and self.to_docstatus >= int(d.docstatus): # don't update deleted
  167. d.docstatus = self.to_docstatus
  168. def prepare_for_save(self, method):
  169. self.check_if_latest(method)
  170. if method != "cancel":
  171. self.check_links()
  172. self.update_timestamps_and_docstatus()
  173. self.update_parent_info()
  174. def update_parent_info(self):
  175. idx_map = {}
  176. is_local = cint(self.doc.fields.get("__islocal"))
  177. if not webnotes.in_import:
  178. parentfields = [d.fieldname for d in self.meta.get({"doctype": "DocField", "fieldtype": "Table"})]
  179. for i, d in enumerate(self.doclist[1:]):
  180. if d.parentfield:
  181. if not webnotes.in_import:
  182. if not d.parentfield in parentfields:
  183. webnotes.msgprint("Bad parentfield %s" % d.parentfield,
  184. raise_exception=True)
  185. d.parenttype = self.doc.doctype
  186. d.parent = self.doc.name
  187. if not d.idx:
  188. d.idx = idx_map.setdefault(d.parentfield, 0) + 1
  189. if is_local:
  190. # if parent is new, all children should be new
  191. d.fields["__islocal"] = 1
  192. idx_map[d.parentfield] = d.idx
  193. def run_method(self, method):
  194. self.make_controller()
  195. if hasattr(self.controller, method):
  196. getattr(self.controller, method)()
  197. if hasattr(self.controller, 'custom_' + method):
  198. getattr(self.controller, 'custom_' + method)()
  199. notify(self.controller, method)
  200. self.set_doclist(self.controller.doclist)
  201. def get_method(self, method):
  202. self.make_controller()
  203. return getattr(self.controller, method, None)
  204. def save_main(self):
  205. try:
  206. self.doc.save(check_links = False, ignore_fields = self.ignore_fields)
  207. except NameError, e:
  208. webnotes.msgprint('%s "%s" already exists' % (self.doc.doctype, self.doc.name))
  209. # prompt if cancelled
  210. if webnotes.conn.get_value(self.doc.doctype, self.doc.name, 'docstatus')==2:
  211. webnotes.msgprint('[%s "%s" has been cancelled]' % (self.doc.doctype, self.doc.name))
  212. webnotes.errprint(webnotes.utils.getTraceback())
  213. raise e
  214. def save_children(self):
  215. child_map = {}
  216. for d in self.doclist[1:]:
  217. if d.fields.get("parent") or d.fields.get("parentfield"):
  218. d.parent = self.doc.name # rename if reqd
  219. d.parenttype = self.doc.doctype
  220. d.save(check_links=False, ignore_fields = self.ignore_fields)
  221. child_map.setdefault(d.doctype, []).append(d.name)
  222. # delete all children in database that are not in the child_map
  223. # get all children types
  224. tablefields = webnotes.model.meta.get_table_fields(self.doc.doctype)
  225. for dt in tablefields:
  226. if dt[0] not in self.ignore_children_type:
  227. cnames = child_map.get(dt[0]) or []
  228. if cnames:
  229. webnotes.conn.sql("""delete from `tab%s` where parent=%s and parenttype=%s and
  230. name not in (%s)""" % (dt[0], '%s', '%s', ','.join(['%s'] * len(cnames))),
  231. tuple([self.doc.name, self.doc.doctype] + cnames))
  232. else:
  233. webnotes.conn.sql("""delete from `tab%s` where parent=%s and parenttype=%s""" \
  234. % (dt[0], '%s', '%s'), (self.doc.name, self.doc.doctype))
  235. def insert(self):
  236. self.doc.fields["__islocal"] = 1
  237. if webnotes.in_test:
  238. if self.meta.get_field("naming_series"):
  239. self.doc.naming_series = "_T-" + self.doc.doctype + "-"
  240. return self.save()
  241. def has_read_perm(self):
  242. return webnotes.has_permission(self.doc.doctype, "read", self.doc)
  243. def save(self, check_links=1):
  244. if self.ignore_permissions or webnotes.has_permission(self.doc.doctype, "write", self.doc):
  245. self.to_docstatus = 0
  246. self.prepare_for_save("save")
  247. if not self.ignore_validate:
  248. self.run_method('validate')
  249. if not self.ignore_mandatory:
  250. self.check_mandatory()
  251. self.save_main()
  252. self.save_children()
  253. self.run_method('on_update')
  254. else:
  255. self.no_permission_to(_("Write"))
  256. return self
  257. def submit(self):
  258. if self.ignore_permissions or webnotes.has_permission(self.doc.doctype, "submit", self.doc):
  259. self.to_docstatus = 1
  260. self.prepare_for_save("submit")
  261. self.run_method('validate')
  262. self.check_mandatory()
  263. self.save_main()
  264. self.save_children()
  265. self.run_method('on_update')
  266. self.run_method('on_submit')
  267. else:
  268. self.no_permission_to(_("Submit"))
  269. return self
  270. def cancel(self):
  271. if self.ignore_permissions or webnotes.has_permission(self.doc.doctype, "cancel", self.doc):
  272. self.to_docstatus = 2
  273. self.prepare_for_save("cancel")
  274. self.run_method('before_cancel')
  275. self.save_main()
  276. self.save_children()
  277. self.run_method('on_cancel')
  278. self.check_no_back_links_exist()
  279. else:
  280. self.no_permission_to(_("Cancel"))
  281. return self
  282. def update_after_submit(self):
  283. if self.doc.docstatus != 1:
  284. webnotes.msgprint("Only to called after submit", raise_exception=1)
  285. if self.ignore_permissions or webnotes.has_permission(self.doc.doctype, "write", self.doc):
  286. self.to_docstatus = 1
  287. self.prepare_for_save("update_after_submit")
  288. self.run_method('before_update_after_submit')
  289. self.save_main()
  290. self.save_children()
  291. self.run_method('on_update_after_submit')
  292. else:
  293. self.no_permission_to(_("Update"))
  294. return self
  295. def delete(self):
  296. webnotes.delete_doc(self.doc.doctype, self.doc.name)
  297. def no_permission_to(self, ptype):
  298. webnotes.msgprint(("%s (%s): " % (self.doc.name, _(self.doc.doctype))) + \
  299. _("No Permission to ") + ptype, raise_exception=BeanPermissionError)
  300. def check_no_back_links_exist(self):
  301. from webnotes.model.utils import check_if_doc_is_linked
  302. check_if_doc_is_linked(self.doc.doctype, self.doc.name, method="Cancel")
  303. def check_mandatory(self):
  304. missing = []
  305. for doc in self.doclist:
  306. for df in self.meta:
  307. if df.doctype=="DocField" and df.reqd and df.parent==doc.doctype and df.fieldname!="naming_series":
  308. msg = ""
  309. if df.fieldtype == "Table":
  310. if not self.doclist.get({"parentfield": df.fieldname}):
  311. msg = _("Error") + ": " + _("Data missing in table") + ": " + _(df.label)
  312. elif doc.fields.get(df.fieldname) is None:
  313. msg = _("Error") + ": "
  314. if doc.parentfield:
  315. msg += _("Row") + (" # %d: " % doc.idx)
  316. msg += _("Value missing for") + ": " + _(df.label)
  317. if msg:
  318. missing.append([msg, df.fieldname])
  319. if missing:
  320. for msg, fieldname in missing:
  321. msgprint(msg)
  322. raise webnotes.MandatoryError, ", ".join([fieldname for msg, fieldname in missing])
  323. def convert_type(self, doc):
  324. if doc.doctype==doc.name and doc.doctype!="DocType":
  325. for df in self.meta.get({"doctype": "DocField", "parent": doc.doctype}):
  326. if df.fieldtype in ("Int", "Check"):
  327. doc.fields[df.fieldname] = cint(doc.fields.get(df.fieldname))
  328. elif df.fieldtype in ("Float", "Currency"):
  329. doc.fields[df.fieldname] = flt(doc.fields.get(df.fieldname))
  330. doc.docstatus = cint(doc.docstatus)
  331. def clone(source_wrapper):
  332. """ make a clone of a document"""
  333. if isinstance(source_wrapper, list):
  334. source_wrapper = Bean(source_wrapper)
  335. new_wrapper = Bean(source_wrapper.doclist.copy())
  336. if new_wrapper.doc.fields.get("amended_from"):
  337. new_wrapper.doc.fields["amended_from"] = None
  338. if new_wrapper.doc.fields.get("amendment_date"):
  339. new_wrapper.doc.fields["amendment_date"] = None
  340. for d in new_wrapper.doclist:
  341. d.fields.update({
  342. "name": None,
  343. "__islocal": 1,
  344. "docstatus": 0,
  345. })
  346. return new_wrapper
  347. def notify(controller, caller_method):
  348. try:
  349. from startup.observers import observer_map
  350. except ImportError:
  351. return
  352. doctype = controller.doc.doctype
  353. def call_observers(key):
  354. if key in observer_map:
  355. observer_list = observer_map[key]
  356. if isinstance(observer_list, basestring):
  357. observer_list = [observer_list]
  358. for observer_method in observer_list:
  359. webnotes.get_method(observer_method)(controller, caller_method)
  360. call_observers("*:*")
  361. call_observers(doctype + ":*")
  362. call_observers("*:" + caller_method)
  363. call_observers(doctype + ":" + caller_method)
  364. # for bc
  365. def getlist(doclist, parentfield):
  366. """
  367. Return child records of a particular type
  368. """
  369. import webnotes.model.utils
  370. return webnotes.model.utils.getlist(doclist, parentfield)
  371. def copy_doclist(doclist, no_copy = []):
  372. """
  373. Make a copy of the doclist
  374. """
  375. import webnotes.model.utils
  376. return webnotes.model.utils.copy_doclist(doclist, no_copy)