Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 
 

1298 rindas
36 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
  5. from dateutil.parser._parser import ParserError
  6. import operator
  7. import json
  8. import re, datetime, math, time
  9. import babel.dates
  10. from babel.core import UnknownLocaleError
  11. from dateutil import parser
  12. from num2words import num2words
  13. from six.moves import html_parser as HTMLParser
  14. from six.moves.urllib.parse import quote, urljoin
  15. from html2text import html2text
  16. from markdown2 import markdown, MarkdownError
  17. from six import iteritems, text_type, string_types, integer_types
  18. DATE_FORMAT = "%Y-%m-%d"
  19. TIME_FORMAT = "%H:%M:%S.%f"
  20. DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT
  21. def is_invalid_date_string(date_string):
  22. # dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00"
  23. return (not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00"))
  24. # datetime functions
  25. def getdate(string_date=None):
  26. """
  27. Converts string date (yyyy-mm-dd) to datetime.date object
  28. """
  29. if not string_date:
  30. return get_datetime().date()
  31. if isinstance(string_date, datetime.datetime):
  32. return string_date.date()
  33. elif isinstance(string_date, datetime.date):
  34. return string_date
  35. if is_invalid_date_string(string_date):
  36. return None
  37. try:
  38. return parser.parse(string_date).date()
  39. except ParserError:
  40. frappe.throw(frappe._('{} is not a valid date string.').format(
  41. frappe.bold(string_date)
  42. ), title=frappe._('Invalid Date'))
  43. def get_datetime(datetime_str=None):
  44. if datetime_str is None:
  45. return now_datetime()
  46. if isinstance(datetime_str, (datetime.datetime, datetime.timedelta)):
  47. return datetime_str
  48. elif isinstance(datetime_str, (list, tuple)):
  49. return datetime.datetime(datetime_str)
  50. elif isinstance(datetime_str, datetime.date):
  51. return datetime.datetime.combine(datetime_str, datetime.time())
  52. if is_invalid_date_string(datetime_str):
  53. return None
  54. try:
  55. return datetime.datetime.strptime(datetime_str, DATETIME_FORMAT)
  56. except ValueError:
  57. return parser.parse(datetime_str)
  58. def to_timedelta(time_str):
  59. if isinstance(time_str, string_types):
  60. t = parser.parse(time_str)
  61. return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)
  62. else:
  63. return time_str
  64. 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):
  65. """Adds `days` to the given date"""
  66. from dateutil.relativedelta import relativedelta
  67. if date==None:
  68. date = now_datetime()
  69. if hours:
  70. as_datetime = True
  71. if isinstance(date, string_types):
  72. as_string = True
  73. if " " in date:
  74. as_datetime = True
  75. try:
  76. date = parser.parse(date)
  77. except ParserError:
  78. frappe.throw(frappe._("Please select a valid date filter"), title=frappe._("Invalid Date"))
  79. date = date + relativedelta(years=years, months=months, weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)
  80. if as_string:
  81. if as_datetime:
  82. return date.strftime(DATETIME_FORMAT)
  83. else:
  84. return date.strftime(DATE_FORMAT)
  85. else:
  86. return date
  87. def add_days(date, days):
  88. return add_to_date(date, days=days)
  89. def add_months(date, months):
  90. return add_to_date(date, months=months)
  91. def add_years(date, years):
  92. return add_to_date(date, years=years)
  93. def date_diff(string_ed_date, string_st_date):
  94. return (getdate(string_ed_date) - getdate(string_st_date)).days
  95. def month_diff(string_ed_date, string_st_date):
  96. ed_date = getdate(string_ed_date)
  97. st_date = getdate(string_st_date)
  98. return (ed_date.year - st_date.year) * 12 + ed_date.month - st_date.month + 1
  99. def time_diff(string_ed_date, string_st_date):
  100. return get_datetime(string_ed_date) - get_datetime(string_st_date)
  101. def time_diff_in_seconds(string_ed_date, string_st_date):
  102. return time_diff(string_ed_date, string_st_date).total_seconds()
  103. def time_diff_in_hours(string_ed_date, string_st_date):
  104. return round(float(time_diff(string_ed_date, string_st_date).total_seconds()) / 3600, 6)
  105. def now_datetime():
  106. dt = convert_utc_to_user_timezone(datetime.datetime.utcnow())
  107. return dt.replace(tzinfo=None)
  108. def get_timestamp(date):
  109. return time.mktime(getdate(date).timetuple())
  110. def get_eta(from_time, percent_complete):
  111. diff = time_diff(now_datetime(), from_time).total_seconds()
  112. return str(datetime.timedelta(seconds=(100 - percent_complete) / percent_complete * diff))
  113. def _get_time_zone():
  114. return frappe.db.get_system_setting('time_zone') or 'Asia/Kolkata' # Default to India ?!
  115. def get_time_zone():
  116. if frappe.local.flags.in_test:
  117. return _get_time_zone()
  118. return frappe.cache().get_value("time_zone", _get_time_zone)
  119. def convert_utc_to_user_timezone(utc_timestamp):
  120. from pytz import timezone, UnknownTimeZoneError
  121. utcnow = timezone('UTC').localize(utc_timestamp)
  122. try:
  123. return utcnow.astimezone(timezone(get_time_zone()))
  124. except UnknownTimeZoneError:
  125. return utcnow
  126. def now():
  127. """return current datetime as yyyy-mm-dd hh:mm:ss"""
  128. if frappe.flags.current_date:
  129. return getdate(frappe.flags.current_date).strftime(DATE_FORMAT) + " " + \
  130. now_datetime().strftime(TIME_FORMAT)
  131. else:
  132. return now_datetime().strftime(DATETIME_FORMAT)
  133. def nowdate():
  134. """return current date as yyyy-mm-dd"""
  135. return now_datetime().strftime(DATE_FORMAT)
  136. def today():
  137. return nowdate()
  138. def nowtime():
  139. """return current time in hh:mm"""
  140. return now_datetime().strftime(TIME_FORMAT)
  141. def get_first_day(dt, d_years=0, d_months=0, as_str=False):
  142. """
  143. Returns the first day of the month for the date specified by date object
  144. Also adds `d_years` and `d_months` if specified
  145. """
  146. dt = getdate(dt)
  147. # d_years, d_months are "deltas" to apply to dt
  148. overflow_years, month = divmod(dt.month + d_months - 1, 12)
  149. year = dt.year + d_years + overflow_years
  150. return datetime.date(year, month + 1, 1).strftime(DATE_FORMAT) if as_str else datetime.date(year, month + 1, 1)
  151. def get_quarter_start(dt, as_str=False):
  152. date = getdate(dt)
  153. quarter = (date.month - 1) // 3 + 1
  154. first_date_of_quarter = datetime.date(date.year, ((quarter - 1) * 3) + 1, 1)
  155. return first_date_of_quarter.strftime(DATE_FORMAT) if as_str else first_date_of_quarter
  156. def get_first_day_of_week(dt, as_str=False):
  157. dt = getdate(dt)
  158. date = dt - datetime.timedelta(days=dt.weekday())
  159. return date.strftime(DATE_FORMAT) if as_str else date
  160. def get_year_start(dt, as_str=False):
  161. dt = getdate(dt)
  162. date = datetime.date(dt.year, 1, 1)
  163. return date.strftime(DATE_FORMAT) if as_str else date
  164. def get_last_day_of_week(dt):
  165. dt = get_first_day_of_week(dt)
  166. return dt + datetime.timedelta(days=6)
  167. def get_last_day(dt):
  168. """
  169. Returns last day of the month using:
  170. `get_first_day(dt, 0, 1) + datetime.timedelta(-1)`
  171. """
  172. return get_first_day(dt, 0, 1) + datetime.timedelta(-1)
  173. def get_time(time_str):
  174. if isinstance(time_str, datetime.datetime):
  175. return time_str.time()
  176. elif isinstance(time_str, datetime.time):
  177. return time_str
  178. else:
  179. if isinstance(time_str, datetime.timedelta):
  180. time_str = str(time_str)
  181. return parser.parse(time_str).time()
  182. def get_datetime_str(datetime_obj):
  183. if isinstance(datetime_obj, string_types):
  184. datetime_obj = get_datetime(datetime_obj)
  185. return datetime_obj.strftime(DATETIME_FORMAT)
  186. def get_date_str(date_obj):
  187. if isinstance(date_obj, string_types):
  188. date_obj = get_datetime(date_obj)
  189. return date_obj.strftime(DATE_FORMAT)
  190. def get_time_str(timedelta_obj):
  191. if isinstance(timedelta_obj, string_types):
  192. timedelta_obj = to_timedelta(timedelta_obj)
  193. hours, remainder = divmod(timedelta_obj.seconds, 3600)
  194. minutes, seconds = divmod(remainder, 60)
  195. return "{0}:{1}:{2}".format(hours, minutes, seconds)
  196. def get_user_date_format():
  197. """Get the current user date format. The result will be cached."""
  198. if getattr(frappe.local, "user_date_format", None) is None:
  199. frappe.local.user_date_format = frappe.db.get_default("date_format")
  200. return frappe.local.user_date_format or "yyyy-mm-dd"
  201. get_user_format = get_user_date_format # for backwards compatibility
  202. def get_user_time_format():
  203. """Get the current user time format. The result will be cached."""
  204. if getattr(frappe.local, "user_time_format", None) is None:
  205. frappe.local.user_time_format = frappe.db.get_default("time_format")
  206. return frappe.local.user_time_format or "HH:mm:ss"
  207. def format_date(string_date=None, format_string=None):
  208. """Converts the given string date to :data:`user_date_format`
  209. User format specified in defaults
  210. Examples:
  211. * dd-mm-yyyy
  212. * mm-dd-yyyy
  213. * dd/mm/yyyy
  214. """
  215. if not string_date:
  216. return ''
  217. date = getdate(string_date)
  218. if not format_string:
  219. format_string = get_user_date_format()
  220. format_string = format_string.replace("mm", "MM")
  221. try:
  222. formatted_date = babel.dates.format_date(
  223. date, format_string,
  224. locale=(frappe.local.lang or "").replace("-", "_"))
  225. except UnknownLocaleError:
  226. format_string = format_string.replace("MM", "%m").replace("dd", "%d").replace("yyyy", "%Y")
  227. formatted_date = date.strftime(format_string)
  228. return formatted_date
  229. formatdate = format_date # For backwards compatibility
  230. def format_time(time_string=None, format_string=None):
  231. """Converts the given string time to :data:`user_time_format`
  232. User format specified in defaults
  233. Examples:
  234. * HH:mm:ss
  235. * HH:mm
  236. """
  237. if not time_string:
  238. return ''
  239. time_ = get_time(time_string)
  240. if not format_string:
  241. format_string = get_user_time_format()
  242. try:
  243. formatted_time = babel.dates.format_time(
  244. time_, format_string,
  245. locale=(frappe.local.lang or "").replace("-", "_"))
  246. except UnknownLocaleError:
  247. formatted_time = time_.strftime("%H:%M:%S")
  248. return formatted_time
  249. def format_datetime(datetime_string, format_string=None):
  250. """Converts the given string time to :data:`user_datetime_format`
  251. User format specified in defaults
  252. Examples:
  253. * dd-mm-yyyy HH:mm:ss
  254. * mm-dd-yyyy HH:mm
  255. """
  256. if not datetime_string:
  257. return
  258. datetime = get_datetime(datetime_string)
  259. if not format_string:
  260. format_string = (
  261. get_user_date_format().replace("mm", "MM")
  262. + ' ' + get_user_time_format())
  263. try:
  264. formatted_datetime = babel.dates.format_datetime(datetime, format_string, locale=(frappe.local.lang or "").replace("-", "_"))
  265. except UnknownLocaleError:
  266. formatted_datetime = datetime.strftime('%Y-%m-%d %H:%M:%S')
  267. return formatted_datetime
  268. def format_duration(seconds, hide_days=False):
  269. total_duration = {
  270. 'days': math.floor(seconds / (3600 * 24)),
  271. 'hours': math.floor(seconds % (3600 * 24) / 3600),
  272. 'minutes': math.floor(seconds % 3600 / 60),
  273. 'seconds': math.floor(seconds % 60)
  274. }
  275. if hide_days:
  276. total_duration['hours'] = math.floor(seconds / 3600)
  277. total_duration['days'] = 0
  278. duration = ''
  279. if total_duration:
  280. if total_duration.get('days'):
  281. duration += str(total_duration.get('days')) + 'd'
  282. if total_duration.get('hours'):
  283. duration += ' ' if len(duration) else ''
  284. duration += str(total_duration.get('hours')) + 'h'
  285. if total_duration.get('minutes'):
  286. duration += ' ' if len(duration) else ''
  287. duration += str(total_duration.get('minutes')) + 'm'
  288. if total_duration.get('seconds'):
  289. duration += ' ' if len(duration) else ''
  290. duration += str(total_duration.get('seconds')) + 's'
  291. return duration
  292. def get_weekdays():
  293. return ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
  294. def get_weekday(datetime=None):
  295. if not datetime:
  296. datetime = now_datetime()
  297. weekdays = get_weekdays()
  298. return weekdays[datetime.weekday()]
  299. def get_timespan_date_range(timespan):
  300. date_range_map = {
  301. "last week": [add_to_date(nowdate(), days=-7), nowdate()],
  302. "last month": [add_to_date(nowdate(), months=-1), nowdate()],
  303. "last quarter": [add_to_date(nowdate(), months=-3), nowdate()],
  304. "last 6 months": [add_to_date(nowdate(), months=-6), nowdate()],
  305. "last year": [add_to_date(nowdate(), years=-1), nowdate()],
  306. "today": [nowdate(), nowdate()],
  307. "this week": [get_first_day_of_week(nowdate(), as_str=True), nowdate()],
  308. "this month": [get_first_day(nowdate(), as_str=True), nowdate()],
  309. "this quarter": [get_quarter_start(nowdate(), as_str=True), nowdate()],
  310. "this year": [get_year_start(nowdate(), as_str=True), nowdate()],
  311. "next week": [nowdate(), add_to_date(nowdate(), days=7)],
  312. "next month": [nowdate(), add_to_date(nowdate(), months=1)],
  313. "next quarter": [nowdate(), add_to_date(nowdate(), months=3)],
  314. "next 6 months": [nowdate(), add_to_date(nowdate(), months=6)],
  315. "next year": [nowdate(), add_to_date(nowdate(), years=1)],
  316. }
  317. return date_range_map.get(timespan)
  318. def global_date_format(date, format="long"):
  319. """returns localized date in the form of January 1, 2012"""
  320. date = getdate(date)
  321. formatted_date = babel.dates.format_date(date, locale=(frappe.local.lang or "en").replace("-", "_"), format=format)
  322. return formatted_date
  323. def has_common(l1, l2):
  324. """Returns truthy value if there are common elements in lists l1 and l2"""
  325. return set(l1) & set(l2)
  326. def flt(s, precision=None):
  327. """Convert to float (ignore commas)"""
  328. if isinstance(s, string_types):
  329. s = s.replace(',','')
  330. try:
  331. num = float(s)
  332. if precision is not None:
  333. num = rounded(num, precision)
  334. except Exception:
  335. num = 0.0
  336. return num
  337. def cint(s):
  338. """Convert to integer"""
  339. try: num = int(float(s))
  340. except: num = 0
  341. return num
  342. def floor(s):
  343. """
  344. A number representing the largest integer less than or equal to the specified number
  345. Parameters
  346. ----------
  347. s : int or str or Decimal object
  348. The mathematical value to be floored
  349. Returns
  350. -------
  351. int
  352. number representing the largest integer less than or equal to the specified number
  353. """
  354. try: num = cint(math.floor(flt(s)))
  355. except: num = 0
  356. return num
  357. def ceil(s):
  358. """
  359. The smallest integer greater than or equal to the given number
  360. Parameters
  361. ----------
  362. s : int or str or Decimal object
  363. The mathematical value to be ceiled
  364. Returns
  365. -------
  366. int
  367. smallest integer greater than or equal to the given number
  368. """
  369. try: num = cint(math.ceil(flt(s)))
  370. except: num = 0
  371. return num
  372. def cstr(s, encoding='utf-8'):
  373. return frappe.as_unicode(s, encoding)
  374. def rounded(num, precision=0):
  375. """round method for round halfs to nearest even algorithm aka banker's rounding - compatible with python3"""
  376. precision = cint(precision)
  377. multiplier = 10 ** precision
  378. # avoid rounding errors
  379. num = round(num * multiplier if precision else num, 8)
  380. floor_num = math.floor(num)
  381. decimal_part = num - floor_num
  382. if not precision and decimal_part == 0.5:
  383. num = floor_num if (floor_num % 2 == 0) else floor_num + 1
  384. else:
  385. if decimal_part == 0.5:
  386. num = floor_num + 1
  387. else:
  388. num = round(num)
  389. return (num / multiplier) if precision else num
  390. def remainder(numerator, denominator, precision=2):
  391. precision = cint(precision)
  392. multiplier = 10 ** precision
  393. if precision:
  394. _remainder = ((numerator * multiplier) % (denominator * multiplier)) / multiplier
  395. else:
  396. _remainder = numerator % denominator
  397. return flt(_remainder, precision)
  398. def safe_div(numerator, denominator, precision=2):
  399. """
  400. SafeMath division that returns zero when divided by zero.
  401. """
  402. precision = cint(precision)
  403. if denominator == 0:
  404. _res = 0.0
  405. else:
  406. _res = float(numerator) / denominator
  407. return flt(_res, precision)
  408. def round_based_on_smallest_currency_fraction(value, currency, precision=2):
  409. smallest_currency_fraction_value = flt(frappe.db.get_value("Currency",
  410. currency, "smallest_currency_fraction_value", cache=True))
  411. if smallest_currency_fraction_value:
  412. remainder_val = remainder(value, smallest_currency_fraction_value, precision)
  413. if remainder_val > (smallest_currency_fraction_value / 2):
  414. value += smallest_currency_fraction_value - remainder_val
  415. else:
  416. value -= remainder_val
  417. else:
  418. value = rounded(value)
  419. return flt(value, precision)
  420. def encode(obj, encoding="utf-8"):
  421. if isinstance(obj, list):
  422. out = []
  423. for o in obj:
  424. if isinstance(o, text_type):
  425. out.append(o.encode(encoding))
  426. else:
  427. out.append(o)
  428. return out
  429. elif isinstance(obj, text_type):
  430. return obj.encode(encoding)
  431. else:
  432. return obj
  433. def parse_val(v):
  434. """Converts to simple datatypes from SQL query results"""
  435. if isinstance(v, (datetime.date, datetime.datetime)):
  436. v = text_type(v)
  437. elif isinstance(v, datetime.timedelta):
  438. v = ":".join(text_type(v).split(":")[:2])
  439. elif isinstance(v, integer_types):
  440. v = int(v)
  441. return v
  442. def fmt_money(amount, precision=None, currency=None):
  443. """
  444. Convert to string with commas for thousands, millions etc
  445. """
  446. number_format = frappe.db.get_default("number_format") or "#,###.##"
  447. if precision is None:
  448. precision = cint(frappe.db.get_default('currency_precision')) or None
  449. decimal_str, comma_str, number_format_precision = get_number_format_info(number_format)
  450. if precision is None:
  451. precision = number_format_precision
  452. # 40,000 -> 40,000.00
  453. # 40,000.00000 -> 40,000.00
  454. # 40,000.23000 -> 40,000.23
  455. if isinstance(amount, string_types):
  456. amount = flt(amount, precision)
  457. if decimal_str:
  458. decimals_after = str(round(amount % 1, precision))
  459. parts = decimals_after.split('.')
  460. parts = parts[1] if len(parts) > 1 else parts[0]
  461. decimals = parts
  462. if precision > 2:
  463. if len(decimals) < 3:
  464. if currency:
  465. fraction = frappe.db.get_value("Currency", currency, "fraction_units", cache=True) or 100
  466. precision = len(cstr(fraction)) - 1
  467. else:
  468. precision = number_format_precision
  469. elif len(decimals) < precision:
  470. precision = len(decimals)
  471. amount = '%.*f' % (precision, round(flt(amount), precision))
  472. if amount.find('.') == -1:
  473. decimals = ''
  474. else:
  475. decimals = amount.split('.')[1]
  476. parts = []
  477. minus = ''
  478. if flt(amount) < 0:
  479. minus = '-'
  480. amount = cstr(abs(flt(amount))).split('.')[0]
  481. if len(amount) > 3:
  482. parts.append(amount[-3:])
  483. amount = amount[:-3]
  484. val = number_format=="#,##,###.##" and 2 or 3
  485. while len(amount) > val:
  486. parts.append(amount[-val:])
  487. amount = amount[:-val]
  488. parts.append(amount)
  489. parts.reverse()
  490. amount = comma_str.join(parts) + ((precision and decimal_str) and (decimal_str + decimals) or "")
  491. if amount != '0':
  492. amount = minus + amount
  493. if currency and frappe.defaults.get_global_default("hide_currency_symbol") != "Yes":
  494. symbol = frappe.db.get_value("Currency", currency, "symbol", cache=True) or currency
  495. amount = symbol + " " + amount
  496. return amount
  497. number_format_info = {
  498. "#,###.##": (".", ",", 2),
  499. "#.###,##": (",", ".", 2),
  500. "# ###.##": (".", " ", 2),
  501. "# ###,##": (",", " ", 2),
  502. "#'###.##": (".", "'", 2),
  503. "#, ###.##": (".", ", ", 2),
  504. "#,##,###.##": (".", ",", 2),
  505. "#,###.###": (".", ",", 3),
  506. "#.###": ("", ".", 0),
  507. "#,###": ("", ",", 0)
  508. }
  509. def get_number_format_info(format):
  510. return number_format_info.get(format) or (".", ",", 2)
  511. #
  512. # convert currency to words
  513. #
  514. def money_in_words(number, main_currency = None, fraction_currency=None):
  515. """
  516. Returns string in words with currency and fraction currency.
  517. """
  518. from frappe.utils import get_defaults
  519. _ = frappe._
  520. try:
  521. # note: `flt` returns 0 for invalid input and we don't want that
  522. number = float(number)
  523. except ValueError:
  524. return ""
  525. number = flt(number)
  526. if number < 0:
  527. return ""
  528. d = get_defaults()
  529. if not main_currency:
  530. main_currency = d.get('currency', 'INR')
  531. if not fraction_currency:
  532. fraction_currency = frappe.db.get_value("Currency", main_currency, "fraction", cache=True) or _("Cent")
  533. number_format = frappe.db.get_value("Currency", main_currency, "number_format", cache=True) or \
  534. frappe.db.get_default("number_format") or "#,###.##"
  535. fraction_length = get_number_format_info(number_format)[2]
  536. n = "%.{0}f".format(fraction_length) % number
  537. numbers = n.split('.')
  538. main, fraction = numbers if len(numbers) > 1 else [n, '00']
  539. if len(fraction) < fraction_length:
  540. zeros = '0' * (fraction_length - len(fraction))
  541. fraction += zeros
  542. in_million = True
  543. if number_format == "#,##,###.##": in_million = False
  544. # 0.00
  545. if main == '0' and fraction in ['00', '000']:
  546. out = "{0} {1}".format(main_currency, _('Zero'))
  547. # 0.XX
  548. elif main == '0':
  549. out = _(in_words(fraction, in_million).title()) + ' ' + fraction_currency
  550. else:
  551. out = main_currency + ' ' + _(in_words(main, in_million).title())
  552. if cint(fraction):
  553. out = out + ' ' + _('and') + ' ' + _(in_words(fraction, in_million).title()) + ' ' + fraction_currency
  554. return out + ' ' + _('only.')
  555. #
  556. # convert number to words
  557. #
  558. def in_words(integer, in_million=True):
  559. """
  560. Returns string in words for the given integer.
  561. """
  562. locale = 'en_IN' if not in_million else frappe.local.lang
  563. integer = int(integer)
  564. try:
  565. ret = num2words(integer, lang=locale)
  566. except NotImplementedError:
  567. ret = num2words(integer, lang='en')
  568. except OverflowError:
  569. ret = num2words(integer, lang='en')
  570. return ret.replace('-', ' ')
  571. def is_html(text):
  572. if not isinstance(text, frappe.string_types):
  573. return False
  574. return re.search('<[^>]+>', text)
  575. def is_image(filepath):
  576. from mimetypes import guess_type
  577. # filepath can be https://example.com/bed.jpg?v=129
  578. filepath = filepath.split('?')[0]
  579. return (guess_type(filepath)[0] or "").startswith("image/")
  580. def get_thumbnail_base64_for_image(src):
  581. from PIL import Image
  582. from frappe.core.doctype.file.file import get_local_image
  583. from frappe import safe_decode, cache
  584. if not src:
  585. frappe.throw('Invalid source for image: {0}'.format(src))
  586. if not src.startswith('/files') or '..' in src:
  587. return
  588. def _get_base64():
  589. try:
  590. image, unused_filename, extn = get_local_image(src)
  591. except IOError:
  592. return
  593. original_size = image.size
  594. size = 50, 50
  595. image.thumbnail(size, Image.ANTIALIAS)
  596. base64_string = image_to_base64(image, extn)
  597. return {
  598. 'base64': 'data:image/{0};base64,{1}'.format(extn, safe_decode(base64_string)),
  599. 'width': original_size[0],
  600. 'height': original_size[1]
  601. }
  602. return cache().hget('thumbnail_base64', src, generator=_get_base64)
  603. def image_to_base64(image, extn):
  604. import base64
  605. from io import BytesIO
  606. buffered = BytesIO()
  607. if extn.lower() == 'jpg':
  608. extn = 'JPEG'
  609. image.save(buffered, extn)
  610. img_str = base64.b64encode(buffered.getvalue())
  611. return img_str
  612. # from Jinja2 code
  613. _striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
  614. def strip_html(text):
  615. """removes anything enclosed in and including <>"""
  616. return _striptags_re.sub("", text)
  617. def escape_html(text):
  618. if not isinstance(text, string_types):
  619. return text
  620. html_escape_table = {
  621. "&": "&amp;",
  622. '"': "&quot;",
  623. "'": "&apos;",
  624. ">": "&gt;",
  625. "<": "&lt;",
  626. }
  627. return "".join(html_escape_table.get(c,c) for c in text)
  628. def pretty_date(iso_datetime):
  629. """
  630. Takes an ISO time and returns a string representing how
  631. long ago the date represents.
  632. Ported from PrettyDate by John Resig
  633. """
  634. from frappe import _
  635. if not iso_datetime: return ''
  636. import math
  637. if isinstance(iso_datetime, string_types):
  638. iso_datetime = datetime.datetime.strptime(iso_datetime, DATETIME_FORMAT)
  639. now_dt = datetime.datetime.strptime(now(), DATETIME_FORMAT)
  640. dt_diff = now_dt - iso_datetime
  641. # available only in python 2.7+
  642. # dt_diff_seconds = dt_diff.total_seconds()
  643. dt_diff_seconds = dt_diff.days * 86400.0 + dt_diff.seconds
  644. dt_diff_days = math.floor(dt_diff_seconds / 86400.0)
  645. # differnt cases
  646. if dt_diff_seconds < 60.0:
  647. return _('just now')
  648. elif dt_diff_seconds < 120.0:
  649. return _('1 minute ago')
  650. elif dt_diff_seconds < 3600.0:
  651. return _('{0} minutes ago').format(cint(math.floor(dt_diff_seconds / 60.0)))
  652. elif dt_diff_seconds < 7200.0:
  653. return _('1 hour ago')
  654. elif dt_diff_seconds < 86400.0:
  655. return _('{0} hours ago').format(cint(math.floor(dt_diff_seconds / 3600.0)))
  656. elif dt_diff_days == 1.0:
  657. return _('Yesterday')
  658. elif dt_diff_days < 7.0:
  659. return _('{0} days ago').format(cint(dt_diff_days))
  660. elif dt_diff_days < 12:
  661. return _('1 week ago')
  662. elif dt_diff_days < 31.0:
  663. return _('{0} weeks ago').format(cint(math.ceil(dt_diff_days / 7.0)))
  664. elif dt_diff_days < 46:
  665. return _('1 month ago')
  666. elif dt_diff_days < 365.0:
  667. return _('{0} months ago').format(cint(math.ceil(dt_diff_days / 30.0)))
  668. elif dt_diff_days < 550.0:
  669. return _('1 year ago')
  670. else:
  671. return '{0} years ago'.format(cint(math.floor(dt_diff_days / 365.0)))
  672. def comma_or(some_list, add_quotes=True):
  673. return comma_sep(some_list, frappe._("{0} or {1}"), add_quotes)
  674. def comma_and(some_list ,add_quotes=True):
  675. return comma_sep(some_list, frappe._("{0} and {1}"), add_quotes)
  676. def comma_sep(some_list, pattern, add_quotes=True):
  677. if isinstance(some_list, (list, tuple)):
  678. # list(some_list) is done to preserve the existing list
  679. some_list = [text_type(s) for s in list(some_list)]
  680. if not some_list:
  681. return ""
  682. elif len(some_list) == 1:
  683. return some_list[0]
  684. else:
  685. some_list = ["'%s'" % s for s in some_list] if add_quotes else ["%s" % s for s in some_list]
  686. return pattern.format(", ".join(frappe._(s) for s in some_list[:-1]), some_list[-1])
  687. else:
  688. return some_list
  689. def new_line_sep(some_list):
  690. if isinstance(some_list, (list, tuple)):
  691. # list(some_list) is done to preserve the existing list
  692. some_list = [text_type(s) for s in list(some_list)]
  693. if not some_list:
  694. return ""
  695. elif len(some_list) == 1:
  696. return some_list[0]
  697. else:
  698. some_list = ["%s" % s for s in some_list]
  699. return format("\n ".join(some_list))
  700. else:
  701. return some_list
  702. def filter_strip_join(some_list, sep):
  703. """given a list, filter None values, strip spaces and join"""
  704. return (cstr(sep)).join((cstr(a).strip() for a in filter(None, some_list)))
  705. def get_url(uri=None, full_address=False):
  706. """get app url from request"""
  707. host_name = frappe.local.conf.host_name or frappe.local.conf.hostname
  708. if uri and (uri.startswith("http://") or uri.startswith("https://")):
  709. return uri
  710. if not host_name:
  711. request_host_name = get_host_name_from_request()
  712. if request_host_name:
  713. host_name = request_host_name
  714. elif frappe.local.site:
  715. protocol = 'http://'
  716. if frappe.local.conf.ssl_certificate:
  717. protocol = 'https://'
  718. elif frappe.local.conf.wildcard:
  719. domain = frappe.local.conf.wildcard.get('domain')
  720. if domain and frappe.local.site.endswith(domain) and frappe.local.conf.wildcard.get('ssl_certificate'):
  721. protocol = 'https://'
  722. host_name = protocol + frappe.local.site
  723. else:
  724. host_name = frappe.db.get_value("Website Settings", "Website Settings",
  725. "subdomain")
  726. if not host_name:
  727. host_name = "http://localhost"
  728. if host_name and not (host_name.startswith("http://") or host_name.startswith("https://")):
  729. host_name = "http://" + host_name
  730. if not uri and full_address:
  731. uri = frappe.get_request_header("REQUEST_URI", "")
  732. port = frappe.conf.http_port or frappe.conf.webserver_port
  733. 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:
  734. host_name = host_name + ':' + str(port)
  735. url = urljoin(host_name, uri) if uri else host_name
  736. return url
  737. def get_host_name_from_request():
  738. if hasattr(frappe.local, "request") and frappe.local.request and frappe.local.request.host:
  739. protocol = 'https://' if 'https' == frappe.get_request_header('X-Forwarded-Proto', "") else 'http://'
  740. return protocol + frappe.local.request.host
  741. def url_contains_port(url):
  742. parts = url.split(':')
  743. return len(parts) > 2
  744. def get_host_name():
  745. return get_url().rsplit("//", 1)[-1]
  746. def get_link_to_form(doctype, name, label=None):
  747. if not label: label = name
  748. return """<a href="{0}">{1}</a>""".format(get_url_to_form(doctype, name), label)
  749. def get_link_to_report(name, label=None, report_type=None, doctype=None, filters=None):
  750. if not label: label = name
  751. if filters:
  752. conditions = []
  753. for k,v in iteritems(filters):
  754. if isinstance(v, list):
  755. for value in v:
  756. conditions.append(str(k)+'='+'["'+str(value[0]+'"'+','+'"'+str(value[1])+'"]'))
  757. else:
  758. conditions.append(str(k)+"="+str(v))
  759. filters = "&".join(conditions)
  760. return """<a href='{0}'>{1}</a>""".format(get_url_to_report_with_filters(name, filters, report_type, doctype), label)
  761. else:
  762. return """<a href='{0}'>{1}</a>""".format(get_url_to_report(name, report_type, doctype), label)
  763. def get_absolute_url(doctype, name):
  764. return "desk#Form/{0}/{1}".format(quoted(doctype), quoted(name))
  765. def get_url_to_form(doctype, name):
  766. return get_url(uri = "desk#Form/{0}/{1}".format(quoted(doctype), quoted(name)))
  767. def get_url_to_list(doctype):
  768. return get_url(uri = "desk#List/{0}".format(quoted(doctype)))
  769. def get_url_to_report(name, report_type = None, doctype = None):
  770. if report_type == "Report Builder":
  771. return get_url(uri = "desk#Report/{0}/{1}".format(quoted(doctype), quoted(name)))
  772. else:
  773. return get_url(uri = "desk#query-report/{0}".format(quoted(name)))
  774. def get_url_to_report_with_filters(name, filters, report_type = None, doctype = None):
  775. if report_type == "Report Builder":
  776. return get_url(uri = "desk#Report/{0}?{1}".format(quoted(doctype), filters))
  777. else:
  778. return get_url(uri = "desk#query-report/{0}?{1}".format(quoted(name), filters))
  779. operator_map = {
  780. # startswith
  781. "^": lambda a, b: (a or "").startswith(b),
  782. # in or not in a list
  783. "in": lambda a, b: operator.contains(b, a),
  784. "not in": lambda a, b: not operator.contains(b, a),
  785. # comparison operators
  786. "=": lambda a, b: operator.eq(a, b),
  787. "!=": lambda a, b: operator.ne(a, b),
  788. ">": lambda a, b: operator.gt(a, b),
  789. "<": lambda a, b: operator.lt(a, b),
  790. ">=": lambda a, b: operator.ge(a, b),
  791. "<=": lambda a, b: operator.le(a, b),
  792. "not None": lambda a, b: a and True or False,
  793. "None": lambda a, b: (not a) and True or False
  794. }
  795. def evaluate_filters(doc, filters):
  796. '''Returns true if doc matches filters'''
  797. if isinstance(filters, dict):
  798. for key, value in iteritems(filters):
  799. f = get_filter(None, {key:value})
  800. if not compare(doc.get(f.fieldname), f.operator, f.value):
  801. return False
  802. elif isinstance(filters, (list, tuple)):
  803. for d in filters:
  804. f = get_filter(None, d)
  805. if not compare(doc.get(f.fieldname), f.operator, f.value):
  806. return False
  807. return True
  808. def compare(val1, condition, val2):
  809. ret = False
  810. if condition in operator_map:
  811. ret = operator_map[condition](val1, val2)
  812. return ret
  813. def get_filter(doctype, f, filters_config=None):
  814. """Returns a _dict like
  815. {
  816. "doctype":
  817. "fieldname":
  818. "operator":
  819. "value":
  820. }
  821. """
  822. from frappe.model import default_fields, optional_fields
  823. if isinstance(f, dict):
  824. key, value = next(iter(f.items()))
  825. f = make_filter_tuple(doctype, key, value)
  826. if not isinstance(f, (list, tuple)):
  827. frappe.throw(frappe._("Filter must be a tuple or list (in a list)"))
  828. if len(f) == 3:
  829. f = (doctype, f[0], f[1], f[2])
  830. elif len(f) > 4:
  831. f = f[0:4]
  832. elif len(f) != 4:
  833. frappe.throw(frappe._("Filter must have 4 values (doctype, fieldname, operator, value): {0}").format(str(f)))
  834. f = frappe._dict(doctype=f[0], fieldname=f[1], operator=f[2], value=f[3])
  835. sanitize_column(f.fieldname)
  836. if not f.operator:
  837. # if operator is missing
  838. f.operator = "="
  839. valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", "is",
  840. "between", "descendants of", "ancestors of", "not descendants of", "not ancestors of",
  841. "timespan", "previous", "next")
  842. if filters_config:
  843. additional_operators = []
  844. for key in filters_config:
  845. additional_operators.append(key.lower())
  846. valid_operators = tuple(set(valid_operators + tuple(additional_operators)))
  847. if f.operator.lower() not in valid_operators:
  848. frappe.throw(frappe._("Operator must be one of {0}").format(", ".join(valid_operators)))
  849. if f.doctype and (f.fieldname not in default_fields + optional_fields):
  850. # verify fieldname belongs to the doctype
  851. meta = frappe.get_meta(f.doctype)
  852. if not meta.has_field(f.fieldname):
  853. # try and match the doctype name from child tables
  854. for df in meta.get_table_fields():
  855. if frappe.get_meta(df.options).has_field(f.fieldname):
  856. f.doctype = df.options
  857. break
  858. return f
  859. def make_filter_tuple(doctype, key, value):
  860. '''return a filter tuple like [doctype, key, operator, value]'''
  861. if isinstance(value, (list, tuple)):
  862. return [doctype, key, value[0], value[1]]
  863. else:
  864. return [doctype, key, "=", value]
  865. def make_filter_dict(filters):
  866. '''convert this [[doctype, key, operator, value], ..]
  867. to this { key: (operator, value), .. }
  868. '''
  869. _filter = frappe._dict()
  870. for f in filters:
  871. _filter[f[1]] = (f[2], f[3])
  872. return _filter
  873. def sanitize_column(column_name):
  874. from frappe import _
  875. regex = re.compile("^.*[,'();].*")
  876. blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case', 'and', 'or']
  877. def _raise_exception():
  878. frappe.throw(_("Invalid field name {0}").format(column_name), frappe.DataError)
  879. if 'ifnull' in column_name:
  880. if regex.match(column_name):
  881. # to avoid and, or
  882. if any(' {0} '.format(keyword) in column_name.split() for keyword in blacklisted_keywords):
  883. _raise_exception()
  884. # to avoid select, delete, drop, update and case
  885. elif any(keyword in column_name.split() for keyword in blacklisted_keywords):
  886. _raise_exception()
  887. elif regex.match(column_name):
  888. _raise_exception()
  889. def scrub_urls(html):
  890. html = expand_relative_urls(html)
  891. # encoding should be responsibility of the composer
  892. # html = quote_urls(html)
  893. return html
  894. def expand_relative_urls(html):
  895. # expand relative urls
  896. url = get_url()
  897. if url.endswith("/"): url = url[:-1]
  898. def _expand_relative_urls(match):
  899. to_expand = list(match.groups())
  900. if not to_expand[2].startswith('mailto') and not to_expand[2].startswith('data:'):
  901. if not to_expand[2].startswith("/"):
  902. to_expand[2] = "/" + to_expand[2]
  903. to_expand.insert(2, url)
  904. if 'url' in to_expand[0] and to_expand[1].startswith('(') and to_expand[-1].endswith(')'):
  905. # background-image: url('/assets/...') - workaround for wkhtmltopdf print-media-type
  906. to_expand.append(' !important')
  907. return "".join(to_expand)
  908. html = re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?!http)[^\'" >]+)([\'"]?)', _expand_relative_urls, html)
  909. # background-image: url('/assets/...')
  910. html = re.sub('(:[\s]?url)(\([\'"]?)((?!http)[^\'" >]+)([\'"]?\))', _expand_relative_urls, html)
  911. return html
  912. def quoted(url):
  913. return cstr(quote(encode(url), safe=b"~@#$&()*!+=:;,.?/'"))
  914. def quote_urls(html):
  915. def _quote_url(match):
  916. groups = list(match.groups())
  917. groups[2] = quoted(groups[2])
  918. return "".join(groups)
  919. return re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?:http)[^\'">]+)([\'"]?)',
  920. _quote_url, html)
  921. def unique(seq):
  922. """use this instead of list(set()) to preserve order of the original list.
  923. Thanks to Stackoverflow: http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order"""
  924. seen = set()
  925. seen_add = seen.add
  926. return [ x for x in seq if not (x in seen or seen_add(x)) ]
  927. def strip(val, chars=None):
  928. # \ufeff is no-width-break, \u200b is no-width-space
  929. return (val or "").replace("\ufeff", "").replace("\u200b", "").strip(chars)
  930. def to_markdown(html):
  931. text = None
  932. try:
  933. text = html2text(html or '')
  934. except HTMLParser.HTMLParseError:
  935. pass
  936. return text
  937. def md_to_html(markdown_text):
  938. extras = {
  939. 'fenced-code-blocks': None,
  940. 'tables': None,
  941. 'header-ids': None,
  942. 'toc': None,
  943. 'highlightjs-lang': None,
  944. 'html-classes': {
  945. 'table': 'table table-bordered',
  946. 'img': 'screenshot'
  947. }
  948. }
  949. html = None
  950. try:
  951. html = markdown(markdown_text or '', extras=extras)
  952. except MarkdownError:
  953. pass
  954. return html
  955. def is_subset(list_a, list_b):
  956. '''Returns whether list_a is a subset of list_b'''
  957. return len(list(set(list_a) & set(list_b))) == len(list_a)
  958. def generate_hash(*args, **kwargs):
  959. return frappe.generate_hash(*args, **kwargs)
  960. def guess_date_format(date_string):
  961. DATE_FORMATS = [
  962. r"%d-%m-%Y",
  963. r"%m-%d-%Y",
  964. r"%Y-%m-%d",
  965. r"%d-%m-%y",
  966. r"%m-%d-%y",
  967. r"%y-%m-%d",
  968. r"%d/%m/%Y",
  969. r"%m/%d/%Y",
  970. r"%Y/%m/%d",
  971. r"%d/%m/%y",
  972. r"%m/%d/%y",
  973. r"%y/%m/%d",
  974. r"%d.%m.%Y",
  975. r"%m.%d.%Y",
  976. r"%Y.%m.%d",
  977. r"%d.%m.%y",
  978. r"%m.%d.%y",
  979. r"%y.%m.%d",
  980. r"%d %b %Y",
  981. r"%d %B %Y",
  982. ]
  983. TIME_FORMATS = [
  984. r"%H:%M:%S.%f",
  985. r"%H:%M:%S",
  986. r"%H:%M",
  987. r"%I:%M:%S.%f %p",
  988. r"%I:%M:%S %p",
  989. r"%I:%M %p",
  990. ]
  991. def _get_date_format(date_str):
  992. for f in DATE_FORMATS:
  993. try:
  994. # if date is parsed without any exception
  995. # capture the date format
  996. datetime.datetime.strptime(date_str, f)
  997. return f
  998. except ValueError:
  999. pass
  1000. def _get_time_format(time_str):
  1001. for f in TIME_FORMATS:
  1002. try:
  1003. # if time is parsed without any exception
  1004. # capture the time format
  1005. datetime.datetime.strptime(time_str, f)
  1006. return f
  1007. except ValueError:
  1008. pass
  1009. date_format = None
  1010. time_format = None
  1011. date_string = date_string.strip()
  1012. # check if date format can be guessed
  1013. date_format = _get_date_format(date_string)
  1014. if date_format:
  1015. return date_format
  1016. # date_string doesnt look like date, it can have a time part too
  1017. # split the date string into date and time parts
  1018. if " " in date_string:
  1019. date_str, time_str = date_string.split(" ", 1)
  1020. date_format = _get_date_format(date_str) or ''
  1021. time_format = _get_time_format(time_str) or ''
  1022. if date_format and time_format:
  1023. return (date_format + ' ' + time_format).strip()
  1024. def validate_json_string(string):
  1025. try:
  1026. json.loads(string)
  1027. except (TypeError, ValueError):
  1028. raise frappe.ValidationError