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.

controller.py 13 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  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 ModelWrapper 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. import json, os
  30. import webnotes.model
  31. import webnotes.model.doc
  32. import webnotes.model.wrapper
  33. import webnotes.utils.cache
  34. from webnotes import _
  35. from webnotes.utils import cint, cstr, now_datetime, get_datetime, get_datetime_str, comma_and, cast
  36. controllers = webnotes.DictObj()
  37. def get(doctype, name=None, module=None):
  38. """return controller object"""
  39. global controllers
  40. doclist = doctype
  41. if isinstance(doctype, list):
  42. doctype = doclist[0]["doctype"]
  43. # return if already loaded
  44. if doctype not in controllers:
  45. from webnotes.modules import get_doc_path, scrub
  46. module_name = module or webnotes.conn.get_value("DocType", doctype, "module") or "Core"
  47. doc_path = get_doc_path(module_name, 'doctype', doctype)
  48. module_path = os.path.join(doc_path, scrub(doctype)+'.py')
  49. # load translations
  50. if webnotes.can_translate():
  51. from webnotes.utils.translate import get_lang_data
  52. webnotes._messages.update(get_lang_data(doc_path, None, 'py'))
  53. # vanilla controller
  54. controllers[doctype] = get_controller_class(module_name, doctype, module_path)
  55. return controllers[doctype](doclist, name)
  56. def get_controller_class(module_name, doctype, module_path):
  57. if os.path.exists(module_path):
  58. from webnotes.modules import get_doc_path, scrub
  59. module = __import__(scrub(module_name) + '.doctype.' + scrub(doctype) + '.' \
  60. + scrub(doctype), fromlist = [scrub(doctype)])
  61. # find controller in module
  62. import inspect
  63. for attr in dir(module):
  64. attrobj = getattr(module, attr)
  65. if inspect.isclass(attrobj) and attr.startswith(doctype.replace(' ', '').replace('-', '')) \
  66. and issubclass(attrobj, ModelWrapperController):
  67. return attrobj
  68. return ModelWrapperController
  69. class ModelWrapperController(object):
  70. """
  71. Collection of Documents with one parent and multiple children
  72. """
  73. def __init__(self, doctype=None, name=None):
  74. if doctype:
  75. self.load(doctype, name)
  76. if hasattr(self, "setup"):
  77. self.setup()
  78. def load(self, doctype, name=None):
  79. if isinstance(doctype, list):
  80. self.set_doclist(doctype)
  81. return
  82. if not name: name = doctype
  83. self.set_doclist(self.load_doclist(doctype, name))
  84. def load_doclist(self, doctype, name):
  85. return webnotes.model.wrapper.load(doctype, name)
  86. def set_doclist(self, doclist):
  87. if not isinstance(doclist, webnotes.model.wrapper.ModelWrapper):
  88. self.doclist = webnotes.model.wrapper.objectify(doclist)
  89. else:
  90. self.doclist = doclist
  91. self.doc = self.doclist[0]
  92. def save(self):
  93. """Save the doclist"""
  94. # if docstatus is null, set it as 0
  95. self.doc.docstatus = self.doc.docstatus or 0
  96. self.prepare_for_save()
  97. self.run('validate')
  98. self.doctype_validate()
  99. from webnotes.model.doctype import get_property
  100. if get_property(self.doc.doctype, "document_type") in ["Master", "Transaction"]\
  101. and not self.doc.get("__islocal"):
  102. from webnotes.model.wrapper import load
  103. # get the old doclist
  104. try:
  105. oldlist = load(self.doc.doctype, self.doc.name)
  106. except NameError, e:
  107. oldlist = None
  108. else:
  109. oldlist = None
  110. self.save_main()
  111. self.save_children()
  112. self.run('on_update')
  113. # version is saved after save, because we need names
  114. if oldlist:
  115. self.save_version(oldlist)
  116. def prepare_for_save(self):
  117. """Set owner, modified etc before saving"""
  118. self.check_if_latest()
  119. self.check_permission()
  120. self.check_links()
  121. self.check_mandatory()
  122. self.update_timestamps()
  123. def save_main(self):
  124. """Save the main doc"""
  125. self.doc.save(cint(self.doc.get('__islocal')))
  126. def save_children(self):
  127. """Save Children, with the new parent name"""
  128. child_map = {}
  129. for d in self.doclist[1:]:
  130. if d.has_key('parentfield'):
  131. d.parent = self.doc.name # rename if reqd
  132. d.parenttype = self.doc.doctype
  133. # set docstatus of children as that of parent
  134. d.docstatus = self.doc.docstatus
  135. d.modified = self.doc.modified
  136. d.modified_by = self.doc.modified_by
  137. d.save(new = cint(d.get('__islocal')))
  138. child_map.setdefault(d.doctype, []).append(d.name)
  139. # delete all children in database that are not in the child_map
  140. self.remove_children(child_map)
  141. def save_version(self, oldlist):
  142. """create a new version of given difflist"""
  143. from webnotes.model.versions import save_version
  144. save_version(oldlist, self.doclist)
  145. def remove_children(self, child_map):
  146. """delete children from database if they do not exist in the doclist"""
  147. # get all children types
  148. tablefields = webnotes.conn.get_table_fields(self.doc.doctype)
  149. for dt in tablefields:
  150. cnames = child_map.get(dt['options']) or []
  151. if cnames:
  152. webnotes.conn.sql("""delete from `tab%s` where parent=%s
  153. and parenttype=%s and name not in (%s)""" % \
  154. (dt['options'], '%s', '%s', ','.join(['%s'] * len(cnames))),
  155. tuple([self.doc.name, self.doc.doctype] + cnames))
  156. else:
  157. webnotes.conn.sql("""delete from `tab%s` where parent=%s
  158. and parenttype=%s""" % (dt['options'], '%s', '%s'),
  159. (self.doc.name, self.doc.doctype))
  160. def check_if_latest(self):
  161. """Raises exception if the modified time is not the same as in the database"""
  162. if not (webnotes.conn.is_single(self.doc.doctype) or \
  163. cint(self.doc.get('__islocal'))):
  164. modified = webnotes.conn.sql("""select modified from `tab%s`
  165. where name=%s for update""" % (self.doc.doctype, "%s"),
  166. self.doc.name or "")
  167. if modified and get_datetime_str(modified[0].modified) != \
  168. get_datetime_str(self.doc.modified):
  169. webnotes.msgprint(_("""Document has been modified after you have opened it.
  170. To maintain the integrity of the data, you will not be able to save
  171. your changes. Please refresh this document.)"""),
  172. raise_exception=webnotes.IntegrityError)
  173. def check_permission(self):
  174. """Raises exception if permission is not valid"""
  175. # hail the administrator - nothing can stop you!
  176. if webnotes.session.user == "Administrator":
  177. return
  178. doctypelist = webnotes.model.get_doctype("DocType", self.doc.doctype)
  179. if not hasattr(self, "user_roles"):
  180. self.user_roles = webnotes.user and webnotes.user.get_roles() or ["Guest"]
  181. if not hasattr(self, "user_defaults"):
  182. self.user_defaults = webnotes.user and webnotes.user.get_defaults() or {}
  183. has_perm = False
  184. match = []
  185. # check if permission exists and if there is any match condition
  186. for perm in doctypelist.get({"doctype": "DocPerm"}):
  187. if cint(perm.permlevel) == 0 and cint(perm.read) == 1 and perm.role in self.user_roles:
  188. has_perm = True
  189. if perm.match and match != -1:
  190. match.append(perm.match)
  191. else:
  192. # this indicates that there exists atleast one permission
  193. # where match is not specified
  194. match = -1
  195. # check match conditions
  196. if has_perm and match and match != -1:
  197. for match_field in match:
  198. if self.doc.get(match_field, "no_value") in self.user_defaults.get(match_field, []):
  199. # field value matches with user's credentials
  200. has_perm = True
  201. break
  202. else:
  203. # oops, illegal value
  204. has_perm = False
  205. webnotes.msgprint(_("""Value: "%s" is not allowed for field "%s" """) % \
  206. (self.doc.get(match_field, "no_value"),
  207. doctypelist.get_field(match_field).label))
  208. if not has_perm:
  209. webnotes.msgprint(_("""Not enough permissions to save %s: "%s" """) % \
  210. (self.doc.doctype, self.doc.name), raise_exception=webnotes.PermissionError)
  211. def check_links(self):
  212. """Checks integrity of links (throws exception if links are invalid)"""
  213. from webnotes.model.doctype import get_link_fields
  214. link_fields = {}
  215. error_list = []
  216. for doc in self.doclist:
  217. for lf in link_fields.setdefault(doc.doctype, get_link_fields(doc.doctype)):
  218. options = (lf.options or "").split("\n")[0].strip()
  219. options = options.startswith("link:") and options[5:] or options
  220. if doc.get(lf.fieldname) and options and \
  221. not webnotes.conn.exists(options, doc[lf.fieldname]):
  222. error_list.append((options, doc[lf.fieldname], lf.label))
  223. if error_list:
  224. webnotes.msgprint(_("""The following values do not exist in the database: %s.
  225. Please correct these values and try to save again.""") % \
  226. comma_and(["%s: \"%s\" (specified in field: %s)" % err for err in error_list]),
  227. raise_exception=webnotes.InvalidLinkError)
  228. def check_mandatory(self):
  229. """check if all required fields have value"""
  230. reqd = []
  231. for doc in self.doclist:
  232. for df in webnotes.model.get_doctype(doc.doctype).get({
  233. "parent": doc.doctype, "doctype": "DocField", "reqd": 1}):
  234. if doc.get(df.fieldname) is None:
  235. reqd.append("""\"%s\" is a Mandatory field [in %s%s]""" % \
  236. (df.label, df.parent, doc.idx and " - %d" % doc.idx or ""))
  237. if reqd:
  238. webnotes.msgprint(_("In") + " %s - %s\n" % (self.doc.doctype, self.doc.name or "") +
  239. "\n".join(reqd),
  240. raise_exception=webnotes.MandatoryError)
  241. def update_timestamps(self):
  242. """Update owner, creation, modified_by, modified, docstatus"""
  243. ts = get_datetime(now_datetime())
  244. for d in self.doclist:
  245. if self.doc.get('__islocal'):
  246. d.owner = webnotes.session.user
  247. d.creation = ts
  248. d.modified_by = webnotes.session.user
  249. d.modified = ts
  250. def doctype_validate(self):
  251. """run DocType Validator"""
  252. from core.doctype.doctype_validator.doctype_validator import validate
  253. validate(self)
  254. def run(self, method, args=None):
  255. if hasattr(self, method):
  256. if args:
  257. getattr(self, method)(args)
  258. else:
  259. getattr(self, method)()
  260. # if possible, deprecate
  261. trigger(method, self.doclist[0])
  262. def clear_table(self, table_field):
  263. self.doclist = filter(lambda d: d.parentfield != table_field, self.doclist)
  264. def add_child(self, doc):
  265. """add a child doc to doclist"""
  266. # make child
  267. if not isinstance(doc, webnotes.model.doc.Document):
  268. doc = webnotes.model.doc.Document(fielddata = doc)
  269. doc.__islocal = 1
  270. doc.parent = self.doc.name
  271. doc.parenttype = self.doc.doctype
  272. # parentfield is to be supplied in the doc
  273. # add to doclist
  274. self.doclist.append(doc)
  275. def export(self):
  276. """export current doc to file system"""
  277. import conf
  278. if (getattr(conf,'developer_mode', 0) and not getattr(webnotes, 'syncing', False)
  279. and not getattr(webnotes, "testing", False)):
  280. from webnotes.modules.export import export_to_files
  281. export_to_files(record_list=self.doclist)
  282. def set_as_default(self, filters=None):
  283. """sets is_default to 0 in rest of the related documents"""
  284. if self.doc.is_default:
  285. conditions, filters = webnotes.conn.build_conditions(filters)
  286. filters.update({"name": self.doc.name})
  287. webnotes.conn.sql("""update `tab%s` set `is_default`=0
  288. where %s and name!=%s""" % (self.doc.doctype, conditions, "%(name)s"),
  289. filters)
  290. def set_default_values(self):
  291. """set's default values in doclist"""
  292. import webnotes.model.utils
  293. for doc in self.doclist:
  294. for df in webnotes.model.get_doctype(doc.doctype).get({
  295. "parent": doc.doctype, "doctype": "DocField"}):
  296. if doc.get(df.fieldname) in [None, ""] and df.default:
  297. doc[df.fieldname] = cast(df, df.default)
  298. # TODO: should this method be here?
  299. def get_csv_from_attachment(self):
  300. """get csv from attachment"""
  301. if not self.doc.file_list:
  302. msgprint("File not attached!")
  303. raise Exception
  304. # get file_id
  305. fid = self.doc.file_list.split(',')[1]
  306. # get file from file_manager
  307. try:
  308. from webnotes.utils import file_manager
  309. fn, content = file_manager.get_file(fid)
  310. except Exception, e:
  311. webnotes.msgprint(_("Unable to open attached file. Please try again."))
  312. raise e
  313. # convert char to string (?)
  314. if not isinstance(content, basestring) and hasattr(content, 'tostring'):
  315. content = content.tostring()
  316. import csv
  317. return csv.reader(content.splitlines())
  318. def trigger(method, doc):
  319. """trigger doctype events"""
  320. try:
  321. import startup.event_handlers
  322. except ImportError:
  323. return
  324. if hasattr(startup.event_handlers, method):
  325. getattr(startup.event_handlers, method)(doc)
  326. if hasattr(startup.event_handlers, 'doclist_all'):
  327. startup.event_handlers.doclist_all(doc, method)