Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

721 строка
21 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. import frappe, sys
  5. from frappe import _
  6. from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta,
  7. sanitize_html, sanitize_email)
  8. from frappe.model import default_fields
  9. from frappe.model.naming import set_new_name
  10. from frappe.modules import load_doctype_module
  11. from frappe.model import display_fieldtypes
  12. from frappe.model.db_schema import type_map, varchar_len
  13. _classes = {}
  14. def get_controller(doctype):
  15. """Returns the **class** object of the given DocType.
  16. For `custom` type, returns `frappe.model.document.Document`.
  17. :param doctype: DocType name as string."""
  18. from frappe.model.document import Document
  19. if not doctype in _classes:
  20. module_name, custom = frappe.db.get_value("DocType", doctype, ["module", "custom"]) \
  21. or ["Core", False]
  22. if custom:
  23. _class = Document
  24. else:
  25. module = load_doctype_module(doctype, module_name)
  26. classname = doctype.replace(" ", "").replace("-", "")
  27. if hasattr(module, classname):
  28. _class = getattr(module, classname)
  29. if issubclass(_class, BaseDocument):
  30. _class = getattr(module, classname)
  31. else:
  32. raise ImportError, doctype
  33. else:
  34. raise ImportError, doctype
  35. _classes[doctype] = _class
  36. return _classes[doctype]
  37. class BaseDocument(object):
  38. ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
  39. def __init__(self, d):
  40. self.update(d)
  41. self.dont_update_if_missing = []
  42. if hasattr(self, "__setup__"):
  43. self.__setup__()
  44. @property
  45. def meta(self):
  46. if not hasattr(self, "_meta"):
  47. self._meta = frappe.get_meta(self.doctype)
  48. return self._meta
  49. def update(self, d):
  50. if "doctype" in d:
  51. self.set("doctype", d.get("doctype"))
  52. # first set default field values of base document
  53. for key in default_fields:
  54. if key in d:
  55. self.set(key, d.get(key))
  56. for key, value in d.iteritems():
  57. self.set(key, value)
  58. return self
  59. def update_if_missing(self, d):
  60. if isinstance(d, BaseDocument):
  61. d = d.get_valid_dict()
  62. if "doctype" in d:
  63. self.set("doctype", d.get("doctype"))
  64. for key, value in d.iteritems():
  65. # dont_update_if_missing is a list of fieldnames, for which, you don't want to set default value
  66. if (self.get(key) is None) and (value is not None) and (key not in self.dont_update_if_missing):
  67. self.set(key, value)
  68. def get_db_value(self, key):
  69. return frappe.db.get_value(self.doctype, self.name, key)
  70. def get(self, key=None, filters=None, limit=None, default=None):
  71. if key:
  72. if isinstance(key, dict):
  73. return _filter(self.get_all_children(), key, limit=limit)
  74. if filters:
  75. if isinstance(filters, dict):
  76. value = _filter(self.__dict__.get(key, []), filters, limit=limit)
  77. else:
  78. default = filters
  79. filters = None
  80. value = self.__dict__.get(key, default)
  81. else:
  82. value = self.__dict__.get(key, default)
  83. if value is None and key not in self.ignore_in_getter \
  84. and key in (d.fieldname for d in self.meta.get_table_fields()):
  85. self.set(key, [])
  86. value = self.__dict__.get(key)
  87. return value
  88. else:
  89. return self.__dict__
  90. def getone(self, key, filters=None):
  91. return self.get(key, filters=filters, limit=1)[0]
  92. def set(self, key, value, as_value=False):
  93. if isinstance(value, list) and not as_value:
  94. self.__dict__[key] = []
  95. self.extend(key, value)
  96. else:
  97. self.__dict__[key] = value
  98. def delete_key(self, key):
  99. if key in self.__dict__:
  100. del self.__dict__[key]
  101. def append(self, key, value=None):
  102. if value==None:
  103. value={}
  104. if isinstance(value, (dict, BaseDocument)):
  105. if not self.__dict__.get(key):
  106. self.__dict__[key] = []
  107. value = self._init_child(value, key)
  108. self.__dict__[key].append(value)
  109. # reference parent document
  110. value.parent_doc = self
  111. return value
  112. else:
  113. raise ValueError, "Document attached to child table must be a dict or BaseDocument, not " + str(type(value))[1:-1]
  114. def extend(self, key, value):
  115. if isinstance(value, list):
  116. for v in value:
  117. self.append(key, v)
  118. else:
  119. raise ValueError
  120. def remove(self, doc):
  121. self.get(doc.parentfield).remove(doc)
  122. def _init_child(self, value, key):
  123. if not self.doctype:
  124. return value
  125. if not isinstance(value, BaseDocument):
  126. if "doctype" not in value:
  127. value["doctype"] = self.get_table_field_doctype(key)
  128. if not value["doctype"]:
  129. raise AttributeError, key
  130. value = get_controller(value["doctype"])(value)
  131. value.init_valid_columns()
  132. value.parent = self.name
  133. value.parenttype = self.doctype
  134. value.parentfield = key
  135. if value.docstatus is None:
  136. value.docstatus = 0
  137. if not getattr(value, "idx", None):
  138. value.idx = len(self.get(key) or []) + 1
  139. if not getattr(value, "name", None):
  140. value.__dict__['__islocal'] = 1
  141. return value
  142. def get_valid_dict(self, sanitize=True):
  143. d = frappe._dict()
  144. for fieldname in self.meta.get_valid_columns():
  145. d[fieldname] = self.get(fieldname)
  146. # if no need for sanitization and value is None, continue
  147. if not sanitize and d[fieldname] is None:
  148. continue
  149. df = self.meta.get_field(fieldname)
  150. if df:
  151. if df.fieldtype in ("Check", "Int") and not isinstance(d[fieldname], int):
  152. d[fieldname] = cint(d[fieldname])
  153. elif df.fieldtype in ("Currency", "Float", "Percent") and not isinstance(d[fieldname], float):
  154. d[fieldname] = flt(d[fieldname])
  155. elif df.fieldtype in ("Datetime", "Date") and d[fieldname]=="":
  156. d[fieldname] = None
  157. elif df.get("unique") and cstr(d[fieldname]).strip()=="":
  158. # unique empty field should be set to None
  159. d[fieldname] = None
  160. return d
  161. def init_valid_columns(self):
  162. for key in default_fields:
  163. if key not in self.__dict__:
  164. self.__dict__[key] = None
  165. if key in ("idx", "docstatus") and self.__dict__[key] is None:
  166. self.__dict__[key] = 0
  167. for key in self.get_valid_columns():
  168. if key not in self.__dict__:
  169. self.__dict__[key] = None
  170. def get_valid_columns(self):
  171. if self.doctype not in frappe.local.valid_columns:
  172. if self.doctype in ("DocField", "DocPerm") and self.parent in ("DocType", "DocField", "DocPerm"):
  173. from frappe.model.meta import get_table_columns
  174. valid = get_table_columns(self.doctype)
  175. else:
  176. valid = self.meta.get_valid_columns()
  177. frappe.local.valid_columns[self.doctype] = valid
  178. return frappe.local.valid_columns[self.doctype]
  179. def is_new(self):
  180. return self.get("__islocal")
  181. def as_dict(self, no_nulls=False, no_default_fields=False):
  182. doc = self.get_valid_dict()
  183. doc["doctype"] = self.doctype
  184. for df in self.meta.get_table_fields():
  185. children = self.get(df.fieldname) or []
  186. doc[df.fieldname] = [d.as_dict(no_nulls=no_nulls) for d in children]
  187. if no_nulls:
  188. for k in doc.keys():
  189. if doc[k] is None:
  190. del doc[k]
  191. if no_default_fields:
  192. for k in doc.keys():
  193. if k in default_fields:
  194. del doc[k]
  195. for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"):
  196. if self.get(key):
  197. doc[key] = self.get(key)
  198. return doc
  199. def as_json(self):
  200. return frappe.as_json(self.as_dict())
  201. def get_table_field_doctype(self, fieldname):
  202. return self.meta.get_field(fieldname).options
  203. def get_parentfield_of_doctype(self, doctype):
  204. fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype]
  205. return fieldname[0] if fieldname else None
  206. def db_insert(self):
  207. """INSERT the document (with valid columns) in the database."""
  208. if not self.name:
  209. # name will be set by document class in most cases
  210. set_new_name(self)
  211. d = self.get_valid_dict()
  212. columns = d.keys()
  213. try:
  214. frappe.db.sql("""insert into `tab{doctype}`
  215. ({columns}) values ({values})""".format(
  216. doctype = self.doctype,
  217. columns = ", ".join(["`"+c+"`" for c in columns]),
  218. values = ", ".join(["%s"] * len(columns))
  219. ), d.values())
  220. except Exception, e:
  221. if e.args[0]==1062:
  222. if "PRIMARY" in cstr(e.args[1]):
  223. if self.meta.autoname=="hash":
  224. # hash collision? try again
  225. self.name = None
  226. self.db_insert()
  227. return
  228. type, value, traceback = sys.exc_info()
  229. frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name))
  230. raise frappe.DuplicateEntryError, (self.doctype, self.name, e), traceback
  231. elif "Duplicate" in cstr(e.args[1]):
  232. # unique constraint
  233. self.show_unique_validation_message(e)
  234. else:
  235. raise
  236. else:
  237. raise
  238. self.set("__islocal", False)
  239. def db_update(self):
  240. if self.get("__islocal") or not self.name:
  241. self.db_insert()
  242. return
  243. d = self.get_valid_dict()
  244. columns = d.keys()
  245. try:
  246. frappe.db.sql("""update `tab{doctype}`
  247. set {values} where name=%s""".format(
  248. doctype = self.doctype,
  249. values = ", ".join(["`"+c+"`=%s" for c in columns])
  250. ), d.values() + [d.get("name")])
  251. except Exception, e:
  252. if e.args[0]==1062 and "Duplicate" in cstr(e.args[1]):
  253. self.show_unique_validation_message(e)
  254. else:
  255. raise
  256. def show_unique_validation_message(self, e):
  257. type, value, traceback = sys.exc_info()
  258. fieldname, label = str(e).split("'")[-2], None
  259. # unique_first_fieldname_second_fieldname is the constraint name
  260. # created using frappe.db.add_unique
  261. if "unique_" in fieldname:
  262. fieldname = fieldname.split("_", 1)[1]
  263. df = self.meta.get_field(fieldname)
  264. if df:
  265. label = df.label
  266. frappe.msgprint(_("{0} must be unique".format(label or fieldname)))
  267. # this is used to preserve traceback
  268. raise frappe.UniqueValidationError, (self.doctype, self.name, e), traceback
  269. def db_set(self, fieldname, value, update_modified=True):
  270. self.set(fieldname, value)
  271. if update_modified:
  272. self.set("modified", now())
  273. self.set("modified_by", frappe.session.user)
  274. frappe.db.set_value(self.doctype, self.name, fieldname, value,
  275. self.modified, self.modified_by, update_modified=update_modified)
  276. def _fix_numeric_types(self):
  277. for df in self.meta.get("fields"):
  278. if df.fieldtype == "Check":
  279. self.set(df.fieldname, cint(self.get(df.fieldname)))
  280. elif self.get(df.fieldname) is not None:
  281. if df.fieldtype == "Int":
  282. self.set(df.fieldname, cint(self.get(df.fieldname)))
  283. elif df.fieldtype in ("Float", "Currency", "Percent"):
  284. self.set(df.fieldname, flt(self.get(df.fieldname)))
  285. if self.docstatus is not None:
  286. self.docstatus = cint(self.docstatus)
  287. def _get_missing_mandatory_fields(self):
  288. """Get mandatory fields that do not have any values"""
  289. def get_msg(df):
  290. if df.fieldtype == "Table":
  291. return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label))
  292. elif self.parentfield:
  293. return "{}: {} #{}: {}: {}".format(_("Error"), _("Row"), self.idx,
  294. _("Value missing for"), _(df.label))
  295. else:
  296. return "{}: {}: {}".format(_("Error"), _("Value missing for"), _(df.label))
  297. missing = []
  298. for df in self.meta.get("fields", {"reqd": 1}):
  299. if self.get(df.fieldname) in (None, []) or not strip_html(cstr(self.get(df.fieldname))).strip():
  300. missing.append((df.fieldname, get_msg(df)))
  301. # check for missing parent and parenttype
  302. if self.meta.istable:
  303. for fieldname in ("parent", "parenttype"):
  304. if not self.get(fieldname):
  305. missing.append((fieldname, get_msg(frappe._dict(label=fieldname))))
  306. return missing
  307. def get_invalid_links(self, is_submittable=False):
  308. def get_msg(df, docname):
  309. if self.parentfield:
  310. return "{} #{}: {}: {}".format(_("Row"), self.idx, _(df.label), docname)
  311. else:
  312. return "{}: {}".format(_(df.label), docname)
  313. invalid_links = []
  314. cancelled_links = []
  315. for df in self.meta.get_link_fields() + self.meta.get("fields",
  316. {"fieldtype":"Dynamic Link"}):
  317. docname = self.get(df.fieldname)
  318. if docname:
  319. if df.fieldtype=="Link":
  320. doctype = df.options
  321. if not doctype:
  322. frappe.throw(_("Options not set for link field {0}").format(df.fieldname))
  323. else:
  324. doctype = self.get(df.options)
  325. if not doctype:
  326. frappe.throw(_("{0} must be set first").format(self.meta.get_label(df.options)))
  327. # MySQL is case insensitive. Preserve case of the original docname in the Link Field.
  328. value = frappe.db.get_value(doctype, docname, "name", cache=True)
  329. setattr(self, df.fieldname, value)
  330. if not value:
  331. invalid_links.append((df.fieldname, docname, get_msg(df, docname)))
  332. elif (df.fieldname != "amended_from"
  333. and (is_submittable or self.meta.is_submittable) and frappe.get_meta(doctype).is_submittable
  334. and cint(frappe.db.get_value(doctype, docname, "docstatus"))==2):
  335. cancelled_links.append((df.fieldname, docname, get_msg(df, docname)))
  336. return invalid_links, cancelled_links
  337. def _validate_selects(self):
  338. if frappe.flags.in_import:
  339. return
  340. for df in self.meta.get_select_fields():
  341. if df.fieldname=="naming_series" or not (self.get(df.fieldname) and df.options):
  342. continue
  343. options = (df.options or "").split("\n")
  344. # if only empty options
  345. if not filter(None, options):
  346. continue
  347. # strip and set
  348. self.set(df.fieldname, cstr(self.get(df.fieldname)).strip())
  349. value = self.get(df.fieldname)
  350. if value not in options and not (frappe.flags.in_test and value.startswith("_T-")):
  351. # show an elaborate message
  352. prefix = _("Row #{0}:").format(self.idx) if self.get("parentfield") else ""
  353. label = _(self.meta.get_label(df.fieldname))
  354. comma_options = '", "'.join(_(each) for each in options)
  355. frappe.throw(_('{0} {1} cannot be "{2}". It should be one of "{3}"').format(prefix, label,
  356. value, comma_options))
  357. def _validate_constants(self):
  358. if frappe.flags.in_import or self.is_new():
  359. return
  360. constants = [d.fieldname for d in self.meta.get("fields", {"set_only_once": 1})]
  361. if constants:
  362. values = frappe.db.get_value(self.doctype, self.name, constants, as_dict=True)
  363. for fieldname in constants:
  364. if self.get(fieldname) != values.get(fieldname):
  365. frappe.throw(_("Value cannot be changed for {0}").format(self.meta.get_label(fieldname)),
  366. frappe.CannotChangeConstantError)
  367. def _validate_length(self):
  368. if frappe.flags.in_install:
  369. return
  370. for fieldname, value in self.get_valid_dict().iteritems():
  371. df = self.meta.get_field(fieldname)
  372. if df and df.fieldtype in type_map and type_map[df.fieldtype][0]=="varchar":
  373. max_length = cint(df.get("length")) or cint(varchar_len)
  374. if len(cstr(value)) > max_length:
  375. if self.parentfield and self.idx:
  376. reference = _("{0}, Row {1}").format(_(self.doctype), self.idx)
  377. else:
  378. reference = "{0} {1}".format(_(self.doctype), self.name)
  379. frappe.throw(_("{0}: '{1}' will get truncated, as max characters allowed is {2}")\
  380. .format(reference, _(df.label), max_length), frappe.CharacterLengthExceededError)
  381. def _validate_update_after_submit(self):
  382. # get the full doc with children
  383. db_values = frappe.get_doc(self.doctype, self.name).as_dict()
  384. for key in self.as_dict():
  385. df = self.meta.get_field(key)
  386. db_value = db_values.get(key)
  387. if df and not df.allow_on_submit and (self.get(key) or db_value):
  388. if df.fieldtype=="Table":
  389. # just check if the table size has changed
  390. # individual fields will be checked in the loop for children
  391. self_value = len(self.get(key))
  392. db_value = len(db_value)
  393. else:
  394. self_value = self.get_value(key)
  395. if self_value != db_value:
  396. frappe.throw(_("Not allowed to change {0} after submission").format(df.label),
  397. frappe.UpdateAfterSubmitError)
  398. def _sanitize_content(self):
  399. """Sanitize HTML and Email in field values. Used to prevent XSS.
  400. - Ignore if 'Ignore XSS Filter' is checked or fieldtype is 'Code'
  401. """
  402. if frappe.flags.in_install:
  403. return
  404. for fieldname, value in self.get_valid_dict().items():
  405. if not value or not isinstance(value, basestring):
  406. continue
  407. elif ("<" not in value and ">" not in value):
  408. # doesn't look like html so no need
  409. continue
  410. elif "<!-- markdown -->" in value and not ("<script" in value or "javascript:" in value):
  411. # should be handled separately via the markdown converter function
  412. continue
  413. df = self.meta.get_field(fieldname)
  414. sanitized_value = value
  415. if df and (df.get("ignore_xss_filter")
  416. or (df.get("fieldtype")=="Code" and df.get("options")!="Email")
  417. or df.get("fieldtype") in ("Attach", "Attach Image")):
  418. continue
  419. elif df and df.get("fieldtype") in ("Data", "Code") and df.get("options")=="Email":
  420. sanitized_value = sanitize_email(value)
  421. else:
  422. sanitized_value = sanitize_html(value)
  423. self.set(fieldname, sanitized_value)
  424. def precision(self, fieldname, parentfield=None):
  425. """Returns float precision for a particular field (or get global default).
  426. :param fieldname: Fieldname for which precision is required.
  427. :param parentfield: If fieldname is in child table."""
  428. from frappe.model.meta import get_field_precision
  429. if parentfield and not isinstance(parentfield, basestring):
  430. parentfield = parentfield.parentfield
  431. cache_key = parentfield or "main"
  432. if not hasattr(self, "_precision"):
  433. self._precision = frappe._dict()
  434. if cache_key not in self._precision:
  435. self._precision[cache_key] = frappe._dict()
  436. if fieldname not in self._precision[cache_key]:
  437. self._precision[cache_key][fieldname] = None
  438. doctype = self.meta.get_field(parentfield).options if parentfield else self.doctype
  439. df = frappe.get_meta(doctype).get_field(fieldname)
  440. if df.fieldtype in ("Currency", "Float", "Percent"):
  441. self._precision[cache_key][fieldname] = get_field_precision(df, self)
  442. return self._precision[cache_key][fieldname]
  443. def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False):
  444. from frappe.utils.formatters import format_value
  445. df = self.meta.get_field(fieldname)
  446. if not df and fieldname in default_fields:
  447. from frappe.model.meta import get_default_df
  448. df = get_default_df(fieldname)
  449. val = self.get(fieldname)
  450. if absolute_value and isinstance(val, (int, float)):
  451. val = abs(self.get(fieldname))
  452. if not doc:
  453. doc = getattr(self, "parent_doc", None) or self
  454. return format_value(val, df=df, doc=doc, currency=currency)
  455. def is_print_hide(self, fieldname, df=None, for_print=True):
  456. """Returns true if fieldname is to be hidden for print.
  457. Print Hide can be set via the Print Format Builder or in the controller as a list
  458. of hidden fields. Example
  459. class MyDoc(Document):
  460. def __setup__(self):
  461. self.print_hide = ["field1", "field2"]
  462. :param fieldname: Fieldname to be checked if hidden.
  463. """
  464. meta_df = self.meta.get_field(fieldname)
  465. if meta_df and meta_df.get("__print_hide"):
  466. return True
  467. print_hide = 0
  468. if self.get(fieldname)==0 and not self.meta.istable:
  469. print_hide = ( df and df.print_hide_if_no_value ) or ( meta_df and meta_df.print_hide_if_no_value )
  470. if not print_hide:
  471. if df and df.print_hide is not None:
  472. print_hide = df.print_hide
  473. elif meta_df:
  474. print_hide = meta_df.print_hide
  475. return print_hide
  476. def in_format_data(self, fieldname):
  477. """Returns True if shown via Print Format::`format_data` property.
  478. Called from within standard print format."""
  479. doc = getattr(self, "parent_doc", self)
  480. if hasattr(doc, "format_data_map"):
  481. return fieldname in doc.format_data_map
  482. else:
  483. return True
  484. def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields):
  485. """If the user does not have permissions at permlevel > 0, then reset the values to original / default"""
  486. to_reset = []
  487. for df in high_permlevel_fields:
  488. if df.permlevel not in has_access_to and df.fieldtype not in display_fieldtypes:
  489. to_reset.append(df)
  490. if to_reset:
  491. if self.is_new():
  492. # if new, set default value
  493. ref_doc = frappe.new_doc(self.doctype)
  494. else:
  495. # get values from old doc
  496. if self.parent:
  497. self.parent_doc.get_latest()
  498. ref_doc = [d for d in self.parent_doc.get(self.parentfield) if d.name == self.name][0]
  499. else:
  500. ref_doc = self.get_latest()
  501. for df in to_reset:
  502. self.set(df.fieldname, ref_doc.get(df.fieldname))
  503. def get_value(self, fieldname):
  504. df = self.meta.get_field(fieldname)
  505. val = self.get(fieldname)
  506. return self.cast(val, df)
  507. def cast(self, val, df):
  508. if df.fieldtype in ("Currency", "Float", "Percent"):
  509. val = flt(val)
  510. elif df.fieldtype in ("Int", "Check"):
  511. val = cint(val)
  512. elif df.fieldtype in ("Data", "Text", "Small Text", "Long Text",
  513. "Text Editor", "Select", "Link", "Dynamic Link"):
  514. val = cstr(val)
  515. elif df.fieldtype == "Date":
  516. val = getdate(val)
  517. elif df.fieldtype == "Datetime":
  518. val = get_datetime(val)
  519. elif df.fieldtype == "Time":
  520. val = to_timedelta(val)
  521. return val
  522. def _extract_images_from_text_editor(self):
  523. from frappe.utils.file_manager import extract_images_from_doc
  524. if self.doctype != "DocType":
  525. for df in self.meta.get("fields", {"fieldtype":"Text Editor"}):
  526. extract_images_from_doc(self, df.fieldname)
  527. def _filter(data, filters, limit=None):
  528. """pass filters as:
  529. {"key": "val", "key": ["!=", "val"],
  530. "key": ["in", "val"], "key": ["not in", "val"], "key": "^val",
  531. "key" : True (exists), "key": False (does not exist) }"""
  532. out = []
  533. for d in data:
  534. add = True
  535. for f in filters:
  536. fval = filters[f]
  537. if fval is True:
  538. fval = ("not None", fval)
  539. elif fval is False:
  540. fval = ("None", fval)
  541. elif not isinstance(fval, (tuple, list)):
  542. if isinstance(fval, basestring) and fval.startswith("^"):
  543. fval = ("^", fval[1:])
  544. else:
  545. fval = ("=", fval)
  546. if not frappe.compare(getattr(d, f, None), fval[0], fval[1]):
  547. add = False
  548. break
  549. if add:
  550. out.append(d)
  551. if limit and (len(out)-1)==limit:
  552. break
  553. return out