Merge branch 'develop' into fix-note-2

This commit is contained in:
Raffael Meyer 2023-03-08 15:38:26 +01:00 committed by GitHub
commit 122b3a88e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 335 additions and 89 deletions

View file

@ -23,8 +23,8 @@
<a href="https://frappeframework.com/docs">
<img src="https://img.shields.io/badge/docs-%F0%9F%93%96-success.svg"/>
</a>
<a href="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml/badge.svg">
<a href="https://github.com/frappe/frappe/actions/workflows/server-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/server-tests.yml/badge.svg">
</a>
<a href="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml/badge.svg?branch=develop">

View file

@ -0,0 +1,44 @@
context("Rounding behaviour", () => {
before(() => {
cy.login();
cy.visit("/app/");
});
it("Rounds floats accurately", () => {
cy.window()
.its("flt")
.then((flt) => {
let rounding_method = "Rounding Half Away From Zero";
expect(flt("0.5", 0, null, rounding_method)).eq(1);
expect(flt("0.3", null, null, rounding_method)).eq(0.3);
expect(flt("1.5", 0, null, rounding_method)).eq(2);
// positive rounding to integers
expect(flt(0.4, 0, null, rounding_method)).eq(0);
expect(flt(0.5, 0, null, rounding_method)).eq(1);
expect(flt(1.455, 0, null, rounding_method)).eq(1);
expect(flt(1.5, 0, null, rounding_method)).eq(2);
// negative rounding to integers
expect(flt(-0.5, 0, null, rounding_method)).eq(-1);
expect(flt(-1.5, 0, null, rounding_method)).eq(-2);
// negative precision i.e. round to nearest 10th
expect(flt(123, -1, null, rounding_method)).eq(120);
expect(flt(125, -1, null, rounding_method)).eq(130);
expect(flt(134.45, -1, null, rounding_method)).eq(130);
expect(flt(135, -1, null, rounding_method)).eq(140);
// positive multiple digit rounding
expect(flt(1.25, 1, null, rounding_method)).eq(1.3);
expect(flt(0.15, 1, null, rounding_method)).eq(0.2);
expect(flt(2.675, 2, null, rounding_method)).eq(2.68);
// negative multiple digit rounding
expect(flt(-1.25, 1, null, rounding_method)).eq(-1.3);
expect(flt(-0.15, 1, null, rounding_method)).eq(-0.2);
});
});
});

View file

@ -21,7 +21,7 @@ from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
)
from frappe.translate import get_lang_dict, get_messages_for_boot, get_translated_doctypes
from frappe.utils import add_user_info, cstr, get_time_zone
from frappe.utils import add_user_info, cstr, get_system_timezone
from frappe.utils.change_log import get_versions
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
@ -402,9 +402,9 @@ def get_link_title_doctypes():
def set_time_zone(bootinfo):
bootinfo.time_zone = {
"system": get_time_zone(),
"system": get_system_timezone(),
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None)
or get_time_zone(),
or get_system_timezone(),
}

View file

@ -554,9 +554,6 @@ def update_parent_document_on_communication(doc):
parent.db_set("status", "Open")
parent.run_method("handle_hold_time", "Replied")
apply_assignment_rule(parent)
else:
# update the modified date for document
parent.update_modified()
update_first_response_time(parent, doc)
set_avg_response_time(parent, doc)

View file

@ -44,6 +44,14 @@ frappe.ui.form.on("Report", {
doc.disabled ? "fa fa-check" : "fa fa-off"
);
}
frm.set_query("ref_doctype", () => {
return {
filters: {
istable: 0,
},
};
});
},
ref_doctype: function (frm) {

View file

@ -15,7 +15,7 @@ from frappe.model.document import Document
from frappe.utils import (
cint,
compare,
convert_utc_to_user_timezone,
convert_utc_to_system_timezone,
create_batch,
make_filter_dict,
)
@ -132,14 +132,14 @@ def serialize_job(job: Job) -> frappe._dict:
queue=job.origin.rsplit(":", 1)[1],
job_name=job_name,
status=job.get_status(),
started_at=convert_utc_to_user_timezone(job.started_at) if job.started_at else "",
ended_at=convert_utc_to_user_timezone(job.ended_at) if job.ended_at else "",
started_at=convert_utc_to_system_timezone(job.started_at) if job.started_at else "",
ended_at=convert_utc_to_system_timezone(job.ended_at) if job.ended_at else "",
time_taken=(job.ended_at - job.started_at).total_seconds() if job.ended_at else "",
exc_info=job.exc_info,
arguments=frappe.as_json(job.kwargs),
timeout=job.timeout,
creation=convert_utc_to_user_timezone(job.created_at),
modified=convert_utc_to_user_timezone(modified),
creation=convert_utc_to_system_timezone(job.created_at),
modified=convert_utc_to_system_timezone(modified),
_comment_count=0,
owner=job.kwargs.get("user"),
modified_by=job.kwargs.get("user"),

View file

@ -8,7 +8,7 @@ from rq import Worker
import frappe
from frappe.model.document import Document
from frappe.utils import cint, convert_utc_to_user_timezone
from frappe.utils import cint, convert_utc_to_system_timezone
from frappe.utils.background_jobs import get_workers
@ -66,14 +66,14 @@ def serialize_worker(worker: Worker) -> frappe._dict:
status=worker.get_state(),
pid=worker.pid,
current_job_id=worker.get_current_job_id(),
last_heartbeat=convert_utc_to_user_timezone(worker.last_heartbeat),
birth_date=convert_utc_to_user_timezone(worker.birth_date),
last_heartbeat=convert_utc_to_system_timezone(worker.last_heartbeat),
birth_date=convert_utc_to_system_timezone(worker.birth_date),
successful_job_count=worker.successful_job_count,
failed_job_count=worker.failed_job_count,
total_working_time=worker.total_working_time,
_comment_count=0,
modified=convert_utc_to_user_timezone(worker.last_heartbeat),
creation=convert_utc_to_user_timezone(worker.birth_date),
modified=convert_utc_to_system_timezone(worker.last_heartbeat),
creation=convert_utc_to_system_timezone(worker.birth_date),
utilization_percent=compute_utilization(worker),
)

View file

@ -39,4 +39,21 @@ frappe.ui.form.on("System Settings", {
first_day_of_the_week(frm) {
frm.re_setup_moment = true;
},
rounding_method: function (frm) {
if (frm.doc.rounding_method == frappe.boot.sysdefaults.rounding_method) return;
let msg = __(
"Changing rounding method on site with data can result in unexpected behaviour."
);
msg += "<br>";
msg += __("Do you still want to proceed?");
frappe.confirm(
msg,
() => {},
() => {
frm.set_value("rounding_method", frappe.boot.sysdefaults.rounding_method);
}
);
},
});

View file

@ -17,10 +17,11 @@
"date_format",
"time_format",
"number_format",
"first_day_of_the_week",
"column_break_7",
"float_precision",
"currency_precision",
"first_day_of_the_week",
"rounding_method",
"sec_backup_limit",
"backup_limit",
"encrypt_backup",
@ -520,12 +521,19 @@
"fieldname": "login_with_email_link_expiry",
"fieldtype": "Int",
"label": "Login with email link expiry (in minutes)"
},
{
"default": "Round Half Even",
"fieldname": "rounding_method",
"fieldtype": "Select",
"label": "Rounding Method",
"options": "Round Half Even\nRounding Half Away From Zero"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2022-12-20 21:45:37.651668",
"modified": "2023-03-06 11:31:19.144956",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@ -544,4 +552,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View file

@ -23,7 +23,7 @@ from frappe.utils import (
flt,
format_datetime,
get_formatted_email,
get_time_zone,
get_system_timezone,
has_gravatar,
now_datetime,
today,
@ -647,7 +647,7 @@ class User(Document):
def set_time_zone(self):
if not self.time_zone:
self.time_zone = get_time_zone()
self.time_zone = get_system_timezone()
@frappe.whitelist()

View file

@ -995,6 +995,9 @@ class Database:
obj.on_rollback()
frappe.local.rollback_observers = []
frappe.local.realtime_log = []
frappe.flags.enqueue_after_commit = []
def field_exists(self, dt, fn):
"""Return true of field exists."""
return self.exists("DocField", {"fieldname": fn, "parent": dt})

View file

@ -4,13 +4,13 @@ import os
import frappe
from frappe import _
from frappe.utils import cint, get_site_path, get_url
from frappe.utils.data import convert_utc_to_user_timezone
from frappe.utils.data import convert_utc_to_system_timezone
def get_context(context):
def get_time(path):
dt = os.path.getmtime(path)
return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime(
return convert_utc_to_system_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime(
"%a %b %d %H:%M %Y"
)

View file

@ -22,7 +22,7 @@ from frappe.email.oauth import Oauth
from frappe.utils import (
add_days,
cint,
convert_utc_to_user_timezone,
convert_utc_to_system_timezone,
cstr,
extract_email_id,
get_datetime,
@ -458,7 +458,7 @@ class Email:
try:
utc = email.utils.mktime_tz(email.utils.parsedate_tz(self.mail["Date"]))
utc_dt = datetime.datetime.utcfromtimestamp(utc)
self.date = convert_utc_to_user_timezone(utc_dt).strftime("%Y-%m-%d %H:%M:%S")
self.date = convert_utc_to_system_timezone(utc_dt).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
self.date = now()
else:

View file

@ -273,6 +273,10 @@ class ExecutableNotFound(FileNotFoundError):
pass
class InvalidRoundingMethod(FileNotFoundError):
pass
class InvalidRemoteException(Exception):
pass

View file

@ -21,7 +21,7 @@ from frappe.utils import (
add_to_date,
get_datetime,
get_request_site_address,
get_time_zone,
get_system_timezone,
get_weekdays,
now_datetime,
)
@ -575,14 +575,14 @@ def google_calendar_to_repeat_on(start, end, recurrence=None):
get_datetime(start.get("date"))
if start.get("date")
else parser.parse(start.get("dateTime"))
.astimezone(ZoneInfo(get_time_zone()))
.astimezone(ZoneInfo(get_system_timezone()))
.replace(tzinfo=None)
),
"ends_on": (
get_datetime(end.get("date"))
if end.get("date")
else parser.parse(end.get("dateTime"))
.astimezone(ZoneInfo(get_time_zone()))
.astimezone(ZoneInfo(get_system_timezone()))
.replace(tzinfo=None)
),
"all_day": 1 if start.get("date") else 0,
@ -648,11 +648,11 @@ def format_date_according_to_google_calendar(all_day, starts_on, ends_on=None):
date_format = {
"start": {
"dateTime": starts_on.isoformat(),
"timeZone": get_time_zone(),
"timeZone": get_system_timezone(),
},
"end": {
"dateTime": ends_on.isoformat(),
"timeZone": get_time_zone(),
"timeZone": get_system_timezone(),
},
}

View file

@ -8,7 +8,7 @@ import pytz
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, get_time_zone
from frappe.utils import cint, cstr, get_system_timezone
class TokenCache(Document):
@ -52,7 +52,7 @@ class TokenCache(Document):
return self
def get_expires_in(self):
system_timezone = pytz.timezone(get_time_zone())
system_timezone = pytz.timezone(get_system_timezone())
modified = frappe.utils.get_datetime(self.modified)
modified = system_timezone.localize(modified)
expiry_utc = modified.astimezone(pytz.utc) + timedelta(seconds=self.expires_in)

View file

@ -11,6 +11,7 @@ from oauthlib.openid import RequestValidator
import frappe
from frappe.auth import LoginManager
from frappe.utils.data import get_system_timezone
class OAuthWebRequestValidator(RequestValidator):
@ -248,7 +249,7 @@ class OAuthWebRequestValidator(RequestValidator):
# Remember to check expiration and scope membership
otoken = frappe.get_doc("OAuth Bearer Token", token)
token_expiration_local = otoken.expiration_time.replace(
tzinfo=pytz.timezone(frappe.utils.get_time_zone())
tzinfo=pytz.timezone(get_system_timezone())
)
token_expiration_utc = token_expiration_local.astimezone(pytz.utc)
is_token_valid = (

View file

@ -221,3 +221,4 @@ execute:frappe.delete_doc("Page", "activity", force=1)
frappe.patches.v14_0.disable_email_accounts_with_oauth
execute:frappe.delete_doc("Page", "translation-tool", force=1)
frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
frappe.patches.v14_0.remove_manage_subscriptions_from_navbar

View file

@ -0,0 +1,10 @@
import frappe
def execute():
navbar_settings = frappe.get_single("Navbar Settings")
for i, l in enumerate(navbar_settings.settings_dropdown):
if l.item_label == "Manage Subscriptions":
navbar_settings.settings_dropdown.pop(i)
navbar_settings.save()
break

View file

@ -5,7 +5,7 @@ import "./datatype";
if (!window.frappe) window.frappe = {};
function flt(v, decimals, number_format) {
function flt(v, decimals, number_format, rounding_method) {
if (v == null || v == "") return 0;
if (!(typeof v === "number" || String(parseFloat(v)) == v)) {
@ -30,7 +30,7 @@ function flt(v, decimals, number_format) {
}
v = parseFloat(v);
if (decimals != null) return _round(v, decimals);
if (decimals != null) return _round(v, decimals, rounding_method);
return v;
}
@ -173,16 +173,40 @@ function get_number_format_info(format) {
return info;
}
function _round(num, precision) {
var is_negative = num < 0 ? true : false;
var d = cint(precision);
var m = Math.pow(10, d);
var n = +(d ? Math.abs(num) * m : Math.abs(num)).toFixed(8); // Avoid rounding errors
var i = Math.floor(n),
f = n - i;
var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n);
r = d ? r / m : r;
return is_negative ? -r : r;
function _round(num, precision, rounding_method) {
rounding_method =
rounding_method || frappe.boot.sysdefaults.rounding_method || "Round Half Even";
let is_negative = num < 0 ? true : false;
if (rounding_method == "Round Half Even") {
var d = cint(precision);
var m = Math.pow(10, d);
var n = +(d ? Math.abs(num) * m : Math.abs(num)).toFixed(8); // Avoid rounding errors
var i = Math.floor(n),
f = n - i;
var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n);
r = d ? r / m : r;
return is_negative ? -r : r;
} else if (rounding_method == "Rounding Half Away From Zero") {
if (num == 0) return 0.0;
let digits = cint(precision);
let multiplier = Math.pow(10, digits);
num = num * multiplier;
// For explanation of this method read python flt implementation notes.
let epsilon = 2.0 ** (Math.log2(Math.abs(num)) - 52.0);
if (is_negative) {
epsilon = -1 * epsilon;
}
num = Math.round(num + epsilon);
return num / multiplier;
} else {
throw new Error(`Unknown rounding method ${rounding_method}`);
}
}
function roundNumber(num, precision) {

View file

@ -241,9 +241,6 @@ export default class QuickListWidget extends Widget {
this.footer.empty();
let filters = frappe.utils.get_filter_from_json(this.quick_list_filter);
if (filters) {
frappe.route_options = filters;
}
let route = frappe.utils.generate_route({ type: "doctype", name: this.document_type });
this.see_all_button = $(`
<div class="see-all btn btn-xs">${__("View List")}</div>
@ -253,6 +250,9 @@ export default class QuickListWidget extends Widget {
if (e.ctrlKey || e.metaKey) {
frappe.open_in_new_tab = true;
}
if (filters) {
frappe.route_options = filters;
}
frappe.set_route(route);
});
}

View file

@ -6,25 +6,28 @@ import json
import os
import sys
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from decimal import ROUND_HALF_UP, Decimal, localcontext
from enum import Enum
from io import StringIO
from mimetypes import guess_type
from unittest.mock import patch
import pytz
from hypothesis import given
from hypothesis import strategies as st
from PIL import Image
import frappe
from frappe.installer import parse_app_name
from frappe.model.document import Document
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import (
ceil,
dict_to_str,
evaluate_filters,
execute_in_shell,
floor,
flt,
format_timedelta,
get_bench_path,
get_file_timestamp,
@ -1001,3 +1004,78 @@ class TestTBSanitization(FrappeTestCase):
self.assertIn("********", traceback)
self.assertIn("password =", traceback)
self.assertIn("safe_value", traceback)
class TestRounding(FrappeTestCase):
@change_settings("System Settings", {"rounding_method": "Rounding Half Away From Zero"})
def test_normal_rounding(self):
self.assertEqual(flt("what"), 0)
self.assertEqual(flt("0.5", 0), 1)
self.assertEqual(flt("0.3"), 0.3)
self.assertEqual(flt("1.5", 0), 2)
# positive rounding to integers
self.assertEqual(flt(0.4, 0), 0)
self.assertEqual(flt(0.5, 0), 1)
self.assertEqual(flt(1.455, 0), 1)
self.assertEqual(flt(1.5, 0), 2)
# negative rounding to integers
self.assertEqual(flt(-0.5, 0), -1)
self.assertEqual(flt(-1.5, 0), -2)
# negative precision i.e. round to nearest 10th
self.assertEqual(flt(123, -1), 120)
self.assertEqual(flt(125, -1), 130)
self.assertEqual(flt(134.45, -1), 130)
self.assertEqual(flt(135, -1), 140)
# positive multiple digit rounding
self.assertEqual(flt(1.25, 1), 1.3)
self.assertEqual(flt(0.15, 1), 0.2)
# negative multiple digit rounding
self.assertEqual(flt(-1.25, 1), -1.3)
self.assertEqual(flt(-0.15, 1), -0.2)
def test_normal_rounding_as_argument(self):
rounding_method = "Rounding Half Away From Zero"
self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 1)
self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3)
self.assertEqual(flt("1.5", 0, rounding_method=rounding_method), 2)
# positive rounding to integers
self.assertEqual(flt(0.4, 0, rounding_method=rounding_method), 0)
self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 1)
self.assertEqual(flt(1.455, 0, rounding_method=rounding_method), 1)
self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2)
# negative rounding to integers
self.assertEqual(flt(-0.5, 0, rounding_method=rounding_method), -1)
self.assertEqual(flt(-1.5, 0, rounding_method=rounding_method), -2)
# negative precision i.e. round to nearest 10th
self.assertEqual(flt(123, -1, rounding_method=rounding_method), 120)
self.assertEqual(flt(125, -1, rounding_method=rounding_method), 130)
self.assertEqual(flt(134.45, -1, rounding_method=rounding_method), 130)
self.assertEqual(flt(135, -1, rounding_method=rounding_method), 140)
# positive multiple digit rounding
self.assertEqual(flt(1.25, 1, rounding_method=rounding_method), 1.3)
self.assertEqual(flt(0.15, 1, rounding_method=rounding_method), 0.2)
self.assertEqual(flt(2.675, 2, rounding_method=rounding_method), 2.68)
# negative multiple digit rounding
self.assertEqual(flt(-1.25, 1, rounding_method=rounding_method), -1.3)
self.assertEqual(flt(-0.15, 1, rounding_method=rounding_method), -0.2)
@change_settings("System Settings", {"rounding_method": "Rounding Half Away From Zero"})
@given(st.decimals(min_value=-1e8, max_value=1e8), st.integers(min_value=-2, max_value=4))
def test_normal_rounding_property(self, number, precision):
with localcontext() as ctx:
ctx.rounding = ROUND_HALF_UP
self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision))

View file

@ -331,6 +331,17 @@ class TestWebsite(FrappeTestCase):
self.assertIn("test.__test", content)
self.assertNotIn("frappe.exceptions.ValidationError: Illegal template", content)
def test_never_render(self):
from pathlib import Path
from random import choices
WWW = Path(frappe.get_app_path("frappe")) / "www"
FILES_TO_SKIP = choices(list(WWW.glob("**/*.py*")), k=10)
for suffix in FILES_TO_SKIP:
content = get_response_content(suffix.relative_to(WWW))
self.assertIn("404", content)
def test_metatags(self):
content = get_response_content("/_test/_test_metatags")
self.assertIn('<meta name="title" content="Test Title Metatag">', content)

View file

@ -18,6 +18,7 @@ from click import secho
import frappe
from frappe.desk.utils import slug
from frappe.utils.deprecations import deprecation_warning
DateTimeLikeObject = Union[str, datetime.date, datetime.datetime]
NumericType = Union[int, float]
@ -304,7 +305,7 @@ def time_diff_in_hours(string_ed_date, string_st_date):
def now_datetime():
dt = convert_utc_to_user_timezone(datetime.datetime.utcnow())
dt = convert_utc_to_system_timezone(datetime.datetime.utcnow())
return dt.replace(tzinfo=None)
@ -317,15 +318,15 @@ def get_eta(from_time, percent_complete):
return str(datetime.timedelta(seconds=(100 - percent_complete) / percent_complete * diff))
def _get_time_zone():
def _get_system_timezone():
return frappe.db.get_system_setting("time_zone") or "Asia/Kolkata" # Default to India ?!
def get_time_zone():
def get_system_timezone():
if frappe.local.flags.in_test:
return _get_time_zone()
return _get_system_timezone()
return frappe.cache().get_value("time_zone", _get_time_zone)
return frappe.cache().get_value("time_zone", _get_system_timezone)
def convert_utc_to_timezone(utc_timestamp, time_zone):
@ -343,8 +344,8 @@ def get_datetime_in_timezone(time_zone):
return convert_utc_to_timezone(utc_timestamp, time_zone)
def convert_utc_to_user_timezone(utc_timestamp):
time_zone = get_time_zone()
def convert_utc_to_system_timezone(utc_timestamp):
time_zone = get_system_timezone()
return convert_utc_to_timezone(utc_timestamp, time_zone)
@ -919,7 +920,9 @@ def flt(s: NumericType | str, precision: int | None = None) -> float:
...
def flt(s: NumericType | str, precision: int | None = None) -> float:
def flt(
s: NumericType | str, precision: int | None = None, rounding_method: str | None = None
) -> float:
"""Convert to float (ignoring commas in string)
:param s: Number in string or other numeric format.
@ -945,8 +948,10 @@ def flt(s: NumericType | str, precision: int | None = None) -> float:
try:
num = float(s)
if precision is not None:
num = rounded(num, precision)
except Exception:
num = rounded(num, precision, rounding_method)
except Exception as e:
if isinstance(e, frappe.InvalidRoundingMethod):
raise
num = 0.0
return num
@ -1045,12 +1050,28 @@ def sbool(x: str) -> bool | Any:
return x
def rounded(num, precision=0):
"""round method for round halfs to nearest even algorithm aka banker's rounding - compatible with python3"""
def rounded(num, precision=0, rounding_method=None):
"""Round according to method set in system setting, defaults to banker's rounding"""
precision = cint(precision)
multiplier = 10**precision
rounding_method = (
rounding_method or frappe.get_system_settings("rounding_method") or "Round Half Even"
)
if rounding_method == "Round Half Even":
return _round_half_even(num, precision)
elif rounding_method == "Rounding Half Away From Zero":
return _round_away_from_zero(num, precision)
else:
frappe.throw(
frappe._("Unknown Rounding Method: {}").format(rounding_method),
exc=frappe.InvalidRoundingMethod,
)
def _round_half_even(num, precision):
# avoid rounding errors
multiplier = 10**precision
num = round(num * multiplier if precision else num, 8)
floor_num = math.floor(num)
@ -1067,6 +1088,32 @@ def rounded(num, precision=0):
return (num / multiplier) if precision else num
def _round_away_from_zero(num, precision):
if num == 0:
return 0.0
# Epsilon is small correctional value added to correctly round numbers which can't be
# represented in IEEE 754 representation.
# In simplified terms, the representation optimizes for absolute errors in representation
# so if a number is not representable it might be represented by a value ever so slighly
# smaller than the value itself. This becomes a problem when breaking ties for numbers
# ending with 5 when it's represented by a smaller number. By adding a very small value
# close to what's "least count" or smallest representable difference in the scale we force
# the number to be bigger than actual value, this increases representation error but
# removes rounding error.
# References:
# - https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
# - https://docs.python.org/3/tutorial/floatingpoint.html#representation-error
# - https://docs.python.org/3/library/functions.html#round
# - easier to understand: https://www.youtube.com/watch?v=pQs_wx8eoQ8
epsilon = 2.0 ** (math.log(abs(num), 2) - 52.0)
return round(num + math.copysign(epsilon, num), precision)
def remainder(numerator: NumericType, denominator: NumericType, precision: int = 2) -> NumericType:
precision = cint(precision)
multiplier = 10**precision

View file

@ -49,9 +49,8 @@ def get_monthly_goal_graph_data(
goal_doctype_link: str,
goal_field: str,
date_field: str,
filter_str: str = None,
aggregation: str = "sum",
filters: dict | None = None,
filters: str | dict | None = None,
) -> dict:
"""
Get month-wise graph data for a doctype based on aggregation values of a field in the goal doctype
@ -65,17 +64,11 @@ def get_monthly_goal_graph_data(
:param goal_doctype: doctype the goal is based on
:param goal_doctype_link: doctype link field in goal_doctype
:param goal_field: field from which the goal is calculated
:param filter_str: [DEPRECATED] where clause condition. Use filters.
:param aggregation: a value like 'count', 'sum', 'avg'
:param filters: optional filters
:return: dict of graph data
"""
if isinstance(filter_str, str):
frappe.throw(
"String filters have been deprecated. Pass Dict filters instead.", exc=DeprecationWarning
) # nosemgrep
doc = frappe.get_doc(doctype, docname)
doc.check_permission()

View file

@ -210,13 +210,6 @@ def add_standard_navbar_items():
"action": "frappe.ui.toolbar.route_to_user()",
"is_standard": 1,
},
{
"item_label": "Manage Subscriptions",
"item_type": "Action",
"action": "frappe.ui.toolbar.redirectToUrl()",
"hidden": 1,
"is_standard": 1,
},
{
"item_label": "Session Defaults",
"item_type": "Action",

View file

@ -445,8 +445,8 @@ VALID_UTILS = (
"now_datetime",
"get_timestamp",
"get_eta",
"get_time_zone",
"convert_utc_to_user_timezone",
"get_system_timezone",
"convert_utc_to_system_timezone",
"now",
"nowdate",
"today",

View file

@ -12,7 +12,7 @@
</div>
<div class="web-list-actions">
{%- if allow_multiple -%}
<a class="btn btn-primary btn-sm button-new" href="/{{ route }}/new">New</a>
<a class="btn btn-primary btn-sm button-new" href="/{{ route }}/new">{{ _("New") }}</a>
{%- endif -%}
</div>
</div>

View file

@ -1,5 +1,5 @@
import io
import os
from importlib.machinery import all_suffixes
import click
@ -17,6 +17,8 @@ from frappe.website.utils import (
is_binary_file,
)
PY_LOADER_SUFFIXES = tuple(all_suffixes())
WEBPAGE_PY_MODULE_PROPERTIES = (
"base_template_path",
"template",
@ -66,7 +68,11 @@ class TemplatePage(BaseTemplatePage):
return
def can_render(self):
return hasattr(self, "template_path") and bool(self.template_path)
return (
hasattr(self, "template_path")
and self.template_path
and not self.template_path.endswith(PY_LOADER_SUFFIXES)
)
@staticmethod
def get_index_path_options(search_path):

View file

@ -12,7 +12,7 @@ from werkzeug.wrappers import Response
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, get_assets_json, get_time_zone, md_to_html
from frappe.utils import cint, get_assets_json, get_system_timezone, md_to_html
FRONTMATTER_PATTERN = re.compile(r"^\s*(?:---|\+\+\+)(.*?)(?:---|\+\+\+)\s*(.+)$", re.S | re.M)
H1_TAG_PATTERN = re.compile("<h1>([^<]*)")
@ -167,8 +167,8 @@ def get_boot_data():
"time_format": frappe.get_system_settings("time_format") or "HH:mm:ss",
},
"time_zone": {
"system": get_time_zone(),
"user": frappe.db.get_value("User", frappe.session.user, "time_zone") or get_time_zone(),
"system": get_system_timezone(),
"user": frappe.db.get_value("User", frappe.session.user, "time_zone") or get_system_timezone(),
},
"assets_json": get_assets_json(),
}

View file

@ -9,7 +9,7 @@ readme = "README.md"
dynamic = ["version"]
dependencies = [
# core dependencies
"Babel~=2.9.0",
"Babel~=2.12.1",
"Click~=8.1.3",
"filelock~=3.8.0",
"GitPython~=3.1.30",
@ -102,3 +102,4 @@ Faker = "~=13.12.1"
pyngrok = "~=5.0.5"
unittest-xml-reporting = "~=3.0.4"
watchdog = "~=2.1.9"
hypothesis = "~=6.68.2"