Merge branch 'frappe:develop' into fix-report-excel-translation

This commit is contained in:
avc 2024-08-05 17:12:28 +02:00 committed by GitHub
commit 9a5aac232b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1057 additions and 802 deletions

View file

@ -7,5 +7,6 @@ hooks.py,frappe.gettext.extractors.navbar.extract
**.py,frappe.gettext.extractors.python.extract
**.js,frappe.gettext.extractors.javascript.extract
**.html,frappe.gettext.extractors.html_template.extract
**.vue,frappe.gettext.extractors.html_template.extract
**/custom/*.json,frappe.gettext.extractors.customization.extract
**/fixtures/custom_field.json,frappe.gettext.extractors.custom_field.extract
1 hooks.py frappe.gettext.extractors.navbar.extract
7 **.py frappe.gettext.extractors.python.extract
8 **.js frappe.gettext.extractors.javascript.extract
9 **.html frappe.gettext.extractors.html_template.extract
10 **.vue frappe.gettext.extractors.html_template.extract
11 **/custom/*.json frappe.gettext.extractors.customization.extract
12 **/fixtures/custom_field.json frappe.gettext.extractors.custom_field.extract

View file

@ -32,6 +32,7 @@
"fetch_if_empty",
"visibility_section",
"hidden",
"show_on_timeline",
"bold",
"allow_in_quick_entry",
"translatable",
@ -578,13 +579,20 @@
"fieldname": "not_nullable",
"fieldtype": "Check",
"label": "Not Nullable"
},
{
"default": "0",
"depends_on": "eval: doc.hidden",
"fieldname": "show_on_timeline",
"fieldtype": "Check",
"label": "Show on Timeline"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-04-12 16:27:34.546314",
"modified": "2024-07-30 13:15:32.037892",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
@ -594,4 +602,4 @@
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

View file

@ -112,6 +112,7 @@ class DocField(Document):
search_index: DF.Check
set_only_once: DF.Check
show_dashboard: DF.Check
show_on_timeline: DF.Check
sort_options: DF.Check
translatable: DF.Check
unique: DF.Check

View file

@ -237,7 +237,8 @@
"options": "Has Role",
"permlevel": 1,
"print_hide": 1,
"read_only": 1
"read_only": 1,
"show_on_timeline": 1
},
{
"collapsible": 1,
@ -428,7 +429,8 @@
"hidden": 1,
"label": "Block Modules",
"options": "Block Module",
"permlevel": 1
"permlevel": 1,
"show_on_timeline": 1
},
{
"fieldname": "home_settings",
@ -796,7 +798,7 @@
"link_fieldname": "user"
}
],
"modified": "2024-04-12 23:25:04.628007",
"modified": "2024-07-15 18:40:18.842915",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -362,7 +362,11 @@ class User(Document):
user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions
)
if not self.flags.no_welcome_mail and cint(self.send_welcome_email):
if (
not self.flags.no_welcome_mail
and cint(self.send_welcome_email)
and not self.flags.email_sent
):
self.send_welcome_mail_to_user()
self.flags.email_sent = 1
if frappe.session.user != "Guest":

View file

@ -40,7 +40,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Type",
"options": "Mention\nEnergy Point\nAssignment\nShare\nAlert"
"options": "\nMention\nEnergy Point\nAssignment\nShare\nAlert"
},
{
"fieldname": "email_content",
@ -103,7 +103,7 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
"modified": "2024-03-23 16:03:31.715461",
"modified": "2024-08-03 09:38:10.497711",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",

View file

@ -28,7 +28,7 @@ class NotificationLog(Document):
link: DF.Data | None
read: DF.Check
subject: DF.Text | None
type: DF.Literal["Mention", "Energy Point", "Assignment", "Share", "Alert"]
type: DF.Literal["", "Mention", "Energy Point", "Assignment", "Share", "Alert"]
# end: auto-generated types
def after_insert(self):

View file

@ -59,9 +59,10 @@ def is_email_notifications_enabled_for_type(user, notification_type):
return False
fieldname = "enable_email_" + frappe.scrub(notification_type)
enabled = frappe.db.get_value("Notification Settings", user, fieldname)
enabled = frappe.db.get_value("Notification Settings", user, fieldname, ignore=True)
if enabled is None:
return True
return enabled

View file

@ -171,11 +171,18 @@ class EmailQueue(Document):
method(self, self.sender, recipient.recipient, message)
elif not frappe.flags.in_test or frappe.flags.testing_email:
if ctx.email_account_doc.service == "Frappe Mail":
ctx.frappe_mail_client.send_raw(
sender=self.sender,
recipients=recipient.recipient,
message=message.decode("utf-8"),
)
if self.reference_doctype == "Newsletter":
ctx.frappe_mail_client.send_newsletter(
sender=self.sender,
recipients=recipient.recipient,
message=message.decode("utf-8"),
)
else:
ctx.frappe_mail_client.send_raw(
sender=self.sender,
recipients=recipient.recipient,
message=message.decode("utf-8"),
)
else:
ctx.smtp_server.session.sendmail(
from_addr=self.sender,
@ -780,7 +787,8 @@ class QueueBuilder:
with suppress(Exception):
q.send(smtp_server_instance=smtp_server_instance, frappe_mail_client=frappe_mail_client)
smtp_server_instance.quit()
if smtp_server_instance:
smtp_server_instance.quit()
def as_dict(self, include_recipients=True):
email_account = self.get_outgoing_email_account()

View file

@ -1,5 +1,5 @@
from datetime import datetime
from typing import TYPE_CHECKING
from typing import Any
from urllib.parse import urljoin
import pytz
@ -9,9 +9,6 @@ from frappe import _
from frappe.frappeclient import FrappeClient, FrappeOAuth2Client
from frappe.utils import convert_utc_to_system_timezone, get_datetime, get_datetime_str, get_system_timezone
if TYPE_CHECKING:
from requests import Response
class FrappeMail:
"""Class to interact with the Frappe Mail API."""
@ -29,6 +26,9 @@ class FrappeMail:
self.api_key = api_key
self.api_secret = api_secret
self.access_token = access_token
self.client = self.get_client(
self.site, self.mailbox, self.api_key, self.api_secret, self.access_token
)
@staticmethod
def get_client(
@ -65,64 +65,51 @@ class FrappeMail:
headers: dict[str, str] | None = None,
timeout: int | tuple[int, int] = (60, 120),
raise_exception: bool = True,
) -> "Response":
"""Makes a HTTP request to the Frappe Mail API."""
) -> Any | None:
"""Makes a request to the Frappe Mail API."""
url = urljoin(self.site, endpoint)
client = self.get_client(self.site, self.mailbox, self.api_key, self.api_secret, self.access_token)
url = urljoin(self.client.url, endpoint)
headers = headers or {}
headers.update(client.headers)
headers.update(self.client.headers)
response = client.session.request(
response = self.client.session.request(
method=method, url=url, params=params, data=data, json=json, headers=headers, timeout=timeout
)
if not response.ok and raise_exception:
error_msg = response.text
if response.status_code == 401:
if self.access_token:
error_msg = _("Authentication Error: Reauthorize OAuth for Email Account {0}.").format(
frappe.bold(self.mailbox)
)
else:
error_msg = _("Authentication Error: Invalid API Key or Secret")
frappe.throw(title=_("Frappe Mail"), msg=error_msg)
return response
return self.client.post_process(response)
def validate(self, for_outbound: bool = False, for_inbound: bool = False) -> None:
"""Validates the mailbox for inbound and outbound emails."""
endpoint = "auth/validate"
endpoint = "/api/method/mail.api.auth.validate"
data = {"mailbox": self.mailbox, "for_outbound": for_outbound, "for_inbound": for_inbound}
response = self.request("POST", endpoint=endpoint, data=data, raise_exception=False)
self.request("POST", endpoint=endpoint, data=data)
if not response.ok:
if error_msg := response.json().get("exception"):
if error_msg == "frappe.exceptions.AuthenticationError":
error_msg += ": Invalid API Key or Secret"
frappe.throw(title="Frappe Mail", msg=error_msg)
def send_raw(self, sender: str, recipients: str, message: str) -> None:
def send_raw(self, sender: str, recipients: str | list, message: str) -> None:
"""Sends an email using the Frappe Mail API."""
endpoint = "outbound/send-raw"
json_data = {"from": sender, "to": recipients, "raw_message": message}
self.request("POST", endpoint=endpoint, json=json_data)
endpoint = "/api/method/mail.api.outbound.send_raw"
data = {"from_": sender, "to": recipients, "raw_message": message}
self.request("POST", endpoint=endpoint, data=data)
def send_newsletter(self, sender: str, recipients: str | list, message: str) -> None:
"""Sends an newsletter using the Frappe Mail API."""
endpoint = "/api/method/mail.api.outbound.send_newsletter"
data = {"from_": sender, "to": recipients, "raw_message": message}
self.request("POST", endpoint=endpoint, json=data)
def pull_raw(self, limit: int = 50, last_synced_at: str | None = None) -> dict[str, list[str] | str]:
"""Pulls emails from the mailbox using the Frappe Mail API."""
endpoint = "inbound/pull-raw"
endpoint = "/api/method/mail.api.inbound.pull_raw"
if last_synced_at:
last_synced_at = convert_to_utc(last_synced_at)
data = {"mailbox": self.mailbox, "limit": limit, "last_synced_at": last_synced_at}
headers = {"X-Site": frappe.utils.get_url()}
response = self.request("GET", endpoint=endpoint, data=data, headers=headers).json()["message"]
response = self.request("GET", endpoint=endpoint, data=data, headers=headers)
last_synced_at = convert_utc_to_system_timezone(get_datetime(response["last_synced_at"]))
return {"latest_messages": response["mails"], "last_synced_at": last_synced_at}

View file

@ -29,8 +29,8 @@ def extract(fileobj, *args, **kwargs):
yield from (
(
None,
"pgettext",
(link.get("link_to") if link.get("link_type") == "DocType" else None, link.get("label")),
"_",
link.get("label"),
[f"Label of a {link.get('type')} in the {workspace_name} Workspace"],
)
for link in data.get("links", [])
@ -38,8 +38,8 @@ def extract(fileobj, *args, **kwargs):
yield from (
(
None,
"pgettext",
(link.get("link_to") if link.get("link_type") == "DocType" else None, link.get("description")),
"_",
link.get("description"),
[f"Description of a {link.get('type')} in the {workspace_name} Workspace"],
)
for link in data.get("links", [])
@ -47,8 +47,8 @@ def extract(fileobj, *args, **kwargs):
yield from (
(
None,
"pgettext",
(shortcut.get("link_to") if shortcut.get("type") == "DocType" else None, shortcut.get("label")),
"_",
shortcut.get("label"),
[f"Label of a shortcut in the {workspace_name} Workspace"],
)
for shortcut in data.get("shortcuts", [])
@ -56,8 +56,8 @@ def extract(fileobj, *args, **kwargs):
yield from (
(
None,
"pgettext",
(shortcut.get("link_to") if shortcut.get("type") == "DocType" else None, shortcut.get("format")),
"_",
shortcut.get("format"),
[f"Count format of shortcut in the {workspace_name} Workspace"],
)
for shortcut in data.get("shortcuts", [])

View file

@ -218,7 +218,7 @@ def update_po(target_app: str | None = None, locale: str | None = None):
pot_catalog = get_catalog(app)
for locale in locales:
po_catalog = get_catalog(app, locale)
po_catalog.update(pot_catalog)
po_catalog.update(pot_catalog, no_fuzzy_matching=True)
po_path = write_catalog(app, po_catalog, locale)
print(f"PO file modified at {po_path}")

File diff suppressed because it is too large Load diff

View file

@ -638,13 +638,18 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
if (value) {
field_value = response[source_field];
}
frappe.model.set_value(
this.df.parent,
this.docname,
target_field,
field_value,
this.df.fieldtype
);
if (this.layout?.set_value) {
this.layout.set_value(target_field, field_value);
} else if (this.frm) {
frappe.model.set_value(
this.df.parent,
this.docname,
target_field,
field_value,
this.df.fieldtype
);
}
}
};
@ -669,8 +674,73 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
}
}
fetch_map_for_quick_entry() {
let me = this;
let fetch_map = {};
function add_fetch(link_field, source_field, target_field, target_doctype) {
if (!target_doctype) target_doctype = "*";
if (!me.layout.fetch_dict) {
me.layout.fetch_dict = {};
}
// Target field kept as key because source field could be non-unique
me.layout.fetch_dict.setDefault(target_doctype, {}).setDefault(link_field, {})[
target_field
] = source_field;
}
function setup_add_fetch(df) {
let is_read_only_field =
[
"Data",
"Read Only",
"Text",
"Small Text",
"Currency",
"Check",
"Text Editor",
"Attach Image",
"Code",
"Link",
"Float",
"Int",
"Date",
"Select",
"Duration",
"Time",
].includes(df.fieldtype) ||
df.read_only == 1 ||
df.is_virtual == 1;
if (is_read_only_field && df.fetch_from && df.fetch_from.indexOf(".") != -1) {
var parts = df.fetch_from.split(".");
add_fetch(parts[0], parts[1], df.fieldname, df.parent);
}
}
$.each(this.layout.fields, (i, field) => setup_add_fetch(field));
for (const key of ["*", this.df.parent]) {
if (!this.layout.fetch_dict) {
this.layout.fetch_dict = {};
}
if (this.layout.fetch_dict[key] && this.layout.fetch_dict[key][this.df.fieldname]) {
Object.assign(fetch_map, this.layout.fetch_dict[key][this.df.fieldname]);
}
}
return fetch_map;
}
get fetch_map() {
const fetch_map = {};
// Create fetch_map from quick entry fields
if (!this.frm && this.layout && this.layout.fields) {
return this.fetch_map_for_quick_entry();
}
if (!this.frm) return fetch_map;
for (const key of ["*", this.df.parent]) {

View file

@ -83,13 +83,17 @@ function get_version_timeline_content(version_doc, frm) {
}
} else {
const df = frappe.meta.get_docfield(frm.doctype, p[0], frm.docname);
if (df && !df.hidden) {
if (df && (!df.hidden || df.show_on_timeline)) {
const field_display_status = frappe.perm.get_field_display_status(
df,
null,
frm.perm
);
if (field_display_status === "Read" || field_display_status === "Write") {
if (
field_display_status === "Read" ||
field_display_status === "Write" ||
(df.hidden && df.show_on_timeline)
) {
parts.push(
__("{0} from {1} to {2}", [
__(df.label, null, df.parent),
@ -142,14 +146,18 @@ function get_version_timeline_content(version_doc, frm) {
frm.docname
);
if (df && !df.hidden) {
if (df && (!df.hidden || df.show_on_timeline)) {
var field_display_status = frappe.perm.get_field_display_status(
df,
null,
frm.perm
);
if (field_display_status === "Read" || field_display_status === "Write") {
if (
field_display_status === "Read" ||
field_display_status === "Write" ||
(df.hidden && df.show_on_timeline)
) {
parts.push(
__("{0} from {1} to {2} in row #{3}", [
frappe.meta.get_label(frm.fields_dict[row[0]].grid.doctype, p[0]),
@ -197,14 +205,19 @@ function get_version_timeline_content(version_doc, frm) {
if (data[key] && data[key].length) {
let parts = (data[key] || []).map(function (p) {
var df = frappe.meta.get_docfield(frm.doctype, p[0], frm.docname);
if (df && !df.hidden) {
if (df && (!df.hidden || df.show_on_timeline)) {
var field_display_status = frappe.perm.get_field_display_status(
df,
null,
frm.perm
);
if (field_display_status === "Read" || field_display_status === "Write") {
if (
field_display_status === "Read" ||
field_display_status === "Write" ||
(df.hidden && df.show_on_timeline)
) {
return __(frappe.meta.get_label(frm.doctype, p[0]));
}
}

View file

@ -515,7 +515,7 @@ frappe.ui.form.Form = class FrappeForm {
// feedback
frappe.msgprint({
message: __("{} Complete", [action.label]),
message: __("{} Complete", [__(action.label)]),
alert: true,
});
});

View file

@ -840,10 +840,12 @@ export default class GridRow {
delete this.grid.filter[df.fieldname];
}
this.grid.grid_sortable.option(
"disabled",
Object.keys(this.grid.filter).length !== 0
);
if (this.grid.grid_sortable) {
this.grid.grid_sortable.option(
"disabled",
Object.keys(this.grid.filter).length !== 0
);
}
this.grid.prevent_build = true;
this.grid.grid_pagination.go_to_page(1);

View file

@ -33,7 +33,7 @@ window.refresh_field = function (n, docname, table_field) {
};
window.set_field_options = function (n, txt) {
cur_frm.set_df_property(n, "options", txt);
cur_frm?.set_df_property(n, "options", txt);
};
window.toggle_field = function (n, hidden) {

View file

@ -918,18 +918,18 @@ Object.assign(frappe.utils, {
let route = route_str.split("/");
if (route[2] === "Report" || route[0] === "query-report") {
return __("{0} Report", [route[3] || route[1]]);
return __("{0} Report", [__(route[3]) || __(route[1])]);
}
if (route[0] === "List") {
return __("{0} List", [route[1]]);
return __("{0} List", [__(route[1])]);
}
if (route[0] === "modules") {
return __("{0} Modules", [route[1]]);
return __("{0} Modules", [__(route[1])]);
}
if (route[0] === "dashboard") {
return __("{0} Dashboard", [route[1]]);
return __("{0} Dashboard", [__(route[1])]);
}
return __(frappe.utils.to_title_case(route[0], true));
return __(frappe.utils.to_title_case(__(route[0]), true));
},
report_column_total: function (values, column, type) {
if (column.column.disable_total) {
@ -1213,9 +1213,7 @@ Object.assign(frappe.utils, {
},
flag(country_code) {
return `<img
src="https://flagcdn.com/${country_code}.svg"
width="20" height="15">`;
return `<img loading="lazy" src="https://flagcdn.com/${country_code}.svg" width="20" height="15">`;
},
make_chart(wrapper, custom_options = {}) {

View file

@ -462,7 +462,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
setup_progress_bar() {
let seconds_elapsed = 0;
const execution_time = this.report_settings.execution_time || 0;
const execution_time = this.report_settings?.execution_time || 0;
if (execution_time < 5) return;

View file

@ -104,8 +104,16 @@ export default class Header extends Block {
this._data = this.normalizeData(data);
if (data.text !== undefined) {
let text = this._data.text || "";
let text = __(this._data.text) || "";
const contains_html_tag = /<[a-z][\s\S]*>/i.test(text);
// apply translation to header text
let div = document.createElement("div");
div.innerHTML = text;
let only_text = div.innerText;
only_text = frappe.utils.escape_html(only_text);
text = text.replace(only_text, __(only_text));
this._element.innerHTML = contains_html_tag
? text
: `<span class="h${this._settings.default_size}">${text}</span>`;

View file

@ -99,15 +99,13 @@ frappe.views.Workspace = class Workspace {
<svg class="es-icon es-line icon-xs" style="" aria-hidden="true">
<use class="" href="#es-line-add"></use>
</svg>
<span class="hidden-xs" data-label="Edit">New</span>
<span class="hidden-xs" data-label="New">${__("New")}</span>
</button>
<button class="btn btn-default btn-sm mr-2 btn-edit-workspace" data-label="Edit">
<svg class="es-icon es-line icon-xs" style="" aria-hidden="true">
<use class="" href="#es-line-edit"></use>
</svg>
<span class="hidden-xs" data-label="Edit">
<span><span class="alt-underline">E</span>dit</span>
</span>
<span class="hidden-xs" data-label="Edit">${__("Edit")}</span>
</button>
</div>
`).appendTo(this.body);

View file

@ -45,8 +45,16 @@
.layout-side-section.print-preview-sidebar {
padding-right: var(--padding-md);
.checkbox label {
align-items: unset;
}
.input-area {
margin-top: 0.2rem;
}
.label-area {
white-space: nowrap;
white-space: unset;
}
}

View file

@ -8,6 +8,7 @@ import xlrd
from openpyxl import load_workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from openpyxl.workbook.child import INVALID_TITLE_REGEX
import frappe
from frappe.utils.html_utils import unescape_html
@ -21,7 +22,8 @@ def make_xlsx(data, sheet_name, wb=None, column_widths=None):
if wb is None:
wb = openpyxl.Workbook(write_only=True)
ws = wb.create_sheet(sheet_name, 0)
sheet_name_sanitized = INVALID_TITLE_REGEX.sub(" ", sheet_name)
ws = wb.create_sheet(sheet_name_sanitized, 0)
for i, column_width in enumerate(column_widths):
if column_width: