Merge branch 'develop' into feat-desk-refresh
This commit is contained in:
commit
1ebbbc772e
44 changed files with 600 additions and 237 deletions
21
.github/workflows/patch-mariadb-tests.yml
vendored
21
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -64,9 +64,7 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: |
|
||||
3.7
|
||||
3.10
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
|
|
@ -112,8 +110,8 @@ jobs:
|
|||
- name: Run Patch Tests
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
wget https://frappeframework.com/files/v10-frappe.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz
|
||||
wget https://frappeframework.com/files/v13-frappe.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v13-frappe.sql.gz
|
||||
|
||||
source env/bin/activate
|
||||
cd apps/frappe/
|
||||
|
|
@ -121,7 +119,6 @@ jobs:
|
|||
|
||||
function update_to_version() {
|
||||
version=$1
|
||||
py=$2
|
||||
|
||||
branch_name="version-$version-hotfix"
|
||||
echo "Updating to v$version"
|
||||
|
|
@ -130,22 +127,22 @@ jobs:
|
|||
|
||||
pgrep honcho | xargs kill
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env --python $py
|
||||
bench start &> ~/frappe-bench/bench_start.log &
|
||||
bench -v setup env
|
||||
bench start &>> ~/frappe-bench/bench_start.log &
|
||||
|
||||
bench --site test_site migrate
|
||||
}
|
||||
|
||||
update_to_version 12 python3.7
|
||||
update_to_version 13 python3.7
|
||||
|
||||
update_to_version 14 python3.10
|
||||
update_to_version 14
|
||||
|
||||
echo "Updating to last commit"
|
||||
pgrep honcho | xargs kill
|
||||
rm -rf ~/frappe-bench/env
|
||||
git checkout -q -f "$GITHUB_SHA"
|
||||
bench -v setup env
|
||||
bench start &>> ~/frappe-bench/bench_start.log &
|
||||
bench --site test_site migrate
|
||||
bench --site test_site execute frappe.tests.utils.check_orpahned_doctypes
|
||||
|
||||
- name: Show bench output
|
||||
if: ${{ always() }}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,17 @@ context("Control Currency", () => {
|
|||
df_options: { precision: 0 },
|
||||
blur_expected: "10",
|
||||
},
|
||||
{
|
||||
input: "10.000",
|
||||
number_format: "#.###,##",
|
||||
df_options: { precision: 0 },
|
||||
blur_expected: "10.000",
|
||||
},
|
||||
{
|
||||
input: "10.000",
|
||||
number_format: "#.###,##",
|
||||
blur_expected: "10.000,00",
|
||||
},
|
||||
{
|
||||
input: "10.101",
|
||||
df_options: { precision: "" },
|
||||
|
|
@ -61,6 +72,7 @@ context("Control Currency", () => {
|
|||
.then((frappe) => {
|
||||
frappe.boot.sysdefaults.currency = test_case.currency;
|
||||
frappe.boot.sysdefaults.currency_precision = test_case.default_precision ?? 2;
|
||||
frappe.boot.sysdefaults.number_format = test_case.number_format ?? "#,###.##";
|
||||
});
|
||||
|
||||
get_dialog_with_currency(test_case.df_options).as("dialog");
|
||||
|
|
|
|||
|
|
@ -83,6 +83,23 @@ context("Control Float", () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// '.' is the parseFloat's decimal separator
|
||||
number_format: "#.###,##",
|
||||
values: [
|
||||
{
|
||||
input: "12.345",
|
||||
blur_expected: "12.345,000",
|
||||
focus_expected: "12345",
|
||||
},
|
||||
{
|
||||
// parseFloat would reduce 12,340 to 12,34 if this string was ever to be parsed
|
||||
input: "12.340",
|
||||
blur_expected: "12.340,000",
|
||||
focus_expected: "12340",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ context("Rounding behaviour", () => {
|
|||
let rounding_method = "Banker's Rounding";
|
||||
|
||||
expect(flt("0.5", 0, null, rounding_method)).eq(0);
|
||||
expect(flt("0.3", null, rounding_method)).eq(0.3);
|
||||
expect(flt("0.3", null, null, rounding_method)).eq(0.3);
|
||||
|
||||
expect(flt("1.5", 0, null, rounding_method)).eq(2);
|
||||
|
||||
|
|
|
|||
|
|
@ -170,6 +170,8 @@ lang = local("lang")
|
|||
# This if block is never executed when running the code. It is only used for
|
||||
# telling static code analyzer where to find dynamically defined attributes.
|
||||
if TYPE_CHECKING:
|
||||
from werkzeug.wrappers import Request
|
||||
|
||||
from frappe.database.mariadb.database import MariaDBDatabase
|
||||
from frappe.database.postgres.database import PostgresDatabase
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -179,6 +181,15 @@ if TYPE_CHECKING:
|
|||
db: MariaDBDatabase | PostgresDatabase
|
||||
qb: MariaDB | Postgres
|
||||
cache: RedisWrapper
|
||||
response: _dict
|
||||
conf: _dict
|
||||
form_dict: _dict
|
||||
flags: _dict
|
||||
request: Request
|
||||
session: _dict
|
||||
user: str
|
||||
flags: _dict
|
||||
lang: str
|
||||
|
||||
|
||||
# end: static analysis hack
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@ from frappe.core.doctype.doctype.doctype import (
|
|||
WrongOptionsDoctypeLinkError,
|
||||
validate_links_table_fieldnames,
|
||||
)
|
||||
from frappe.core.doctype.rq_job.test_rq_job import wait_for_completion
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.desk.form.load import getdoc
|
||||
from frappe.model.delete_doc import delete_controllers
|
||||
from frappe.model.sync import remove_orphan_doctypes
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
|
|
@ -739,6 +742,21 @@ class TestDocType(FrappeTestCase):
|
|||
self.assertEqual(frappe.get_meta(doctype).get_field(field).default, "DELETETHIS")
|
||||
frappe.delete_doc("DocType", doctype)
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
|
||||
)
|
||||
@patch.dict(frappe.conf, {"developer_mode": 1})
|
||||
def test_delete_orphaned_doctypes(self):
|
||||
doctype = new_doctype(custom=0).insert()
|
||||
frappe.db.commit()
|
||||
|
||||
delete_controllers(doctype.name, doctype.module)
|
||||
job = frappe.enqueue(remove_orphan_doctypes)
|
||||
wait_for_completion(job)
|
||||
|
||||
frappe.db.rollback()
|
||||
self.assertFalse(frappe.db.exists("DocType", doctype.name))
|
||||
|
||||
def test_not_in_list_view_for_not_allowed_mandatory_field(self):
|
||||
doctype = new_doctype(
|
||||
fields=[
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class Report(Document):
|
|||
if (
|
||||
self.is_standard == "Yes"
|
||||
and not cint(getattr(frappe.local.conf, "developer_mode", 0))
|
||||
and not frappe.flags.in_patch
|
||||
and not (frappe.flags.in_migrate or frappe.flags.in_patch)
|
||||
):
|
||||
frappe.throw(_("You are not allowed to delete Standard Report"))
|
||||
delete_custom_role("report", self.name)
|
||||
|
|
|
|||
|
|
@ -15,16 +15,20 @@ from frappe.utils import cstr, execute_in_shell
|
|||
from frappe.utils.background_jobs import get_job_status, is_job_enqueued
|
||||
|
||||
|
||||
@timeout(seconds=20)
|
||||
def wait_for_completion(job: Job):
|
||||
while True:
|
||||
if not (job.is_queued or job.is_started):
|
||||
break
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
class TestRQJob(FrappeTestCase):
|
||||
BG_JOB = "frappe.core.doctype.rq_job.test_rq_job.test_func"
|
||||
|
||||
@timeout(seconds=20)
|
||||
def check_status(self, job: Job, status, wait=True):
|
||||
while wait:
|
||||
if not (job.is_queued or job.is_started):
|
||||
break
|
||||
time.sleep(0.2)
|
||||
|
||||
if wait:
|
||||
wait_for_completion(job)
|
||||
self.assertEqual(frappe.get_doc("RQ Job", job.id).status, status)
|
||||
|
||||
def test_serialization(self):
|
||||
|
|
|
|||
|
|
@ -44,22 +44,23 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
}
|
||||
|
||||
setup_page() {
|
||||
this.doctype_select = this.wrapper.page
|
||||
.add_select(
|
||||
__("Document Type"),
|
||||
[{ value: "", label: __("Select Document Type") + "..." }].concat(
|
||||
this.options.doctypes
|
||||
)
|
||||
)
|
||||
.change(function () {
|
||||
frappe.set_route("permission-manager", $(this).val());
|
||||
});
|
||||
this.doctype_select = this.wrapper.page.add_field({
|
||||
fieldname: "doctype_select",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Link",
|
||||
options: "DocType",
|
||||
change: function () {
|
||||
frappe.set_route("permission-manager", this.get_value());
|
||||
},
|
||||
});
|
||||
|
||||
this.role_select = this.wrapper.page
|
||||
.add_select(__("Roles"), [__("Select Role") + "..."].concat(this.options.roles))
|
||||
.change(() => {
|
||||
this.refresh();
|
||||
});
|
||||
this.role_select = this.wrapper.page.add_field({
|
||||
fieldname: "role_select",
|
||||
label: __("Roles"),
|
||||
fieldtype: "Link",
|
||||
options: "Role",
|
||||
change: () => this.refresh(),
|
||||
});
|
||||
|
||||
this.page.add_inner_button(__("Set User Permissions"), () => {
|
||||
return frappe.set_route("List", "User Permission");
|
||||
|
|
@ -76,13 +77,13 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
return;
|
||||
}
|
||||
if (frappe.get_route()[1]) {
|
||||
this.doctype_select.val(frappe.get_route()[1]);
|
||||
this.doctype_select.set_value(frappe.get_route()[1]);
|
||||
} else if (frappe.route_options) {
|
||||
if (frappe.route_options.doctype) {
|
||||
this.doctype_select.val(frappe.route_options.doctype);
|
||||
this.doctype_select.set_value(frappe.route_options.doctype);
|
||||
}
|
||||
if (frappe.route_options.role) {
|
||||
this.role_select.val(frappe.route_options.role);
|
||||
this.role_select.set_value(frappe.route_options.role);
|
||||
}
|
||||
frappe.route_options = null;
|
||||
}
|
||||
|
|
@ -140,13 +141,11 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
}
|
||||
|
||||
get_doctype() {
|
||||
let doctype = this.doctype_select.val();
|
||||
return this.doctype_select.get(0).selectedIndex == 0 ? null : doctype;
|
||||
return this.doctype_select.get_value();
|
||||
}
|
||||
|
||||
get_role() {
|
||||
let role = this.role_select.val();
|
||||
return this.role_select.get(0).selectedIndex == 0 ? null : role;
|
||||
return this.role_select.get_value();
|
||||
}
|
||||
|
||||
set_empty_message(message) {
|
||||
|
|
@ -292,6 +291,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
.attr("data-ptype", fieldname)
|
||||
.attr("data-role", d.role)
|
||||
.attr("data-permlevel", d.permlevel)
|
||||
.attr("data-if_owner", d.if_owner)
|
||||
.attr("data-doctype", d.parent);
|
||||
|
||||
checkbox.find("label").css("text-transform", "capitalize");
|
||||
|
|
@ -371,6 +371,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
doctype: d.parent,
|
||||
role: d.role,
|
||||
permlevel: d.permlevel,
|
||||
if_owner: d.if_owner,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.exc) {
|
||||
|
|
@ -399,6 +400,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
doctype: chk.attr("data-doctype"),
|
||||
ptype: chk.attr("data-ptype"),
|
||||
value: chk.prop("checked") ? 1 : 0,
|
||||
if_owner: chk.attr("data-if_owner"),
|
||||
};
|
||||
return frappe.call({
|
||||
module: "frappe.core",
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ def add(parent, role, permlevel):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update(doctype, role, permlevel, ptype, value=None):
|
||||
def update(doctype, role, permlevel, ptype, value=None, if_owner=0):
|
||||
"""Update role permission params
|
||||
|
||||
Args:
|
||||
|
|
@ -127,7 +127,7 @@ def update(doctype, role, permlevel, ptype, value=None):
|
|||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
frappe.only_for("System Manager")
|
||||
out = update_permission_property(doctype, role, permlevel, ptype, value)
|
||||
out = update_permission_property(doctype, role, permlevel, ptype, value, if_owner=if_owner)
|
||||
|
||||
frappe.db.after_commit.add(clear_cache)
|
||||
|
||||
|
|
@ -135,11 +135,14 @@ def update(doctype, role, permlevel, ptype, value=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove(doctype, role, permlevel):
|
||||
def remove(doctype, role, permlevel, if_owner=0):
|
||||
frappe.only_for("System Manager")
|
||||
setup_custom_perms(doctype)
|
||||
|
||||
frappe.db.delete("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel})
|
||||
frappe.db.delete(
|
||||
"Custom DocPerm",
|
||||
{"parent": doctype, "role": role, "permlevel": permlevel, "if_owner": if_owner},
|
||||
)
|
||||
|
||||
if not frappe.get_all("Custom DocPerm", {"parent": doctype}):
|
||||
frappe.throw(_("There must be atleast one permission rule."), title=_("Cannot Remove"))
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class ModuleOnboarding(Document):
|
|||
is_complete = [bool(step.is_complete or step.is_skipped) for step in steps]
|
||||
if all(is_complete):
|
||||
self.is_complete = True
|
||||
self.save()
|
||||
self.save(ignore_permissions=True)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
import typing
|
||||
from urllib.parse import quote
|
||||
|
||||
import frappe
|
||||
|
|
@ -14,6 +15,9 @@ from frappe.model.utils.user_settings import get_user_settings
|
|||
from frappe.permissions import get_doc_permissions
|
||||
from frappe.utils.data import cstr
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def getdoc(doctype, name, user=None):
|
||||
|
|
@ -117,7 +121,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
|||
"assignments": get_assignments(doc.doctype, doc.name),
|
||||
"permissions": get_doc_permissions(doc),
|
||||
"shared": get_docshares(doc),
|
||||
"views": get_view_logs(doc.doctype, doc.name),
|
||||
"views": get_view_logs(doc),
|
||||
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
|
||||
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
|
||||
"milestones": get_milestones(doc.doctype, doc.name),
|
||||
|
|
@ -149,29 +153,22 @@ def add_comments(doc, docinfo):
|
|||
)
|
||||
|
||||
for c in comments:
|
||||
if c.comment_type == "Comment":
|
||||
c.content = frappe.utils.markdown(c.content)
|
||||
docinfo.comments.append(c)
|
||||
|
||||
elif c.comment_type in ("Shared", "Unshared"):
|
||||
docinfo.shared.append(c)
|
||||
|
||||
elif c.comment_type in ("Assignment Completed", "Assigned"):
|
||||
docinfo.assignment_logs.append(c)
|
||||
|
||||
elif c.comment_type in ("Attachment", "Attachment Removed"):
|
||||
docinfo.attachment_logs.append(c)
|
||||
|
||||
elif c.comment_type in ("Info", "Edit", "Label"):
|
||||
docinfo.info_logs.append(c)
|
||||
|
||||
elif c.comment_type == "Like":
|
||||
docinfo.like_logs.append(c)
|
||||
|
||||
elif c.comment_type == "Workflow":
|
||||
docinfo.workflow_logs.append(c)
|
||||
|
||||
frappe.utils.add_user_info(c.owner, docinfo.user_info)
|
||||
match c.comment_type:
|
||||
case "Comment":
|
||||
c.content = frappe.utils.markdown(c.content)
|
||||
docinfo.comments.append(c)
|
||||
case "Shared" | "Unshared":
|
||||
docinfo.shared.append(c)
|
||||
case "Assignment Completed" | "Assigned":
|
||||
docinfo.assignment_logs.append(c)
|
||||
case "Attachment" | "Attachment Removed":
|
||||
docinfo.attachment_logs.append(c)
|
||||
case "Info" | "Edit" | "Label":
|
||||
docinfo.info_logs.append(c)
|
||||
case "Like":
|
||||
docinfo.like_logs.append(c)
|
||||
case "Workflow":
|
||||
docinfo.workflow_logs.append(c)
|
||||
|
||||
return comments
|
||||
|
||||
|
|
@ -192,7 +189,9 @@ def get_attachments(dt, dn):
|
|||
)
|
||||
|
||||
|
||||
def get_versions(doc):
|
||||
def get_versions(doc: "Document") -> list[dict]:
|
||||
if not doc.meta.track_changes:
|
||||
return []
|
||||
return frappe.get_all(
|
||||
"Version",
|
||||
filters=dict(ref_doctype=doc.doctype, docname=doc.name),
|
||||
|
|
@ -362,32 +361,29 @@ def run_onload(doc):
|
|||
doc.run_method("onload")
|
||||
|
||||
|
||||
def get_view_logs(doctype, docname):
|
||||
def get_view_logs(doc: "Document") -> list[dict]:
|
||||
"""get and return the latest view logs if available"""
|
||||
logs = []
|
||||
if getattr(frappe.get_meta(doctype), "track_views", None):
|
||||
view_logs = frappe.get_all(
|
||||
"View Log",
|
||||
filters={
|
||||
"reference_doctype": doctype,
|
||||
"reference_name": docname,
|
||||
},
|
||||
fields=["name", "creation", "owner"],
|
||||
order_by="creation desc",
|
||||
)
|
||||
if not doc.meta.track_views:
|
||||
return []
|
||||
|
||||
if view_logs:
|
||||
logs = view_logs
|
||||
return logs
|
||||
return frappe.get_all(
|
||||
"View Log",
|
||||
filters={
|
||||
"reference_doctype": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
},
|
||||
fields=["name", "creation", "owner"],
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
|
||||
def get_tags(doctype, name):
|
||||
tags = [
|
||||
tag.tag
|
||||
for tag in frappe.get_all(
|
||||
"Tag Link", filters={"document_type": doctype, "document_name": name}, fields=["tag"]
|
||||
)
|
||||
]
|
||||
def get_tags(doctype: str, name: str) -> str:
|
||||
tags = frappe.get_all(
|
||||
"Tag Link",
|
||||
filters={"document_type": doctype, "document_name": name},
|
||||
fields=["tag"],
|
||||
pluck="tag",
|
||||
)
|
||||
|
||||
return ",".join(tags)
|
||||
|
||||
|
|
@ -478,17 +474,20 @@ def send_link_titles(link_titles):
|
|||
|
||||
|
||||
def update_user_info(docinfo):
|
||||
for d in docinfo.communications:
|
||||
frappe.utils.add_user_info(d.sender, docinfo.user_info)
|
||||
users = set()
|
||||
|
||||
for d in docinfo.shared:
|
||||
frappe.utils.add_user_info(d.user, docinfo.user_info)
|
||||
users.update(d.sender for d in docinfo.communications)
|
||||
users.update(d.user for d in docinfo.shared)
|
||||
users.update(d.owner for d in docinfo.assignments)
|
||||
users.update(d.owner for d in docinfo.views)
|
||||
users.update(d.owner for d in docinfo.workflow_logs)
|
||||
users.update(d.owner for d in docinfo.like_logs)
|
||||
users.update(d.owner for d in docinfo.info_logs)
|
||||
users.update(d.owner for d in docinfo.attachment_logs)
|
||||
users.update(d.owner for d in docinfo.assignment_logs)
|
||||
users.update(d.owner for d in docinfo.comments)
|
||||
|
||||
for d in docinfo.assignments:
|
||||
frappe.utils.add_user_info(d.owner, docinfo.user_info)
|
||||
|
||||
for d in docinfo.views:
|
||||
frappe.utils.add_user_info(d.owner, docinfo.user_info)
|
||||
frappe.utils.add_user_info(users, docinfo.user_info)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"include_name_field": 0,
|
||||
"is_standard": 1,
|
||||
"list_name": "",
|
||||
"modified": "2023-08-24 11:01:18.688875",
|
||||
"modified": "2023-05-24 12:43:43.741781",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Main Workspace Tour",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"steps": [
|
||||
{
|
||||
"description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.",
|
||||
"element_selector": "#navbar-search[aria-expanded=\"true\"]",
|
||||
"element_selector": "#navbar-search",
|
||||
"fieldtype": "0",
|
||||
"has_next_condition": 0,
|
||||
"hide_buttons": 0,
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ def get_report_doc(report_name):
|
|||
|
||||
if doc.report_type == "Custom Report":
|
||||
custom_report_doc = doc
|
||||
reference_report = custom_report_doc.reference_report
|
||||
doc = frappe.get_doc("Report", reference_report)
|
||||
doc = get_reference_report(doc)
|
||||
doc.custom_report = report_name
|
||||
if custom_report_doc.json:
|
||||
data = json.loads(custom_report_doc.json)
|
||||
|
|
@ -172,9 +171,17 @@ def get_script(report_name):
|
|||
"html_format": html_format,
|
||||
"execution_time": frappe.cache.hget("report_execution_time", report_name) or 0,
|
||||
"filters": report.filters,
|
||||
"custom_report_name": report.name if report.get("is_custom_report") else None,
|
||||
}
|
||||
|
||||
|
||||
def get_reference_report(report):
|
||||
if report.report_type != "Custom Report":
|
||||
return report
|
||||
reference_report = frappe.get_doc("Report", report.reference_report)
|
||||
return get_reference_report(reference_report)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def run(
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ class SiteMigration:
|
|||
sync_customizations()
|
||||
sync_languages()
|
||||
flush_deferred_inserts()
|
||||
frappe.model.sync.remove_orphan_doctypes()
|
||||
|
||||
frappe.get_single("Portal Settings").sync_menu()
|
||||
frappe.get_single("Installed Applications").update_versions()
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ class Document(BaseDocument):
|
|||
def check_permission(self, permtype="read", permlevel=None):
|
||||
"""Raise `frappe.PermissionError` if not permitted"""
|
||||
if not self.has_permission(permtype):
|
||||
self.raise_no_permission_to(permlevel or permtype)
|
||||
self.raise_no_permission_to(permtype)
|
||||
|
||||
def has_permission(self, permtype="read") -> bool:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
import os
|
||||
|
||||
import frappe
|
||||
from frappe.cache_manager import clear_controller_cache
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.modules.import_file import import_file_by_path
|
||||
from frappe.modules.patch_handler import _patch_mode
|
||||
from frappe.utils import update_progress_bar
|
||||
|
|
@ -135,3 +137,37 @@ def get_doc_files(files, start_path):
|
|||
files.append(doc_path)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def remove_orphan_doctypes():
|
||||
"""Find and remove any orphaned doctypes.
|
||||
|
||||
These are doctypes for which code and schema file is
|
||||
deleted but entry is present in DocType table.
|
||||
|
||||
Note: Deleting the entry doesn't delete any data.
|
||||
So this is supposed to be non-destrictive operation.
|
||||
"""
|
||||
|
||||
doctype_names = frappe.get_all("DocType", {"custom": 0}, pluck="name")
|
||||
orphan_doctypes = []
|
||||
|
||||
clear_controller_cache()
|
||||
|
||||
for doctype in doctype_names:
|
||||
try:
|
||||
get_controller(doctype=doctype)
|
||||
except ImportError:
|
||||
orphan_doctypes.append(doctype)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not orphan_doctypes:
|
||||
return
|
||||
|
||||
print(f"Orphaned DocType(s) found: {', '.join(orphan_doctypes)}")
|
||||
for i, name in enumerate(orphan_doctypes):
|
||||
frappe.delete_doc("DocType", name, force=True, ignore_missing=True)
|
||||
update_progress_bar("Deleting orphaned DocTypes", i, len(orphan_doctypes))
|
||||
frappe.db.commit()
|
||||
print()
|
||||
|
|
|
|||
|
|
@ -228,6 +228,5 @@ frappe.patches.v15_0.remove_background_jobs_from_dropdown
|
|||
frappe.desk.doctype.form_tour.patches.introduce_ui_tours
|
||||
execute:frappe.delete_doc_if_exists("Workspace", "Customization")
|
||||
execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter")
|
||||
execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot")
|
||||
frappe.patches.v15_0.move_event_cancelled_to_status
|
||||
frappe.patches.v15_0.set_file_type
|
||||
|
|
|
|||
|
|
@ -546,13 +546,23 @@ def can_export(doctype, raise_exception=False):
|
|||
return has_access
|
||||
|
||||
|
||||
def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True):
|
||||
def update_permission_property(
|
||||
doctype,
|
||||
role,
|
||||
permlevel,
|
||||
ptype,
|
||||
value=None,
|
||||
validate=True,
|
||||
if_owner=0,
|
||||
):
|
||||
"""Update a property in Custom Perm"""
|
||||
from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype
|
||||
|
||||
out = setup_custom_perms(doctype)
|
||||
|
||||
name = frappe.db.get_value("Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel))
|
||||
name = frappe.db.get_value(
|
||||
"Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel, if_owner=if_owner)
|
||||
)
|
||||
table = DocType("Custom DocPerm")
|
||||
frappe.qb.update(table).set(ptype, value).where(table.name == name).run()
|
||||
|
||||
|
|
@ -579,6 +589,12 @@ def add_permission(doctype, role, permlevel=0, ptype=None):
|
|||
if frappe.db.get_value(
|
||||
"Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel, if_owner=0)
|
||||
):
|
||||
frappe.msgprint(
|
||||
_("Rule for this doctype, role, permlevel and if-owner combination already exists.").format(
|
||||
doctype,
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
if not ptype:
|
||||
|
|
|
|||
|
|
@ -120,6 +120,12 @@
|
|||
<path d="M3.14886 8.08422L5.23297 6.00012L3.14886 3.91602" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-external-link">
|
||||
<path d="M9.75003 7.83333V9C9.75003 9.82843 9.07846 10.5 8.25003 10.5H3.25C2.42157 10.5 1.75 9.82843 1.75 9V4C1.75 3.17158 2.42151 2.50001 3.24993 2.50001C3.62327 2.5 4.02808 2.5 4.4167 2.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.75 1.5H10.25V4.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.75 5L9.75 2" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" fill="#112B42" id="icon-up">
|
||||
<path d="M3 5h6L6 2 3 5z"></path>
|
||||
<path opacity=".5" d="M6 10l3-3H3l3 3z"></path>
|
||||
|
|
@ -302,7 +308,7 @@
|
|||
<path stroke="#E24C4C" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M13.2 14.4v8.571"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-external-link">
|
||||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-pen">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.348 3.207a1 1 0 0 1 1.415 0l1.03 1.03a1 1 0 0 1 0 1.415l-6.626 6.626L2.5 13.5l1.222-3.667 6.626-6.626z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 114 KiB |
|
|
@ -1,22 +1,40 @@
|
|||
<template>
|
||||
<div class="tree-node" :class="{ opened: node.open }">
|
||||
<span
|
||||
ref="reference"
|
||||
class="tree-link"
|
||||
@click="emit('node-click', node)"
|
||||
:class="{ active: node.value === selected_node.value }"
|
||||
:disabled="node.fetching"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
>
|
||||
<div v-html="icon"></div>
|
||||
<a class="tree-label">{{ node.label }}</a>
|
||||
<!-- Icon open File record in new tab -->
|
||||
<a
|
||||
v-if="node.is_leaf"
|
||||
:href="open_file(node.value)"
|
||||
:disabled="node.fetching"
|
||||
target="_blank"
|
||||
class="file-doc-link ml-2"
|
||||
v-html="frappe.utils.icon('external-link', 'sm')"
|
||||
@click.stop
|
||||
/>
|
||||
</span>
|
||||
<div v-if="node.file_url && frappe.utils.is_image_file(node.file_url)">
|
||||
<div v-show="isOpen" class="popover" ref="popover" role="tooltip">
|
||||
<img :src="node.file_url" />
|
||||
</div>
|
||||
</div>
|
||||
<ul class="tree-children" v-show="node.open">
|
||||
<TreeNode
|
||||
v-for="n in node.children"
|
||||
:key="n.value"
|
||||
:node="n"
|
||||
:selected_node="selected_node"
|
||||
@node-click="n => emit('node-click', n)"
|
||||
@load-more="n => emit('load-more', n)"
|
||||
@node-click="(n) => emit('node-click', n)"
|
||||
@load-more="(n) => emit('load-more', n)"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-xs btn-load-more"
|
||||
|
|
@ -32,7 +50,8 @@
|
|||
|
||||
<script setup>
|
||||
import TreeNode from "./TreeNode.vue";
|
||||
import { computed } from "vue";
|
||||
import { createPopper } from "@popperjs/core";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// props
|
||||
const props = defineProps({
|
||||
|
|
@ -50,7 +69,7 @@ let icon = computed(() => {
|
|||
open: frappe.utils.icon("folder-open", "md"),
|
||||
closed: frappe.utils.icon("folder-normal", "md"),
|
||||
leaf: frappe.utils.icon("primitive-dot", "xs"),
|
||||
search: frappe.utils.icon("search")
|
||||
search: frappe.utils.icon("search"),
|
||||
};
|
||||
|
||||
if (props.node.by_search) return icons.search;
|
||||
|
|
@ -59,6 +78,52 @@ let icon = computed(() => {
|
|||
return icons.closed;
|
||||
});
|
||||
|
||||
let open_file = (filename) => {
|
||||
return frappe.utils.get_form_link("File", filename);
|
||||
};
|
||||
|
||||
const reference = ref(null);
|
||||
const popover = ref(null);
|
||||
let isOpen = ref(false);
|
||||
|
||||
let popper = ref(null);
|
||||
|
||||
function setupPopper() {
|
||||
if (!popper.value) {
|
||||
popper.value = createPopper(reference.value, popover.value, {
|
||||
placement: "top",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 4],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
popper.value.update();
|
||||
}
|
||||
}
|
||||
|
||||
let hoverTimer = null;
|
||||
let leaveTimer = null;
|
||||
|
||||
function onMouseover() {
|
||||
leaveTimer && clearTimeout(leaveTimer) && (leaveTimer = null);
|
||||
hoverTimer && clearTimeout(hoverTimer);
|
||||
hoverTimer = setTimeout(() => {
|
||||
isOpen.value = true;
|
||||
setupPopper();
|
||||
}, 800);
|
||||
}
|
||||
function onMouseleave() {
|
||||
hoverTimer && clearTimeout(hoverTimer) && (hoverTimer = null);
|
||||
leaveTimer && clearTimeout(leaveTimer);
|
||||
leaveTimer = setTimeout(() => {
|
||||
isOpen.value = false;
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -66,4 +131,7 @@ let icon = computed(() => {
|
|||
margin-left: 1.6rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.popover {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,44 @@
|
|||
<div class="clearfix"></div>
|
||||
{% for(var i=0, l=addr_list.length; i<l; i++) { %}
|
||||
<div class="address-box">
|
||||
<p class="h6">
|
||||
{%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
|
||||
<span class="text-muted">({%= __(addr_list[i].address_type) %})</span>{% } %}
|
||||
{% if(addr_list[i].is_primary_address) { %}
|
||||
<span class="text-muted">({%= __("Primary") %})</span>{% } %}
|
||||
{% if(addr_list[i].is_shipping_address) { %}
|
||||
<span class="text-muted">({%= __("Shipping") %})</span>{% } %}
|
||||
{% if(addr_list[i].disabled) { %}
|
||||
<span class="text-muted">({%= __("Disabled") %})</span>{% } %}
|
||||
{% for (const addr of addr_list) { %}
|
||||
<div class="address-box">
|
||||
<a
|
||||
href="{%= frappe.utils.get_form_link('Address', addr.name) %}"
|
||||
class="btn btn-xs btn-default edit-btn"
|
||||
title="{%= __('Edit') %}"
|
||||
>
|
||||
<svg class="icon icon-xs">
|
||||
<use href="#icon-edit"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<p class="h6 flex flex-wrap">
|
||||
<span>{%= addr.address_title %}</span>
|
||||
{% if (addr.address_type !== "Other") { %}
|
||||
·
|
||||
<span class="text-muted">{%= __(addr.address_type) %}</span>
|
||||
{% } %}
|
||||
{% if (addr.is_primary_address) { %}
|
||||
·
|
||||
<span class="text-muted">{%= __("Primary Address") %}</span>
|
||||
{% } %}
|
||||
{% if (addr.is_shipping_address) { %}
|
||||
·
|
||||
<span class="text-muted">{%= __("Shipping Address") %}</span>
|
||||
{% } %}
|
||||
{% if (addr.disabled) { %}
|
||||
·
|
||||
<span class="text-muted">{%= __("Disabled") %}</span>
|
||||
{% } %}
|
||||
</p>
|
||||
<p>{%= addr.display %}</p>
|
||||
</div>
|
||||
{% } %}
|
||||
|
||||
<a href="/app/Form/Address/{%= encodeURIComponent(addr_list[i].name) %}" class="btn btn-default btn-xs pull-right"
|
||||
style="margin-top:-3px; margin-right: -5px;">
|
||||
{%= __("Edit") %}</a>
|
||||
</p>
|
||||
<p>{%= addr_list[i].display %}</p>
|
||||
</div>
|
||||
{% if (!addr_list.length) { %}
|
||||
<p class="text-muted small">{%= __("No address added yet.") %}</p>
|
||||
{% } %}
|
||||
{% if(!addr_list.length) { %}
|
||||
<p class="text-muted small">{%= __("No address added yet.") %}</p>
|
||||
{% } %}
|
||||
<p><button class="btn btn-xs btn-default btn-address">{{ __("New Address") }}</button></p>
|
||||
|
||||
<p>
|
||||
<button class="btn btn-xs btn-default btn-address">
|
||||
{{ __("New Address") }}
|
||||
</button>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,74 @@
|
|||
<div class="clearfix"></div>
|
||||
{% for(const contact of contact_list) { %}
|
||||
{% for (const contact of contact_list) { %}
|
||||
<div class="address-box">
|
||||
<p class="h6 flex align-center">
|
||||
{%= contact.first_name %} {%= contact.last_name %}
|
||||
{% if(contact.is_primary_contact) { %}
|
||||
<span class="text-muted"> ({%= __("Primary") %})</span>
|
||||
<a
|
||||
href="{%= frappe.utils.get_form_link('Contact', contact.name) %}"
|
||||
class="btn btn-xs btn-default edit-btn"
|
||||
title="{%= __('Edit') %}"
|
||||
>
|
||||
<svg class="icon icon-xs">
|
||||
<use href="#icon-edit"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<p class="h6 flex flex-wrap">
|
||||
<span>{%= contact.first_name %} {%= contact.last_name %}</span>
|
||||
{% if (contact.is_primary_contact) { %}
|
||||
·
|
||||
<span class="text-muted">{%= __("Primary Contact") %}</span>
|
||||
{% } %}
|
||||
{% if(contact.designation){ %}
|
||||
<span class="text-muted">– {%= contact.designation %}</span>
|
||||
{% if (contact.is_billing_contact) { %}
|
||||
·
|
||||
<span class="text-muted">{%= __("Billing Contact") %}</span>
|
||||
{% } %}
|
||||
{% if (contact.designation){ %}
|
||||
·
|
||||
<span class="text-muted"> {%= contact.designation %}</span>
|
||||
{% } %}
|
||||
<a href="/app/Form/Contact/{%= encodeURIComponent(contact.name) %}"
|
||||
class="btn btn-xs btn-default ml-auto">
|
||||
{%= __("Edit") %}
|
||||
</a>
|
||||
</p>
|
||||
{% if (contact.phone || contact.mobile_no || contact.phone_nos.length > 0) { %}
|
||||
<p>
|
||||
{% if(contact.phone) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(contact.phone) %}">{%= frappe.utils.escape_html(contact.phone) %}</a> · <span class="text-muted">{%= __("Primary Phone") %}</span><br>
|
||||
{% if (contact.phone) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(contact.phone) %}">
|
||||
{%= frappe.utils.escape_html(contact.phone) %}
|
||||
</a>
|
||||
·
|
||||
<span class="text-muted">{%= __("Primary Phone") %}</span>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if(contact.mobile_no) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(contact.mobile_no) %}">{%= frappe.utils.escape_html(contact.mobile_no) %}</a> · <span class="text-muted">{%= __("Primary Mobile") %}</span><br>
|
||||
{% if (contact.mobile_no) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(contact.mobile_no) %}">
|
||||
{%= frappe.utils.escape_html(contact.mobile_no) %}
|
||||
</a>
|
||||
·
|
||||
<span class="text-muted">{%= __("Primary Mobile") %}</span>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if(contact.phone_nos) { %}
|
||||
{% for(const phone_no of contact.phone_nos) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(phone_no.phone) %}">{%= frappe.utils.escape_html(phone_no.phone) %}</a><br>
|
||||
{% if (contact.phone_nos) { %}
|
||||
{% for (const phone_no of contact.phone_nos) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(phone_no.phone) %}">
|
||||
{%= frappe.utils.escape_html(phone_no.phone) %}
|
||||
</a>
|
||||
<br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if (contact.email_id || contact.email_ids.length > 0) { %}
|
||||
<p>
|
||||
{% if(contact.email_id) { %}
|
||||
<a href="mailto:{%= frappe.utils.escape_html(contact.email_id) %}">{%= frappe.utils.escape_html(contact.email_id) %}</a> · <span class="text-muted">{%= __("Primary Email") %}</span><br>
|
||||
{% if (contact.email_id) { %}
|
||||
<a href="mailto:{%= frappe.utils.escape_html(contact.email_id) %}">
|
||||
{%= frappe.utils.escape_html(contact.email_id) %}
|
||||
</a>
|
||||
·
|
||||
<span class="text-muted">{%= __("Primary Email") %}</span>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if(contact.email_ids) { %}
|
||||
{% for(const email_id of contact.email_ids) { %}
|
||||
<a href="mailto:{%= frappe.utils.escape_html(email_id.email_id) %}">{%= frappe.utils.escape_html(email_id.email_id) %}</a><br>
|
||||
{% if (contact.email_ids) { %}
|
||||
{% for (const email_id of contact.email_ids) { %}
|
||||
<a href="mailto:{%= frappe.utils.escape_html(email_id.email_id) %}">
|
||||
{%= frappe.utils.escape_html(email_id.email_id) %}
|
||||
</a>
|
||||
<br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
|
@ -48,9 +80,13 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
{% } %}
|
||||
{% if(!contact_list.length) { %}
|
||||
<p class="text-muted small">{%= __("No contacts added yet.") %}</p>
|
||||
|
||||
{% if (!contact_list.length) { %}
|
||||
<p class="text-muted small">{%= __("No contacts added yet.") %}</p>
|
||||
{% } %}
|
||||
<p><button class="btn btn-xs btn-default btn-contact">
|
||||
{{ __("New Contact") }}</button>
|
||||
|
||||
<p>
|
||||
<button class="btn btn-xs btn-default btn-contact">
|
||||
{{ __("New Contact") }}
|
||||
</button>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -378,7 +378,8 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
function () {
|
||||
me.frm.copy_doc();
|
||||
},
|
||||
true
|
||||
true,
|
||||
"Shift+D"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -634,7 +634,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
<span class="level-item list-liked-by-me hidden-xs">
|
||||
<span title="${__("Likes")}">${frappe.utils.icon("es-solid-heart", "sm", "like-icon")}</span>
|
||||
</span>
|
||||
<span class="level-item">${__(subject_field.label)}</span>
|
||||
<span class="level-item" data-sort-by="${subject_field.fieldname}"
|
||||
title="${__("Click to sort by {0}", [subject_field.label])}">
|
||||
${__(subject_field.label)}
|
||||
</span>
|
||||
`;
|
||||
const $columns = this.columns
|
||||
.map((col) => {
|
||||
|
|
@ -645,15 +648,21 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
frappe.model.is_numeric_field(col.df) ? "text-right" : "",
|
||||
].join(" ");
|
||||
|
||||
return `
|
||||
<div class="${classes}">
|
||||
${
|
||||
col.type === "Subject"
|
||||
? subject_html
|
||||
: `
|
||||
<span>${__((col.df && col.df.label) || col.type)}</span>`
|
||||
}
|
||||
</div>
|
||||
let html = "";
|
||||
if (col.type === "Subject") {
|
||||
html = subject_html;
|
||||
} else {
|
||||
const fieldname = col.df?.fieldname;
|
||||
const attrs = fieldname
|
||||
? ` data-sort-by="${fieldname}"
|
||||
title="${__("Click to sort by {0}", [col.df?.label])}"`
|
||||
: "";
|
||||
html = `<span ${attrs}>
|
||||
${__(col.df?.label || col.type)}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
return `<div class="${classes}">${html}</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
|
@ -1062,6 +1071,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
setup_events() {
|
||||
this.setup_filterable();
|
||||
this.setup_sort_by();
|
||||
this.setup_list_click();
|
||||
this.setup_drag_click();
|
||||
this.setup_tag_event();
|
||||
|
|
@ -1203,6 +1213,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
});
|
||||
}
|
||||
|
||||
setup_sort_by() {
|
||||
this.$result.on("click", "[data-sort-by]", (e) => {
|
||||
const sort_by = e.currentTarget.getAttribute("data-sort-by");
|
||||
if (!sort_by) return;
|
||||
let sort_order = "asc"; // always start with asc
|
||||
if (this.sort_by === sort_by) {
|
||||
// unless it's the same field, then toggle
|
||||
sort_order = this.sort_order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
this.sort_selector.set_value(sort_by, sort_order);
|
||||
this.on_sort_change(sort_by, sort_order);
|
||||
});
|
||||
}
|
||||
|
||||
setup_list_click() {
|
||||
this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => {
|
||||
const $target = $(e.target);
|
||||
|
|
|
|||
|
|
@ -23,26 +23,35 @@ frappe.ui.SortSelector = class SortSelector {
|
|||
|
||||
// order
|
||||
this.wrapper.find(".btn-order").on("click", function () {
|
||||
let btn = $(this);
|
||||
const order = $(this).attr("data-value") === "desc" ? "asc" : "desc";
|
||||
const title =
|
||||
$(this).attr("data-value") === "desc" ? __("ascending") : __("descending");
|
||||
|
||||
btn.attr("data-value", order);
|
||||
btn.attr("title", title);
|
||||
me.sort_order = order;
|
||||
const icon_name = order === "asc" ? "sort-ascending" : "sort-descending";
|
||||
btn.find(".sort-order").html(frappe.utils.icon(icon_name, "sm"));
|
||||
me.set_value(me.sort_by, order);
|
||||
(me.onchange || me.change)(me.sort_by, me.sort_order);
|
||||
});
|
||||
|
||||
// select field
|
||||
this.wrapper.find(".dropdown-menu a.option").on("click", function () {
|
||||
me.sort_by = $(this).attr("data-value");
|
||||
me.wrapper.find(".dropdown-text").html($(this).html());
|
||||
me.set_value($(this).attr("data-value"), me.sort_order);
|
||||
(me.onchange || me.change)(me.sort_by, me.sort_order);
|
||||
});
|
||||
}
|
||||
set_value(sort_by, sort_order) {
|
||||
const $btn = this.wrapper.find(".btn-order");
|
||||
const $icon = $btn.find(".sort-order");
|
||||
const $text = this.wrapper.find(".dropdown-text");
|
||||
|
||||
if (this.sort_by !== sort_by) {
|
||||
this.sort_by = sort_by;
|
||||
$text.html(__(this.get_label(sort_by)));
|
||||
}
|
||||
if (this.sort_order !== sort_order) {
|
||||
this.sort_order = sort_order;
|
||||
const title = sort_order === "desc" ? __("ascending") : __("descending");
|
||||
const icon_name = sort_order === "asc" ? "sort-ascending" : "sort-descending";
|
||||
$btn.attr("data-value", sort_order);
|
||||
$btn.attr("title", title);
|
||||
$icon.html(frappe.utils.icon(icon_name, "sm"));
|
||||
}
|
||||
}
|
||||
prepare_args() {
|
||||
var me = this;
|
||||
if (!this.args) {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,7 @@ if (!window.frappe) window.frappe = {};
|
|||
function flt(v, decimals, number_format, rounding_method) {
|
||||
if (v == null || v == "") return 0;
|
||||
|
||||
if (!(typeof v === "number" || String(parseFloat(v)) == v)) {
|
||||
// cases in which this block should not run
|
||||
// 1. 'v' is already a number
|
||||
// 2. v is already parsed but in string form
|
||||
// if (typeof v !== "number") {
|
||||
|
||||
if (typeof v !== "number") {
|
||||
v = v + "";
|
||||
|
||||
// strip currency symbol if exists
|
||||
|
|
@ -29,7 +24,6 @@ function flt(v, decimals, number_format, rounding_method) {
|
|||
if (isNaN(v)) v = 0;
|
||||
}
|
||||
|
||||
v = parseFloat(v);
|
||||
if (decimals != null) return _round(v, decimals, rounding_method);
|
||||
return v;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,7 +419,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
.then((settings) => {
|
||||
frappe.dom.eval(settings.script || "");
|
||||
frappe.after_ajax(() => {
|
||||
this.report_settings = this.get_local_report_settings();
|
||||
this.report_settings = this.get_local_report_settings(
|
||||
settings.custom_report_name
|
||||
);
|
||||
this.report_settings.html_format = settings.html_format;
|
||||
this.report_settings.execution_time = settings.execution_time || 0;
|
||||
frappe.query_reports[this.report_name] = this.report_settings;
|
||||
|
|
@ -437,10 +439,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
});
|
||||
}
|
||||
|
||||
get_local_report_settings() {
|
||||
get_local_report_settings(custom_report_name) {
|
||||
let report_script_name =
|
||||
this.report_doc.report_type === "Custom Report"
|
||||
? this.report_doc.reference_report
|
||||
? custom_report_name
|
||||
? custom_report_name
|
||||
: this.report_doc.reference_report
|
||||
: this.report_name;
|
||||
return frappe.query_reports[report_script_name] || {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ frappe.ui.OnboardingTour = class OnboardingTour {
|
|||
allowClose: false,
|
||||
padding: 10,
|
||||
overlayClickNext: false,
|
||||
keyboardControl: true,
|
||||
keyboardControl: false,
|
||||
nextBtnText: __("Next"),
|
||||
prevBtnText: __("Previous"),
|
||||
doneBtnText: __("Done"),
|
||||
closeBtnText: __("Close"),
|
||||
closeBtnText: __("Skip"),
|
||||
opacity: 0.5,
|
||||
onHighlighted: (step) => {
|
||||
frappe.ui.next_form_tour = step.options.step_info?.next_form_tour;
|
||||
|
|
|
|||
|
|
@ -125,14 +125,8 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
let docfield = "Workflow Document State";
|
||||
let df = frappe.model.get_new_doc(docfield);
|
||||
df.name = frappe.utils.get_random(8);
|
||||
df.state = data.state;
|
||||
Object.assign(df, data);
|
||||
df.doc_status = doc_status_map[data.doc_status];
|
||||
df.allow_edit = data.allow_edit;
|
||||
df.update_field = data.update_field;
|
||||
df.update_value = data.update_value;
|
||||
df.is_optional_state = data.is_optional_state;
|
||||
df.next_action_email_template = data.next_action_email_template;
|
||||
df.message = data.message;
|
||||
return df;
|
||||
}
|
||||
|
||||
|
|
@ -146,14 +140,11 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
return states;
|
||||
}
|
||||
|
||||
function get_transition_df({ state, action, next_state, allowed }) {
|
||||
function get_transition_df(data) {
|
||||
let docfield = "Workflow Transition";
|
||||
let df = frappe.model.get_new_doc(docfield);
|
||||
df.name = frappe.utils.get_random(8);
|
||||
df.state = state;
|
||||
df.action = action;
|
||||
df.next_state = next_state;
|
||||
df.allowed = allowed;
|
||||
Object.assign(df, data);
|
||||
return df;
|
||||
}
|
||||
|
||||
|
|
@ -180,10 +171,9 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
}
|
||||
transitions.push(
|
||||
get_transition_df({
|
||||
...action.data,
|
||||
state: action.data.from,
|
||||
action: action.data.action,
|
||||
next_state: action.data.to,
|
||||
allowed: action.data.allowed,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -150,9 +150,19 @@ select.form-control {
|
|||
border-radius: var(--border-radius);
|
||||
@include get_textstyle("sm", "regular");
|
||||
word-wrap: break-word;
|
||||
position: relative;
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.edit-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--padding-sm);
|
||||
}
|
||||
}
|
||||
.action-btn {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -178,6 +178,11 @@
|
|||
a {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
& > [data-sort-by]:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
$level-margin-right: 8px;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tree-link .file-doc-link {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tree-link:hover .file-doc-link {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.tree-link .file-doc-link:hover {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tree-hover {
|
||||
background-color: var(--highlight-color);
|
||||
min-height: 25px;
|
||||
|
|
|
|||
|
|
@ -477,9 +477,9 @@ class TestPermissions(FrappeTestCase):
|
|||
# check if user is not granted access if the user is not the owner of the doc
|
||||
# Blogger has only read access on the blog post unless he is the owner of the blog
|
||||
update("Blog Post", "Blogger", 0, "if_owner", 1)
|
||||
update("Blog Post", "Blogger", 0, "read", 1)
|
||||
update("Blog Post", "Blogger", 0, "write", 1)
|
||||
update("Blog Post", "Blogger", 0, "delete", 1)
|
||||
update("Blog Post", "Blogger", 0, "read", 1, 1)
|
||||
update("Blog Post", "Blogger", 0, "write", 1, 1)
|
||||
update("Blog Post", "Blogger", 0, "delete", 1, 1)
|
||||
|
||||
# currently test2 user has not created any document
|
||||
# still he should be able to do get_list query which should
|
||||
|
|
@ -588,9 +588,9 @@ class TestPermissions(FrappeTestCase):
|
|||
|
||||
def test_if_owner_permission_on_delete(self):
|
||||
update("Blog Post", "Blogger", 0, "if_owner", 1)
|
||||
update("Blog Post", "Blogger", 0, "read", 1)
|
||||
update("Blog Post", "Blogger", 0, "write", 1)
|
||||
update("Blog Post", "Blogger", 0, "delete", 1)
|
||||
update("Blog Post", "Blogger", 0, "read", 1, 1)
|
||||
update("Blog Post", "Blogger", 0, "write", 1, 1)
|
||||
update("Blog Post", "Blogger", 0, "delete", 1, 1)
|
||||
|
||||
# Remove delete perm
|
||||
update("Blog Post", "Website Manager", 0, "delete", 0)
|
||||
|
|
@ -627,7 +627,7 @@ class TestPermissions(FrappeTestCase):
|
|||
|
||||
frappe.set_user("test2@example.com")
|
||||
frappe.delete_doc("Blog Post", "-test-blog-post-title-new-1")
|
||||
update("Blog Post", "Website Manager", 0, "delete", 1)
|
||||
update("Blog Post", "Website Manager", 0, "delete", 1, 1)
|
||||
|
||||
def test_clear_user_permissions(self):
|
||||
current_user = frappe.session.user
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from contextlib import contextmanager
|
|||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.model.base_document import BaseDocument
|
||||
from frappe.model.base_document import BaseDocument, get_controller
|
||||
from frappe.utils import cint
|
||||
|
||||
datetime_like_types = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
|
||||
|
|
@ -253,3 +253,21 @@ def patch_hooks(overridden_hoooks):
|
|||
|
||||
with patch.object(frappe, "get_hooks", patched_hooks):
|
||||
yield
|
||||
|
||||
|
||||
def check_orpahned_doctypes():
|
||||
"""Check that all doctypes in DB actually exist after patch test"""
|
||||
|
||||
doctypes = frappe.get_all("DocType", {"custom": 0}, pluck="name")
|
||||
orpahned_doctypes = []
|
||||
|
||||
for doctype in doctypes:
|
||||
try:
|
||||
get_controller(doctype)
|
||||
except ImportError:
|
||||
orpahned_doctypes.append(doctype)
|
||||
|
||||
if orpahned_doctypes:
|
||||
frappe.throw(
|
||||
"Following doctypes exist in DB without controller.\n {}".format("\n".join(orpahned_doctypes))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1384,7 +1384,7 @@ Is Globally Pinned,Wird global gepinnt,
|
|||
Is Home Folder,Ist Ordner für Startseite,
|
||||
Is Mandatory Field,Ist Pflichtfeld,
|
||||
Is Pinned,Ist angeheftet,
|
||||
Is Primary Contact,Ist primärer Ansprechpartner,
|
||||
Is Primary Contact,Ist Hauptkontakt,
|
||||
Is Private,Ist Privat,
|
||||
Is Published Field,Ist Veröffentlicht Feld,
|
||||
Is Published Field must be a valid fieldname,Ist Veröffentlicht Feld muss eine gültige Feldname sein,
|
||||
|
|
@ -1940,6 +1940,11 @@ Preview Message,Vorschau Nachricht,
|
|||
Previous,Vorhergehende,
|
||||
Previous Hash,Vorheriger Hash,
|
||||
Primary Color,Primärfarbe,
|
||||
Primary Address,Hauptadresse,
|
||||
Primary Contact,Hauptkontakt,
|
||||
Primary Email,Haupt-E-Mail,
|
||||
Primary Mobile,Haupt-Mobiltelefon,
|
||||
Primary Phone,Haupttelefon,
|
||||
Print Documents,Dokumente drucken,
|
||||
Print Format Builder,Programm zum Erstellen von Druckformaten,
|
||||
Print Format Help,Hilfe zu Druckformaten,
|
||||
|
|
@ -4851,6 +4856,7 @@ Anonymous,Anonym,
|
|||
Author,Autor,
|
||||
Basic,Grundeinkommen,
|
||||
Billing,Abrechnung,
|
||||
Billing Contact,Abrechnungskontakt,
|
||||
Contact Details,Kontakt-Details,
|
||||
Datetime,Datum und Uhrzeit,
|
||||
Enable,ermöglichen,
|
||||
|
|
@ -4879,6 +4885,7 @@ Saved,Gespeichert,
|
|||
Series {0} already used in {1},Serie {0} bereits verwendet in {1},
|
||||
Set as Default,Als Standard festlegen,
|
||||
Shipping,Versand,
|
||||
Shipping Address,Lieferadresse,
|
||||
Standard,Standard,
|
||||
Test,Test,
|
||||
Traceback,Zurück verfolgen,
|
||||
|
|
|
|||
|
|
|
@ -206,3 +206,4 @@ class TypeExporter:
|
|||
# Ideally this should be longest common substring but I don't l33tc0de.
|
||||
# If someone really needs it, add support via hooks.
|
||||
self.indent = " " * 4
|
||||
break
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from collections.abc import (
|
|||
)
|
||||
from email.header import decode_header, make_header
|
||||
from email.utils import formataddr, parseaddr
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, TypedDict
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
from redis.exceptions import ConnectionError
|
||||
|
|
@ -1082,15 +1082,36 @@ def dictify(arg):
|
|||
return arg
|
||||
|
||||
|
||||
def add_user_info(user, user_info):
|
||||
if user not in user_info:
|
||||
info = (
|
||||
frappe.db.get_value(
|
||||
"User", user, ["full_name", "user_image", "name", "email", "time_zone"], as_dict=True
|
||||
)
|
||||
or frappe._dict()
|
||||
)
|
||||
user_info[user] = frappe._dict(
|
||||
class _UserInfo(TypedDict):
|
||||
fullname: str
|
||||
image: str
|
||||
name: str
|
||||
email: str
|
||||
time_zone: str
|
||||
|
||||
|
||||
def add_user_info(user: str | list[str] | set[str], user_info: dict[str, _UserInfo]) -> None:
|
||||
if not user:
|
||||
return
|
||||
|
||||
if isinstance(user, str):
|
||||
user = [user]
|
||||
|
||||
missing_users = [u for u in user if u not in user_info]
|
||||
if not missing_users:
|
||||
return
|
||||
|
||||
for missing_user in missing_users:
|
||||
user_info[missing_user] = frappe._dict()
|
||||
|
||||
missing_info = frappe.get_all(
|
||||
"User",
|
||||
{"name": ("in", missing_users)},
|
||||
["full_name", "user_image", "name", "email", "time_zone"],
|
||||
)
|
||||
|
||||
for info in missing_info:
|
||||
user_info[info.name].update(
|
||||
fullname=info.full_name or user,
|
||||
image=info.user_image,
|
||||
name=user,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ def safe_exec(script, _globals=None, _locals=None, restrict_commit_rollback=Fals
|
|||
if not is_safe_exec_enabled():
|
||||
|
||||
msg = _("Server Scripts are disabled. Please enable server scripts from bench configuration.")
|
||||
docs_cta = _("Read the documentation to know")
|
||||
docs_cta = _("Read the documentation to know more")
|
||||
msg += f"<br><a href='https://frappeframework.com/docs/user/en/desk/scripting/server-script'>{docs_cta}</a>"
|
||||
frappe.throw(msg, ServerScriptNotEnabled, title="Server Scripts Disabled")
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class PortalSettings(Document):
|
|||
dirty = True
|
||||
|
||||
if dirty:
|
||||
self.remove_deleted_doctype_items()
|
||||
self.save()
|
||||
|
||||
def on_update(self):
|
||||
|
|
@ -65,3 +66,9 @@ class PortalSettings(Document):
|
|||
|
||||
# clears role based home pages
|
||||
frappe.clear_cache()
|
||||
|
||||
def remove_deleted_doctype_items(self):
|
||||
existing_doctypes = set(frappe.get_list("DocType", pluck="name"))
|
||||
for menu_item in list(self.get("menu")):
|
||||
if menu_item.reference_doctype not in existing_doctypes:
|
||||
self.remove(menu_item)
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@ from frappe.website.utils import can_cache, get_home_page
|
|||
|
||||
|
||||
class PathResolver:
|
||||
__slots__ = ("path",)
|
||||
__slots__ = ("path", "http_status_code")
|
||||
|
||||
def __init__(self, path):
|
||||
def __init__(self, path, http_status_code=None):
|
||||
self.path = path.strip("/ ")
|
||||
self.http_status_code = http_status_code
|
||||
|
||||
def resolve(self):
|
||||
"""Returns endpoint and a renderer instance that can render the endpoint"""
|
||||
|
|
@ -41,7 +42,7 @@ class PathResolver:
|
|||
|
||||
# WARN: Hardcoded for better performance
|
||||
if endpoint == "app":
|
||||
return endpoint, TemplatePage(endpoint, 200)
|
||||
return endpoint, TemplatePage(endpoint, self.http_status_code)
|
||||
|
||||
custom_renderers = self.get_custom_page_renderers()
|
||||
renderers = custom_renderers + [
|
||||
|
|
@ -54,7 +55,7 @@ class PathResolver:
|
|||
]
|
||||
|
||||
for renderer in renderers:
|
||||
renderer_instance = renderer(endpoint, 200)
|
||||
renderer_instance = renderer(endpoint, self.http_status_code)
|
||||
if renderer_instance.can_render():
|
||||
return endpoint, renderer_instance
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ def get_response(path=None, http_status_code=200):
|
|||
endpoint = path
|
||||
|
||||
try:
|
||||
path_resolver = PathResolver(path)
|
||||
path_resolver = PathResolver(path, http_status_code)
|
||||
endpoint, renderer_instance = path_resolver.resolve()
|
||||
response = renderer_instance.render()
|
||||
except frappe.Redirect:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
},
|
||||
"homepage": "https://frappeframework.com",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"@editorjs/editorjs": "^2.26.3",
|
||||
"@frappe/esbuild-plugin-postcss2": "^0.1.3",
|
||||
"@redis/client": "^1.5.8",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@popperjs/core@^2.11.2":
|
||||
version "2.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||
|
||||
"@redis/client@^1.5.8":
|
||||
version "1.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.8.tgz#a375ba7861825bd0d2dc512282b8bff7b98dbcb1"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue