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.
 
 
 
 
 
 

1724 rivejä
48 KiB

  1. # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import base64
  4. import datetime
  5. import json
  6. import math
  7. import operator
  8. import re
  9. import time
  10. from code import compile_command
  11. from enum import Enum
  12. from typing import Any, Dict, List, Optional, Tuple, Union
  13. from urllib.parse import quote, urljoin
  14. from click import secho
  15. import frappe
  16. from frappe.desk.utils import slug
  17. DATE_FORMAT = "%Y-%m-%d"
  18. TIME_FORMAT = "%H:%M:%S.%f"
  19. DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT
  20. class Weekday(Enum):
  21. Sunday = 0
  22. Monday = 1
  23. Tuesday = 2
  24. Wednesday = 3
  25. Thursday = 4
  26. Friday = 5
  27. Saturday = 6
  28. def get_first_day_of_the_week():
  29. return frappe.get_system_settings('first_day_of_the_week') or "Sunday"
  30. def get_start_of_week_index():
  31. return Weekday[get_first_day_of_the_week()].value
  32. def is_invalid_date_string(date_string):
  33. # dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00"
  34. return not isinstance(date_string, str) or ((not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00")))
  35. # datetime functions
  36. def getdate(string_date: Optional[str] = None):
  37. """
  38. Converts string date (yyyy-mm-dd) to datetime.date object.
  39. If no input is provided, current date is returned.
  40. """
  41. from dateutil import parser
  42. from dateutil.parser._parser import ParserError
  43. if not string_date:
  44. return get_datetime().date()
  45. if isinstance(string_date, datetime.datetime):
  46. return string_date.date()
  47. elif isinstance(string_date, datetime.date):
  48. return string_date
  49. if is_invalid_date_string(string_date):
  50. return None
  51. try:
  52. return parser.parse(string_date).date()
  53. except ParserError:
  54. frappe.throw(frappe._('{} is not a valid date string.').format(
  55. frappe.bold(string_date)
  56. ), title=frappe._('Invalid Date'))
  57. def get_datetime(datetime_str=None):
  58. from dateutil import parser
  59. if datetime_str is None:
  60. return now_datetime()
  61. if isinstance(datetime_str, (datetime.datetime, datetime.timedelta)):
  62. return datetime_str
  63. elif isinstance(datetime_str, (list, tuple)):
  64. return datetime.datetime(datetime_str)
  65. elif isinstance(datetime_str, datetime.date):
  66. return datetime.datetime.combine(datetime_str, datetime.time())
  67. if is_invalid_date_string(datetime_str):
  68. return None
  69. try:
  70. return datetime.datetime.strptime(datetime_str, DATETIME_FORMAT)
  71. except ValueError:
  72. return parser.parse(datetime_str)
  73. def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]:
  74. """Return `datetime.timedelta` object from string value of a
  75. valid time format. Returns None if `time` is not a valid format
  76. Args:
  77. time (str): A valid time representation. This string is parsed
  78. using `dateutil.parser.parse`. Examples of valid inputs are:
  79. '0:0:0', '17:21:00', '2012-01-19 17:21:00'. Checkout
  80. https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.parse
  81. Returns:
  82. datetime.timedelta: Timedelta object equivalent of the passed `time` string
  83. """
  84. from dateutil import parser
  85. from dateutil.parser import ParserError
  86. time = time or "0:0:0"
  87. try:
  88. try:
  89. t = parser.parse(time)
  90. except ParserError as e:
  91. if "day" in e.args[1] or "hour must be in" in e.args[0]:
  92. return parse_timedelta(time)
  93. raise e
  94. return datetime.timedelta(
  95. hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond
  96. )
  97. except Exception:
  98. return None
  99. def to_timedelta(time_str):
  100. from dateutil import parser
  101. if isinstance(time_str, datetime.time):
  102. time_str = str(time_str)
  103. if isinstance(time_str, str):
  104. t = parser.parse(time_str)
  105. return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)
  106. else:
  107. return time_str
  108. def add_to_date(date, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, as_string=False, as_datetime=False):
  109. """Adds `days` to the given date"""
  110. from dateutil import parser
  111. from dateutil.parser._parser import ParserError
  112. from dateutil.relativedelta import relativedelta
  113. if date is None:
  114. date = now_datetime()
  115. if hours:
  116. as_datetime = True
  117. if isinstance(date, str):
  118. as_string = True
  119. if " " in date:
  120. as_datetime = True
  121. try:
  122. date = parser.parse(date)
  123. except ParserError:
  124. frappe.throw(frappe._("Please select a valid date filter"), title=frappe._("Invalid Date"))
  125. date = date + relativedelta(years=years, months=months, weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)
  126. if as_string:
  127. if as_datetime:
  128. return date.strftime(DATETIME_FORMAT)
  129. else:
  130. return date.strftime(DATE_FORMAT)
  131. else:
  132. return date
  133. def add_days(date, days):
  134. return add_to_date(date, days=days)
  135. def add_months(date, months):
  136. return add_to_date(date, months=months)
  137. def add_years(date, years):
  138. return add_to_date(date, years=years)
  139. def date_diff(string_ed_date, string_st_date):
  140. return (getdate(string_ed_date) - getdate(string_st_date)).days
  141. def month_diff(string_ed_date, string_st_date):
  142. ed_date = getdate(string_ed_date)
  143. st_date = getdate(string_st_date)
  144. return (ed_date.year - st_date.year) * 12 + ed_date.month - st_date.month + 1
  145. def time_diff(string_ed_date, string_st_date):
  146. return get_datetime(string_ed_date) - get_datetime(string_st_date)
  147. def time_diff_in_seconds(string_ed_date, string_st_date):
  148. return time_diff(string_ed_date, string_st_date).total_seconds()
  149. def time_diff_in_hours(string_ed_date, string_st_date):
  150. return round(float(time_diff(string_ed_date, string_st_date).total_seconds()) / 3600, 6)
  151. def now_datetime():
  152. dt = convert_utc_to_user_timezone(datetime.datetime.utcnow())
  153. return dt.replace(tzinfo=None)
  154. def get_timestamp(date):
  155. return time.mktime(getdate(date).timetuple())
  156. def get_eta(from_time, percent_complete):
  157. diff = time_diff(now_datetime(), from_time).total_seconds()
  158. return str(datetime.timedelta(seconds=(100 - percent_complete) / percent_complete * diff))
  159. def _get_time_zone():
  160. return frappe.db.get_system_setting('time_zone') or 'Asia/Kolkata' # Default to India ?!
  161. def get_time_zone():
  162. if frappe.local.flags.in_test:
  163. return _get_time_zone()
  164. return frappe.cache().get_value("time_zone", _get_time_zone)
  165. def convert_utc_to_timezone(utc_timestamp, time_zone):
  166. from pytz import UnknownTimeZoneError, timezone
  167. utcnow = timezone('UTC').localize(utc_timestamp)
  168. try:
  169. return utcnow.astimezone(timezone(time_zone))
  170. except UnknownTimeZoneError:
  171. return utcnow
  172. def get_datetime_in_timezone(time_zone):
  173. utc_timestamp = datetime.datetime.utcnow()
  174. return convert_utc_to_timezone(utc_timestamp, time_zone)
  175. def convert_utc_to_user_timezone(utc_timestamp):
  176. time_zone = get_time_zone()
  177. return convert_utc_to_timezone(utc_timestamp, time_zone)
  178. def now():
  179. """return current datetime as yyyy-mm-dd hh:mm:ss"""
  180. if frappe.flags.current_date:
  181. return getdate(frappe.flags.current_date).strftime(DATE_FORMAT) + " " + \
  182. now_datetime().strftime(TIME_FORMAT)
  183. else:
  184. return now_datetime().strftime(DATETIME_FORMAT)
  185. def nowdate():
  186. """return current date as yyyy-mm-dd"""
  187. return now_datetime().strftime(DATE_FORMAT)
  188. def today():
  189. return nowdate()
  190. def get_abbr(string, max_len=2):
  191. abbr=''
  192. for part in string.split(' '):
  193. if len(abbr) < max_len and part:
  194. abbr += part[0]
  195. return abbr or '?'
  196. def nowtime():
  197. """return current time in hh:mm"""
  198. return now_datetime().strftime(TIME_FORMAT)
  199. def get_first_day(dt, d_years=0, d_months=0, as_str=False):
  200. """
  201. Returns the first day of the month for the date specified by date object
  202. Also adds `d_years` and `d_months` if specified
  203. """
  204. dt = getdate(dt)
  205. # d_years, d_months are "deltas" to apply to dt
  206. overflow_years, month = divmod(dt.month + d_months - 1, 12)
  207. year = dt.year + d_years + overflow_years
  208. return datetime.date(year, month + 1, 1).strftime(DATE_FORMAT) if as_str else datetime.date(year, month + 1, 1)
  209. def get_quarter_start(dt, as_str=False):
  210. date = getdate(dt)
  211. quarter = (date.month - 1) // 3 + 1
  212. first_date_of_quarter = datetime.date(date.year, ((quarter - 1) * 3) + 1, 1)
  213. return first_date_of_quarter.strftime(DATE_FORMAT) if as_str else first_date_of_quarter
  214. def get_first_day_of_week(dt, as_str=False):
  215. dt = getdate(dt)
  216. date = dt - datetime.timedelta(days=get_week_start_offset_days(dt))
  217. return date.strftime(DATE_FORMAT) if as_str else date
  218. def get_week_start_offset_days(dt):
  219. current_day_index = get_normalized_weekday_index(dt)
  220. start_of_week_index = get_start_of_week_index()
  221. if current_day_index >= start_of_week_index:
  222. return current_day_index - start_of_week_index
  223. else:
  224. return 7 - (start_of_week_index - current_day_index)
  225. def get_normalized_weekday_index(dt):
  226. # starts Sunday with 0
  227. return (dt.weekday() + 1) % 7
  228. def get_year_start(dt, as_str=False):
  229. dt = getdate(dt)
  230. date = datetime.date(dt.year, 1, 1)
  231. return date.strftime(DATE_FORMAT) if as_str else date
  232. def get_last_day_of_week(dt):
  233. dt = get_first_day_of_week(dt)
  234. return dt + datetime.timedelta(days=6)
  235. def get_last_day(dt):
  236. """
  237. Returns last day of the month using:
  238. `get_first_day(dt, 0, 1) + datetime.timedelta(-1)`
  239. """
  240. return get_first_day(dt, 0, 1) + datetime.timedelta(-1)
  241. def get_quarter_ending(date):
  242. date = getdate(date)
  243. # find the earliest quarter ending date that is after
  244. # the given date
  245. for month in (3, 6, 9, 12):
  246. quarter_end_month = getdate('{}-{}-01'.format(date.year, month))
  247. quarter_end_date = getdate(get_last_day(quarter_end_month))
  248. if date <= quarter_end_date:
  249. date = quarter_end_date
  250. break
  251. return date
  252. def get_year_ending(date):
  253. ''' returns year ending of the given date '''
  254. date = getdate(date)
  255. # first day of next year (note year starts from 1)
  256. date = add_to_date('{}-01-01'.format(date.year), months = 12)
  257. # last day of this month
  258. return add_to_date(date, days=-1)
  259. def get_time(time_str: str) -> datetime.time:
  260. from dateutil import parser
  261. from dateutil.parser import ParserError
  262. if isinstance(time_str, datetime.datetime):
  263. return time_str.time()
  264. elif isinstance(time_str, datetime.time):
  265. return time_str
  266. elif isinstance(time_str, datetime.timedelta):
  267. return (datetime.datetime.min + time_str).time()
  268. try:
  269. return parser.parse(time_str).time()
  270. except ParserError as e:
  271. if "day" in e.args[1] or "hour must be in" in e.args[0]:
  272. return (
  273. datetime.datetime.min + parse_timedelta(time_str)
  274. ).time()
  275. raise e
  276. def get_datetime_str(datetime_obj):
  277. if isinstance(datetime_obj, str):
  278. datetime_obj = get_datetime(datetime_obj)
  279. return datetime_obj.strftime(DATETIME_FORMAT)
  280. def get_date_str(date_obj):
  281. if isinstance(date_obj, str):
  282. date_obj = get_datetime(date_obj)
  283. return date_obj.strftime(DATE_FORMAT)
  284. def get_time_str(timedelta_obj):
  285. if isinstance(timedelta_obj, str):
  286. timedelta_obj = to_timedelta(timedelta_obj)
  287. hours, remainder = divmod(timedelta_obj.seconds, 3600)
  288. minutes, seconds = divmod(remainder, 60)
  289. return "{0}:{1}:{2}".format(hours, minutes, seconds)
  290. def get_user_date_format():
  291. """Get the current user date format. The result will be cached."""
  292. if getattr(frappe.local, "user_date_format", None) is None:
  293. frappe.local.user_date_format = frappe.db.get_default("date_format")
  294. return frappe.local.user_date_format or "yyyy-mm-dd"
  295. get_user_format = get_user_date_format # for backwards compatibility
  296. def get_user_time_format():
  297. """Get the current user time format. The result will be cached."""
  298. if getattr(frappe.local, "user_time_format", None) is None:
  299. frappe.local.user_time_format = frappe.db.get_default("time_format")
  300. return frappe.local.user_time_format or "HH:mm:ss"
  301. def format_date(string_date=None, format_string=None):
  302. """Converts the given string date to :data:`user_date_format`
  303. User format specified in defaults
  304. Examples:
  305. * dd-mm-yyyy
  306. * mm-dd-yyyy
  307. * dd/mm/yyyy
  308. """
  309. import babel.dates
  310. from babel.core import UnknownLocaleError
  311. if not string_date:
  312. return ''
  313. date = getdate(string_date)
  314. if not format_string:
  315. format_string = get_user_date_format()
  316. format_string = format_string.replace("mm", "MM").replace("Y", "y")
  317. try:
  318. formatted_date = babel.dates.format_date(
  319. date, format_string,
  320. locale=(frappe.local.lang or "").replace("-", "_"))
  321. except UnknownLocaleError:
  322. format_string = format_string.replace("MM", "%m").replace("dd", "%d").replace("yyyy", "%Y")
  323. formatted_date = date.strftime(format_string)
  324. return formatted_date
  325. formatdate = format_date # For backwards compatibility
  326. def format_time(time_string=None, format_string=None):
  327. """Converts the given string time to :data:`user_time_format`
  328. User format specified in defaults
  329. Examples:
  330. * HH:mm:ss
  331. * HH:mm
  332. """
  333. import babel.dates
  334. from babel.core import UnknownLocaleError
  335. if not time_string:
  336. return ''
  337. time_ = get_time(time_string)
  338. if not format_string:
  339. format_string = get_user_time_format()
  340. try:
  341. formatted_time = babel.dates.format_time(
  342. time_, format_string,
  343. locale=(frappe.local.lang or "").replace("-", "_"))
  344. except UnknownLocaleError:
  345. formatted_time = time_.strftime("%H:%M:%S")
  346. return formatted_time
  347. def format_datetime(datetime_string, format_string=None):
  348. """Converts the given string time to :data:`user_datetime_format`
  349. User format specified in defaults
  350. Examples:
  351. * dd-mm-yyyy HH:mm:ss
  352. * mm-dd-yyyy HH:mm
  353. """
  354. import babel.dates
  355. from babel.core import UnknownLocaleError
  356. if not datetime_string:
  357. return
  358. datetime = get_datetime(datetime_string)
  359. if not format_string:
  360. format_string = (
  361. get_user_date_format().replace("mm", "MM")
  362. + ' ' + get_user_time_format())
  363. try:
  364. formatted_datetime = babel.dates.format_datetime(datetime, format_string, locale=(frappe.local.lang or "").replace("-", "_"))
  365. except UnknownLocaleError:
  366. formatted_datetime = datetime.strftime('%Y-%m-%d %H:%M:%S')
  367. return formatted_datetime
  368. def format_duration(seconds, hide_days=False):
  369. """Converts the given duration value in float(seconds) to duration format
  370. example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float
  371. """
  372. seconds = cint(seconds)
  373. total_duration = {
  374. 'days': math.floor(seconds / (3600 * 24)),
  375. 'hours': math.floor(seconds % (3600 * 24) / 3600),
  376. 'minutes': math.floor(seconds % 3600 / 60),
  377. 'seconds': math.floor(seconds % 60)
  378. }
  379. if hide_days:
  380. total_duration['hours'] = math.floor(seconds / 3600)
  381. total_duration['days'] = 0
  382. duration = ''
  383. if total_duration:
  384. if total_duration.get('days'):
  385. duration += str(total_duration.get('days')) + 'd'
  386. if total_duration.get('hours'):
  387. duration += ' ' if len(duration) else ''
  388. duration += str(total_duration.get('hours')) + 'h'
  389. if total_duration.get('minutes'):
  390. duration += ' ' if len(duration) else ''
  391. duration += str(total_duration.get('minutes')) + 'm'
  392. if total_duration.get('seconds'):
  393. duration += ' ' if len(duration) else ''
  394. duration += str(total_duration.get('seconds')) + 's'
  395. return duration
  396. def duration_to_seconds(duration):
  397. """Converts the given duration formatted value to duration value in seconds
  398. example: converts '3h 34m 45s' to 12885 (value in seconds)
  399. """
  400. validate_duration_format(duration)
  401. value = 0
  402. if 'd' in duration:
  403. val = duration.split('d')
  404. days = val[0]
  405. value += cint(days) * 24 * 60 * 60
  406. duration = val[1]
  407. if 'h' in duration:
  408. val = duration.split('h')
  409. hours = val[0]
  410. value += cint(hours) * 60 * 60
  411. duration = val[1]
  412. if 'm' in duration:
  413. val = duration.split('m')
  414. mins = val[0]
  415. value += cint(mins) * 60
  416. duration = val[1]
  417. if 's' in duration:
  418. val = duration.split('s')
  419. secs = val[0]
  420. value += cint(secs)
  421. return value
  422. def validate_duration_format(duration):
  423. import re
  424. is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", duration)
  425. if not is_valid_duration:
  426. frappe.throw(frappe._("Value {0} must be in the valid duration format: d h m s").format(frappe.bold(duration)))
  427. def get_weekdays():
  428. return ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
  429. def get_weekday(datetime=None):
  430. if not datetime:
  431. datetime = now_datetime()
  432. weekdays = get_weekdays()
  433. return weekdays[datetime.weekday()]
  434. def get_timespan_date_range(timespan):
  435. today = nowdate()
  436. date_range_map = {
  437. "last week": lambda: (get_first_day_of_week(add_to_date(today, days=-7)), get_last_day_of_week(add_to_date(today, days=-7))),
  438. "last month": lambda: (get_first_day(add_to_date(today, months=-1)), get_last_day(add_to_date(today, months=-1))),
  439. "last quarter": lambda: (get_quarter_start(add_to_date(today, months=-3)), get_quarter_ending(add_to_date(today, months=-3))),
  440. "last 6 months": lambda: (get_quarter_start(add_to_date(today, months=-6)), get_quarter_ending(add_to_date(today, months=-3))),
  441. "last year": lambda: (get_year_start(add_to_date(today, years=-1)), get_year_ending(add_to_date(today, years=-1))),
  442. "yesterday": lambda: (add_to_date(today, days=-1),) * 2,
  443. "today": lambda: (today, today),
  444. "tomorrow": lambda: (add_to_date(today, days=1),) * 2,
  445. "this week": lambda: (get_first_day_of_week(today), get_last_day_of_week(today)),
  446. "this month": lambda: (get_first_day(today), get_last_day(today)),
  447. "this quarter": lambda: (get_quarter_start(today), get_quarter_ending(today)),
  448. "this year": lambda: (get_year_start(today), get_year_ending(today)),
  449. "next week": lambda: (get_first_day_of_week(add_to_date(today, days=7)), get_last_day_of_week(add_to_date(today, days=7))),
  450. "next month": lambda: (get_first_day(add_to_date(today, months=1)), get_last_day(add_to_date(today, months=1))),
  451. "next quarter": lambda: (get_quarter_start(add_to_date(today, months=3)), get_quarter_ending(add_to_date(today, months=3))),
  452. "next 6 months": lambda: (get_quarter_start(add_to_date(today, months=3)), get_quarter_ending(add_to_date(today, months=6))),
  453. "next year": lambda: (get_year_start(add_to_date(today, years=1)), get_year_ending(add_to_date(today, years=1))),
  454. }
  455. if timespan in date_range_map:
  456. return date_range_map[timespan]()
  457. def global_date_format(date, format="long"):
  458. """returns localized date in the form of January 1, 2012"""
  459. import babel.dates
  460. date = getdate(date)
  461. formatted_date = babel.dates.format_date(date, locale=(frappe.local.lang or "en").replace("-", "_"), format=format)
  462. return formatted_date
  463. def has_common(l1, l2):
  464. """Returns truthy value if there are common elements in lists l1 and l2"""
  465. return set(l1) & set(l2)
  466. def cast_fieldtype(fieldtype, value, show_warning=True):
  467. if show_warning:
  468. message = (
  469. "Function `frappe.utils.data.cast` has been deprecated in favour"
  470. " of `frappe.utils.data.cast`. Use the newer util for safer type casting."
  471. )
  472. secho(message, fg="yellow")
  473. if fieldtype in ("Currency", "Float", "Percent"):
  474. value = flt(value)
  475. elif fieldtype in ("Int", "Check"):
  476. value = cint(value)
  477. elif fieldtype in ("Data", "Text", "Small Text", "Long Text",
  478. "Text Editor", "Select", "Link", "Dynamic Link"):
  479. value = cstr(value)
  480. elif fieldtype == "Date":
  481. value = getdate(value)
  482. elif fieldtype == "Datetime":
  483. value = get_datetime(value)
  484. elif fieldtype == "Time":
  485. value = to_timedelta(value)
  486. return value
  487. def cast(fieldtype, value=None):
  488. """Cast the value to the Python native object of the Frappe fieldtype provided.
  489. If value is None, the first/lowest value of the `fieldtype` will be returned.
  490. If value can't be cast as fieldtype due to an invalid input, None will be returned.
  491. Mapping of Python types => Frappe types:
  492. * str => ("Data", "Text", "Small Text", "Long Text", "Text Editor", "Select", "Link", "Dynamic Link")
  493. * float => ("Currency", "Float", "Percent")
  494. * int => ("Int", "Check")
  495. * datetime.datetime => ("Datetime",)
  496. * datetime.date => ("Date",)
  497. * datetime.time => ("Time",)
  498. """
  499. if fieldtype in ("Currency", "Float", "Percent"):
  500. value = flt(value)
  501. elif fieldtype in ("Int", "Check"):
  502. value = cint(sbool(value))
  503. elif fieldtype in ("Data", "Text", "Small Text", "Long Text",
  504. "Text Editor", "Select", "Link", "Dynamic Link"):
  505. value = cstr(value)
  506. elif fieldtype == "Date":
  507. if value:
  508. value = getdate(value)
  509. else:
  510. value = datetime.datetime(1, 1, 1).date()
  511. elif fieldtype == "Datetime":
  512. if value:
  513. value = get_datetime(value)
  514. else:
  515. value = datetime.datetime(1, 1, 1)
  516. elif fieldtype == "Time":
  517. value = get_timedelta(value)
  518. return value
  519. def flt(s, precision=None):
  520. """Convert to float (ignoring commas in string)
  521. :param s: Number in string or other numeric format.
  522. :param precision: optional argument to specify precision for rounding.
  523. :returns: Converted number in python float type.
  524. Returns 0 if input can not be converted to float.
  525. Examples:
  526. >>> flt("43.5", precision=0)
  527. 44
  528. >>> flt("42.5", precision=0)
  529. 42
  530. >>> flt("10,500.5666", precision=2)
  531. 10500.57
  532. >>> flt("a")
  533. 0.0
  534. """
  535. if isinstance(s, str):
  536. s = s.replace(',','')
  537. try:
  538. num = float(s)
  539. if precision is not None:
  540. num = rounded(num, precision)
  541. except Exception:
  542. num = 0.0
  543. return num
  544. def cint(s, default=0):
  545. """Convert to integer
  546. :param s: Number in string or other numeric format.
  547. :returns: Converted number in python integer type.
  548. Returns default if input can not be converted to integer.
  549. Examples:
  550. >>> cint("100")
  551. 100
  552. >>> cint("a")
  553. 0
  554. """
  555. try:
  556. return int(float(s))
  557. except Exception:
  558. return default
  559. def floor(s):
  560. """
  561. A number representing the largest integer less than or equal to the specified number
  562. Parameters
  563. ----------
  564. s : int or str or Decimal object
  565. The mathematical value to be floored
  566. Returns
  567. -------
  568. int
  569. number representing the largest integer less than or equal to the specified number
  570. """
  571. try: num = cint(math.floor(flt(s)))
  572. except: num = 0
  573. return num
  574. def ceil(s):
  575. """
  576. The smallest integer greater than or equal to the given number
  577. Parameters
  578. ----------
  579. s : int or str or Decimal object
  580. The mathematical value to be ceiled
  581. Returns
  582. -------
  583. int
  584. smallest integer greater than or equal to the given number
  585. """
  586. try: num = cint(math.ceil(flt(s)))
  587. except: num = 0
  588. return num
  589. def cstr(s, encoding='utf-8'):
  590. return frappe.as_unicode(s, encoding)
  591. def sbool(x: str) -> Union[bool, Any]:
  592. """Converts str object to Boolean if possible.
  593. Example:
  594. "true" becomes True
  595. "1" becomes True
  596. "{}" remains "{}"
  597. Args:
  598. x (str): String to be converted to Bool
  599. Returns:
  600. object: Returns Boolean or x
  601. """
  602. try:
  603. val = x.lower()
  604. if val in ('true', '1'):
  605. return True
  606. elif val in ('false', '0'):
  607. return False
  608. return x
  609. except Exception:
  610. return x
  611. def rounded(num, precision=0):
  612. """round method for round halfs to nearest even algorithm aka banker's rounding - compatible with python3"""
  613. precision = cint(precision)
  614. multiplier = 10 ** precision
  615. # avoid rounding errors
  616. num = round(num * multiplier if precision else num, 8)
  617. floor_num = math.floor(num)
  618. decimal_part = num - floor_num
  619. if not precision and decimal_part == 0.5:
  620. num = floor_num if (floor_num % 2 == 0) else floor_num + 1
  621. else:
  622. if decimal_part == 0.5:
  623. num = floor_num + 1
  624. else:
  625. num = round(num)
  626. return (num / multiplier) if precision else num
  627. def remainder(numerator, denominator, precision=2):
  628. precision = cint(precision)
  629. multiplier = 10 ** precision
  630. if precision:
  631. _remainder = ((numerator * multiplier) % (denominator * multiplier)) / multiplier
  632. else:
  633. _remainder = numerator % denominator
  634. return flt(_remainder, precision)
  635. def safe_div(numerator, denominator, precision=2):
  636. """
  637. SafeMath division that returns zero when divided by zero.
  638. """
  639. precision = cint(precision)
  640. if denominator == 0:
  641. _res = 0.0
  642. else:
  643. _res = float(numerator) / denominator
  644. return flt(_res, precision)
  645. def round_based_on_smallest_currency_fraction(value, currency, precision=2):
  646. smallest_currency_fraction_value = flt(frappe.db.get_value("Currency",
  647. currency, "smallest_currency_fraction_value", cache=True))
  648. if smallest_currency_fraction_value:
  649. remainder_val = remainder(value, smallest_currency_fraction_value, precision)
  650. if remainder_val > (smallest_currency_fraction_value / 2):
  651. value += smallest_currency_fraction_value - remainder_val
  652. else:
  653. value -= remainder_val
  654. else:
  655. value = rounded(value)
  656. return flt(value, precision)
  657. def encode(obj, encoding="utf-8"):
  658. if isinstance(obj, list):
  659. out = []
  660. for o in obj:
  661. if isinstance(o, str):
  662. out.append(o.encode(encoding))
  663. else:
  664. out.append(o)
  665. return out
  666. elif isinstance(obj, str):
  667. return obj.encode(encoding)
  668. else:
  669. return obj
  670. def parse_val(v):
  671. """Converts to simple datatypes from SQL query results"""
  672. if isinstance(v, (datetime.date, datetime.datetime)):
  673. v = str(v)
  674. elif isinstance(v, datetime.timedelta):
  675. v = ":".join(str(v).split(":")[:2])
  676. elif isinstance(v, int):
  677. v = int(v)
  678. return v
  679. def fmt_money(amount, precision=None, currency=None, format=None):
  680. """
  681. Convert to string with commas for thousands, millions etc
  682. """
  683. number_format = format or frappe.db.get_default("number_format") or "#,###.##"
  684. if precision is None:
  685. precision = cint(frappe.db.get_default('currency_precision')) or None
  686. decimal_str, comma_str, number_format_precision = get_number_format_info(number_format)
  687. if precision is None:
  688. precision = number_format_precision
  689. # 40,000 -> 40,000.00
  690. # 40,000.00000 -> 40,000.00
  691. # 40,000.23000 -> 40,000.23
  692. if isinstance(amount, str):
  693. amount = flt(amount, precision)
  694. if decimal_str:
  695. decimals_after = str(round(amount % 1, precision))
  696. parts = decimals_after.split('.')
  697. parts = parts[1] if len(parts) > 1 else parts[0]
  698. decimals = parts
  699. if precision > 2:
  700. if len(decimals) < 3:
  701. if currency:
  702. fraction = frappe.db.get_value("Currency", currency, "fraction_units", cache=True) or 100
  703. precision = len(cstr(fraction)) - 1
  704. else:
  705. precision = number_format_precision
  706. elif len(decimals) < precision:
  707. precision = len(decimals)
  708. amount = '%.*f' % (precision, round(flt(amount), precision))
  709. if amount.find('.') == -1:
  710. decimals = ''
  711. else:
  712. decimals = amount.split('.')[1]
  713. parts = []
  714. minus = ''
  715. if flt(amount) < 0:
  716. minus = '-'
  717. amount = cstr(abs(flt(amount))).split('.')[0]
  718. if len(amount) > 3:
  719. parts.append(amount[-3:])
  720. amount = amount[:-3]
  721. val = number_format=="#,##,###.##" and 2 or 3
  722. while len(amount) > val:
  723. parts.append(amount[-val:])
  724. amount = amount[:-val]
  725. parts.append(amount)
  726. parts.reverse()
  727. amount = comma_str.join(parts) + ((precision and decimal_str) and (decimal_str + decimals) or "")
  728. if amount != '0':
  729. amount = minus + amount
  730. if currency and frappe.defaults.get_global_default("hide_currency_symbol") != "Yes":
  731. symbol = frappe.db.get_value("Currency", currency, "symbol", cache=True) or currency
  732. amount = frappe._(symbol) + " " + amount
  733. return amount
  734. number_format_info = {
  735. "#,###.##": (".", ",", 2),
  736. "#.###,##": (",", ".", 2),
  737. "# ###.##": (".", " ", 2),
  738. "# ###,##": (",", " ", 2),
  739. "#'###.##": (".", "'", 2),
  740. "#, ###.##": (".", ", ", 2),
  741. "#,##,###.##": (".", ",", 2),
  742. "#,###.###": (".", ",", 3),
  743. "#.###": ("", ".", 0),
  744. "#,###": ("", ",", 0),
  745. "#.########": (".", "", 8)
  746. }
  747. def get_number_format_info(format: str) -> Tuple[str, str, int]:
  748. return number_format_info.get(format) or (".", ",", 2)
  749. #
  750. # convert currency to words
  751. #
  752. def money_in_words(number: str, main_currency: Optional[str] = None, fraction_currency: Optional[str] = None):
  753. """
  754. Returns string in words with currency and fraction currency.
  755. """
  756. from frappe.utils import get_defaults
  757. _ = frappe._
  758. try:
  759. # note: `flt` returns 0 for invalid input and we don't want that
  760. number = float(number)
  761. except ValueError:
  762. return ""
  763. number = flt(number)
  764. if number < 0:
  765. return ""
  766. d = get_defaults()
  767. if not main_currency:
  768. main_currency = d.get('currency', 'INR')
  769. if not fraction_currency:
  770. fraction_currency = frappe.db.get_value("Currency", main_currency, "fraction", cache=True) or _("Cent")
  771. number_format = frappe.db.get_value("Currency", main_currency, "number_format", cache=True) or \
  772. frappe.db.get_default("number_format") or "#,###.##"
  773. fraction_length = get_number_format_info(number_format)[2]
  774. n = "%.{0}f".format(fraction_length) % number
  775. numbers = n.split('.')
  776. main, fraction = numbers if len(numbers) > 1 else [n, '00']
  777. if len(fraction) < fraction_length:
  778. zeros = '0' * (fraction_length - len(fraction))
  779. fraction += zeros
  780. in_million = True
  781. if number_format == "#,##,###.##": in_million = False
  782. # 0.00
  783. if main == '0' and fraction in ['00', '000']:
  784. out = "{0} {1}".format(main_currency, _('Zero'))
  785. # 0.XX
  786. elif main == '0':
  787. out = _(in_words(fraction, in_million).title()) + ' ' + fraction_currency
  788. else:
  789. out = main_currency + ' ' + _(in_words(main, in_million).title())
  790. if cint(fraction):
  791. out = out + ' ' + _('and') + ' ' + _(in_words(fraction, in_million).title()) + ' ' + fraction_currency
  792. return out + ' ' + _('only.')
  793. #
  794. # convert number to words
  795. #
  796. def in_words(integer, in_million=True):
  797. """
  798. Returns string in words for the given integer.
  799. """
  800. from num2words import num2words
  801. locale = 'en_IN' if not in_million else frappe.local.lang
  802. integer = int(integer)
  803. try:
  804. ret = num2words(integer, lang=locale)
  805. except NotImplementedError:
  806. ret = num2words(integer, lang='en')
  807. except OverflowError:
  808. ret = num2words(integer, lang='en')
  809. return ret.replace('-', ' ')
  810. def is_html(text):
  811. if not isinstance(text, str):
  812. return False
  813. return re.search('<[^>]+>', text)
  814. def is_image(filepath):
  815. from mimetypes import guess_type
  816. # filepath can be https://example.com/bed.jpg?v=129
  817. filepath = (filepath or "").split('?')[0]
  818. return (guess_type(filepath)[0] or "").startswith("image/")
  819. def get_thumbnail_base64_for_image(src):
  820. from os.path import exists as file_exists
  821. from PIL import Image
  822. from frappe import cache, safe_decode
  823. from frappe.core.doctype.file.file import get_local_image
  824. if not src:
  825. frappe.throw('Invalid source for image: {0}'.format(src))
  826. if not src.startswith('/files') or '..' in src:
  827. return
  828. if src.endswith('.svg'):
  829. return
  830. def _get_base64():
  831. file_path = frappe.get_site_path("public", src.lstrip("/"))
  832. if not file_exists(file_path):
  833. return
  834. try:
  835. image, unused_filename, extn = get_local_image(src)
  836. except IOError:
  837. return
  838. original_size = image.size
  839. size = 50, 50
  840. image.thumbnail(size, Image.ANTIALIAS)
  841. base64_string = image_to_base64(image, extn)
  842. return {
  843. 'base64': 'data:image/{0};base64,{1}'.format(extn, safe_decode(base64_string)),
  844. 'width': original_size[0],
  845. 'height': original_size[1]
  846. }
  847. return cache().hget('thumbnail_base64', src, generator=_get_base64)
  848. def image_to_base64(image, extn):
  849. from io import BytesIO
  850. buffered = BytesIO()
  851. if extn.lower() in ('jpg', 'jpe'):
  852. extn = 'JPEG'
  853. image.save(buffered, extn)
  854. img_str = base64.b64encode(buffered.getvalue())
  855. return img_str
  856. def pdf_to_base64(filename):
  857. from frappe.utils.file_manager import get_file_path
  858. if '../' in filename or filename.rsplit('.')[-1] not in ['pdf', 'PDF']:
  859. return
  860. file_path = get_file_path(filename)
  861. if not file_path:
  862. return
  863. with open(file_path, 'rb') as pdf_file:
  864. base64_string = base64.b64encode(pdf_file.read())
  865. return base64_string
  866. # from Jinja2 code
  867. _striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
  868. def strip_html(text):
  869. """removes anything enclosed in and including <>"""
  870. return _striptags_re.sub("", text)
  871. def escape_html(text):
  872. if not isinstance(text, str):
  873. return text
  874. html_escape_table = {
  875. "&": "&amp;",
  876. '"': "&quot;",
  877. "'": "&apos;",
  878. ">": "&gt;",
  879. "<": "&lt;",
  880. }
  881. return "".join(html_escape_table.get(c,c) for c in text)
  882. def pretty_date(iso_datetime):
  883. """
  884. Takes an ISO time and returns a string representing how
  885. long ago the date represents.
  886. Ported from PrettyDate by John Resig
  887. """
  888. from frappe import _
  889. if not iso_datetime: return ''
  890. import math
  891. if isinstance(iso_datetime, str):
  892. iso_datetime = datetime.datetime.strptime(iso_datetime, DATETIME_FORMAT)
  893. now_dt = datetime.datetime.strptime(now(), DATETIME_FORMAT)
  894. dt_diff = now_dt - iso_datetime
  895. # available only in python 2.7+
  896. # dt_diff_seconds = dt_diff.total_seconds()
  897. dt_diff_seconds = dt_diff.days * 86400.0 + dt_diff.seconds
  898. dt_diff_days = math.floor(dt_diff_seconds / 86400.0)
  899. # differnt cases
  900. if dt_diff_seconds < 60.0:
  901. return _('just now')
  902. elif dt_diff_seconds < 120.0:
  903. return _('1 minute ago')
  904. elif dt_diff_seconds < 3600.0:
  905. return _('{0} minutes ago').format(cint(math.floor(dt_diff_seconds / 60.0)))
  906. elif dt_diff_seconds < 7200.0:
  907. return _('1 hour ago')
  908. elif dt_diff_seconds < 86400.0:
  909. return _('{0} hours ago').format(cint(math.floor(dt_diff_seconds / 3600.0)))
  910. elif dt_diff_days == 1.0:
  911. return _('Yesterday')
  912. elif dt_diff_days < 7.0:
  913. return _('{0} days ago').format(cint(dt_diff_days))
  914. elif dt_diff_days < 12:
  915. return _('1 week ago')
  916. elif dt_diff_days < 31.0:
  917. return _('{0} weeks ago').format(cint(math.ceil(dt_diff_days / 7.0)))
  918. elif dt_diff_days < 46:
  919. return _('1 month ago')
  920. elif dt_diff_days < 365.0:
  921. return _('{0} months ago').format(cint(math.ceil(dt_diff_days / 30.0)))
  922. elif dt_diff_days < 550.0:
  923. return _('1 year ago')
  924. else:
  925. return '{0} years ago'.format(cint(math.floor(dt_diff_days / 365.0)))
  926. def comma_or(some_list, add_quotes=True):
  927. return comma_sep(some_list, frappe._("{0} or {1}"), add_quotes)
  928. def comma_and(some_list ,add_quotes=True):
  929. return comma_sep(some_list, frappe._("{0} and {1}"), add_quotes)
  930. def comma_sep(some_list, pattern, add_quotes=True):
  931. if isinstance(some_list, (list, tuple)):
  932. # list(some_list) is done to preserve the existing list
  933. some_list = [str(s) for s in list(some_list)]
  934. if not some_list:
  935. return ""
  936. elif len(some_list) == 1:
  937. return some_list[0]
  938. else:
  939. some_list = ["'%s'" % s for s in some_list] if add_quotes else ["%s" % s for s in some_list]
  940. return pattern.format(", ".join(frappe._(s) for s in some_list[:-1]), some_list[-1])
  941. else:
  942. return some_list
  943. def new_line_sep(some_list):
  944. if isinstance(some_list, (list, tuple)):
  945. # list(some_list) is done to preserve the existing list
  946. some_list = [str(s) for s in list(some_list)]
  947. if not some_list:
  948. return ""
  949. elif len(some_list) == 1:
  950. return some_list[0]
  951. else:
  952. some_list = ["%s" % s for s in some_list]
  953. return format("\n ".join(some_list))
  954. else:
  955. return some_list
  956. def filter_strip_join(some_list, sep):
  957. """given a list, filter None values, strip spaces and join"""
  958. return (cstr(sep)).join((cstr(a).strip() for a in filter(None, some_list)))
  959. def get_url(uri=None, full_address=False):
  960. """get app url from request"""
  961. host_name = frappe.local.conf.host_name or frappe.local.conf.hostname
  962. if uri and (uri.startswith("http://") or uri.startswith("https://")):
  963. return uri
  964. if not host_name:
  965. request_host_name = get_host_name_from_request()
  966. if request_host_name:
  967. host_name = request_host_name
  968. elif frappe.local.site:
  969. protocol = 'http://'
  970. if frappe.local.conf.ssl_certificate:
  971. protocol = 'https://'
  972. elif frappe.local.conf.wildcard:
  973. domain = frappe.local.conf.wildcard.get('domain')
  974. if domain and frappe.local.site.endswith(domain) and frappe.local.conf.wildcard.get('ssl_certificate'):
  975. protocol = 'https://'
  976. host_name = protocol + frappe.local.site
  977. else:
  978. host_name = frappe.db.get_value("Website Settings", "Website Settings",
  979. "subdomain")
  980. if not host_name:
  981. host_name = "http://localhost"
  982. if host_name and not (host_name.startswith("http://") or host_name.startswith("https://")):
  983. host_name = "http://" + host_name
  984. if not uri and full_address:
  985. uri = frappe.get_request_header("REQUEST_URI", "")
  986. port = frappe.conf.http_port or frappe.conf.webserver_port
  987. if not (frappe.conf.restart_supervisor_on_update or frappe.conf.restart_systemd_on_update) and host_name and not url_contains_port(host_name) and port:
  988. host_name = host_name + ':' + str(port)
  989. url = urljoin(host_name, uri) if uri else host_name
  990. return url
  991. def get_host_name_from_request():
  992. if hasattr(frappe.local, "request") and frappe.local.request and frappe.local.request.host:
  993. protocol = 'https://' if 'https' == frappe.get_request_header('X-Forwarded-Proto', "") else 'http://'
  994. return protocol + frappe.local.request.host
  995. def url_contains_port(url):
  996. parts = url.split(':')
  997. return len(parts) > 2
  998. def get_host_name():
  999. return get_url().rsplit("//", 1)[-1]
  1000. def get_link_to_form(doctype, name, label=None):
  1001. if not label: label = name
  1002. return """<a href="{0}">{1}</a>""".format(get_url_to_form(doctype, name), label)
  1003. def get_link_to_report(name, label=None, report_type=None, doctype=None, filters=None):
  1004. if not label: label = name
  1005. if filters:
  1006. conditions = []
  1007. for k,v in filters.items():
  1008. if isinstance(v, list):
  1009. for value in v:
  1010. conditions.append(str(k)+'='+'["'+str(value[0]+'"'+','+'"'+str(value[1])+'"]'))
  1011. else:
  1012. conditions.append(str(k)+"="+str(v))
  1013. filters = "&".join(conditions)
  1014. return """<a href='{0}'>{1}</a>""".format(get_url_to_report_with_filters(name, filters, report_type, doctype), label)
  1015. else:
  1016. return """<a href='{0}'>{1}</a>""".format(get_url_to_report(name, report_type, doctype), label)
  1017. def get_absolute_url(doctype, name):
  1018. return "/app/{0}/{1}".format(quoted(slug(doctype)), quoted(name))
  1019. def get_url_to_form(doctype, name):
  1020. return get_url(uri = "/app/{0}/{1}".format(quoted(slug(doctype)), quoted(name)))
  1021. def get_url_to_list(doctype):
  1022. return get_url(uri = "/app/{0}".format(quoted(slug(doctype))))
  1023. def get_url_to_report(name, report_type = None, doctype = None):
  1024. if report_type == "Report Builder":
  1025. return get_url(uri = "/app/{0}/view/report/{1}".format(quoted(slug(doctype)), quoted(name)))
  1026. else:
  1027. return get_url(uri = "/app/query-report/{0}".format(quoted(name)))
  1028. def get_url_to_report_with_filters(name, filters, report_type = None, doctype = None):
  1029. if report_type == "Report Builder":
  1030. return get_url(uri = "/app/{0}/view/report?{1}".format(quoted(doctype), filters))
  1031. else:
  1032. return get_url(uri = "/app/query-report/{0}?{1}".format(quoted(name), filters))
  1033. operator_map = {
  1034. # startswith
  1035. "^": lambda a, b: (a or "").startswith(b),
  1036. # in or not in a list
  1037. "in": lambda a, b: operator.contains(b, a),
  1038. "not in": lambda a, b: not operator.contains(b, a),
  1039. # comparison operators
  1040. "=": lambda a, b: operator.eq(a, b),
  1041. "!=": lambda a, b: operator.ne(a, b),
  1042. ">": lambda a, b: operator.gt(a, b),
  1043. "<": lambda a, b: operator.lt(a, b),
  1044. ">=": lambda a, b: operator.ge(a, b),
  1045. "<=": lambda a, b: operator.le(a, b),
  1046. "not None": lambda a, b: a and True or False,
  1047. "None": lambda a, b: (not a) and True or False
  1048. }
  1049. def evaluate_filters(doc, filters: Union[Dict, List, Tuple]):
  1050. '''Returns true if doc matches filters'''
  1051. if isinstance(filters, dict):
  1052. for key, value in filters.items():
  1053. f = get_filter(None, {key:value})
  1054. if not compare(doc.get(f.fieldname), f.operator, f.value, f.fieldtype):
  1055. return False
  1056. elif isinstance(filters, (list, tuple)):
  1057. for d in filters:
  1058. f = get_filter(None, d)
  1059. if not compare(doc.get(f.fieldname), f.operator, f.value, f.fieldtype):
  1060. return False
  1061. return True
  1062. def compare(val1: Any, condition: str, val2: Any, fieldtype: Optional[str] = None):
  1063. ret = False
  1064. if fieldtype:
  1065. val2 = cast(fieldtype, val2)
  1066. if condition in operator_map:
  1067. ret = operator_map[condition](val1, val2)
  1068. return ret
  1069. def get_filter(doctype: str, f: Union[Dict, List, Tuple], filters_config=None) -> "frappe._dict":
  1070. """Returns a _dict like
  1071. {
  1072. "doctype":
  1073. "fieldname":
  1074. "operator":
  1075. "value":
  1076. "fieldtype":
  1077. }
  1078. """
  1079. from frappe.model import default_fields, optional_fields, child_table_fields
  1080. if isinstance(f, dict):
  1081. key, value = next(iter(f.items()))
  1082. f = make_filter_tuple(doctype, key, value)
  1083. if not isinstance(f, (list, tuple)):
  1084. frappe.throw(frappe._("Filter must be a tuple or list (in a list)"))
  1085. if len(f) == 3:
  1086. f = (doctype, f[0], f[1], f[2])
  1087. elif len(f) > 4:
  1088. f = f[0:4]
  1089. elif len(f) != 4:
  1090. frappe.throw(frappe._("Filter must have 4 values (doctype, fieldname, operator, value): {0}").format(str(f)))
  1091. f = frappe._dict(doctype=f[0], fieldname=f[1], operator=f[2], value=f[3])
  1092. sanitize_column(f.fieldname)
  1093. if not f.operator:
  1094. # if operator is missing
  1095. f.operator = "="
  1096. valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", "is",
  1097. "between", "descendants of", "ancestors of", "not descendants of", "not ancestors of",
  1098. "timespan", "previous", "next")
  1099. if filters_config:
  1100. additional_operators = []
  1101. for key in filters_config:
  1102. additional_operators.append(key.lower())
  1103. valid_operators = tuple(set(valid_operators + tuple(additional_operators)))
  1104. if f.operator.lower() not in valid_operators:
  1105. frappe.throw(frappe._("Operator must be one of {0}").format(", ".join(valid_operators)))
  1106. if f.doctype and (f.fieldname not in default_fields + optional_fields + child_table_fields):
  1107. # verify fieldname belongs to the doctype
  1108. meta = frappe.get_meta(f.doctype)
  1109. if not meta.has_field(f.fieldname):
  1110. # try and match the doctype name from child tables
  1111. for df in meta.get_table_fields():
  1112. if frappe.get_meta(df.options).has_field(f.fieldname):
  1113. f.doctype = df.options
  1114. break
  1115. try:
  1116. df = frappe.get_meta(f.doctype).get_field(f.fieldname)
  1117. except frappe.exceptions.DoesNotExistError:
  1118. df = None
  1119. f.fieldtype = df.fieldtype if df else None
  1120. return f
  1121. def make_filter_tuple(doctype, key, value):
  1122. '''return a filter tuple like [doctype, key, operator, value]'''
  1123. if isinstance(value, (list, tuple)):
  1124. return [doctype, key, value[0], value[1]]
  1125. else:
  1126. return [doctype, key, "=", value]
  1127. def make_filter_dict(filters):
  1128. '''convert this [[doctype, key, operator, value], ..]
  1129. to this { key: (operator, value), .. }
  1130. '''
  1131. _filter = frappe._dict()
  1132. for f in filters:
  1133. _filter[f[1]] = (f[2], f[3])
  1134. return _filter
  1135. def sanitize_column(column_name):
  1136. import sqlparse
  1137. from frappe import _
  1138. regex = re.compile("^.*[,'();].*")
  1139. column_name = sqlparse.format(column_name, strip_comments=True, keyword_case="lower")
  1140. blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case', 'and', 'or']
  1141. def _raise_exception():
  1142. frappe.throw(_("Invalid field name {0}").format(column_name), frappe.DataError)
  1143. if 'ifnull' in column_name:
  1144. if regex.match(column_name):
  1145. # to avoid and, or
  1146. if any(' {0} '.format(keyword) in column_name.split() for keyword in blacklisted_keywords):
  1147. _raise_exception()
  1148. # to avoid select, delete, drop, update and case
  1149. elif any(keyword in column_name.split() for keyword in blacklisted_keywords):
  1150. _raise_exception()
  1151. elif regex.match(column_name):
  1152. _raise_exception()
  1153. def scrub_urls(html):
  1154. html = expand_relative_urls(html)
  1155. # encoding should be responsibility of the composer
  1156. # html = quote_urls(html)
  1157. return html
  1158. def expand_relative_urls(html):
  1159. # expand relative urls
  1160. url = get_url()
  1161. if url.endswith("/"): url = url[:-1]
  1162. def _expand_relative_urls(match):
  1163. to_expand = list(match.groups())
  1164. if not to_expand[2].startswith('mailto') and not to_expand[2].startswith('data:'):
  1165. if not to_expand[2].startswith("/"):
  1166. to_expand[2] = "/" + to_expand[2]
  1167. to_expand.insert(2, url)
  1168. if 'url' in to_expand[0] and to_expand[1].startswith('(') and to_expand[-1].endswith(')'):
  1169. # background-image: url('/assets/...') - workaround for wkhtmltopdf print-media-type
  1170. to_expand.append(' !important')
  1171. return "".join(to_expand)
  1172. html = re.sub(r'(href|src){1}([\s]*=[\s]*[\'"]?)((?!http)[^\'" >]+)([\'"]?)', _expand_relative_urls, html)
  1173. # background-image: url('/assets/...')
  1174. html = re.sub(r'(:[\s]?url)(\([\'"]?)((?!http)[^\'" >]+)([\'"]?\))', _expand_relative_urls, html)
  1175. return html
  1176. def quoted(url):
  1177. return cstr(quote(encode(cstr(url)), safe=b"~@#$&()*!+=:;,.?/'"))
  1178. def quote_urls(html):
  1179. def _quote_url(match):
  1180. groups = list(match.groups())
  1181. groups[2] = quoted(groups[2])
  1182. return "".join(groups)
  1183. return re.sub(r'(href|src){1}([\s]*=[\s]*[\'"]?)((?:http)[^\'">]+)([\'"]?)',
  1184. _quote_url, html)
  1185. def unique(seq):
  1186. """use this instead of list(set()) to preserve order of the original list.
  1187. Thanks to Stackoverflow: http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order"""
  1188. seen = set()
  1189. seen_add = seen.add
  1190. return [ x for x in seq if not (x in seen or seen_add(x)) ]
  1191. def strip(val, chars=None):
  1192. # \ufeff is no-width-break, \u200b is no-width-space
  1193. return (val or "").replace("\ufeff", "").replace("\u200b", "").strip(chars)
  1194. def to_markdown(html):
  1195. from html.parser import HTMLParser
  1196. from html2text import html2text
  1197. text = None
  1198. try:
  1199. text = html2text(html or '')
  1200. except HTMLParser.HTMLParseError:
  1201. pass
  1202. return text
  1203. def md_to_html(markdown_text):
  1204. from markdown2 import MarkdownError
  1205. from markdown2 import markdown as _markdown
  1206. extras = {
  1207. 'fenced-code-blocks': None,
  1208. 'tables': None,
  1209. 'header-ids': None,
  1210. 'toc': None,
  1211. 'highlightjs-lang': None,
  1212. 'html-classes': {
  1213. 'table': 'table table-bordered',
  1214. 'img': 'screenshot'
  1215. }
  1216. }
  1217. html = None
  1218. try:
  1219. html = UnicodeWithAttrs(_markdown(markdown_text or '', extras=extras))
  1220. except MarkdownError:
  1221. pass
  1222. return html
  1223. def markdown(markdown_text):
  1224. return md_to_html(markdown_text)
  1225. def is_subset(list_a: List, list_b: List) -> bool:
  1226. '''Returns whether list_a is a subset of list_b'''
  1227. return len(list(set(list_a) & set(list_b))) == len(list_a)
  1228. def generate_hash(*args, **kwargs) -> str:
  1229. return frappe.generate_hash(*args, **kwargs)
  1230. def guess_date_format(date_string: str) -> str:
  1231. DATE_FORMATS = [
  1232. r"%d/%b/%y",
  1233. r"%d-%m-%Y",
  1234. r"%m-%d-%Y",
  1235. r"%Y-%m-%d",
  1236. r"%d-%m-%y",
  1237. r"%m-%d-%y",
  1238. r"%y-%m-%d",
  1239. r"%y-%b-%d",
  1240. r"%d/%m/%Y",
  1241. r"%m/%d/%Y",
  1242. r"%Y/%m/%d",
  1243. r"%d/%m/%y",
  1244. r"%m/%d/%y",
  1245. r"%y/%m/%d",
  1246. r"%d.%m.%Y",
  1247. r"%m.%d.%Y",
  1248. r"%Y.%m.%d",
  1249. r"%d.%m.%y",
  1250. r"%m.%d.%y",
  1251. r"%y.%m.%d",
  1252. r"%d %b %Y",
  1253. r"%d %B %Y",
  1254. ]
  1255. TIME_FORMATS = [
  1256. r"%H:%M:%S.%f",
  1257. r"%H:%M:%S",
  1258. r"%H:%M",
  1259. r"%I:%M:%S.%f %p",
  1260. r"%I:%M:%S %p",
  1261. r"%I:%M %p",
  1262. ]
  1263. def _get_date_format(date_str):
  1264. for f in DATE_FORMATS:
  1265. try:
  1266. # if date is parsed without any exception
  1267. # capture the date format
  1268. datetime.datetime.strptime(date_str, f)
  1269. return f
  1270. except ValueError:
  1271. pass
  1272. def _get_time_format(time_str):
  1273. for f in TIME_FORMATS:
  1274. try:
  1275. # if time is parsed without any exception
  1276. # capture the time format
  1277. datetime.datetime.strptime(time_str, f)
  1278. return f
  1279. except ValueError:
  1280. pass
  1281. date_format = None
  1282. time_format = None
  1283. date_string = date_string.strip()
  1284. # check if date format can be guessed
  1285. date_format = _get_date_format(date_string)
  1286. if date_format:
  1287. return date_format
  1288. # date_string doesnt look like date, it can have a time part too
  1289. # split the date string into date and time parts
  1290. if " " in date_string:
  1291. date_str, time_str = date_string.split(" ", 1)
  1292. date_format = _get_date_format(date_str) or ''
  1293. time_format = _get_time_format(time_str) or ''
  1294. if date_format and time_format:
  1295. return (date_format + ' ' + time_format).strip()
  1296. def validate_json_string(string: str) -> None:
  1297. try:
  1298. json.loads(string)
  1299. except (TypeError, ValueError):
  1300. raise frappe.ValidationError
  1301. def get_user_info_for_avatar(user_id: str) -> Dict:
  1302. try:
  1303. user = frappe.get_cached_doc("User", user_id)
  1304. return {
  1305. "email": user.email,
  1306. "image": user.user_image,
  1307. "name": user.full_name
  1308. }
  1309. except frappe.DoesNotExistError:
  1310. frappe.clear_last_message()
  1311. return {
  1312. "email": user_id,
  1313. "image": "",
  1314. "name": user_id
  1315. }
  1316. def validate_python_code(string: str, fieldname=None, is_expression: bool = True) -> None:
  1317. """ Validate python code fields by using compile_command to ensure that expression is valid python.
  1318. args:
  1319. fieldname: name of field being validated.
  1320. is_expression: true for validating simple single line python expression, else validated as script.
  1321. """
  1322. if not string:
  1323. return
  1324. try:
  1325. compile_command(string, symbol="eval" if is_expression else "exec")
  1326. except SyntaxError as se:
  1327. line_no = se.lineno - 1 or 0
  1328. offset = se.offset - 1 or 0
  1329. error_line = string if is_expression else string.split("\n")[line_no]
  1330. msg = (frappe._("{} Invalid python code on line {}")
  1331. .format(fieldname + ":" if fieldname else "", line_no+1))
  1332. msg += f"<br><pre>{error_line}</pre>"
  1333. msg += f"<pre>{' ' * offset}^</pre>"
  1334. frappe.throw(msg, title=frappe._("Syntax Error"))
  1335. except Exception as e:
  1336. frappe.msgprint(frappe._("{} Possibly invalid python code. <br>{}")
  1337. .format(fieldname + ": " or "", str(e)), indicator="orange")
  1338. class UnicodeWithAttrs(str):
  1339. def __init__(self, text):
  1340. self.toc_html = text.toc_html
  1341. self.metadata = text.metadata
  1342. def format_timedelta(o: datetime.timedelta) -> str:
  1343. # mariadb allows a wide diff range - https://mariadb.com/kb/en/time/
  1344. # but frappe doesnt - i think via babel : only allows 0..23 range for hour
  1345. total_seconds = o.total_seconds()
  1346. hours, remainder = divmod(total_seconds, 3600)
  1347. minutes, seconds = divmod(remainder, 60)
  1348. rounded_seconds = round(seconds, 6)
  1349. int_seconds = int(seconds)
  1350. if rounded_seconds == int_seconds:
  1351. seconds = int_seconds
  1352. else:
  1353. seconds = rounded_seconds
  1354. return "{:01}:{:02}:{:02}".format(int(hours), int(minutes), seconds)
  1355. def parse_timedelta(s: str) -> datetime.timedelta:
  1356. # ref: https://stackoverflow.com/a/21074460/10309266
  1357. if 'day' in s:
  1358. m = re.match(r"(?P<days>[-\d]+) day[s]*, (?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
  1359. else:
  1360. m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
  1361. return datetime.timedelta(**{key: float(val) for key, val in m.groupdict().items()})