Merge branch 'develop' into fix-document-signature-2
This commit is contained in:
commit
0cd41fbf0c
60 changed files with 1131 additions and 235 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -2,7 +2,7 @@ name: Generate Semantic Release
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
- version-14-beta
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
{
|
||||
"branches": ["version-13"],
|
||||
"branches": [
|
||||
{"name": "version-14-beta", "channel": "beta", "prerelease": true}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
"releaseRules": [
|
||||
{"breaking": true, "release": false}
|
||||
]
|
||||
"preset": "angular"
|
||||
},
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1258,8 +1258,10 @@ def get_module_path(module, *joins):
|
|||
|
||||
:param module: Module name.
|
||||
:param *joins: Join additional path elements using `os.path.join`."""
|
||||
module = scrub(module)
|
||||
return get_pymodule_path(local.module_app[module] + "." + module, *joins)
|
||||
from frappe.modules.utils import get_module_app
|
||||
|
||||
app = get_module_app(module)
|
||||
return get_pymodule_path(app + "." + scrub(module), *joins)
|
||||
|
||||
|
||||
def get_app_path(app_name, *joins):
|
||||
|
|
@ -1501,18 +1503,26 @@ def call(fn, *args, **kwargs):
|
|||
|
||||
|
||||
def get_newargs(fn, kwargs):
|
||||
|
||||
# if function has any **kwargs parameter that capture arbitrary keyword arguments
|
||||
# Ref: https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind
|
||||
varkw_exist = False
|
||||
|
||||
if hasattr(fn, "fnargs"):
|
||||
fnargs = fn.fnargs
|
||||
else:
|
||||
signature = inspect.signature(fn)
|
||||
fnargs = list(signature.parameters)
|
||||
varkw = "kwargs" in fnargs
|
||||
if varkw:
|
||||
fnargs.pop(-1)
|
||||
|
||||
for param_name, parameter in signature.parameters.items():
|
||||
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
varkw_exist = True
|
||||
fnargs.remove(param_name)
|
||||
break
|
||||
|
||||
newargs = {}
|
||||
for a in kwargs:
|
||||
if (a in fnargs) or varkw:
|
||||
if (a in fnargs) or varkw_exist:
|
||||
newargs[a] = kwargs.get(a)
|
||||
|
||||
newargs.pop("ignore_permissions", None)
|
||||
|
|
@ -1808,18 +1818,21 @@ def get_value(*args, **kwargs):
|
|||
return db.get_value(*args, **kwargs)
|
||||
|
||||
|
||||
def as_json(obj: Union[Dict, List], indent=1) -> str:
|
||||
def as_json(obj: Union[Dict, List], indent=1, separators=None) -> str:
|
||||
from frappe.utils.response import json_handler
|
||||
|
||||
if separators is None:
|
||||
separators = (",", ": ")
|
||||
|
||||
try:
|
||||
return json.dumps(
|
||||
obj, indent=indent, sort_keys=True, default=json_handler, separators=(",", ": ")
|
||||
obj, indent=indent, sort_keys=True, default=json_handler, separators=separators
|
||||
)
|
||||
except TypeError:
|
||||
# this would break in case the keys are not all os "str" type - as defined in the JSON
|
||||
# adding this to ensure keys are sorted (expected behaviour)
|
||||
sorted_obj = dict(sorted(obj.items(), key=lambda kv: str(kv[0])))
|
||||
return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=(",", ": "))
|
||||
return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=separators)
|
||||
|
||||
|
||||
def are_emails_muted():
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ class LoginManager:
|
|||
self.set_user_info()
|
||||
|
||||
def get_user_info(self):
|
||||
self.info = frappe.db.get_value(
|
||||
self.info = frappe.get_cached_value(
|
||||
"User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1
|
||||
)
|
||||
|
||||
|
|
@ -412,10 +412,16 @@ def clear_cookies():
|
|||
|
||||
def validate_ip_address(user):
|
||||
"""check if IP Address is valid"""
|
||||
user = (
|
||||
frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
|
||||
from frappe.core.doctype.user.user import get_restricted_ip_list
|
||||
|
||||
# Only fetch required fields - for perf
|
||||
user_fields = ["restrict_ip", "bypass_restrict_ip_check_if_2fa_enabled"]
|
||||
user_info = (
|
||||
frappe.get_cached_value("User", user, user_fields, as_dict=True)
|
||||
if not frappe.flags.in_test
|
||||
else frappe.db.get_value("User", user, user_fields, as_dict=True)
|
||||
)
|
||||
ip_list = user.get_restricted_ip_list()
|
||||
ip_list = get_restricted_ip_list(user_info)
|
||||
if not ip_list:
|
||||
return
|
||||
|
||||
|
|
@ -430,7 +436,7 @@ def validate_ip_address(user):
|
|||
# check if two factor auth is enabled
|
||||
if system_settings.enable_two_factor_auth and not bypass_restrict_ip_check:
|
||||
# check if bypass restrict ip is enabled for login user
|
||||
bypass_restrict_ip_check = user.bypass_restrict_ip_check_if_2fa_enabled
|
||||
bypass_restrict_ip_check = user_info.bypass_restrict_ip_check_if_2fa_enabled
|
||||
|
||||
for ip in ip_list:
|
||||
if frappe.local.request_ip.startswith(ip) or bypass_restrict_ip_check:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from frappe.geo.country_info import get_all
|
|||
from frappe.model.base_document import get_controller
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.query_builder.terms import subqry
|
||||
from frappe.query_builder.terms import SubQuery
|
||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
|
||||
is_energy_point_enabled,
|
||||
|
|
@ -211,7 +211,7 @@ def get_user_pages_or_reports(parent, cache=False):
|
|||
if parent == "Report":
|
||||
has_role[p.name].update({"ref_doctype": p.ref_doctype})
|
||||
|
||||
no_of_roles = (
|
||||
no_of_roles = SubQuery(
|
||||
frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name)
|
||||
)
|
||||
|
||||
|
|
@ -221,7 +221,7 @@ def get_user_pages_or_reports(parent, cache=False):
|
|||
pages_with_no_roles = (
|
||||
frappe.qb.from_(parentTable)
|
||||
.select(parentTable.name, parentTable.modified, *columns)
|
||||
.where(subqry(no_of_roles) == 0)
|
||||
.where(no_of_roles == 0)
|
||||
).run(as_dict=True)
|
||||
|
||||
for p in pages_with_no_roles:
|
||||
|
|
@ -327,7 +327,7 @@ def get_unseen_notes():
|
|||
(note.notify_on_every_login == 1)
|
||||
& (note.expire_notification_on > frappe.utils.now())
|
||||
& (
|
||||
subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin(
|
||||
SubQuery(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin(
|
||||
[frappe.session.user]
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from frappe.utils import (
|
|||
cint,
|
||||
get_datetime,
|
||||
get_formatted_email,
|
||||
get_string_between,
|
||||
list_to_str,
|
||||
split_emails,
|
||||
validate_email_address,
|
||||
|
|
@ -152,7 +153,7 @@ def _make(
|
|||
"reference_doctype": doctype,
|
||||
"reference_name": name,
|
||||
"email_template": email_template,
|
||||
"message_id": get_message_id().strip(" <>"),
|
||||
"message_id": get_string_between("<", get_message_id(), ">"),
|
||||
"read_receipt": read_receipt,
|
||||
"has_attachment": 1 if attachments else 0,
|
||||
"communication_type": communication_type,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import random
|
||||
import string
|
||||
import unittest
|
||||
from typing import Dict, List, Optional
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.cache_manager import clear_doctype_cache
|
||||
from frappe.core.doctype.doctype.doctype import (
|
||||
CannotIndexedError,
|
||||
DoctypeLinkError,
|
||||
|
|
@ -15,8 +19,8 @@ from frappe.core.doctype.doctype.doctype import (
|
|||
WrongOptionsDoctypeLinkError,
|
||||
validate_links_table_fieldnames,
|
||||
)
|
||||
|
||||
# test_records = frappe.get_test_records('DocType')
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.desk.form.load import getdoc
|
||||
|
||||
|
||||
class TestDocType(unittest.TestCase):
|
||||
|
|
@ -628,10 +632,55 @@ class TestDocType(unittest.TestCase):
|
|||
|
||||
self.assertEqual(test_json.test_json_field["hello"], "world")
|
||||
|
||||
@patch.dict(frappe.conf, {"developer_mode": 1})
|
||||
def test_delete_doctype_with_customization(self):
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
custom_field = "customfield"
|
||||
|
||||
doctype = new_doctype(custom=0).insert().name
|
||||
|
||||
# Create property setter and custom field
|
||||
field = "some_fieldname"
|
||||
make_property_setter(doctype, field, "default", "DELETETHIS", "Data")
|
||||
create_custom_fields({doctype: [{"fieldname": custom_field, "fieldtype": "Data"}]})
|
||||
|
||||
# Create 1 record
|
||||
original_doc = frappe.get_doc(doctype=doctype, custom_field_name="wat").insert()
|
||||
self.assertEqual(original_doc.some_fieldname, "DELETETHIS")
|
||||
|
||||
# delete doctype
|
||||
frappe.delete_doc("DocType", doctype)
|
||||
clear_doctype_cache(doctype)
|
||||
|
||||
# "restore" doctype by inserting doctype with same schema again
|
||||
new_doctype(doctype, custom=0).insert()
|
||||
|
||||
# Ensure basically same doctype getting "restored"
|
||||
restored_doc = frappe.get_last_doc(doctype)
|
||||
verify_fields = ["doctype", field, custom_field]
|
||||
for f in verify_fields:
|
||||
self.assertEqual(original_doc.get(f), restored_doc.get(f))
|
||||
|
||||
# Check form load of restored doctype
|
||||
getdoc(doctype, restored_doc.name)
|
||||
|
||||
# ensure meta - property setter
|
||||
self.assertEqual(frappe.get_meta(doctype).get_field(field).default, "DELETETHIS")
|
||||
frappe.delete_doc("DocType", doctype)
|
||||
|
||||
|
||||
def new_doctype(
|
||||
name, unique: bool = False, depends_on: str = "", fields: Optional[List[Dict]] = None, **kwargs
|
||||
name: Optional[str] = None,
|
||||
unique: bool = False,
|
||||
depends_on: str = "",
|
||||
fields: Optional[List[Dict]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if not name:
|
||||
# Test prefix is required to avoid coverage
|
||||
name = "Test " + "".join(random.sample(string.ascii_lowercase, 10))
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
|
|
|
|||
0
frappe/core/doctype/document_naming_settings/__init__.py
Normal file
0
frappe/core/doctype/document_naming_settings/__init__.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) 2022, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Document Naming Settings", {
|
||||
refresh: function(frm) {
|
||||
frm.trigger("setup_transaction_autocomplete");
|
||||
frm.disable_save();
|
||||
},
|
||||
|
||||
setup_transaction_autocomplete: function(frm) {
|
||||
frappe.call({
|
||||
method: "get_transactions_and_prefixes",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
frm.fields_dict.transaction_type.set_data(r.message.transactions);
|
||||
frm.fields_dict.prefix.set_data(r.message.prefixes);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
transaction_type: function(frm) {
|
||||
frm.set_value("user_must_always_select", 0);
|
||||
frappe.call({
|
||||
method: "get_options",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
frm.set_value("naming_series_options", r.message);
|
||||
if (r.message && r.message.split("\n")[0] == "")
|
||||
frm.set_value("user_must_always_select", 1);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
prefix: function(frm) {
|
||||
frappe.call({
|
||||
method: "get_current",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
frm.refresh_field("current_value");
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
update: function(frm) {
|
||||
frappe.call({
|
||||
method: "update_series",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_msg: __("Updating naming series options"),
|
||||
callback: function(r) {
|
||||
frm.trigger("setup_transaction_autocomplete");
|
||||
frm.trigger("transaction_type");
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
try_naming_series(frm) {
|
||||
frappe.call({
|
||||
method: "preview_series",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
frm.set_value("series_preview", r.message);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"series_preview",
|
||||
__("Failed to generate preview of series")
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2022-05-30 07:24:07.736646",
|
||||
"description": "Configure various aspects of how document naming works like naming series, current counter.",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series_tab",
|
||||
"setup_series",
|
||||
"transaction_type",
|
||||
"naming_series_options",
|
||||
"user_must_always_select",
|
||||
"update",
|
||||
"column_break_9",
|
||||
"try_naming_series",
|
||||
"series_preview",
|
||||
"help_html",
|
||||
"update_series",
|
||||
"prefix",
|
||||
"current_value",
|
||||
"update_series_start"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"collapsible": 1,
|
||||
"description": "Set Naming Series options on your transactions.",
|
||||
"fieldname": "setup_series",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Setup Series for transactions"
|
||||
},
|
||||
{
|
||||
"depends_on": "transaction_type",
|
||||
"fieldname": "help_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Help HTML",
|
||||
"options": "<div class=\"well\">\n Edit list of Series in the box. Rules:\n <ul>\n <li>Each Series Prefix on a new line.</li>\n <li>Allowed special characters are \"/\" and \"-\"</li>\n <li>\n Optionally, set the number of digits in the series using dot (.)\n followed by hashes (#). For example, \".####\" means that the series\n will have four digits. Default is five digits.\n </li>\n <li>\n You can also use variables in the series name by putting them\n between (.) dots\n <br>\n Supported Variables:\n <ul>\n <li><code>.YYYY.</code> - Year in 4 digits</li>\n <li><code>.YY.</code> - Year in 2 digits</li>\n <li><code>.MM.</code> - Month</li>\n <li><code>.DD.</code> - Day of month</li>\n <li><code>.WW.</code> - Week of the year</li>\n <li><code>.FY.</code> - Fiscal Year</li>\n <li>\n <code>.{fieldname}.</code> - fieldname on the document e.g.\n <code>branch</code>\n </li>\n </ul>\n </li>\n </ul>\n Examples:\n <ul>\n <li>INV-</li>\n <li>INV-10-</li>\n <li>INVK-</li>\n <li>INV-.YYYY.-.{branch}.-.MM.-.####</li>\n </ul>\n</div>\n<br>\n"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "transaction_type",
|
||||
"description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.",
|
||||
"fieldname": "user_must_always_select",
|
||||
"fieldtype": "Check",
|
||||
"label": "User must always select"
|
||||
},
|
||||
{
|
||||
"depends_on": "transaction_type",
|
||||
"fieldname": "update",
|
||||
"fieldtype": "Button",
|
||||
"label": "Update"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"description": "Change the starting / current sequence number of an existing series. <br>\n\nWarning: Incorrectly updating counters can prevent documents from getting created. ",
|
||||
"fieldname": "update_series",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Update Series Counter"
|
||||
},
|
||||
{
|
||||
"fieldname": "prefix",
|
||||
"fieldtype": "Autocomplete",
|
||||
"label": "Prefix"
|
||||
},
|
||||
{
|
||||
"description": "This is the number of the last created transaction with this prefix",
|
||||
"fieldname": "current_value",
|
||||
"fieldtype": "Int",
|
||||
"label": "Current Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "update_series_start",
|
||||
"fieldtype": "Button",
|
||||
"label": "Update Series Number",
|
||||
"options": "update_series_start"
|
||||
},
|
||||
{
|
||||
"depends_on": "transaction_type",
|
||||
"fieldname": "naming_series_options",
|
||||
"fieldtype": "Text",
|
||||
"label": "Series List for this Transaction"
|
||||
},
|
||||
{
|
||||
"depends_on": "transaction_type",
|
||||
"description": "Generate 3 preview of names generate by any valid series.",
|
||||
"fieldname": "try_naming_series",
|
||||
"fieldtype": "Data",
|
||||
"label": "Try a naming Series"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_type",
|
||||
"fieldtype": "Autocomplete",
|
||||
"label": "Select Transaction"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Naming Series"
|
||||
},
|
||||
{
|
||||
"fieldname": "series_preview",
|
||||
"fieldtype": "Text",
|
||||
"label": "Preview of generated names",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "fa fa-sort-by-order",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-30 23:51:36.136535",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Document Naming Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
# Copyright (c) 2022, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from typing import List, Set
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.doctype.doctype import validate_series
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import NamingSeries
|
||||
from frappe.permissions import get_doctypes_with_read
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
class NamingSeriesNotSetError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class DocumentNamingSettings(Document):
|
||||
@frappe.whitelist()
|
||||
def get_transactions_and_prefixes(self):
|
||||
|
||||
transactions = self._get_transactions()
|
||||
prefixes = self._get_prefixes(transactions)
|
||||
|
||||
return {"transactions": transactions, "prefixes": prefixes}
|
||||
|
||||
def _get_transactions(self) -> List[str]:
|
||||
|
||||
readable_doctypes = set(get_doctypes_with_read())
|
||||
|
||||
standard = frappe.get_all("DocField", {"fieldname": "naming_series"}, "parent", pluck="parent")
|
||||
custom = frappe.get_all("Custom Field", {"fieldname": "naming_series"}, "dt", pluck="dt")
|
||||
|
||||
return sorted(readable_doctypes.intersection(standard + custom))
|
||||
|
||||
def _get_prefixes(self, doctypes) -> List[str]:
|
||||
"""Get all prefixes for naming series.
|
||||
|
||||
- For all templates prefix is evaluated considering today's date
|
||||
- All existing prefix in DB are shared as is.
|
||||
"""
|
||||
series_templates = set()
|
||||
for d in doctypes:
|
||||
try:
|
||||
options = frappe.get_meta(d).get_naming_series_options()
|
||||
series_templates.update(options)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.msgprint(_("Unable to find DocType {0}").format(d))
|
||||
continue
|
||||
|
||||
custom_templates = frappe.get_all(
|
||||
"DocType",
|
||||
fields=["autoname"],
|
||||
filters={
|
||||
"name": ("not in", doctypes),
|
||||
"autoname": ("like", "%.#%"),
|
||||
"module": ("not in", ["Core"]),
|
||||
},
|
||||
)
|
||||
if custom_templates:
|
||||
series_templates.update([d.autoname.rsplit(".", 1)[0] for d in custom_templates])
|
||||
|
||||
return self._evaluate_and_clean_templates(series_templates)
|
||||
|
||||
def _evaluate_and_clean_templates(self, series_templates: Set[str]) -> List[str]:
|
||||
evalauted_prefix = set()
|
||||
|
||||
series = frappe.qb.DocType("Series")
|
||||
prefixes_from_db = frappe.qb.from_(series).select(series.name).run(pluck=True)
|
||||
evalauted_prefix.update(prefixes_from_db)
|
||||
|
||||
for series_template in series_templates:
|
||||
prefix = NamingSeries(series_template).get_prefix()
|
||||
if "{" in prefix:
|
||||
# fieldnames can't be evalauted, rely on data in DB instead
|
||||
continue
|
||||
evalauted_prefix.add(prefix)
|
||||
|
||||
return sorted(evalauted_prefix)
|
||||
|
||||
def get_options_list(self, options: str) -> List[str]:
|
||||
return [op.strip() for op in options.split("\n") if op.strip()]
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_series(self):
|
||||
"""update series list"""
|
||||
self.validate_set_series()
|
||||
self.check_duplicate()
|
||||
self.set_series_options_in_meta(self.transaction_type, self.naming_series_options)
|
||||
|
||||
frappe.msgprint(
|
||||
_("Series Updated for {}").format(self.transaction_type), alert=True, indicator="green"
|
||||
)
|
||||
|
||||
def validate_set_series(self):
|
||||
if self.transaction_type and not self.naming_series_options:
|
||||
frappe.throw(_("Please set the series to be used."))
|
||||
|
||||
def set_series_options_in_meta(self, doctype: str, options: str) -> None:
|
||||
options = self.get_options_list(options)
|
||||
|
||||
# validate names
|
||||
for series in options:
|
||||
self.validate_series_name(series)
|
||||
|
||||
if options and self.user_must_always_select:
|
||||
options = [""] + options
|
||||
|
||||
default = options[0] if options else ""
|
||||
|
||||
option_string = "\n".join(options)
|
||||
|
||||
self.update_naming_series_property_setter(doctype, "options", option_string)
|
||||
self.update_naming_series_property_setter(doctype, "default", default)
|
||||
|
||||
self.naming_series_options = option_string
|
||||
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
def update_naming_series_property_setter(self, doctype, property, value):
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
make_property_setter(doctype, "naming_series", property, value, "Text")
|
||||
|
||||
def check_duplicate(self):
|
||||
def stripped_series(s: str) -> str:
|
||||
return s.strip().rstrip("#")
|
||||
|
||||
standard = frappe.get_all("DocField", {"fieldname": "naming_series"}, "parent", pluck="parent")
|
||||
custom = frappe.get_all("Custom Field", {"fieldname": "naming_series"}, "dt", pluck="dt")
|
||||
|
||||
all_doctypes_with_naming_series = set(standard + custom)
|
||||
all_doctypes_with_naming_series.remove(self.transaction_type)
|
||||
|
||||
existing_series = {}
|
||||
for doctype in all_doctypes_with_naming_series:
|
||||
for series in frappe.get_meta(doctype).get_naming_series_options():
|
||||
existing_series[stripped_series(series)] = doctype
|
||||
|
||||
dt = frappe.get_doc("DocType", self.transaction_type)
|
||||
|
||||
options = self.get_options_list(self.naming_series_options)
|
||||
for series in options:
|
||||
if stripped_series(series) in existing_series:
|
||||
frappe.throw(_("Series {0} already used in {1}").format(series, existing_series[series]))
|
||||
validate_series(dt, series)
|
||||
|
||||
def validate_series_name(self, series):
|
||||
NamingSeries(series).validate()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_options(self, doctype=None):
|
||||
doctype = doctype or self.transaction_type
|
||||
if not doctype:
|
||||
return
|
||||
|
||||
if frappe.get_meta(doctype or self.transaction_type).get_field("naming_series"):
|
||||
return frappe.get_meta(doctype or self.transaction_type).get_field("naming_series").options
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_current(self):
|
||||
"""get series current"""
|
||||
if self.prefix:
|
||||
self.current_value = NamingSeries(self.prefix).get_current_value()
|
||||
return self.current_value
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_series_start(self):
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
if not self.prefix:
|
||||
frappe.throw(_("Please select prefix first"))
|
||||
|
||||
naming_series = NamingSeries(self.prefix)
|
||||
previous_value = naming_series.get_current_value()
|
||||
naming_series.update_counter(self.current_value)
|
||||
|
||||
self.create_version_log_for_change(
|
||||
naming_series.get_prefix(), previous_value, self.current_value
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("Series counter for {} updated to {} successfully").format(self.prefix, self.current_value),
|
||||
alert=True,
|
||||
indicator="green",
|
||||
)
|
||||
|
||||
def create_version_log_for_change(self, series, old, new):
|
||||
version = frappe.new_doc("Version")
|
||||
version.ref_doctype = "Series"
|
||||
version.docname = series
|
||||
version.data = frappe.as_json({"changed": [["current", old, new]]})
|
||||
version.flags.ignore_links = True # series is not a "real" doctype
|
||||
version.flags.ignore_permissions = True
|
||||
version.insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def preview_series(self) -> str:
|
||||
"""Preview what the naming series will generate."""
|
||||
|
||||
series = self.try_naming_series
|
||||
if not series:
|
||||
return ""
|
||||
try:
|
||||
doc = self._fetch_last_doc_if_available()
|
||||
return "\n".join(NamingSeries(series).get_preview(doc=doc))
|
||||
except Exception as e:
|
||||
if frappe.message_log:
|
||||
frappe.message_log.pop()
|
||||
return _("Failed to generate names from the series") + f"\n{str(e)}"
|
||||
|
||||
def _fetch_last_doc_if_available(self):
|
||||
"""Fetch last doc for evaluating naming series with fields."""
|
||||
try:
|
||||
return frappe.get_last_doc(self.transaction_type)
|
||||
except Exception:
|
||||
return None
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Copyright (c) 2022, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.document_naming_settings.document_naming_settings import (
|
||||
DocumentNamingSettings,
|
||||
)
|
||||
from frappe.model.naming import NamingSeries, get_default_naming_series
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
class TestNamingSeries(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.dns: DocumentNamingSettings = frappe.get_doc("Document Naming Settings")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def get_valid_serieses(self):
|
||||
VALID_SERIES = ["SINV-", "SI-.{field}.", "SI-#.###", ""]
|
||||
exisiting_series = self.dns.get_transactions_and_prefixes()["prefixes"]
|
||||
return VALID_SERIES + exisiting_series
|
||||
|
||||
def test_naming_preview(self):
|
||||
self.dns.transaction_type = "Webhook"
|
||||
|
||||
self.dns.try_naming_series = "AXBZ.####"
|
||||
serieses = self.dns.preview_series().split("\n")
|
||||
self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses)
|
||||
|
||||
self.dns.try_naming_series = "AXBZ-.{currency}.-"
|
||||
serieses = self.dns.preview_series().split("\n")
|
||||
|
||||
def test_get_transactions(self):
|
||||
|
||||
naming_info = self.dns.get_transactions_and_prefixes()
|
||||
self.assertIn("Webhook", naming_info["transactions"])
|
||||
|
||||
existing_naming_series = frappe.get_meta("Webhook").get_field("naming_series").options
|
||||
|
||||
for series in existing_naming_series.split("\n"):
|
||||
self.assertIn(NamingSeries(series).get_prefix(), naming_info["prefixes"])
|
||||
|
||||
def test_default_naming_series(self):
|
||||
self.assertIn("HOOK", get_default_naming_series("Webhook"))
|
||||
self.assertIsNone(get_default_naming_series("DocType"))
|
||||
|
||||
def test_updates_naming_options(self):
|
||||
self.dns.transaction_type = "Webhook"
|
||||
test_series = "KOOHBEW.###"
|
||||
self.dns.naming_series_options = self.dns.get_options() + "\n" + test_series
|
||||
self.dns.update_series()
|
||||
self.assertIn(test_series, frappe.get_meta("Webhook").get_naming_series_options())
|
||||
|
||||
def test_update_series_counter(self):
|
||||
for series in self.get_valid_serieses():
|
||||
if not series:
|
||||
continue
|
||||
self.dns.prefix = series
|
||||
current_count = cint(self.dns.get_current())
|
||||
new_count = self.dns.current_value = current_count + 1
|
||||
self.dns.update_series_start()
|
||||
|
||||
self.assertEqual(self.dns.get_current(), new_count, f"Incorrect update for {series}")
|
||||
|
|
@ -89,7 +89,9 @@ class Report(Document):
|
|||
]
|
||||
|
||||
custom_roles = get_custom_allowed_roles("report", self.name)
|
||||
allowed.extend(custom_roles)
|
||||
|
||||
if custom_roles:
|
||||
allowed = custom_roles
|
||||
|
||||
if not allowed:
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -186,6 +186,38 @@ class TestReport(FrappeTestCase):
|
|||
self.assertNotEqual(report.is_permitted(), True)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_report_custom_permissions(self):
|
||||
frappe.set_user("test@example.com")
|
||||
frappe.db.delete("Custom Role", {"report": "Test Custom Role Report"})
|
||||
frappe.db.commit() # nosemgrep
|
||||
if not frappe.db.exists("Report", "Test Custom Role Report"):
|
||||
report = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Report",
|
||||
"ref_doctype": "User",
|
||||
"report_name": "Test Custom Role Report",
|
||||
"report_type": "Query Report",
|
||||
"is_standard": "No",
|
||||
"roles": [{"role": "_Test Role"}, {"role": "System Manager"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
else:
|
||||
report = frappe.get_doc("Report", "Test Custom Role Report")
|
||||
|
||||
self.assertEqual(report.is_permitted(), True)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Custom Role",
|
||||
"report": "Test Custom Role Report",
|
||||
"roles": [{"role": "_Test Role 2"}],
|
||||
"ref_doctype": "User",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
self.assertNotEqual(report.is_permitted(), True)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
# test for the `_format` method if report data doesn't have sort_by parameter
|
||||
def test_format_method(self):
|
||||
if frappe.db.exists("Report", "User Activity Report Without Sort"):
|
||||
|
|
|
|||
|
|
@ -194,14 +194,14 @@ frappe.ui.form.on('User', {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (frm.doc.user_emails){
|
||||
var found =0;
|
||||
for (var i = 0;i<frm.doc.user_emails.length;i++){
|
||||
if (frm.doc.email==frm.doc.user_emails[i].email_id){
|
||||
if (frm.doc.user_emails && frappe.model.can_create("Email Account")) {
|
||||
var found = 0;
|
||||
for (var i = 0; i < frm.doc.user_emails.length; i++) {
|
||||
if (frm.doc.email == frm.doc.user_emails[i].email_id) {
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
if (!found){
|
||||
if (!found) {
|
||||
frm.add_custom_button(__("Create User Email"), function() {
|
||||
frm.events.create_user_email(frm);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -424,6 +424,9 @@ class User(Document):
|
|||
|
||||
frappe.cache().delete_key("enabled_users")
|
||||
|
||||
# delete user permissions
|
||||
frappe.db.delete("User Permission", {"user": self.name})
|
||||
|
||||
def before_rename(self, old_name, new_name, merge=False):
|
||||
frappe.clear_cache(user=old_name)
|
||||
self.validate_rename(old_name, new_name)
|
||||
|
|
@ -586,10 +589,7 @@ class User(Document):
|
|||
self.append("social_logins", social_logins)
|
||||
|
||||
def get_restricted_ip_list(self):
|
||||
if not self.restrict_ip:
|
||||
return
|
||||
|
||||
return [i.strip() for i in self.restrict_ip.split(",")]
|
||||
return get_restricted_ip_list(self)
|
||||
|
||||
@classmethod
|
||||
def find_by_credentials(cls, user_name: str, password: str, validate_password: bool = True):
|
||||
|
|
@ -1156,6 +1156,13 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
|
|||
contact.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def get_restricted_ip_list(user):
|
||||
if not user.restrict_ip:
|
||||
return
|
||||
|
||||
return [i.strip() for i in user.restrict_ip.split(",")]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_keys(user):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class Version(Document):
|
|||
if diff:
|
||||
self.ref_doctype = new.doctype
|
||||
self.docname = new.name
|
||||
self.data = frappe.as_json(diff)
|
||||
self.data = frappe.as_json(diff, indent=None, separators=(",", ":"))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -148,6 +148,6 @@ def trigger_indicator_hide():
|
|||
|
||||
def set_notifications_as_unseen(user):
|
||||
try:
|
||||
frappe.db.set_value("Notification Settings", user, "seen", 0)
|
||||
frappe.db.set_value("Notification Settings", user, "seen", 0, update_modified=False)
|
||||
except frappe.DoesNotExistError:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -48,9 +48,15 @@ def create_notification_settings(user):
|
|||
_doc.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def toggle_notifications(user, enable=False):
|
||||
if frappe.db.exists("Notification Settings", user):
|
||||
frappe.db.set_value("Notification Settings", user, "enabled", enable)
|
||||
def toggle_notifications(user: str, enable: bool = False):
|
||||
try:
|
||||
settings = frappe.get_doc("Notification Settings", user)
|
||||
except frappe.DoesNotExistError:
|
||||
return
|
||||
|
||||
if settings.enabled != enable:
|
||||
settings.enabled = enable
|
||||
settings.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ def raise_invalid_field(fieldname):
|
|||
|
||||
def is_standard(fieldname):
|
||||
if "." in fieldname:
|
||||
parenttype, fieldname = get_parenttype_and_fieldname(fieldname, None)
|
||||
fieldname = fieldname.split(".")[1].strip("`")
|
||||
return (
|
||||
fieldname in default_fields or fieldname in optional_fields or fieldname in child_table_fields
|
||||
)
|
||||
|
|
@ -235,7 +235,16 @@ def parse_json(data):
|
|||
|
||||
def get_parenttype_and_fieldname(field, data):
|
||||
if "." in field:
|
||||
parenttype, fieldname = field.split(".")[0][4:-1], field.split(".")[1].strip("`")
|
||||
parts = field.split(".")
|
||||
parenttype = parts[0]
|
||||
fieldname = parts[1]
|
||||
if parenttype.startswith("`tab"):
|
||||
# `tabChild DocType`.`fieldname`
|
||||
parenttype = parenttype[4:-1]
|
||||
fieldname = fieldname.strip("`")
|
||||
else:
|
||||
# tablefield.fieldname
|
||||
parenttype = frappe.get_meta(data.doctype).get_field(parenttype).options
|
||||
else:
|
||||
parenttype = data.doctype
|
||||
fieldname = field.strip("`")
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ class TestEmailAccount(unittest.TestCase):
|
|||
messages = {
|
||||
# append_to = ToDo
|
||||
'"INBOX"': {
|
||||
"latest_messages": [f.read().replace("{{ message_id }}", last_mail.message_id)],
|
||||
"latest_messages": [f.read().replace("{{ message_id }}", "<" + last_mail.message_id + ">")],
|
||||
"seen_status": {2: "UNSEEN"},
|
||||
"uid_list": [2],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,15 @@ from frappe.email.email_body import add_attachment, get_email, get_formatted_htm
|
|||
from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import add_days, cint, cstr, get_hook_method, nowdate, split_emails
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
cstr,
|
||||
get_hook_method,
|
||||
get_string_between,
|
||||
nowdate,
|
||||
split_emails,
|
||||
)
|
||||
|
||||
MAX_RETRY_COUNT = 3
|
||||
|
||||
|
|
@ -635,7 +643,7 @@ class QueueBuilder:
|
|||
d = {
|
||||
"priority": self.send_priority,
|
||||
"attachments": json.dumps(self.get_attachments()),
|
||||
"message_id": mail.msg_root["Message-Id"].strip(" <>"),
|
||||
"message_id": get_string_between("<", mail.msg_root["Message-Id"], ">"),
|
||||
"message": mail_to_string,
|
||||
"sender": self.sender,
|
||||
"reference_doctype": self.reference_doctype,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See LICENSE
|
||||
|
||||
import unittest
|
||||
from random import choice
|
||||
from typing import Union
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
|
@ -17,9 +16,9 @@ from frappe.email.doctype.newsletter.newsletter import (
|
|||
send_scheduled_email,
|
||||
)
|
||||
from frappe.email.queue import flush
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
test_dependencies = ["Email Group"]
|
||||
emails = [
|
||||
"test_subscriber1@example.com",
|
||||
"test_subscriber2@example.com",
|
||||
|
|
@ -63,6 +62,10 @@ class TestNewsletterMixin:
|
|||
for email in emails:
|
||||
doctype = "Email Group Member"
|
||||
email_filters = {"email": email, "email_group": "_Test Email Group"}
|
||||
|
||||
savepoint = "setup_email_group"
|
||||
frappe.db.savepoint(savepoint)
|
||||
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
|
|
@ -71,8 +74,11 @@ class TestNewsletterMixin:
|
|||
}
|
||||
).insert()
|
||||
except Exception:
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
frappe.db.update(doctype, email_filters, "unsubscribed", 0)
|
||||
|
||||
frappe.db.release_savepoint(savepoint)
|
||||
|
||||
def send_newsletter(self, published=0, schedule_send=None) -> Union[str, None]:
|
||||
frappe.db.delete("Email Queue")
|
||||
frappe.db.delete("Email Queue Recipient")
|
||||
|
|
@ -127,7 +133,7 @@ class TestNewsletterMixin:
|
|||
return newsletter
|
||||
|
||||
|
||||
class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
|
||||
class TestNewsletter(TestNewsletterMixin, FrappeTestCase):
|
||||
def test_send(self):
|
||||
self.send_newsletter()
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from frappe.utils import (
|
|||
cstr,
|
||||
extract_email_id,
|
||||
get_datetime,
|
||||
get_string_between,
|
||||
markdown,
|
||||
now,
|
||||
parse_addr,
|
||||
|
|
@ -425,7 +426,9 @@ class Email:
|
|||
self.set_content_and_type()
|
||||
self.set_subject()
|
||||
self.set_from()
|
||||
self.message_id = (self.mail.get("Message-ID") or "").strip(" <>")
|
||||
|
||||
message_id = self.mail.get("Message-ID") or ""
|
||||
self.message_id = get_string_between("<", message_id, ">")
|
||||
|
||||
if self.mail["Date"]:
|
||||
try:
|
||||
|
|
@ -441,7 +444,8 @@ class Email:
|
|||
|
||||
@property
|
||||
def in_reply_to(self):
|
||||
return (self.mail.get("In-Reply-To") or "").strip(" <>")
|
||||
in_reply_to = self.mail.get("In-Reply-To") or ""
|
||||
return get_string_between("<", in_reply_to, ">")
|
||||
|
||||
def parse(self):
|
||||
"""Walk and process multi-part email."""
|
||||
|
|
|
|||
|
|
@ -2412,7 +2412,7 @@
|
|||
"Singapore": {
|
||||
"code": "sg",
|
||||
"currency": "SGD",
|
||||
"currency_fraction": "Sen",
|
||||
"currency_fraction": "Cent",
|
||||
"currency_fraction_units": 100,
|
||||
"currency_name": "Singapore Dollar",
|
||||
"currency_symbol": "$",
|
||||
|
|
|
|||
|
|
@ -473,13 +473,20 @@ def set_all_patches_as_completed(app):
|
|||
|
||||
|
||||
def init_singles():
|
||||
singles = [single["name"] for single in frappe.get_all("DocType", filters={"issingle": True})]
|
||||
singles = frappe.get_all("DocType", filters={"issingle": True}, pluck="name")
|
||||
for single in singles:
|
||||
if not frappe.db.get_singles_dict(single):
|
||||
if frappe.db.get_singles_dict(single):
|
||||
continue
|
||||
|
||||
try:
|
||||
doc = frappe.new_doc(single)
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.save()
|
||||
except ImportError:
|
||||
# The doctype exists, but controller is deleted,
|
||||
# no need to attempt to init such single, ref: #16917
|
||||
continue
|
||||
|
||||
|
||||
def make_conf(
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class DatabaseQuery(object):
|
|||
def __init__(self, doctype, user=None):
|
||||
self.doctype = doctype
|
||||
self.tables = []
|
||||
self.link_tables = []
|
||||
self.conditions = []
|
||||
self.or_conditions = []
|
||||
self.fields = None
|
||||
|
|
@ -216,6 +217,10 @@ class DatabaseQuery(object):
|
|||
parent_name = cast_name(f"{self.tables[0]}.name")
|
||||
args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})"
|
||||
|
||||
# left join link tables
|
||||
for link in self.link_tables:
|
||||
args.tables += f" {self.join} `tab{link.doctype}` on (`tab{link.doctype}`.`name` = {self.tables[0]}.`{link.fieldname}`)"
|
||||
|
||||
if self.grouped_or_conditions:
|
||||
self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})")
|
||||
|
||||
|
|
@ -287,6 +292,29 @@ class DatabaseQuery(object):
|
|||
# remove empty strings / nulls in fields
|
||||
self.fields = [f for f in self.fields if f]
|
||||
|
||||
# convert child_table.fieldname to `tabChild DocType`.`fieldname`
|
||||
for field in self.fields:
|
||||
if "." in field and "tab" not in field:
|
||||
original_field = field
|
||||
alias = None
|
||||
if " as " in field:
|
||||
field, alias = field.split(" as ")
|
||||
linked_fieldname, fieldname = field.split(".")
|
||||
linked_field = frappe.get_meta(self.doctype).get_field(linked_fieldname)
|
||||
linked_doctype = linked_field.options
|
||||
if linked_field.fieldtype == "Link":
|
||||
self.link_tables.append(
|
||||
frappe._dict(
|
||||
doctype=linked_doctype, fieldname=linked_fieldname, table_name=f"`tab{linked_doctype}`"
|
||||
)
|
||||
)
|
||||
|
||||
field = field.replace(linked_fieldname, f"`tab{linked_doctype}`")
|
||||
field = field.replace(fieldname, f"`{fieldname}`")
|
||||
if alias:
|
||||
field = f"{field} as {alias}"
|
||||
self.fields[self.fields.index(original_field)] = field
|
||||
|
||||
for filter_name in ["filters", "or_filters"]:
|
||||
filters = getattr(self, filter_name)
|
||||
if isinstance(filters, str):
|
||||
|
|
@ -396,7 +424,9 @@ class DatabaseQuery(object):
|
|||
table_name = table_name[13:]
|
||||
if not table_name[0] == "`":
|
||||
table_name = f"`{table_name}`"
|
||||
if table_name not in self.tables:
|
||||
if table_name not in self.tables and table_name not in (
|
||||
d.table_name for d in self.link_tables
|
||||
):
|
||||
self.append_table(table_name)
|
||||
|
||||
def append_table(self, table_name):
|
||||
|
|
@ -418,7 +448,7 @@ class DatabaseQuery(object):
|
|||
methods = ("count(", "avg(", "sum(", "extract(", "dayofyear(")
|
||||
return field.lower().startswith(methods)
|
||||
|
||||
if len(self.tables) > 1:
|
||||
if len(self.tables) > 1 or len(self.link_tables) > 0:
|
||||
for idx, field in enumerate(self.fields):
|
||||
if "." not in field and not _in_standard_sql_methods(field):
|
||||
self.fields[idx] = f"{self.tables[0]}.{field}"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import os
|
||||
import shutil
|
||||
from typing import List
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
|
|
@ -89,10 +90,6 @@ def delete_doc(
|
|||
update_flags(doc, flags, ignore_permissions)
|
||||
check_permission_and_not_submitted(doc)
|
||||
|
||||
frappe.db.delete("Custom Field", {"dt": name})
|
||||
frappe.db.delete("Client Script", {"dt": name})
|
||||
frappe.db.delete("Property Setter", {"doc_type": name})
|
||||
frappe.db.delete("Report", {"ref_doctype": name})
|
||||
frappe.db.delete("Custom DocPerm", {"parent": name})
|
||||
frappe.db.delete("__global_search", {"doctype": name})
|
||||
|
||||
|
|
@ -193,39 +190,24 @@ def update_naming_series(doc):
|
|||
revert_series_if_last(doc.meta.autoname, doc.name, doc)
|
||||
|
||||
|
||||
def delete_from_table(doctype, name, ignore_doctypes, doc):
|
||||
def delete_from_table(doctype: str, name: str, ignore_doctypes: List[str], doc):
|
||||
if doctype != "DocType" and doctype == name:
|
||||
frappe.db.delete("Singles", {"doctype": name})
|
||||
else:
|
||||
frappe.db.delete(doctype, {"name": name})
|
||||
# get child tables
|
||||
if doc:
|
||||
tables = [d.options for d in doc.meta.get_table_fields()]
|
||||
|
||||
child_doctypes = [d.options for d in doc.meta.get_table_fields()]
|
||||
else:
|
||||
child_doctypes = frappe.get_all(
|
||||
"DocField",
|
||||
fields="options",
|
||||
filters={"fieldtype": ["in", frappe.model.table_fields], "parent": doctype},
|
||||
pluck="options",
|
||||
)
|
||||
|
||||
def get_table_fields(field_doctype):
|
||||
if field_doctype == "Custom Field":
|
||||
return []
|
||||
|
||||
return [
|
||||
r[0]
|
||||
for r in frappe.get_all(
|
||||
field_doctype,
|
||||
fields="options",
|
||||
filters={"fieldtype": ["in", frappe.model.table_fields], "parent": doctype},
|
||||
as_list=1,
|
||||
)
|
||||
]
|
||||
|
||||
tables = get_table_fields("DocField")
|
||||
if not frappe.flags.in_install == "frappe":
|
||||
tables += get_table_fields("Custom Field")
|
||||
|
||||
# delete from child tables
|
||||
for t in list(set(tables)):
|
||||
if t not in ignore_doctypes:
|
||||
frappe.db.delete(t, {"parenttype": doctype, "parent": name})
|
||||
child_doctypes_to_delete = set(child_doctypes) - set(ignore_doctypes)
|
||||
for child_doctype in child_doctypes_to_delete:
|
||||
frappe.db.delete(child_doctype, {"parenttype": doctype, "parent": name})
|
||||
|
||||
|
||||
def update_flags(doc, flags=None, ignore_permissions=False):
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ Example:
|
|||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
import click
|
||||
|
||||
|
|
@ -341,6 +342,16 @@ class Meta(Document):
|
|||
def get_workflow(self):
|
||||
return get_workflow_name(self.name)
|
||||
|
||||
def get_naming_series_options(self) -> List[str]:
|
||||
"""Get list naming series options."""
|
||||
|
||||
field = self.get_field("naming_series")
|
||||
if field:
|
||||
options = field.options or ""
|
||||
|
||||
return options.split("\n")
|
||||
return []
|
||||
|
||||
def add_custom_fields(self):
|
||||
if not frappe.db.table_exists("Custom Field"):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional, Union
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -11,6 +11,7 @@ from frappe.query_builder import DocType
|
|||
from frappe.utils import cint, cstr, now_datetime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import Meta
|
||||
|
||||
|
||||
|
|
@ -18,6 +19,95 @@ if TYPE_CHECKING:
|
|||
# whether `log_types` have autoincremented naming set for the site or not.
|
||||
autoincremented_site_status_map = {}
|
||||
|
||||
NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE)
|
||||
|
||||
|
||||
class InvalidNamingSeriesError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class NamingSeries:
|
||||
__slots__ = ("series",)
|
||||
|
||||
def __init__(self, series: str):
|
||||
self.series = series
|
||||
|
||||
# Add default number part if missing
|
||||
if "#" not in self.series:
|
||||
self.series += ".#####"
|
||||
|
||||
def validate(self):
|
||||
if "." not in self.series:
|
||||
frappe.throw(
|
||||
_("Invalid naming series {}: dot (.) missing").format(frappe.bold(self.series)),
|
||||
exc=InvalidNamingSeriesError,
|
||||
)
|
||||
|
||||
if not NAMING_SERIES_PATTERN.match(self.series):
|
||||
frappe.throw(
|
||||
_(
|
||||
'Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series',
|
||||
),
|
||||
exc=InvalidNamingSeriesError,
|
||||
)
|
||||
|
||||
def generate_next_name(self, doc: "Document") -> str:
|
||||
self.validate()
|
||||
parts = self.series.split(".")
|
||||
return parse_naming_series(parts, doc=doc)
|
||||
|
||||
def get_prefix(self) -> str:
|
||||
"""Naming series stores prefix to maintain a counter in DB. This prefix can be used to update counter or validations.
|
||||
|
||||
e.g. `SINV-.YY.-.####` has prefix of `SINV-22-` in database for year 2022.
|
||||
"""
|
||||
|
||||
prefix = None
|
||||
|
||||
def fake_counter_backend(partial_series, digits):
|
||||
nonlocal prefix
|
||||
prefix = partial_series
|
||||
return "#" * digits
|
||||
|
||||
# This function evaluates all parts till we hit numerical parts and then
|
||||
# sends prefix + digits to DB to find next number.
|
||||
# Instead of reimplementing the whole parsing logic in multiple places we
|
||||
# can just ask this function to give us the prefix.
|
||||
parse_naming_series(self.series, number_generator=fake_counter_backend)
|
||||
|
||||
if prefix is None:
|
||||
frappe.throw(_("Invalid Naming Series"))
|
||||
|
||||
return prefix
|
||||
|
||||
def get_preview(self, doc=None) -> List[str]:
|
||||
"""Generate preview of naming series without using DB counters"""
|
||||
generated_names = []
|
||||
for count in range(1, 4):
|
||||
|
||||
def fake_counter(_prefix, digits):
|
||||
return str(count).zfill(digits)
|
||||
|
||||
generated_names.append(parse_naming_series(self.series, doc=doc, number_generator=fake_counter))
|
||||
return generated_names
|
||||
|
||||
def update_counter(self, new_count: int) -> None:
|
||||
"""Warning: Incorrectly updating series can result in unusable transactions"""
|
||||
Series = frappe.qb.DocType("Series")
|
||||
prefix = self.get_prefix()
|
||||
|
||||
# Initialize if not present in DB
|
||||
if frappe.db.get_value("Series", prefix, "name", order_by="name") is None:
|
||||
frappe.qb.into(Series).insert(prefix, 0).columns("name", "current").run()
|
||||
|
||||
(
|
||||
frappe.qb.update(Series).set(Series.current, cint(new_count)).where(Series.name == prefix)
|
||||
).run()
|
||||
|
||||
def get_current_value(self) -> int:
|
||||
prefix = self.get_prefix()
|
||||
return cint(frappe.db.get_value("Series", prefix, "current", order_by="name"))
|
||||
|
||||
|
||||
def set_new_name(doc):
|
||||
"""
|
||||
|
|
@ -175,24 +265,32 @@ def make_autoname(key="", doctype="", doc=""):
|
|||
if key == "hash":
|
||||
return frappe.generate_hash(doctype, 10)
|
||||
|
||||
if "#" not in key:
|
||||
key = key + ".#####"
|
||||
elif "." not in key:
|
||||
error_message = _("Invalid naming series (. missing)")
|
||||
if doctype:
|
||||
error_message = _("Invalid naming series (. missing) for {0}").format(doctype)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
parts = key.split(".")
|
||||
n = parse_naming_series(parts, doctype, doc)
|
||||
return n
|
||||
series = NamingSeries(key)
|
||||
return series.generate_next_name(doc)
|
||||
|
||||
|
||||
def parse_naming_series(parts, doctype="", doc=""):
|
||||
n = ""
|
||||
def parse_naming_series(
|
||||
parts: Union[List[str], str],
|
||||
doctype=None,
|
||||
doc: Optional["Document"] = None,
|
||||
number_generator: Optional[Callable[[str, int], str]] = None,
|
||||
) -> str:
|
||||
|
||||
"""Parse the naming series and get next name.
|
||||
|
||||
args:
|
||||
parts: naming series parts (split by `.`)
|
||||
doc: document to use for series that have parts using fieldnames
|
||||
number_generator: Use different counter backend other than `tabSeries`. Primarily used for testing.
|
||||
"""
|
||||
|
||||
name = ""
|
||||
if isinstance(parts, str):
|
||||
parts = parts.split(".")
|
||||
|
||||
if not number_generator:
|
||||
number_generator = getseries
|
||||
|
||||
series_set = False
|
||||
today = now_datetime()
|
||||
for e in parts:
|
||||
|
|
@ -200,7 +298,7 @@ def parse_naming_series(parts, doctype="", doc=""):
|
|||
if e.startswith("#"):
|
||||
if not series_set:
|
||||
digits = len(e)
|
||||
part = getseries(n, digits)
|
||||
part = number_generator(name, digits)
|
||||
series_set = True
|
||||
elif e == "YY":
|
||||
part = today.strftime("%y")
|
||||
|
|
@ -225,9 +323,9 @@ def parse_naming_series(parts, doctype="", doc=""):
|
|||
part = e
|
||||
|
||||
if isinstance(part, str):
|
||||
n += part
|
||||
name += part
|
||||
|
||||
return n
|
||||
return name
|
||||
|
||||
|
||||
def determine_consecutive_week_number(datetime):
|
||||
|
|
@ -311,14 +409,15 @@ def revert_series_if_last(key, name, doc=None):
|
|||
frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix)
|
||||
|
||||
|
||||
def get_default_naming_series(doctype):
|
||||
def get_default_naming_series(doctype: str) -> Optional[str]:
|
||||
"""get default value for `naming_series` property"""
|
||||
naming_series = frappe.get_meta(doctype).get_field("naming_series").options or ""
|
||||
if naming_series:
|
||||
naming_series = naming_series.split("\n")
|
||||
return naming_series[0] or naming_series[1]
|
||||
else:
|
||||
return None
|
||||
naming_series_options = frappe.get_meta(doctype).get_naming_series_options()
|
||||
|
||||
# Return first truthy options
|
||||
# Empty strings are used to avoid populating forms by default
|
||||
for option in naming_series_options:
|
||||
if option:
|
||||
return option
|
||||
|
||||
|
||||
def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None):
|
||||
|
|
|
|||
|
|
@ -207,13 +207,18 @@ def export_doc(doctype, name, module=None):
|
|||
write_document_file(frappe.get_doc(doctype, name), module)
|
||||
|
||||
|
||||
def get_doctype_module(doctype):
|
||||
def get_doctype_module(doctype) -> str:
|
||||
"""Returns **Module Def** name of given doctype."""
|
||||
|
||||
def make_modules_dict():
|
||||
return dict(frappe.db.sql("select name, module from tabDocType"))
|
||||
|
||||
return frappe.cache().get_value("doctype_modules", make_modules_dict)[doctype]
|
||||
doctype_module_map = frappe.cache().get_value("doctype_modules", make_modules_dict)
|
||||
|
||||
if module_name := doctype_module_map.get(doctype):
|
||||
return module_name
|
||||
else:
|
||||
frappe.throw(_("DocType {} not found").format(doctype), exc=frappe.DoesNotExistError)
|
||||
|
||||
|
||||
doctype_python_modules = {}
|
||||
|
|
@ -234,9 +239,9 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""):
|
|||
if key not in doctype_python_modules:
|
||||
doctype_python_modules[key] = frappe.get_module(module_name)
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Module import failed for {0} ({1})".format(doctype, module_name + " Error: " + str(e))
|
||||
)
|
||||
msg = f"Module import failed for {doctype}, the DocType you're trying to open might be deleted."
|
||||
msg += f"<br> Error: {e}"
|
||||
raise ImportError(msg) from e
|
||||
|
||||
return doctype_python_modules[key]
|
||||
|
||||
|
|
@ -251,12 +256,15 @@ def get_module_name(doctype, module, prefix="", suffix="", app=None):
|
|||
)
|
||||
|
||||
|
||||
def get_module_app(module):
|
||||
return frappe.local.module_app[scrub(module)]
|
||||
def get_module_app(module: str) -> str:
|
||||
app = frappe.local.module_app.get(scrub(module))
|
||||
if app is None:
|
||||
frappe.throw(_("Module {} not found").format(module), exc=frappe.DoesNotExistError)
|
||||
return app
|
||||
|
||||
|
||||
def get_app_publisher(module):
|
||||
app = frappe.local.module_app[scrub(module)]
|
||||
def get_app_publisher(module: str) -> str:
|
||||
app = get_module_app(module)
|
||||
if not app:
|
||||
frappe.throw(_("App not found"))
|
||||
app_publisher = frappe.get_hooks(hook="app_publisher", app_name=app)[0]
|
||||
|
|
@ -321,7 +329,7 @@ def make_boilerplate(template, doc, opts=None):
|
|||
base_class=base_class,
|
||||
doctype=doc.name,
|
||||
**opts,
|
||||
custom_controller=custom_controller
|
||||
custom_controller=custom_controller,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
const field = $(e.currentTarget).parent();
|
||||
// new dialog
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: "Set Properties",
|
||||
title: __("Set Properties"),
|
||||
fields: [
|
||||
{
|
||||
label: __("Label"),
|
||||
|
|
@ -452,7 +452,8 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
label: __("Align Value"),
|
||||
fieldname: "align",
|
||||
fieldtype: "Select",
|
||||
options: [{'label': __('Left'), 'value': 'left'}, {'label': __('Right'), 'value': 'right'}]
|
||||
options: [{'label': __('Left', null, 'alignment'), 'value': 'left'},
|
||||
{'label': __('Right', null, 'alignment'), 'value': 'right'}]
|
||||
},
|
||||
{
|
||||
label: __("Remove Field"),
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@ frappe.ui.form.ControlHTML = class ControlHTML extends frappe.ui.form.Control {
|
|||
this.df.options = html;
|
||||
this.html(html);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
|
|||
var f = this.fields_dict[key];
|
||||
if (f) {
|
||||
f.set_value(val).then(() => {
|
||||
f.set_input(val);
|
||||
f.set_input?.(val);
|
||||
this.refresh_dependency();
|
||||
resolve();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -181,8 +181,8 @@ export default {
|
|||
return [
|
||||
{ label: __("Top"), fieldname: "margin_top" },
|
||||
{ label: __("Bottom"), fieldname: "margin_bottom" },
|
||||
{ label: __("Left"), fieldname: "margin_left" },
|
||||
{ label: __("Right"), fieldname: "margin_right" }
|
||||
{ label: __("Left", null, 'alignment'), fieldname: "margin_left" },
|
||||
{ label: __("Right", null, 'alignment'), fieldname: "margin_right" }
|
||||
];
|
||||
},
|
||||
fields() {
|
||||
|
|
|
|||
|
|
@ -104,12 +104,12 @@
|
|||
}
|
||||
|
||||
.group-by-button.btn-primary-light {
|
||||
color: var(--blue-500);
|
||||
color: var(--text-on-blue);
|
||||
}
|
||||
|
||||
.group-by-icon.active {
|
||||
use {
|
||||
stroke: var(--blue-500);
|
||||
stroke: var(--text-on-blue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.page_content {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.25rem;
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class ParameterizedFunction(Function):
|
|||
return function_sql
|
||||
|
||||
|
||||
class subqry(Criterion):
|
||||
class SubQuery(Criterion):
|
||||
def __init__(
|
||||
self,
|
||||
subq: QueryBuilder,
|
||||
|
|
@ -112,3 +112,6 @@ class subqry(Criterion):
|
|||
def get_sql(self, **kwg: Any) -> str:
|
||||
kwg["subquery"] = True
|
||||
return self.subq.get_sql(**kwg)
|
||||
|
||||
|
||||
subqry = SubQuery
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ def set_permission(doctype, name, user, permission_to, value=1, everyone=0):
|
|||
|
||||
if not (share.read or share.write or share.submit or share.share):
|
||||
share.delete()
|
||||
share = {}
|
||||
share = None
|
||||
|
||||
return share
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ $(document).ready(function(){
|
|||
var options = {
|
||||
"key": "{{ api_key }}",
|
||||
"amount": cint({{ amount }} * 100), // 2000 paise = INR 20
|
||||
"currency": "{{ currency }}",
|
||||
"name": "{{ title }}",
|
||||
"description": "{{ description }}",
|
||||
"subscription_id": "{{ subscription_id }}",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ expected_keys = (
|
|||
"payer_name",
|
||||
"payer_email",
|
||||
"order_id",
|
||||
"currency",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,33 +2,32 @@
|
|||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.page-card {
|
||||
max-width: 360px;
|
||||
padding: 15px;
|
||||
margin: 70px auto;
|
||||
border-radius: 4px;
|
||||
background-color: var(--fg-color);
|
||||
/* box-shadow: var(--shadow-base); */
|
||||
max-width: 360px;
|
||||
padding: 15px;
|
||||
margin: 70px auto;
|
||||
border-radius: 4px;
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: var(--shadow-base);
|
||||
}
|
||||
|
||||
.for-reset-password {
|
||||
margin: 80px 0;
|
||||
margin: 80px 0;
|
||||
}
|
||||
|
||||
.for-reset-password .page-card {
|
||||
border: 0;
|
||||
max-width: 450px;
|
||||
margin: auto;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
max-width: 450px;
|
||||
margin: auto;
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 40px 60px;
|
||||
}
|
||||
|
||||
@media (min-width: 567px) {
|
||||
@media (max-width: 425px) {
|
||||
.for-reset-password .page-card {
|
||||
box-shadow: var(--shadow-base);
|
||||
padding: 40px 60px;
|
||||
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,35 @@ class TestReportview(unittest.TestCase):
|
|||
|
||||
clear_custom_fields("DocType")
|
||||
|
||||
def test_child_table_field_syntax(self):
|
||||
note = frappe.get_doc(
|
||||
doctype="Note",
|
||||
title=f"Test {frappe.utils.random_string(8)}",
|
||||
content="test",
|
||||
seen_by=[{"user": "Administrator"}],
|
||||
).insert()
|
||||
result = frappe.db.get_all(
|
||||
"Note",
|
||||
filters={"name": note.name},
|
||||
fields=["name", "seen_by.user as seen_by"],
|
||||
limit=1,
|
||||
)
|
||||
self.assertEqual(result[0].seen_by, "Administrator")
|
||||
note.delete()
|
||||
|
||||
def test_link_field_syntax(self):
|
||||
todo = frappe.get_doc(
|
||||
doctype="ToDo", description="Test ToDo", allocated_to="Administrator"
|
||||
).insert()
|
||||
result = frappe.db.get_all(
|
||||
"ToDo",
|
||||
filters={"name": todo.name},
|
||||
fields=["name", "allocated_to.email as allocated_user_email"],
|
||||
limit=1,
|
||||
)
|
||||
self.assertEqual(result[0].allocated_user_email, "admin@example.com")
|
||||
todo.delete()
|
||||
|
||||
def test_build_match_conditions(self):
|
||||
clear_user_permissions_for_doctype("Blog Post", "test2@example.com")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
from frappe.model.naming import (
|
||||
InvalidNamingSeriesError,
|
||||
NamingSeries,
|
||||
append_number_if_name_exists,
|
||||
determine_consecutive_week_number,
|
||||
getseries,
|
||||
revert_series_if_last,
|
||||
)
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import now_datetime
|
||||
|
||||
|
||||
class TestNaming(unittest.TestCase):
|
||||
class TestNaming(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Note")
|
||||
|
||||
|
|
@ -52,16 +53,13 @@ class TestNaming(unittest.TestCase):
|
|||
self.assertEqual(country.name, country.country_name)
|
||||
|
||||
def test_child_table_naming(self):
|
||||
child_dt_with_naming = new_doctype(
|
||||
"childtable_with_autonaming", istable=1, autoname="field:some_fieldname"
|
||||
).insert()
|
||||
child_dt_with_naming = new_doctype(istable=1, autoname="field:some_fieldname").insert()
|
||||
dt_with_child_autoname = new_doctype(
|
||||
"dt_with_childtable_naming",
|
||||
fields=[
|
||||
{
|
||||
"label": "table with naming",
|
||||
"fieldname": "table_with_naming",
|
||||
"options": "childtable_with_autonaming",
|
||||
"options": child_dt_with_naming.name,
|
||||
"fieldtype": "Table",
|
||||
}
|
||||
],
|
||||
|
|
@ -69,7 +67,7 @@ class TestNaming(unittest.TestCase):
|
|||
|
||||
name = frappe.generate_hash(length=10)
|
||||
|
||||
doc = frappe.new_doc("dt_with_childtable_naming")
|
||||
doc = frappe.new_doc(dt_with_child_autoname.name)
|
||||
doc.append("table_with_naming", {"some_fieldname": name})
|
||||
doc.save()
|
||||
self.assertEqual(doc.table_with_naming[0].name, name)
|
||||
|
|
@ -89,31 +87,18 @@ class TestNaming(unittest.TestCase):
|
|||
"""
|
||||
Test if braced params are replaced in format autoname
|
||||
"""
|
||||
doctype = "ToDo"
|
||||
|
||||
todo_doctype = frappe.get_doc("DocType", doctype)
|
||||
todo_doctype.autoname = "format:TODO-{MM}-{status}-{##}"
|
||||
todo_doctype.save()
|
||||
doctype = new_doctype(autoname="format:TODO-{MM}-{some_fieldname}-{##}").insert()
|
||||
|
||||
description = "Format"
|
||||
|
||||
todo = frappe.new_doc(doctype)
|
||||
todo.description = description
|
||||
todo.insert()
|
||||
doc = frappe.new_doc(doctype.name)
|
||||
doc.some_fieldname = description
|
||||
doc.insert()
|
||||
|
||||
series = getseries("", 2)
|
||||
series = int(series) - 1
|
||||
|
||||
series = str(int(series) - 1)
|
||||
|
||||
if len(series) < 2:
|
||||
series = "0" + series
|
||||
|
||||
self.assertEqual(
|
||||
todo.name,
|
||||
"TODO-{month}-{status}-{series}".format(
|
||||
month=now_datetime().strftime("%m"), status=todo.status, series=series
|
||||
),
|
||||
)
|
||||
self.assertEqual(doc.name, f"TODO-{now_datetime().strftime('%m')}-{description}-{series:02}")
|
||||
|
||||
def test_format_autoname_for_consecutive_week_number(self):
|
||||
"""
|
||||
|
|
@ -303,6 +288,46 @@ class TestNaming(unittest.TestCase):
|
|||
|
||||
dt.delete(ignore_permissions=True)
|
||||
|
||||
def test_naming_series_prefix(self):
|
||||
today = now_datetime()
|
||||
year = today.strftime("%y")
|
||||
month = today.strftime("%m")
|
||||
|
||||
prefix_test_cases = {
|
||||
"SINV-.YY.-.####": f"SINV-{year}-",
|
||||
"SINV-.YY.-.MM.-.####": f"SINV-{year}-{month}-",
|
||||
"SINV": "SINV",
|
||||
"SINV-.": "SINV-",
|
||||
}
|
||||
|
||||
for series, prefix in prefix_test_cases.items():
|
||||
self.assertEqual(prefix, NamingSeries(series).get_prefix())
|
||||
|
||||
def test_naming_series_validation(self):
|
||||
dns = frappe.get_doc("Document Naming Settings")
|
||||
exisiting_series = dns.get_transactions_and_prefixes()["prefixes"]
|
||||
valid = ["SINV-", "SI-.{field}.", "SI-#.###", ""] + exisiting_series
|
||||
invalid = ["$INV-", r"WINDOWS\NAMING"]
|
||||
|
||||
for series in valid:
|
||||
if series.strip():
|
||||
try:
|
||||
NamingSeries(series).validate()
|
||||
except Exception as e:
|
||||
self.fail(f"{series} should be valid\n{e}")
|
||||
|
||||
for series in invalid:
|
||||
self.assertRaises(InvalidNamingSeriesError, NamingSeries(series).validate)
|
||||
|
||||
def test_naming_using_fields(self):
|
||||
|
||||
webhook = frappe.new_doc("Webhook")
|
||||
webhook.webhook_docevent = "on_update"
|
||||
name = NamingSeries("KOOH-.{webhook_docevent}.").generate_next_name(webhook)
|
||||
self.assertTrue(
|
||||
name.startswith("KOOH-on_update"), f"incorrect name generated {name}, missing field value"
|
||||
)
|
||||
|
||||
|
||||
def make_invalid_todo():
|
||||
frappe.get_doc({"doctype": "ToDo", "description": "Test"}).insert(set_name="ToDo")
|
||||
|
|
|
|||
|
|
@ -615,3 +615,29 @@ class TestAppParser(unittest.TestCase):
|
|||
self.assertEqual("healthcare", parse_app_name("https://github.com/frappe/healthcare.git"))
|
||||
self.assertEqual("healthcare", parse_app_name("git@github.com:frappe/healthcare.git"))
|
||||
self.assertEqual("healthcare", parse_app_name("frappe/healthcare@develop"))
|
||||
|
||||
|
||||
class TestIntrospectionMagic(unittest.TestCase):
|
||||
"""Test utils that inspect live objects"""
|
||||
|
||||
def test_get_newargs(self):
|
||||
# `kwargs` is just convention any **varname should work.
|
||||
def f(a, b=2, **args):
|
||||
pass
|
||||
|
||||
safe_kwargs = {"company": "Wind Power", "b": 1}
|
||||
self.assertEqual(frappe.get_newargs(f, safe_kwargs), safe_kwargs)
|
||||
|
||||
unsafe_args = dict(safe_kwargs)
|
||||
unsafe_args.update({"ignore_permissions": True, "flags": {"ignore_mandatory": True}})
|
||||
self.assertEqual(frappe.get_newargs(f, unsafe_args), safe_kwargs)
|
||||
|
||||
def test_strip_off_kwargs_when_not_supported(self):
|
||||
def f(a, b=2):
|
||||
pass
|
||||
|
||||
args = {"company": "Wind Power", "b": 1}
|
||||
self.assertEqual(frappe.get_newargs(f, args), {"b": 1})
|
||||
|
||||
# No args
|
||||
self.assertEqual(frappe.get_newargs(lambda: None, args), {})
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class TestWebsite(unittest.TestCase):
|
|||
def test_error_page(self):
|
||||
set_request(method="GET", path="/_test/problematic_page")
|
||||
response = get_response()
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertEqual(response.status_code, 417)
|
||||
|
||||
def test_login(self):
|
||||
set_request(method="GET", path="/login")
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ def _restore_thread_locals(flags):
|
|||
frappe.local.realtime_log = []
|
||||
frappe.local.conf = frappe._dict(frappe.get_site_config())
|
||||
frappe.local.cache = {}
|
||||
frappe.local.lang = "en"
|
||||
frappe.local.lang_full_dict = None
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
|
|||
|
|
@ -4717,3 +4717,6 @@ Document has been cancelled,Document annulé
|
|||
Document is in draft state,Document au statut brouillon
|
||||
Copy to Clipboard,Copier vers le presse-papiers
|
||||
Don't have an account?,Vous n'avez pas de compte?
|
||||
Left:alignment,Gauche
|
||||
Right:alignment,Droite
|
||||
Set Properties,Gérer les proriétés
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 421.
|
|
|
@ -840,7 +840,7 @@ Default Sending and Inbox,По умолчанию отправка и получ
|
|||
Default Sort Field,Поле сортировки по умолчанию,
|
||||
Default Sort Order,Порядок сортировки по умолчанию,
|
||||
Default Value,Значение по умолчанию,
|
||||
"Default: ""Contact Us""","По умолчанию: ""Обратная связь""",
|
||||
"Default: ""Contact Us""","По умолчанию: ""Contact Us""",
|
||||
DefaultValue,DefaultValue,
|
||||
Define workflows for forms.,Определите рабочие процессы для форм.,
|
||||
Defines actions on states and the next step and allowed roles.,"Определяет действия на статусах, следующий шаг и роли, обладающие правами перевода статусов.",
|
||||
|
|
@ -849,7 +849,7 @@ Delayed,Задерживается,
|
|||
Delete Data,Удалить данные,
|
||||
Delete comment?,Удалить комментарий?,
|
||||
Delete this record to allow sending to this email address,"Удалить эту запись, чтобы разрешить отправку на этот адрес электронной почты",
|
||||
Delete {0} items permanently?,Удалить {0} продуктов навсегда?,
|
||||
Delete {0} items permanently?,Удалить {0} объектов навсегда?,
|
||||
Deleted,Удаленный,
|
||||
Deleted DocType,Удаленный DocType,
|
||||
Deleted Document,Удаленный документ,
|
||||
|
|
@ -914,7 +914,7 @@ Document can't saved.,Документ не может быть сохранен
|
|||
Document {0} has been set to state {1} by {2},Документ {0} установлен в состояние {1} на {2},
|
||||
Documents,Документы,
|
||||
Documents assigned to you and by you.,"Документы, назначенные вам и вами.",
|
||||
Domain Settings,Настройки домена,
|
||||
Domain Settings,Настройка сфер деятельности,
|
||||
Domains HTML,Домены HTML,
|
||||
"Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field","Не HTML Кодировать HTML-теги, такие как <скрипт> или просто символы, такие как <или>, так как они могут быть преднамеренно использованы в этой области",
|
||||
Don't Override Status,Не переопределять статус,
|
||||
|
|
@ -990,7 +990,7 @@ Enable Auto Reply,Включить автоматический ответ,
|
|||
Enable Automatic Backup,Включить автоматическое резервное копирование,
|
||||
Enable Chat,Включить чат,
|
||||
Enable Comments,Включить комментарии,
|
||||
Enable Incoming,Включение входящей,
|
||||
Enable Incoming,Включить входящие,
|
||||
Enable Outgoing,Включить исходящие,
|
||||
Enable Password Policy,Включить политику паролей,
|
||||
Enable Print Server,Включить сервер печати,
|
||||
|
|
@ -1000,7 +1000,7 @@ Enable Scheduled Jobs,Включить запланированных задан
|
|||
Enable Social Login,Включить социальный вход,
|
||||
Enable Two Factor Auth,Включить двухфакторный аут,
|
||||
Enabled email inbox for user {0},Включен почтовый ящик для пользователя {0},
|
||||
"Encryption key is invalid, Please check site_config.json","Ключ шифрования недействителен, проверьте сайт_config.json",
|
||||
"Encryption key is invalid, Please check site_config.json","Ключ шифрования недействителен, проверьте site_config.json",
|
||||
End Date Field,Поле конечной даты,
|
||||
End Date cannot be before Start Date!,Дата окончания не может быть до даты начала!,
|
||||
Endpoint URL,URL конечной точки,
|
||||
|
|
@ -1029,8 +1029,8 @@ Error in Notification: {},Ошибка в уведомлении: {},
|
|||
Error while connecting to email account {0},Ошибка при подключении к учетной записи электронной почты {0},
|
||||
Error while evaluating Notification {0}. Please fix your template.,Ошибка при оценке уведомления {0}. Исправьте шаблон.,
|
||||
Error: Document has been modified after you have opened it,"Ошибка: документ был изменен после того, как вы открыли его",
|
||||
Error: Value missing for {0}: {1},Ошибка: значение отсутствует для {0}: {1},
|
||||
Errors in Background Events,Ошибки в фоновых событий,
|
||||
Error: Value missing for {0}: {1},Ошибка: отсутствует значение для {0}: {1},
|
||||
Errors in Background Events,Ошибки в фоновых событиях,
|
||||
Event Category,Категория события,
|
||||
Event Participants,Участники мероприятия,
|
||||
Event Type,Тип события,
|
||||
|
|
@ -1184,7 +1184,7 @@ Get Contacts,Получить контакты,
|
|||
Get Fields,Получить поля,
|
||||
Get your globally recognized avatar from Gravatar.com,Получить всемирно признанный аватара из Gravatar.com,
|
||||
GitHub,GitHub,
|
||||
Give Review Points,Дайте очки обзора,
|
||||
Give Review Points,Дайте баллы обзора,
|
||||
Global Unsubscribe,Глобальная отписка,
|
||||
Go to the document,Перейти к документу,
|
||||
Go to this URL after completing the form (only for Guest users),Перейдите по этому URL-адресу после заполнения формы (только для гостевых пользователей),
|
||||
|
|
@ -1461,8 +1461,8 @@ Letter Head Name,Название заголовка письма,
|
|||
Letter Head in HTML,Заголовок письма в HTML,
|
||||
Level Name,Название уровня,
|
||||
Liked,Понравилось,
|
||||
Liked By,В избранное К,
|
||||
Liked by {0},В избранное {0},
|
||||
Liked By,Нравится,
|
||||
Liked by {0},Нравится {0},
|
||||
Likes,Понравившееся,
|
||||
Limit Number of DB Backups,Ограничение количества резервных копий БД,
|
||||
Line,Линия,
|
||||
|
|
@ -1470,7 +1470,7 @@ Link DocType,Ссылка DocType,
|
|||
Link Expired,Срок действия ссылки,
|
||||
Link Name,Имя ссылки,
|
||||
Link Title,Название ссылки,
|
||||
"Link that is the website home page. Standard Links (index, login, products, blog, about, contact)","Ссылка, которая является стартовой страницей сайта. Стандартные ссылки (индекс, логин, продукты, блог, о, контакт)",
|
||||
"Link that is the website home page. Standard Links (index, login, products, blog, about, contact)","Ссылка, которая является стартовой страницей сайта. Стандартные ссылки (index, login, products, blog, about, contact)",
|
||||
Link to the page you want to open. Leave blank if you want to make it a group parent.,"Ссылка на страницу, которую вы хотите открыть. Оставьте пустым, если хотите сделать его родительским элементом группы.",
|
||||
Linked,Связанный,
|
||||
Linked With,Связанные с,
|
||||
|
|
@ -2096,7 +2096,7 @@ Revert Of,Вернуть из,
|
|||
Reverted,Отменено,
|
||||
Review Level,Уровень обзора,
|
||||
Review Levels,Уровни обзора,
|
||||
Review Points,Очки обзора,
|
||||
Review Points,Баллы обзора,
|
||||
Reviews,Отзывы,
|
||||
Revoke,Аннулировать,
|
||||
Revoked,Аннулировано,
|
||||
|
|
@ -2141,10 +2141,10 @@ SMS sent to following numbers: {0},SMS отправлено следующим
|
|||
SMTP Server,SMTP-сервер,
|
||||
SMTP Settings for outgoing emails,Настройки SMTP для исходящих писем,
|
||||
"SQL Conditions. Example: status=""Open""",SQL условия. Пример: статус = "Открыть",
|
||||
SSL/TLS Mode,Режим SSL / TLS,
|
||||
SSL/TLS Mode,Режим SSL/TLS,
|
||||
Salesforce,Salesforce,
|
||||
Same Field is entered more than once,Одно и то же поле вводится не один раз,
|
||||
Save API Secret: ,Сохранить API-интерфейс:,
|
||||
Save API Secret: ,Сохранить API секрет: ,
|
||||
Save As,Сохранить как,
|
||||
Save Filter,Сохранить фильтр,
|
||||
Save Report,Сохранить отчет,
|
||||
|
|
@ -2258,7 +2258,7 @@ Set Property After Alert,Задать свойство после оповеще
|
|||
Set Quantity,Установите Количество,
|
||||
Set Role For,Установить роль для,
|
||||
Set User Permissions,Задание разрешений пользователя,
|
||||
Set Value,Задать значение,
|
||||
Set Value,Установить значение,
|
||||
Set custom roles for page and report,Набор пользовательских ролей для страницы и отчета,
|
||||
"Set default format, page size, print style etc.","Установить форму, размер страницы, стиль печати и т.д., используюмых по умолчанию",
|
||||
Set non-standard precision for a Float or Currency field,Установите нестандартные точность для поплавка или валютной области,
|
||||
|
|
@ -2337,7 +2337,7 @@ Slideshow like display for the website,"Слайд-шоу, как дисплей
|
|||
Small Text,Маленьикий текст,
|
||||
Smallest Currency Fraction Value,Минимальное дробное значение,
|
||||
Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01,"Минимальная разменная денежная единица (монета). Например, для доллара — 1 цент, и его нужно ввести как 0,01",
|
||||
Snapshot View,Снимок Посмотреть,
|
||||
Snapshot View,Просмотр снимка,
|
||||
Social,Сообщество,
|
||||
Social Login Key,Ключ социального входа,
|
||||
Social Login Provider,Социальный провайдер,
|
||||
|
|
@ -2418,7 +2418,7 @@ Suspend Sending,Приостановить Отправка,
|
|||
Switch To Desk,Переключение на рабочий стол,
|
||||
Symbol,Символ,
|
||||
Sync,Синхронизация,
|
||||
Sync on Migrate,Синхронизация по Migrate,
|
||||
Sync on Migrate,Синхронизировать при переносе,
|
||||
Syntax error in template,Синтаксическая ошибка в шаблоне,
|
||||
System,Система,
|
||||
System Page,Страница системы,
|
||||
|
|
@ -2450,7 +2450,7 @@ Thank you for your interest in subscribing to our updates,Спасибо за в
|
|||
Thank you for your message,Спасибо за ваше сообщение,
|
||||
The CSV format is case sensitive,Формат CSV чувствителен к регистру,
|
||||
The Condition '{0}' is invalid,Условие '{0}' является недействительным,
|
||||
The First User: You,Первый пользователя: Вы,
|
||||
The First User: You,Первый пользователь: Вы,
|
||||
"The application has been updated to a new version, please refresh this page","Приложение был обновлен до новой версии, пожалуйста, обновите эту страницу",
|
||||
The attachments could not be correctly linked to the new document,Вложения не могут быть правильно связаны с новым документом,
|
||||
The document could not be correctly assigned,Документ не может быть правильно назначен,
|
||||
|
|
@ -2653,7 +2653,7 @@ User Field,Поле пользователя,
|
|||
User ID of a Blogger,ID пользователя-блоггера,
|
||||
User Image,Изображение пользователя,
|
||||
User Name,Имя пользователя,
|
||||
User Permission,Пользователь Введено,
|
||||
User Permission,Разрешения пользователя,
|
||||
User Permissions,Разрешения пользователей,
|
||||
User Permissions are used to limit users to specific records.,Пользовательские разрешения используются для ограничения пользователей конкретными записями.,
|
||||
User Permissions created sucessfully,Пользовательские разрешения созданы успешно,
|
||||
|
|
@ -3068,8 +3068,8 @@ zoom-out,отдалить,
|
|||
{0} or {1},{0} или {1},
|
||||
{0} record deleted,{0} запись удалена,
|
||||
{0} records deleted,{0} записей удалено,
|
||||
{0} reverted your point on {1},{0} вернул вашу точку на {1},
|
||||
{0} reverted your points on {1},{0} вернул ваши очки на {1},
|
||||
{0} reverted your point on {1},{0} вернул ваш балл на {1},
|
||||
{0} reverted your points on {1},{0} вернул ваши баллы на {1},
|
||||
{0} reverted {1},{0} вернул {1},
|
||||
{0} room must have atmost one user.,{0} номер должен иметь самого одного пользователя.,
|
||||
{0} rows for {1},{0} строк для {1},
|
||||
|
|
@ -3148,7 +3148,7 @@ Access not allowed from this IP Address,Доступ с этого IP-адрес
|
|||
Action Type,Тип действия,
|
||||
Activity Log by ,Активность Журнал по,
|
||||
Add Fields,Добавить поля,
|
||||
Administration,Администрация,
|
||||
Administration,Администрирование,
|
||||
After Cancel,После отмены,
|
||||
After Delete,После удаления,
|
||||
After Save,После сохранения,
|
||||
|
|
@ -3157,7 +3157,7 @@ After Submit,После отправки,
|
|||
Aggregate Function Based On,"Агрегатная функция, основанная на",
|
||||
Aggregate Function field is required to create a dashboard chart,Поле Aggregate Function необходимо для создания диаграммы панели мониторинга.,
|
||||
All Records,Все записи,
|
||||
Allot Points To Assigned Users,Выделить очки назначенным пользователям,
|
||||
Allot Points To Assigned Users,Выделить баллы назначенным пользователям,
|
||||
Allow Auto Repeat,Разрешить автоматическое повторение,
|
||||
Allow Google Calendar Access,Разрешить доступ к Календарю Google,
|
||||
Allow Google Contacts Access,Разрешить доступ к контактам Google,
|
||||
|
|
@ -3380,7 +3380,7 @@ Invalid field name: {0},Неверное имя поля: {0},
|
|||
Invalid file URL. Please contact System Administrator.,"Неверный URL файла. Пожалуйста, свяжитесь с системным администратором.",
|
||||
Invalid include path,Неверный путь включения,
|
||||
Invalid username or password,неправильное имя пользователя или пароль,
|
||||
Is Primary,Первичный,
|
||||
Is Primary,Основной,
|
||||
Is Primary Mobile,Основной мобильный,
|
||||
Is Primary Phone,Основной телефон,
|
||||
Is Tree,Дерево,
|
||||
|
|
@ -3667,7 +3667,7 @@ via Data Import,через импорт данных,
|
|||
{0} are mandatory fields,{0} обязательные поля,
|
||||
{0} are required,{0} требуется,
|
||||
{0} assigned a new task {1} {2} to you,{0} назначил вам новое задание {1} {2},
|
||||
{0} gained {1} point for {2} {3},{0} набрал {1} очко за {2} {3},
|
||||
{0} gained {1} point for {2} {3},{0} получил {1} балл за {2} {3},
|
||||
{0} gained {1} points for {2} {3},{0} набрал {1} баллов за {2} {3},
|
||||
{0} has no versions tracked.,{0} не отслеживает версии.,
|
||||
{0} is not a valid report format. Report format should one of the following {1},{0} не является допустимым форматом отчета. Формат отчета должен быть одним из следующих {1},
|
||||
|
|
@ -4045,7 +4045,7 @@ No Permitted Charts on this Dashboard,На этой панели инструм
|
|||
No Permitted Charts,Нет разрешенных графиков,
|
||||
Reset Chart,Сбросить график,
|
||||
via {0},через {0},
|
||||
{0} is not a valid Phone Number,{0} не является действительным номером телефона,
|
||||
{0} is not a valid Phone Number,{0} недействительный номер телефона,
|
||||
Failed Transactions,Неудачные транзакции,
|
||||
Value for field {0} is too long in {1}. Length should be lesser than {2} characters,Значение поля {0} слишком длинное в {1}. Длина должна быть меньше {2} симв.,
|
||||
Data Too Long,Данные слишком длинные,
|
||||
|
|
@ -4121,8 +4121,8 @@ Using this console may allow attackers to impersonate you and steal your informa
|
|||
{0} w,{0} н,
|
||||
{0} M,{0} М,
|
||||
{0} y,{0} г,
|
||||
yesterday,вчерашний день,
|
||||
{0} years ago,{0} лет назад,
|
||||
yesterday,вчера,
|
||||
{0} years ago,{0} год назад,
|
||||
New Chart,Новый график,
|
||||
New Shortcut,Новый ярлык,
|
||||
Edit Chart,Изменить диаграмму,
|
||||
|
|
@ -4700,3 +4700,11 @@ Value cannot be negative for {0}: {1},Значение не может быть
|
|||
Negative Value,Отрицательное значение,
|
||||
Authentication failed while receiving emails from Email Account: {0}.,Ошибка аутентификации при получении писем из учетной записи электронной почты: {0}.,
|
||||
Message from server: {0},Сообщение с сервера: {0},
|
||||
Documentation,Документация,
|
||||
User Forum,Форум пользователей,
|
||||
Report an issue,Сообщить об ошибке,
|
||||
My Profile,Мой профиль,
|
||||
My Settings,Мои настройки,
|
||||
Toggle Full Width,Переключить ширину,
|
||||
Toggle Theme,Переключить тему,
|
||||
Modules,Модули,
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 1588 and column 67.
|
|
|
@ -277,7 +277,9 @@ def get_email_subject_for_qr_code(kwargs_dict):
|
|||
|
||||
def get_email_body_for_qr_code(kwargs_dict):
|
||||
"""Get QRCode email body."""
|
||||
body_template = "Please click on the following link and follow the instructions on the page.<br><br> {{qrcode_link}}"
|
||||
body_template = _(
|
||||
"Please click on the following link and follow the instructions on the page. {0}"
|
||||
).format("<br><br> {{qrcode_link}}")
|
||||
body = frappe.render_template(body_template, kwargs_dict)
|
||||
return body
|
||||
|
||||
|
|
|
|||
|
|
@ -1887,6 +1887,16 @@ def strip(val: str, chars: Optional[str] = None) -> str:
|
|||
return (val or "").replace("\ufeff", "").replace("\u200b", "").strip(chars)
|
||||
|
||||
|
||||
def get_string_between(start: str, string: str, end: str) -> str:
|
||||
if not string:
|
||||
return ""
|
||||
|
||||
regex = "{0}(.*){1}".format(start, end)
|
||||
out = re.search(regex, string)
|
||||
|
||||
return out.group(1) if out else string
|
||||
|
||||
|
||||
def to_markdown(html: str) -> str:
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from frappe import _
|
|||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Coalesce, Max
|
||||
from frappe.query_builder.terms import SubQuery
|
||||
from frappe.query_builder.utils import DocType
|
||||
|
||||
|
||||
|
|
@ -336,14 +337,15 @@ class NestedSet(Document):
|
|||
def get_root_of(doctype):
|
||||
"""Get root element of a DocType with a tree structure"""
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.query_builder.terms import subqry
|
||||
|
||||
Table = DocType(doctype)
|
||||
t1 = Table.as_("t1")
|
||||
t2 = Table.as_("t2")
|
||||
|
||||
subq = frappe.qb.from_(t2).select(Count("*")).where((t2.lft < t1.lft) & (t2.rgt > t1.rgt))
|
||||
result = frappe.qb.from_(t1).select(t1.name).where((subqry(subq) == 0) & (t1.rgt > t1.lft)).run()
|
||||
node_query = SubQuery(
|
||||
frappe.qb.from_(t2).select(Count("*")).where((t2.lft < t1.lft) & (t2.rgt > t1.rgt))
|
||||
)
|
||||
result = frappe.qb.from_(t1).select(t1.name).where((node_query == 0) & (t1.rgt > t1.lft)).run()
|
||||
|
||||
return result[0][0] if result else None
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
from frappe.core.doctype.user.user import create_contact
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.website.doctype.personal_data_download_request.personal_data_download_request import (
|
||||
get_user_data,
|
||||
)
|
||||
|
||||
|
||||
class TestRequestPersonalData(unittest.TestCase):
|
||||
class TestRequestPersonalData(FrappeTestCase):
|
||||
def setUp(self):
|
||||
create_user_if_not_exists(email="test_privacy@example.com")
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class TestRequestPersonalData(unittest.TestCase):
|
|||
email_queue = frappe.get_all(
|
||||
"Email Queue", fields=["message"], order_by="creation DESC", limit=1
|
||||
)
|
||||
self.assertTrue("Subject: Download Your Data" in email_queue[0].message)
|
||||
self.assertIn(frappe._("Download Your Data"), email_queue[0].message)
|
||||
|
||||
frappe.db.delete("Email Queue")
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"website_theme_image_link",
|
||||
"brand",
|
||||
"banner_image",
|
||||
"splash_image",
|
||||
"brand_html",
|
||||
"set_banner_from_image",
|
||||
"favicon",
|
||||
|
|
@ -413,6 +414,11 @@
|
|||
"fieldname": "footer_powered",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Footer \"Powered By\""
|
||||
},
|
||||
{
|
||||
"fieldname": "splash_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Splash Image"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
|
|
@ -420,7 +426,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-09 01:47:31.094462",
|
||||
"modified": "2022-05-27 12:33:29.019998",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Settings",
|
||||
|
|
@ -445,4 +451,4 @@
|
|||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ def get_website_settings(context=None):
|
|||
}
|
||||
)
|
||||
|
||||
settings = frappe.get_single("Website Settings")
|
||||
settings: "WebsiteSettings" = frappe.get_single("Website Settings")
|
||||
for k in [
|
||||
"banner_html",
|
||||
"banner_image",
|
||||
|
|
@ -203,6 +203,9 @@ def get_website_settings(context=None):
|
|||
|
||||
context["hide_login"] = settings.hide_login
|
||||
|
||||
if splash_image := settings.splash_image or context.get("splash_image"):
|
||||
context["splash_image"] = splash_image
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@ class ErrorPage(TemplatePage):
|
|||
def __init__(self, path=None, http_status_code=None, exception=None):
|
||||
path = "error"
|
||||
super().__init__(path=path, http_status_code=http_status_code)
|
||||
self.http_status_code = getattr(exception, "http_status_code", None) or http_status_code or 500
|
||||
self.exception = exception
|
||||
|
||||
def can_render(self):
|
||||
return True
|
||||
|
||||
def init_context(self):
|
||||
super().init_context()
|
||||
self.context.http_status_code = getattr(self.exception, "http_status_code", None) or 500
|
||||
self.context.error_title = getattr(self.exception, "title", None)
|
||||
self.context.error_message = getattr(self.exception, "message", None)
|
||||
|
|
|
|||
|
|
@ -212,19 +212,13 @@ class TemplatePage(BaseTemplatePage):
|
|||
|
||||
def run_pymodule_method(self, method_name):
|
||||
if hasattr(self.pymodule, method_name):
|
||||
try:
|
||||
import inspect
|
||||
import inspect
|
||||
|
||||
method = getattr(self.pymodule, method_name)
|
||||
if inspect.getfullargspec(method).args:
|
||||
return method(self.context)
|
||||
else:
|
||||
return method()
|
||||
except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect):
|
||||
raise
|
||||
except Exception:
|
||||
if not frappe.flags.in_migrate:
|
||||
frappe.errprint(frappe.utils.get_traceback())
|
||||
method = getattr(self.pymodule, method_name)
|
||||
if inspect.getfullargspec(method).args:
|
||||
return method(self.context)
|
||||
else:
|
||||
return method()
|
||||
|
||||
def render_template(self):
|
||||
if self.template_path.endswith("min.js"):
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@
|
|||
<script></script>
|
||||
<div class="page-card">
|
||||
<div class="page-card-head">
|
||||
<span class="indicator red">{{_("Uncaught Server Exception")}}</span>
|
||||
<span class="indicator red">{{ error_title }}</span>
|
||||
</div>
|
||||
<p>{{_("There was an error building this page")}}</p>
|
||||
<p>{{ error_message }}</p>
|
||||
<div>
|
||||
<a href="/" class="btn btn-primary btn-sm">{{ _("Home") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted text-center small" style="margin-top: -20px;">
|
||||
{{ _("Error Code: {0}").format('500') }}
|
||||
{{ _("Error Code: {0}").format(http_status_code) }}
|
||||
</p>
|
||||
<div class="text-center mt-3">
|
||||
<button class="btn btn-xs btn-link text-muted small view-error" >{{ _("Show Traceback") if not dev_server else _("Hide Traceback") }}</a>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
no_cache = 1
|
||||
|
||||
|
|
@ -8,7 +9,8 @@ no_cache = 1
|
|||
def get_context(context):
|
||||
if frappe.flags.in_migrate:
|
||||
return
|
||||
context.http_status_code = 500
|
||||
|
||||
print(frappe.get_traceback())
|
||||
context.error_title = context.error_title or _("Uncaught Server Exception")
|
||||
context.error_message = context.error_message or _("There was an error building this page")
|
||||
|
||||
return {"error": frappe.get_traceback().replace("<", "<").replace(">", ">")}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ PyMySQL~=1.0.2
|
|||
pyOpenSSL~=20.0.1
|
||||
pyotp~=2.6.0
|
||||
PyPDF2~=1.26.0
|
||||
PyPika~=0.48.6
|
||||
PyPika~=0.48.9
|
||||
pypng~=0.0.20
|
||||
PyQRCode~=1.2.1
|
||||
python-dateutil~=2.8.1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue