Merge branch 'develop' into fix-note-2
This commit is contained in:
commit
122b3a88e4
31 changed files with 335 additions and 89 deletions
|
|
@ -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">
|
||||
|
|
|
|||
44
cypress/integration/rounding.js
Normal file
44
cypress/integration/rounding.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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(),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -273,6 +273,10 @@ class ExecutableNotFound(FileNotFoundError):
|
|||
pass
|
||||
|
||||
|
||||
class InvalidRoundingMethod(FileNotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRemoteException(Exception):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue