Pārlūkot izejas kodu

Merge branch 'develop' into set-property-after-alert-fix

version-14
Suraj Shetty pirms 3 gadiem
committed by GitHub
vecāks
revīzija
7a25cc2ae9
Šim parakstam datu bāzē netika atrasta zināma atslēga GPG atslēgas ID: 4AEE18F83AFDEB23
20 mainītis faili ar 240 papildinājumiem un 138 dzēšanām
  1. +6
    -45
      .github/workflows/server-mariadb-tests.yml
  2. +11
    -1
      .github/workflows/server-postgres-tests.yml
  3. +3
    -1
      .mergify.yml
  4. +2
    -2
      README.md
  5. +9
    -0
      codecov.yml
  6. +4
    -0
      frappe/__init__.py
  7. +2
    -1
      frappe/coverage.py
  8. +2
    -19
      frappe/database/database.py
  9. +2
    -1
      frappe/desk/doctype/workspace/workspace.json
  10. +1
    -1
      frappe/desk/doctype/workspace/workspace.py
  11. +1
    -1
      frappe/model/base_document.py
  12. +6
    -6
      frappe/model/meta.py
  13. +0
    -4
      frappe/public/js/frappe/desk.js
  14. +1
    -0
      frappe/public/js/frappe/form/grid.js
  15. +52
    -45
      frappe/public/js/frappe/form/link_selector.js
  16. +8
    -1
      frappe/public/js/frappe/list/list_view.js
  17. +1
    -1
      frappe/public/scss/common/indicator.scss
  18. +42
    -3
      frappe/tests/test_utils.py
  19. +78
    -4
      frappe/utils/data.py
  20. +9
    -2
      frappe/utils/safe_exec.py

+ 6
- 45
.github/workflows/server-mariadb-tests.yml Parādīt failu

@@ -120,49 +120,10 @@ jobs:
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io

- name: Upload Coverage Data
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: upload-coverage-data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true

- run: echo ${{ steps.check-build.outputs.build }} > guess-the-fruit.txt
- uses: actions/upload-artifact@v1
with:
name: fruit
path: guess-the-fruit.txt

coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- uses: actions/download-artifact@v1
- name: Upload coverage data
uses: codecov/codecov-action@v2
with:
name: fruit
- run: echo "WILDCARD=$(cat fruit/guess-the-fruit.txt)" >> $GITHUB_ENV

- name: Clone
if: ${{ env.WILDCARD == 'strawberry' }}
uses: actions/checkout@v2

- name: Coveralls Finished
if: ${{ env.WILDCARD == 'strawberry' }}
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: MariaDB
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true

+ 11
- 1
.github/workflows/server-postgres-tests.yml Parādīt failu

@@ -3,6 +3,8 @@ name: Server
on:
pull_request:
workflow_dispatch:
push:
branches: [ develop ]

concurrency:
group: server-postgres-develop-${{ github.event.number }}
@@ -116,7 +118,15 @@ jobs:

- name: Run Tests
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
env:
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io

- name: Upload coverage data
uses: codecov/codecov-action@v2
with:
name: Postgres
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true

+ 3
- 1
.mergify.yml Parādīt failu

@@ -2,7 +2,9 @@ pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- and:
- author!=surajshetty3416
- and:
- author!=surajshetty3416
- author!=gavindsouza
- or:
- base=version-13
- base=version-12


+ 2
- 2
README.md Parādīt failu

@@ -26,8 +26,8 @@
<a href='https://www.codetriage.com/frappe/frappe'>
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
</a>
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'>
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'>
<a href="https://codecov.io/gh/frappe/frappe">
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
</a>
</div>



+ 9
- 0
codecov.yml Parādīt failu

@@ -0,0 +1,9 @@
codecov:
require_ci_to_pass: yes
status:
project:
default:
threshold: 0.5%
comment:
layout: "diff, flags, files"
require_changes: true

+ 4
- 0
frappe/__init__.py Parādīt failu

@@ -140,7 +140,11 @@ lang = local("lang")
if typing.TYPE_CHECKING:
from frappe.database.mariadb.database import MariaDBDatabase
from frappe.database.postgres.database import PostgresDatabase
from pypika import Query

db: typing.Union[MariaDBDatabase, PostgresDatabase]
qb: Query

# end: static analysis hack

def init(site, sites_path=None, new_site=False):


+ 2
- 1
frappe/coverage.py Parādīt failu

@@ -58,4 +58,5 @@ class CodeCoverage():
def __exit__(self, exc_type, exc_value, traceback):
if self.with_coverage:
self.coverage.stop()
self.coverage.save()
self.coverage.save()
self.coverage.xml_report()

+ 2
- 19
frappe/database/database.py Parādīt failu

@@ -14,7 +14,7 @@ import frappe.model.meta

from frappe import _
from time import time
from frappe.utils import now, getdate, cast_fieldtype, get_datetime, get_table_name
from frappe.utils import now, getdate, cast, get_datetime, get_table_name
from frappe.model.utils.link_count import flush_local_link_count


@@ -516,7 +516,6 @@ class Database(object):
FROM `tabSingles`
WHERE doctype = %s
""", doctype)
# result = _cast_result(doctype, result)

dict_ = frappe._dict(result)

@@ -557,7 +556,7 @@ class Database(object):
if not df:
frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName)

val = cast_fieldtype(df.fieldtype, val)
val = cast(df.fieldtype, val)

self.value_cache[doctype][fieldname] = val

@@ -1052,19 +1051,3 @@ def enqueue_jobs_after_commit():
q.enqueue_call(execute_job, timeout=job.get("timeout"),
kwargs=job.get("queue_args"))
frappe.flags.enqueue_after_commit = []

# Helpers
def _cast_result(doctype, result):
batch = [ ]

try:
for field, value in result:
df = frappe.get_meta(doctype).get_field(field)
if df:
value = cast_fieldtype(df.fieldtype, value)

batch.append(tuple([field, value]))
except frappe.exceptions.DoesNotExistError:
return result

return tuple(batch)

+ 2
- 1
frappe/desk/doctype/workspace/workspace.json Parādīt failu

@@ -242,6 +242,7 @@
"label": "Parent Page"
},
{
"default": "[]",
"fieldname": "content",
"fieldtype": "Long Text",
"hidden": 1,
@@ -265,7 +266,7 @@
}
],
"links": [],
"modified": "2021-08-19 12:51:00.233017",
"modified": "2021-08-30 18:47:18.227154",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",


+ 1
- 1
frappe/desk/doctype/workspace/workspace.py Parādīt failu

@@ -62,7 +62,7 @@ class Workspace(Document):
for link in self.links:
link = link.as_dict()
if link.type == "Card Break":
if card_links and (not current_card['only_for'] or current_card['only_for'] == frappe.get_system_settings('country')):
if card_links and (not current_card.get('only_for') or current_card.get('only_for') == frappe.get_system_settings('country')):
current_card['links'] = card_links
cards.append(current_card)



+ 1
- 1
frappe/model/base_document.py Parādīt failu

@@ -969,7 +969,7 @@ class BaseDocument(object):
return self.cast(val, df)

def cast(self, value, df):
return cast_fieldtype(df.fieldtype, value)
return cast_fieldtype(df.fieldtype, value, show_warning=False)

def _extract_images_from_text_editor(self):
from frappe.core.doctype.file.file import extract_images_from_doc


+ 6
- 6
frappe/model/meta.py Parādīt failu

@@ -16,7 +16,7 @@ Example:
'''
from datetime import datetime
import frappe, json, os
from frappe.utils import cstr, cint, cast_fieldtype
from frappe.utils import cstr, cint, cast
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields
from frappe.model.document import Document
from frappe.model.base_document import BaseDocument
@@ -322,24 +322,24 @@ class Meta(Document):

for ps in property_setters:
if ps.doctype_or_field=='DocType':
self.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
self.set(ps.property, cast(ps.property_type, ps.value))

elif ps.doctype_or_field=='DocField':
for d in self.fields:
if d.fieldname == ps.field_name:
d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
d.set(ps.property, cast(ps.property_type, ps.value))
break

elif ps.doctype_or_field=='DocType Link':
for d in self.links:
if d.name == ps.row_name:
d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
d.set(ps.property, cast(ps.property_type, ps.value))
break

elif ps.doctype_or_field=='DocType Action':
for d in self.actions:
if d.name == ps.row_name:
d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
d.set(ps.property, cast(ps.property_type, ps.value))
break

def add_custom_links_and_actions(self):
@@ -532,7 +532,7 @@ class Meta(Document):
label = link.group,
items = [link.parent_doctype or link.link_doctype]
))
if not link.is_child_table:
if link.link_fieldname != data.fieldname:
if data.fieldname:


+ 0
- 4
frappe/public/js/frappe/desk.js Parādīt failu

@@ -285,10 +285,6 @@ frappe.Application = class Application {
frappe.modules[page.module]=page;
frappe.workspaces[frappe.router.slug(page.title)] = page;
}
if (!frappe.workspaces['home']) {
// default workspace is settings for Frappe
frappe.workspaces['home'] = frappe.workspaces[Object.keys(frappe.workspaces)[0]];
}
}

load_user_permissions() {


+ 1
- 0
frappe/public/js/frappe/form/grid.js Parādīt failu

@@ -786,6 +786,7 @@ export default class Grid {
doctype: link_field.options,
fieldname: link,
qty_fieldname: qty,
get_query: link_field.get_query,
target: this,
txt: ""
});


+ 52
- 45
frappe/public/js/frappe/form/link_selector.js Parādīt failu

@@ -96,10 +96,10 @@ frappe.ui.form.LinkSelector = class LinkSelector {
.attr('data-value', v[0])
.click(function () {
var value = $(this).attr("data-value");
var $link = this;
if (me.target.is_grid) {
// set in grid
me.set_in_grid(value);
// call search after value is set to get latest filtered results
me.set_in_grid(value).then(() => me.search());
} else {
if (me.target.doctype)
me.target.parse_validate_and_set_in_model(value);
@@ -110,8 +110,8 @@ frappe.ui.form.LinkSelector = class LinkSelector {
me.dialog.hide();
}
return false;
})
})
});
});
} else {
$('<p><br><span class="text-muted">' + __("No Results") + '</span>'
+ (frappe.model.can_create(me.doctype) ?
@@ -130,49 +130,56 @@ frappe.ui.form.LinkSelector = class LinkSelector {
}, this.dialog.get_primary_btn());

}
set_in_grid (value) {
var me = this, updated = false;
var d = null;
if (this.qty_fieldname) {
frappe.prompt({
fieldname: "qty", fieldtype: "Float", label: "Qty",
"default": 1, reqd: 1
}, function (data) {
$.each(me.target.frm.doc[me.target.df.fieldname] || [], function (i, d) {
if (d[me.fieldname] === value) {
frappe.model.set_value(d.doctype, d.name, me.qty_fieldname, data.qty);
frappe.show_alert(__("Added {0} ({1})", [value, d[me.qty_fieldname]]));
updated = true;
return false;
set_in_grid(value) {
return new Promise((resolve) => {
if (this.qty_fieldname) {
frappe.prompt({
fieldname: "qty",
fieldtype: "Float",
label: "Qty",
default: 1,
reqd: 1
}, (data) => {
let updated = (this.target.frm.doc[this.target.df.fieldname] || []).some(d => {
if (d[this.fieldname] === value) {
frappe.model.set_value(d.doctype, d.name, this.qty_fieldname, data.qty).then(() => {
frappe.show_alert(__("Added {0} ({1})", [value, d[this.qty_fieldname]]));
resolve();
});
return true;
}
});
if (!updated) {
let d = null;
frappe.run_serially([
() => d = this.target.add_new_row(),
() => frappe.timeout(0.1),
() => {
let args = {};
args[this.fieldname] = value;
args[this.qty_fieldname] = data.qty;
return frappe.model.set_value(d.doctype, d.name, args);
},
() => frappe.show_alert(__("Added {0} ({1})", [value, data.qty])),
() => resolve()
]);
}
}, __("Set Quantity"), __("Set"));
} else if (this.dynamic_link_field) {
let d = this.target.add_new_row();
frappe.model.set_value(d.doctype, d.name, this.dynamic_link_field, this.dynamic_link_reference);
frappe.model.set_value(d.doctype, d.name, this.fieldname, value).then(() => {
frappe.show_alert(__("{0} {1} added", [this.dynamic_link_reference, value]));
resolve();
});
if (!updated) {
frappe.run_serially([
() => {
d = me.target.add_new_row();
},
() => frappe.timeout(0.1),
() => {
let args = {};
args[me.fieldname] = value;
args[me.qty_fieldname] = data.qty;

return frappe.model.set_value(d.doctype, d.name, args);
},
() => frappe.show_alert(__("Added {0} ({1})", [value, data.qty]))
]);
}
}, __("Set Quantity"), __("Set"));
} else if (me.dynamic_link_field) {
var d = me.target.add_new_row();
frappe.model.set_value(d.doctype, d.name, me.dynamic_link_field, me.dynamic_link_reference);
frappe.model.set_value(d.doctype, d.name, me.fieldname, value);
frappe.show_alert(__("{0} {1} added", [me.dynamic_link_reference, value]));
} else {
var d = me.target.add_new_row();
frappe.model.set_value(d.doctype, d.name, me.fieldname, value);
frappe.show_alert(__("{0} added", [value]));
}
} else {
let d = this.target.add_new_row();
frappe.model.set_value(d.doctype, d.name, this.fieldname, value).then(() => {
frappe.show_alert(__("{0} added", [value]));
resolve();
});
}
});
}
};



+ 8
- 1
frappe/public/js/frappe/list/list_view.js Parādīt failu

@@ -952,9 +952,16 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {

get_indicator_html(doc) {
const indicator = frappe.get_indicator(doc, this.doctype);
// sequence is important
const docstatus_description = [
__('Document is in draft state'),
__('Document has been submitted'),
__('Document has been cancelled')
];
const title = docstatus_description[doc.docstatus || 0];
if (indicator) {
return `<span class="indicator-pill ${indicator[1]} filterable ellipsis"
data-filter='${indicator[2]}'>
data-filter='${indicator[2]}' title='${title}'>
<span class="ellipsis"> ${__(indicator[0])}</span>
<span>`;
}


+ 1
- 1
frappe/public/scss/common/indicator.scss Parādīt failu

@@ -175,4 +175,4 @@

@keyframes blink {
50% { opacity: 0.5; }
}
}

+ 42
- 3
frappe/tests/test_utils.py Parādīt failu

@@ -6,12 +6,13 @@ import frappe
from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url
from frappe.utils import validate_url, validate_email_address
from frappe.utils import ceil, floor
from frappe.utils.data import validate_python_code
from frappe.utils.data import cast, validate_python_code

from PIL import Image
from frappe.utils.image import strip_exif_data, optimize_image
import io
from mimetypes import guess_type
from datetime import datetime, timedelta, date

class TestFilters(unittest.TestCase):
def test_simple_dict(self):
@@ -93,6 +94,45 @@ class TestDataManipulation(unittest.TestCase):
self.assertTrue('style="background-image: url(\'{0}/assets/frappe/bg.jpg\') !important"'.format(url) in html)
self.assertTrue('<a href="mailto:test@example.com">email</a>' in html)

class TestFieldCasting(unittest.TestCase):
def test_str_types(self):
STR_TYPES = (
"Data", "Text", "Small Text", "Long Text", "Text Editor", "Select", "Link", "Dynamic Link"
)
for fieldtype in STR_TYPES:
self.assertIsInstance(cast(fieldtype, value=None), str)
self.assertIsInstance(cast(fieldtype, value="12-12-2021"), str)
self.assertIsInstance(cast(fieldtype, value=""), str)
self.assertIsInstance(cast(fieldtype, value=[]), str)
self.assertIsInstance(cast(fieldtype, value=set()), str)

def test_float_types(self):
FLOAT_TYPES = ("Currency", "Float", "Percent")
for fieldtype in FLOAT_TYPES:
self.assertIsInstance(cast(fieldtype, value=None), float)
self.assertIsInstance(cast(fieldtype, value=1.12), float)
self.assertIsInstance(cast(fieldtype, value=112), float)

def test_int_types(self):
INT_TYPES = ("Int", "Check")

for fieldtype in INT_TYPES:
self.assertIsInstance(cast(fieldtype, value=None), int)
self.assertIsInstance(cast(fieldtype, value=1.12), int)
self.assertIsInstance(cast(fieldtype, value=112), int)

def test_datetime_types(self):
self.assertIsInstance(cast("Datetime", value=None), datetime)
self.assertIsInstance(cast("Datetime", value="12-2-22"), datetime)

def test_date_types(self):
self.assertIsInstance(cast("Date", value=None), date)
self.assertIsInstance(cast("Date", value="12-12-2021"), date)

def test_time_types(self):
self.assertIsInstance(cast("Time", value=None), timedelta)
self.assertIsInstance(cast("Time", value="12:03:34"), timedelta)

class TestMathUtils(unittest.TestCase):
def test_floor(self):
from decimal import Decimal
@@ -205,7 +245,6 @@ class TestImage(unittest.TestCase):
self.assertLess(len(optimized_content), len(original_content))

class TestPythonExpressions(unittest.TestCase):

def test_validation_for_good_python_expression(self):
valid_expressions = [
"foo == bar",
@@ -229,4 +268,4 @@ class TestPythonExpressions(unittest.TestCase):
"oops = forgot_equals",
]
for expr in invalid_expressions:
self.assertRaises(frappe.ValidationError, validate_python_code, expr)
self.assertRaises(frappe.ValidationError, validate_python_code, expr)

+ 78
- 4
frappe/utils/data.py Parādīt failu

@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from typing import Optional
import frappe
import operator
import json
@@ -8,6 +9,7 @@ import re, datetime, math, time
from code import compile_command
from urllib.parse import quote, urljoin
from frappe.desk.utils import slug
from click import secho

DATE_FORMAT = "%Y-%m-%d"
TIME_FORMAT = "%H:%M:%S.%f"
@@ -16,10 +18,10 @@ DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT

def is_invalid_date_string(date_string):
# dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00"
return (not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00"))
return not isinstance(date_string, str) or ((not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00")))

# datetime functions
def getdate(string_date=None):
def getdate(string_date: Optional[str] = None):
"""
Converts string date (yyyy-mm-dd) to datetime.date object.
If no input is provided, current date is returned.
@@ -67,6 +69,31 @@ def get_datetime(datetime_str=None):
except ValueError:
return parser.parse(datetime_str)

def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]:
"""Return `datetime.timedelta` object from string value of a
valid time format. Returns None if `time` is not a valid format

Args:
time (str): A valid time representation. This string is parsed
using `dateutil.parser.parse`. Examples of valid inputs are:
'0:0:0', '17:21:00', '2012-01-19 17:21:00'. Checkout
https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.parse

Returns:
datetime.timedelta: Timedelta object equivalent of the passed `time` string
"""
from dateutil import parser

time = time or "0:0:0"

try:
t = parser.parse(time)
return datetime.timedelta(
hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond
)
except Exception:
return None

def to_timedelta(time_str):
from dateutil import parser

@@ -505,7 +532,14 @@ def has_common(l1, l2):
"""Returns truthy value if there are common elements in lists l1 and l2"""
return set(l1) & set(l2)

def cast_fieldtype(fieldtype, value):
def cast_fieldtype(fieldtype, value, show_warning=True):
if show_warning:
message = (
"Function `frappe.utils.data.cast` has been deprecated in favour"
" of `frappe.utils.data.cast`. Use the newer util for safer type casting."
)
secho(message, fg="yellow")

if fieldtype in ("Currency", "Float", "Percent"):
value = flt(value)

@@ -527,6 +561,46 @@ def cast_fieldtype(fieldtype, value):

return value

def cast(fieldtype, value=None):
"""Cast the value to the Python native object of the Frappe fieldtype provided.
If value is None, the first/lowest value of the `fieldtype` will be returned.
If value can't be cast as fieldtype due to an invalid input, None will be returned.

Mapping of Python types => Frappe types:
* str => ("Data", "Text", "Small Text", "Long Text", "Text Editor", "Select", "Link", "Dynamic Link")
* float => ("Currency", "Float", "Percent")
* int => ("Int", "Check")
* datetime.datetime => ("Datetime",)
* datetime.date => ("Date",)
* datetime.time => ("Time",)
"""
if fieldtype in ("Currency", "Float", "Percent"):
value = flt(value)

elif fieldtype in ("Int", "Check"):
value = cint(value)

elif fieldtype in ("Data", "Text", "Small Text", "Long Text",
"Text Editor", "Select", "Link", "Dynamic Link"):
value = cstr(value)

elif fieldtype == "Date":
if value:
value = getdate(value)
else:
value = datetime.datetime(1, 1, 1).date()

elif fieldtype == "Datetime":
if value:
value = get_datetime(value)
else:
value = datetime.datetime(1, 1, 1)

elif fieldtype == "Time":
value = get_timedelta(value)

return value

def flt(s, precision=None):
"""Convert to float (ignoring commas in string)

@@ -1202,7 +1276,7 @@ def evaluate_filters(doc, filters):
def compare(val1, condition, val2, fieldtype=None):
ret = False
if fieldtype:
val2 = cast_fieldtype(fieldtype, val2)
val2 = cast(fieldtype, val2)
if condition in operator_map:
ret = operator_map[condition](val1, val2)



+ 9
- 2
frappe/utils/safe_exec.py Parādīt failu

@@ -30,8 +30,14 @@ class NamespaceDict(frappe._dict):


def safe_exec(script, _globals=None, _locals=None):
# script reports must be enabled via site_config.json
if not frappe.conf.server_script_enabled:
# server scripts can be disabled via site_config.json
# they are enabled by default
if 'server_script_enabled' in frappe.conf:
enabled = frappe.conf.server_script_enabled
else:
enabled = True

if not enabled:
frappe.throw(_('Please Enable Server Scripts'), ServerScriptNotEnabled)

# build globals
@@ -228,6 +234,7 @@ VALID_UTILS = (
"getdate",
"get_datetime",
"to_timedelta",
"get_timedelta",
"add_to_date",
"add_days",
"add_months",


Notiek ielāde…
Atcelt
Saglabāt