Merge branch 'develop' into 32489-role-perm-based-masking
This commit is contained in:
commit
590fe7e520
185 changed files with 94958 additions and 30590 deletions
23
.coveragerc
23
.coveragerc
|
|
@ -1,23 +0,0 @@
|
|||
[run]
|
||||
omit =
|
||||
tests/*
|
||||
.github/*
|
||||
commands/*
|
||||
**/test_*.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
|
||||
exclude_also =
|
||||
def __repr__
|
||||
if self.debug:
|
||||
if settings.DEBUG
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
if TYPE_CHECKING:
|
||||
class .*\bProtocol\):
|
||||
@(abc\.)?abstractmethod
|
||||
2
.github/workflows/_base-migration.yml
vendored
2
.github/workflows/_base-migration.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
env:
|
||||
MARIADB_ROOT_PASSWORD: ${{ env.DB_ROOT_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup
|
||||
name: Environment Setup
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/_base-server-tests.yml
vendored
2
.github/workflows/_base-server-tests.yml
vendored
|
|
@ -78,7 +78,7 @@ jobs:
|
|||
- 2525:25
|
||||
- 3000:80
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup
|
||||
name: Environment Setup
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/_base-type-check.yml
vendored
2
.github/workflows/_base-type-check.yml
vendored
|
|
@ -63,7 +63,7 @@ jobs:
|
|||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
if: steps.check-changes.outputs.result == 'true'
|
||||
|
||||
- name: Cache pip
|
||||
|
|
|
|||
2
.github/workflows/_base-ui-tests.yml
vendored
2
.github/workflows/_base-ui-tests.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
env:
|
||||
MARIADB_ROOT_PASSWORD: ${{ env.DB_ROOT_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/setup
|
||||
name: Environment Setup
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout Actions
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: "frappe/backport"
|
||||
path: ./actions
|
||||
|
|
|
|||
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
|
|
|||
2
.github/workflows/generate-pot-file.yml
vendored
2
.github/workflows/generate-pot-file.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
|
||||
|
|
|
|||
2
.github/workflows/labeller.yml
vendored
2
.github/workflows/labeller.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
|
|
|
|||
10
.github/workflows/linters.yml
vendored
10
.github/workflows/linters.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 200
|
||||
- uses: actions/setup-node@v4
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Validate Docs
|
||||
env:
|
||||
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
|
@ -80,7 +80,7 @@ jobs:
|
|||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v4
|
||||
|
|
@ -103,7 +103,7 @@ jobs:
|
|||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
|
|
|||
2
.github/workflows/on_release.yml
vendored
2
.github/workflows/on_release.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
path: 'frappe'
|
||||
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
path: 'frappe'
|
||||
- uses: actions/setup-node@v4
|
||||
|
|
|
|||
4
.github/workflows/run-indinvidual-tests.yml
vendored
4
.github/workflows/run-indinvidual-tests.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- id: set-matrix
|
||||
run: |
|
||||
# Use grep and find to get the list of test files
|
||||
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
|
|
|
|||
6
.github/workflows/server-tests.yml
vendored
6
.github/workflows/server-tests.yml
vendored
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
build: ${{ steps.check-build.outputs.build }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Check if unit tests should be run
|
||||
id: check-build
|
||||
run: |
|
||||
|
|
@ -68,9 +68,9 @@ jobs:
|
|||
if: ${{ github.event_name != 'pull_request' }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
- name: Upload coverage data
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
|
|
|||
6
.github/workflows/ui-tests.yml
vendored
6
.github/workflows/ui-tests.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
|
|
@ -55,9 +55,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
- name: Upload python coverage data
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -21,10 +21,15 @@
|
|||
## Frappe Framework
|
||||
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for ERPNext.
|
||||
|
||||
### Motivation
|
||||
## Philosophy
|
||||
|
||||
> The best code is the one that is not written
|
||||
|
||||
Started in 2005, Frappe Framework was inspired by the Semantic Web. The "big idea" behind semantic web was of a framework that not only described how information is shown (like headings, body etc), but also what it means, like name, address etc.
|
||||
|
||||
By creating a web framework that allowed for easy definition of metadata, it made building complex applications easy. Applications usually designed around how users interact with a system, but not based on semantics of the underlying system. Applications built on semantics end up being much more consistent and extensible. The first application built on Framework was ERPNext, a beast with more than 700 object types. Framework is not for the light hearted - it is not the first thing you might want to learn if you are beginning to learn web programming, but if you are ready to do real work, then Framework is the right tool for the job.
|
||||
By creating a web framework that allowed for easy definition of metadata, it made building complex applications easy. Applications usually designed around how users interact with a system, but not based on semantics of the underlying system. Applications built on semantics end up being much more consistent and extensible.
|
||||
|
||||
The first application built on Framework was ERPNext, a beast with more than 700 object types. Framework is not for the light hearted - it is not the first thing you might want to learn if you are beginning to learn web programming, but if you are ready to do real work, then Framework is the right tool for the job.
|
||||
|
||||
### Key Features
|
||||
|
||||
|
|
|
|||
|
|
@ -65,10 +65,9 @@ context("List View", () => {
|
|||
cy.go_to_list("ToDo");
|
||||
|
||||
// Check if the 'Open' button is present in the ToDo list view
|
||||
cy.get(".btn-default[data-name='" + todo_name + "']")
|
||||
.should((el) => {
|
||||
expect(el).to.exist;
|
||||
})
|
||||
cy.get(`.btn-default[data-name="${todo_name}"]`)
|
||||
.scrollIntoView({ inline: "center", block: "nearest" })
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
cy.window()
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ context("List View Settings", () => {
|
|||
cy.clear_filters();
|
||||
cy.wait(300);
|
||||
cy.get(".list-count").should("contain", "20 of");
|
||||
cy.get("[href='#es-line-chat-alt']").should("be.visible");
|
||||
cy.get(".frappe-list svg.es-icon.es-line").should("be.visible");
|
||||
cy.get(".menu-btn-group button").click();
|
||||
cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click();
|
||||
cy.get(".modal-dialog").should("contain", "DocType Settings");
|
||||
cy.get(".modal-dialog").should("contain", "DocType List View Settings");
|
||||
|
||||
cy.findByLabelText("Disable Count").check({ force: true });
|
||||
cy.findByLabelText("Disable Comment Count").check({ force: true });
|
||||
|
|
@ -33,7 +33,7 @@ context("List View Settings", () => {
|
|||
|
||||
cy.get(".menu-btn-group button").click({ force: true });
|
||||
cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click();
|
||||
cy.get(".modal-dialog").should("contain", "DocType Settings");
|
||||
cy.get(".modal-dialog").should("contain", "DocType List View Settings");
|
||||
cy.findByLabelText("Disable Count").uncheck({ force: true });
|
||||
cy.findByLabelText("Disable Comment Count").uncheck({ force: true });
|
||||
cy.findByLabelText("Disable Sidebar Stats").uncheck({ force: true });
|
||||
|
|
|
|||
|
|
@ -1473,18 +1473,28 @@ def logger(
|
|||
|
||||
|
||||
def get_desk_link(doctype, name, show_title_with_name=False, open_in_new_tab=False):
|
||||
from urllib.parse import quote
|
||||
|
||||
meta = get_meta(doctype)
|
||||
title = get_value(doctype, name, meta.get_title_field())
|
||||
|
||||
target_attr = ' target="_blank"' if open_in_new_tab else ""
|
||||
|
||||
# encode for href
|
||||
encoded_name = quote(name)
|
||||
|
||||
if show_title_with_name and name != title:
|
||||
html = '<a href="/app/Form/{doctype}/{name}"{target} style="font-weight: bold;">{doctype_local} {name}: {title_local}</a>'
|
||||
html = '<a href="/app/Form/{doctype}/{encoded_name}"{target} style="font-weight: bold;">{doctype_local} {name}: {title_local}</a>'
|
||||
else:
|
||||
html = '<a href="/app/Form/{doctype}/{name}"{target} style="font-weight: bold;">{doctype_local} {title_local}</a>'
|
||||
html = '<a href="/app/Form/{doctype}/{encoded_name}"{target} style="font-weight: bold;">{doctype_local} {title_local}</a>'
|
||||
|
||||
return html.format(
|
||||
doctype=doctype, name=name, doctype_local=_(doctype), title_local=_(title), target=target_attr
|
||||
doctype=doctype,
|
||||
name=name,
|
||||
encoded_name=encoded_name,
|
||||
doctype_local=_(doctype),
|
||||
title_local=_(title),
|
||||
target=target_attr,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1511,7 +1521,13 @@ def is_setup_complete():
|
|||
if not frappe.db.table_exists("Installed Application"):
|
||||
return setup_complete
|
||||
|
||||
if all(frappe.get_all("Installed Application", {"has_setup_wizard": 1}, pluck="is_setup_complete")):
|
||||
if all(
|
||||
frappe.get_all(
|
||||
"Installed Application",
|
||||
{"app_name": ("in", ["frappe", "erpnext"])},
|
||||
pluck="is_setup_complete",
|
||||
)
|
||||
):
|
||||
setup_complete = True
|
||||
|
||||
return setup_complete
|
||||
|
|
|
|||
|
|
@ -92,9 +92,8 @@ class AutoRepeat(Document):
|
|||
|
||||
def before_insert(self):
|
||||
if not frappe.in_test:
|
||||
start_date = getdate(self.start_date)
|
||||
today_date = getdate(today())
|
||||
if start_date <= today_date:
|
||||
today_date = getdate()
|
||||
if getdate(self.start_date) < today_date:
|
||||
self.start_date = today_date
|
||||
|
||||
def on_update(self):
|
||||
|
|
@ -139,14 +138,18 @@ class AutoRepeat(Document):
|
|||
return
|
||||
|
||||
if self.end_date:
|
||||
end_date = getdate(self.end_date)
|
||||
|
||||
self.validate_from_to_dates("start_date", "end_date")
|
||||
|
||||
if self.end_date == self.start_date:
|
||||
frappe.throw(
|
||||
_("{0} should not be same as {1}").format(
|
||||
frappe.bold(_("End Date")), frappe.bold(_("Start Date"))
|
||||
if end_date == getdate():
|
||||
frappe.throw(_("End Date cannot be today."))
|
||||
if end_date == getdate(self.start_date):
|
||||
frappe.throw(
|
||||
_("{0} should not be same as {1}").format(
|
||||
frappe.bold(_("End Date")), frappe.bold(_("Start Date"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_email_id(self):
|
||||
if self.notify_by_email:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import frappe.defaults
|
|||
import frappe.desk.desk_page
|
||||
from frappe.core.doctype.installed_applications.installed_applications import (
|
||||
get_setup_wizard_completed_apps,
|
||||
get_setup_wizard_not_required_apps,
|
||||
)
|
||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
|
||||
from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items
|
||||
|
|
@ -121,34 +120,9 @@ def get_bootinfo():
|
|||
bootinfo.sentry_dsn = sentry_dsn
|
||||
|
||||
bootinfo.setup_wizard_completed_apps = get_setup_wizard_completed_apps() or []
|
||||
bootinfo.setup_wizard_not_required_apps = get_setup_wizard_not_required_apps() or []
|
||||
remove_apps_with_incomplete_dependencies(bootinfo)
|
||||
|
||||
return bootinfo
|
||||
|
||||
|
||||
def remove_apps_with_incomplete_dependencies(bootinfo):
|
||||
remove_apps = set()
|
||||
|
||||
for app in bootinfo.setup_wizard_not_required_apps:
|
||||
if app in bootinfo.setup_wizard_completed_apps:
|
||||
continue
|
||||
|
||||
for required_apps in frappe.get_hooks("required_apps"):
|
||||
required_apps = required_apps.split("/")
|
||||
|
||||
for required_app in required_apps:
|
||||
if app not in bootinfo.setup_wizard_not_required_apps:
|
||||
continue
|
||||
|
||||
if required_app not in bootinfo.setup_wizard_completed_apps:
|
||||
remove_apps.add(app)
|
||||
|
||||
for app in remove_apps:
|
||||
if app in bootinfo.setup_wizard_not_required_apps:
|
||||
bootinfo.setup_wizard_not_required_apps.remove(app)
|
||||
|
||||
|
||||
def get_letter_heads():
|
||||
letter_heads = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ def _restore(
|
|||
click.secho("Failed to detect type of backup file", fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
if "cipher" in out.decode().split(":")[-1].strip():
|
||||
if "AES" in out.decode().split(":")[-1].strip():
|
||||
if encryption_key:
|
||||
click.secho("Encrypted backup file detected. Decrypting using provided key.", fg="yellow")
|
||||
|
||||
|
|
@ -691,8 +691,9 @@ def disable_user(context: CliCtxObj, email):
|
|||
@click.command("migrate")
|
||||
@click.option("--skip-failing", is_flag=True, help="Skip patches that fail to run")
|
||||
@click.option("--skip-search-index", is_flag=True, help="Skip search indexing for web documents")
|
||||
@click.option("--skip-fixtures", is_flag=True, help="Skip loading fixtures")
|
||||
@pass_context
|
||||
def migrate(context: CliCtxObj, skip_failing=False, skip_search_index=False):
|
||||
def migrate(context: CliCtxObj, skip_failing=False, skip_search_index=False, skip_fixtures=False):
|
||||
"Run patches, sync schema and rebuild files/translations"
|
||||
|
||||
from frappe.migrate import SiteMigration
|
||||
|
|
@ -701,8 +702,7 @@ def migrate(context: CliCtxObj, skip_failing=False, skip_search_index=False):
|
|||
click.secho(f"Migrating {site}", fg="green")
|
||||
try:
|
||||
SiteMigration(
|
||||
skip_failing=skip_failing,
|
||||
skip_search_index=skip_search_index,
|
||||
skip_failing=skip_failing, skip_search_index=skip_search_index, skip_fixtures=skip_fixtures
|
||||
).run(site=site)
|
||||
finally:
|
||||
print()
|
||||
|
|
|
|||
|
|
@ -914,7 +914,7 @@ def set_config(context: CliCtxObj, key, value, global_=False, parse=False):
|
|||
"output",
|
||||
type=click.Choice(["plain", "table", "json", "legacy"]),
|
||||
help="Output format",
|
||||
default="legacy",
|
||||
default="plain",
|
||||
)
|
||||
def get_version(output):
|
||||
"""Show the versions of all the installed apps."""
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@
|
|||
{
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone"
|
||||
"label": "Phone",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "fax",
|
||||
|
|
|
|||
|
|
@ -17,9 +17,19 @@ def invite_by_email(
|
|||
frappe.throw(title=_("Invalid input"), msg=_("No email addresses to invite"))
|
||||
|
||||
# get relevant data from the database
|
||||
disabled_user_emails = frappe.db.get_all(
|
||||
"User",
|
||||
filters={"email": ["in", email_list], "enabled": 0},
|
||||
pluck="email",
|
||||
)
|
||||
accepted_invite_emails = frappe.db.get_all(
|
||||
"User Invitation",
|
||||
filters={"email": ["in", email_list], "status": "Accepted", "app_name": app_name},
|
||||
filters={
|
||||
"email": ["in", email_list],
|
||||
"status": "Accepted",
|
||||
"app_name": app_name,
|
||||
"user": ["is", "set"],
|
||||
},
|
||||
pluck="email",
|
||||
)
|
||||
pending_invite_emails = frappe.db.get_all(
|
||||
|
|
@ -29,7 +39,9 @@ def invite_by_email(
|
|||
)
|
||||
|
||||
# create invitation documents
|
||||
to_invite = list(set(email_list) - set(accepted_invite_emails) - set(pending_invite_emails))
|
||||
to_invite = list(
|
||||
set(email_list) - set(disabled_user_emails) - set(accepted_invite_emails) - set(pending_invite_emails)
|
||||
)
|
||||
for email in to_invite:
|
||||
frappe.get_doc(
|
||||
doctype="User Invitation",
|
||||
|
|
@ -40,6 +52,7 @@ def invite_by_email(
|
|||
).insert(ignore_permissions=True)
|
||||
|
||||
return {
|
||||
"disabled_user_emails": disabled_user_emails,
|
||||
"accepted_invite_emails": accepted_invite_emails,
|
||||
"pending_invite_emails": pending_invite_emails,
|
||||
"invited_emails": to_invite,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "User ",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
},
|
||||
|
|
|
|||
|
|
@ -500,7 +500,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
|
||||
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"fieldname": "non_negative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Non Negative"
|
||||
|
|
@ -622,7 +622,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-17 00:48:20.359702",
|
||||
"modified": "2025-08-26 22:08:20.940308",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
},
|
||||
{
|
||||
"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. ",
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@ from frappe.database.schema import SPECIAL_CHAR_PATTERN
|
|||
from frappe.exceptions import DoesNotExistError
|
||||
from frappe.model.document import Document
|
||||
from frappe.permissions import SYSTEM_USER_ROLE, get_doctypes_with_read
|
||||
from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url
|
||||
from frappe.utils import (
|
||||
call_hook_method,
|
||||
cint,
|
||||
get_files_path,
|
||||
get_hook_method,
|
||||
get_url,
|
||||
)
|
||||
from frappe.utils.file_manager import is_safe_path
|
||||
from frappe.utils.image import optimize_image, strip_exif_data
|
||||
|
||||
|
|
@ -31,7 +37,7 @@ from .utils import *
|
|||
|
||||
exclude_from_linked_with = True
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
URL_PREFIXES = ("http://", "https://")
|
||||
URL_PREFIXES = ("http://", "https://", "/api/method/")
|
||||
FILE_ENCODING_OPTIONS = ("utf-8-sig", "utf-8", "windows-1250", "windows-1252")
|
||||
|
||||
|
||||
|
|
@ -139,7 +145,10 @@ class File(Document):
|
|||
return
|
||||
|
||||
if not self.attached_to_name or not isinstance(self.attached_to_name, str | int):
|
||||
frappe.throw(_("Attached To Name must be a string or an integer"), frappe.ValidationError)
|
||||
frappe.throw(
|
||||
_("Attached To Name must be a string or an integer"),
|
||||
frappe.ValidationError,
|
||||
)
|
||||
|
||||
if self.attached_to_field and SPECIAL_CHAR_PATTERN.search(self.attached_to_field):
|
||||
frappe.throw(_("The fieldname you've specified in Attached To Field is invalid"))
|
||||
|
|
@ -213,8 +222,8 @@ class File(Document):
|
|||
if self.is_remote_file or not self.file_url:
|
||||
return
|
||||
|
||||
if not self.file_url.startswith(("/files/", "/private/files/")):
|
||||
# Probably an invalid URL since it doesn't start with http either
|
||||
if not self.file_url.startswith(("/files/", "/private/files/", "/api/method/")):
|
||||
# Probably an invalid URL since it doesn't start with http and isn't an internal URL either
|
||||
frappe.throw(
|
||||
_("URL must start with http:// or https://"),
|
||||
title=_("Invalid URL"),
|
||||
|
|
@ -318,7 +327,9 @@ class File(Document):
|
|||
if current_attachment_count >= attachment_limit:
|
||||
frappe.throw(
|
||||
_("Maximum Attachment Limit of {0} has been reached for {1} {2}.").format(
|
||||
frappe.bold(attachment_limit), self.attached_to_doctype, self.attached_to_name
|
||||
frappe.bold(attachment_limit),
|
||||
self.attached_to_doctype,
|
||||
self.attached_to_name,
|
||||
),
|
||||
exc=AttachmentLimitReached,
|
||||
title=_("Attachment Limit Reached"),
|
||||
|
|
@ -372,7 +383,10 @@ class File(Document):
|
|||
return
|
||||
|
||||
if self.file_type not in allowed_extensions.splitlines():
|
||||
frappe.throw(_("File type of {0} is not allowed").format(self.file_type), exc=FileTypeNotAllowed)
|
||||
frappe.throw(
|
||||
_("File type of {0} is not allowed").format(self.file_type),
|
||||
exc=FileTypeNotAllowed,
|
||||
)
|
||||
|
||||
def validate_duplicate_entry(self):
|
||||
if not self.flags.ignore_duplicate_entry_error and not self.is_folder:
|
||||
|
|
@ -407,7 +421,8 @@ class File(Document):
|
|||
def set_file_name(self):
|
||||
if not self.file_name and not self.file_url:
|
||||
frappe.throw(
|
||||
_("Fields `file_name` or `file_url` must be set for File"), exc=frappe.MandatoryError
|
||||
_("Fields `file_name` or `file_url` must be set for File"),
|
||||
exc=frappe.MandatoryError,
|
||||
)
|
||||
elif not self.file_name and self.file_url:
|
||||
self.file_name = self.file_url.split("/")[-1]
|
||||
|
|
@ -779,6 +794,9 @@ class File(Document):
|
|||
frappe.clear_messages()
|
||||
|
||||
def set_is_private(self):
|
||||
if self.is_private:
|
||||
return
|
||||
|
||||
if self.file_url:
|
||||
self.is_private = cint(self.file_url.startswith("/private"))
|
||||
|
||||
|
|
|
|||
|
|
@ -32,15 +32,18 @@ class InstalledApplications(Document):
|
|||
|
||||
self.delete_key("installed_applications")
|
||||
for app in frappe.utils.get_installed_apps_info():
|
||||
has_setup_wizard = 0
|
||||
if app.get("app_name") == "frappe" or frappe.get_hooks(app_name=app.get("app_name")).get(
|
||||
"setup_wizard_stages"
|
||||
):
|
||||
has_setup_wizard = 1
|
||||
|
||||
has_setup_wizard = 1
|
||||
setup_complete = app_wise_setup_details.get(app.get("app_name")) or 0
|
||||
if app.get("app_name") == "india_compliance":
|
||||
setup_complete = app_wise_setup_details.get("erpnext") or 0
|
||||
if app.get("app_name") in ["frappe", "erpnext"] and not setup_complete:
|
||||
if app.get("app_name") == "frappe" and has_non_admin_user():
|
||||
setup_complete = 1
|
||||
|
||||
if app.get("app_name") == "erpnext" and has_company():
|
||||
setup_complete = 1
|
||||
|
||||
if app.get("app_name") not in ["frappe", "erpnext"]:
|
||||
setup_complete = 0
|
||||
has_setup_wizard = 0
|
||||
|
||||
self.append(
|
||||
"installed_applications",
|
||||
|
|
@ -77,6 +80,22 @@ class InstalledApplications(Document):
|
|||
frappe.reload_doc("integrations", "doctype", "webhook")
|
||||
|
||||
|
||||
def has_non_admin_user():
|
||||
if frappe.db.has_table("User") and frappe.db.get_value(
|
||||
"User", {"user_type": "System User", "name": ["not in", ["Administrator", "Guest"]]}
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def has_company():
|
||||
if frappe.db.has_table("Company") and frappe.get_all("Company", limit=1):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_installed_apps_order(new_order: list[str] | str):
|
||||
"""Change the ordering of `installed_apps` global
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
@ -25,3 +28,37 @@ class ModuleProfile(Document):
|
|||
|
||||
def get_permission_log_options(self, event=None):
|
||||
return {"fields": ["block_modules"]}
|
||||
|
||||
def on_update(self):
|
||||
self.clear_cache()
|
||||
self.queue_action(
|
||||
"update_all_users",
|
||||
now=frappe.flags.in_test or frappe.flags.in_install,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
def update_all_users(self):
|
||||
"""Changes in module_profile reflected across all its user"""
|
||||
block_module = frappe.qb.DocType("Block Module")
|
||||
user = frappe.qb.DocType("User")
|
||||
|
||||
all_current_modules = (
|
||||
frappe.qb.from_(user)
|
||||
.join(block_module)
|
||||
.on(user.name == block_module.parent)
|
||||
.where(user.module_profile == self.name)
|
||||
.select(user.name, block_module.module)
|
||||
).run()
|
||||
user_modules = defaultdict(set)
|
||||
for user, module in all_current_modules:
|
||||
user_modules[user].add(module)
|
||||
|
||||
module_profile_modules = {module.module for module in self.block_modules}
|
||||
|
||||
for user_name, modules in user_modules.items():
|
||||
if modules != module_profile_modules:
|
||||
user = frappe.get_doc("User", user_name)
|
||||
user.block_modules = []
|
||||
for module in module_profile_modules:
|
||||
user.append("block_modules", {"module": module})
|
||||
user.save()
|
||||
|
|
|
|||
|
|
@ -5,25 +5,152 @@ from frappe.tests import IntegrationTestCase
|
|||
|
||||
|
||||
class TestModuleProfile(IntegrationTestCase):
|
||||
def test_make_new_module_profile(self):
|
||||
if not frappe.db.get_value("Module Profile", "_Test Module Profile"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Module Profile",
|
||||
"module_profile_name": "_Test Module Profile",
|
||||
"block_modules": [{"module": "Accounts"}],
|
||||
}
|
||||
).insert()
|
||||
def setUp(self):
|
||||
frappe.delete_doc_if_exists("Module Profile", "_Test Module Profile", force=1)
|
||||
frappe.delete_doc_if_exists("Module Profile", "_Test Module Profile 2", force=1)
|
||||
frappe.delete_doc_if_exists("User", "test-module-user1@example.com", force=1)
|
||||
frappe.delete_doc_if_exists("User", "test-module-user2@example.com", force=1)
|
||||
|
||||
# add to user and check
|
||||
if not frappe.db.get_value("User", "test-for-module_profile@example.com"):
|
||||
new_user = frappe.get_doc(
|
||||
{"doctype": "User", "email": "test-for-module_profile@example.com", "first_name": "Test User"}
|
||||
).insert()
|
||||
else:
|
||||
new_user = frappe.get_doc("User", "test-for-module_profile@example.com")
|
||||
def test_make_new_module_profile(self):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Module Profile",
|
||||
"module_profile_name": "_Test Module Profile",
|
||||
"block_modules": [{"module": "Accounts"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
new_user = frappe.get_doc(
|
||||
{"doctype": "User", "email": "test-module-user1@example.com", "first_name": "Test User"}
|
||||
).insert()
|
||||
|
||||
new_user.module_profile = "_Test Module Profile"
|
||||
new_user.save()
|
||||
|
||||
self.assertEqual(new_user.block_modules[0].module, "Accounts")
|
||||
|
||||
def test_multiple_block_modules(self):
|
||||
"""Assign multiple blocked modules from profile to user"""
|
||||
module_profile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Module Profile",
|
||||
"module_profile_name": "_Test Module Profile",
|
||||
"block_modules": [{"module": "Accounts"}, {"module": "CRM"}, {"module": "HR"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
user = frappe.get_doc(
|
||||
{"doctype": "User", "email": "test-module-user1@example.com", "first_name": "Test User"}
|
||||
).insert()
|
||||
|
||||
user.module_profile = module_profile.name
|
||||
user.save()
|
||||
|
||||
self.assertSetEqual({bm.module for bm in user.block_modules}, {"Accounts", "CRM", "HR"})
|
||||
|
||||
def test_update_module_profile_propagates_to_users(self):
|
||||
"""Updating block_modules in profile should update linked users"""
|
||||
module_profile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Module Profile",
|
||||
"module_profile_name": "_Test Module Profile",
|
||||
"block_modules": [{"module": "Accounts"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
user = frappe.get_doc(
|
||||
{"doctype": "User", "email": "test-module-user1@example.com", "first_name": "Test User"}
|
||||
).insert()
|
||||
|
||||
user.module_profile = module_profile.name
|
||||
user.save()
|
||||
|
||||
self.assertEqual({bm.module for bm in user.block_modules}, {"Accounts"})
|
||||
|
||||
module_profile.append("block_modules", {"module": "Projects"})
|
||||
module_profile.save()
|
||||
|
||||
user.reload()
|
||||
self.assertSetEqual({bm.module for bm in user.block_modules}, {"Accounts", "Projects"})
|
||||
|
||||
def test_clear_block_modules(self):
|
||||
"""Clearing block_modules in profile should also clear them for users"""
|
||||
module_profile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Module Profile",
|
||||
"module_profile_name": "_Test Module Profile",
|
||||
"block_modules": [{"module": "Accounts"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
user = frappe.get_doc(
|
||||
{"doctype": "User", "email": "test-module-user1@example.com", "first_name": "Test User"}
|
||||
).insert()
|
||||
|
||||
user.module_profile = module_profile.name
|
||||
user.save()
|
||||
self.assertTrue(user.block_modules)
|
||||
|
||||
module_profile.block_modules = []
|
||||
module_profile.save()
|
||||
|
||||
user.reload()
|
||||
self.assertEqual(user.block_modules, [])
|
||||
|
||||
def test_multiple_users_same_profile(self):
|
||||
"""Updates should propagate to all users linked to the same profile"""
|
||||
module_profile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Module Profile",
|
||||
"module_profile_name": "_Test Module Profile",
|
||||
"block_modules": [{"module": "Accounts"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
user1 = frappe.get_doc(
|
||||
{"doctype": "User", "email": "test-module-user1@example.com", "first_name": "User One"}
|
||||
).insert()
|
||||
user2 = frappe.get_doc(
|
||||
{"doctype": "User", "email": "test-module-user2@example.com", "first_name": "User Two"}
|
||||
).insert()
|
||||
|
||||
for u in (user1, user2):
|
||||
u.module_profile = module_profile.name
|
||||
u.save()
|
||||
|
||||
module_profile.append("block_modules", {"module": "Projects"})
|
||||
module_profile.save()
|
||||
|
||||
user1.reload()
|
||||
user2.reload()
|
||||
self.assertEqual([bm.module for bm in user1.block_modules], ["Accounts", "Projects"])
|
||||
self.assertEqual([bm.module for bm in user2.block_modules], ["Accounts", "Projects"])
|
||||
|
||||
def test_switch_user_module_profile(self):
|
||||
"""Switching user to a different profile updates their block_modules"""
|
||||
profile1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Module Profile",
|
||||
"module_profile_name": "_Test Module Profile",
|
||||
"block_modules": [{"module": "Accounts"}],
|
||||
}
|
||||
).insert()
|
||||
profile2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Module Profile",
|
||||
"module_profile_name": "_Test Module Profile 2",
|
||||
"block_modules": [{"module": "HR"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
user = frappe.get_doc(
|
||||
{"doctype": "User", "email": "test-module-user1@example.com", "first_name": "Test User"}
|
||||
).insert()
|
||||
|
||||
user.module_profile = profile1.name
|
||||
user.save()
|
||||
self.assertEqual([bm.module for bm in user.block_modules], ["Accounts"])
|
||||
|
||||
user.module_profile = profile2.name
|
||||
user.save()
|
||||
self.assertEqual([bm.module for bm in user.block_modules], ["HR"])
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@
|
|||
"label": "Filters"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.report_type != \"Custom Report\"",
|
||||
"depends_on": "eval:![\"Custom Report\", \"Report Builder\"].includes(doc.report_type)",
|
||||
"fieldname": "filters",
|
||||
"fieldtype": "Table",
|
||||
"label": "Filters",
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.report_type != \"Custom Report\"",
|
||||
"depends_on": "eval:![\"Custom Report\", \"Report Builder\"].includes(doc.report_type)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Table",
|
||||
"label": "Columns",
|
||||
|
|
@ -207,7 +207,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-12 17:08:09.629411",
|
||||
"modified": "2025-08-28 18:28:32.510719",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Report",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ frappe.ui.form.on("RQ Job", {
|
|||
frm.add_custom_button(__("Force Stop job"), () => {
|
||||
frappe.confirm(
|
||||
__(
|
||||
"This will terminate the job immediately and might be dangerous, are you sure? "
|
||||
"This will terminate the job immediately and might be dangerous, are you sure?"
|
||||
),
|
||||
() => {
|
||||
frappe
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ def get_contact_number(contact_name, ref_doctype, ref_name):
|
|||
|
||||
@frappe.whitelist()
|
||||
def send_sms(receiver_list, msg, sender_name="", success_msg=True):
|
||||
send_sms_hook_methods = frappe.get_hooks("send_sms")
|
||||
if send_sms_hook_methods:
|
||||
return frappe.get_attr(send_sms_hook_methods[-1])(receiver_list, msg, sender_name, success_msg)
|
||||
|
||||
import json
|
||||
|
||||
if isinstance(receiver_list, str):
|
||||
|
|
@ -78,10 +82,6 @@ def send_sms(receiver_list, msg, sender_name="", success_msg=True):
|
|||
"success_msg": success_msg,
|
||||
}
|
||||
|
||||
send_sms_hook_methods = frappe.get_hooks("send_sms")
|
||||
if send_sms_hook_methods:
|
||||
return frappe.get_attr(send_sms_hook_methods[-1])(receiver_list, msg, sender_name, success_msg)
|
||||
|
||||
if frappe.db.get_single_value("SMS Settings", "sms_gateway_url"):
|
||||
send_via_gateway(arg)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@
|
|||
"default": "10",
|
||||
"fieldname": "allow_consecutive_login_attempts",
|
||||
"fieldtype": "Int",
|
||||
"label": "Allow Consecutive Login Attempts "
|
||||
"label": "Allow Consecutive Login Attempts"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_34",
|
||||
|
|
@ -706,7 +706,7 @@
|
|||
},
|
||||
{
|
||||
"default": "100000",
|
||||
"description": "This value specifies the max number of rows that can be rendered in report view. ",
|
||||
"description": "This value specifies the max number of rows that can be rendered in report view.",
|
||||
"fieldname": "max_report_rows",
|
||||
"fieldtype": "Int",
|
||||
"label": "Max Report Rows"
|
||||
|
|
|
|||
|
|
@ -156,9 +156,8 @@ class SystemSettings(Document):
|
|||
|
||||
social_login_enabled = frappe.db.exists("Social Login Key", {"enable_social_login": 1})
|
||||
ldap_enabled = frappe.db.get_single_value("LDAP Settings", "enabled")
|
||||
login_with_email_link_enabled = frappe.db.get_single_value("System Settings", "login_with_email_link")
|
||||
|
||||
if not (social_login_enabled or ldap_enabled or login_with_email_link_enabled):
|
||||
if not (social_login_enabled or ldap_enabled or self.login_with_email_link):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please enable atleast one Social Login Key or LDAP or Login With Email Link before disabling username/password based login."
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
# Transaction Log Changelog
|
||||
|
||||
## v1.0.0
|
||||
Initial version
|
||||
|
||||
The line hash summarizes:
|
||||
- The index of the row
|
||||
- The timestamp
|
||||
- The document raw data
|
||||
|
||||
The chain hash summarizes:
|
||||
- The previous line hash
|
||||
- The current line hash
|
||||
|
||||
## v1.0.1
|
||||
Modification of the timestamp fieldtype from "Time" to "Datetime"
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# Copyright (c) 2018, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import hashlib
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestTransactionLog(IntegrationTestCase):
|
||||
def test_validate_chaining(self):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Transaction Log",
|
||||
"reference_doctype": "Test Doctype",
|
||||
"document_name": "Test Document 1",
|
||||
"data": "first_data",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
second_log = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Transaction Log",
|
||||
"reference_doctype": "Test Doctype",
|
||||
"document_name": "Test Document 2",
|
||||
"data": "second_data",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
third_log = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Transaction Log",
|
||||
"reference_doctype": "Test Doctype",
|
||||
"document_name": "Test Document 3",
|
||||
"data": "third_data",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
sha = hashlib.sha256()
|
||||
sha.update(
|
||||
frappe.safe_encode(str(third_log.transaction_hash))
|
||||
+ frappe.safe_encode(str(second_log.chaining_hash))
|
||||
)
|
||||
|
||||
self.assertEqual(sha.hexdigest(), third_log.chaining_hash)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
// Copyright (c) 2018, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Transaction Log", {});
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2018-02-06 11:48:51.270524",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"row_index",
|
||||
"section_break_2",
|
||||
"reference_doctype",
|
||||
"document_name",
|
||||
"column_break_5",
|
||||
"timestamp",
|
||||
"checksum_version",
|
||||
"section_break_8",
|
||||
"previous_hash",
|
||||
"transaction_hash",
|
||||
"chaining_hash",
|
||||
"data",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "row_index",
|
||||
"fieldtype": "Data",
|
||||
"label": "Row Index",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Document Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "document_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Document Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "timestamp",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Timestamp",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "checksum_version",
|
||||
"fieldtype": "Data",
|
||||
"label": "Checksum Version",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "previous_hash",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Previous Hash",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_hash",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Transaction Hash",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "chaining_hash",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Chaining Hash",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "data",
|
||||
"fieldtype": "Long Text",
|
||||
"hidden": 1,
|
||||
"label": "Data",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Transaction Log",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:59.373102",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Transaction Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import hashlib
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, now_datetime
|
||||
|
||||
|
||||
class TransactionLog(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amended_from: DF.Link | None
|
||||
chaining_hash: DF.SmallText | None
|
||||
checksum_version: DF.Data | None
|
||||
data: DF.LongText | None
|
||||
document_name: DF.Data | None
|
||||
previous_hash: DF.SmallText | None
|
||||
reference_doctype: DF.Data | None
|
||||
row_index: DF.Data | None
|
||||
timestamp: DF.Datetime | None
|
||||
transaction_hash: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_insert(self):
|
||||
index = get_current_index()
|
||||
self.row_index = index
|
||||
self.timestamp = now_datetime()
|
||||
if index != 1:
|
||||
prev_hash = frappe.get_all(
|
||||
"Transaction Log", filters={"row_index": str(index - 1)}, pluck="chaining_hash", limit=1
|
||||
)
|
||||
if prev_hash:
|
||||
self.previous_hash = prev_hash[0]
|
||||
else:
|
||||
self.previous_hash = "Indexing broken"
|
||||
else:
|
||||
self.previous_hash = self.hash_line()
|
||||
self.transaction_hash = self.hash_line()
|
||||
self.chaining_hash = self.hash_chain()
|
||||
self.checksum_version = "v1.0.1"
|
||||
|
||||
def hash_line(self):
|
||||
sha = hashlib.sha256()
|
||||
sha.update(
|
||||
frappe.safe_encode(str(self.row_index))
|
||||
+ frappe.safe_encode(str(self.timestamp))
|
||||
+ frappe.safe_encode(str(self.data))
|
||||
)
|
||||
return sha.hexdigest()
|
||||
|
||||
def hash_chain(self):
|
||||
sha = hashlib.sha256()
|
||||
sha.update(
|
||||
frappe.safe_encode(str(self.transaction_hash)) + frappe.safe_encode(str(self.previous_hash))
|
||||
)
|
||||
return sha.hexdigest()
|
||||
|
||||
|
||||
def get_current_index():
|
||||
series = DocType("Series")
|
||||
current = (
|
||||
frappe.qb.from_(series).where(series.name == "TRANSACTLOG").for_update().select("current")
|
||||
).run()
|
||||
|
||||
if current and current[0][0] is not None:
|
||||
current = current[0][0]
|
||||
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabSeries`
|
||||
SET `current` = `current` + 1
|
||||
where `name` = 'TRANSACTLOG'"""
|
||||
)
|
||||
current = cint(current) + 1
|
||||
else:
|
||||
frappe.db.sql("INSERT INTO `tabSeries` (name, current) VALUES ('TRANSACTLOG', 1)")
|
||||
current = 1
|
||||
return current
|
||||
|
|
@ -62,6 +62,7 @@ frappe.ui.form.on("User", {
|
|||
let d = frm.add_child("block_modules");
|
||||
d.module = v.module;
|
||||
});
|
||||
frm.module_editor.disable = 1;
|
||||
frm.module_editor && frm.module_editor.show();
|
||||
},
|
||||
});
|
||||
|
|
@ -93,7 +94,11 @@ frappe.ui.form.on("User", {
|
|||
|
||||
if (frm.doc.user_type == "System User") {
|
||||
var module_area = $("<div>").appendTo(frm.fields_dict.modules_html.wrapper);
|
||||
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
|
||||
frm.module_editor = new frappe.ModuleEditor(
|
||||
frm,
|
||||
module_area,
|
||||
frm.doc.module_profile ? 1 : 0
|
||||
);
|
||||
}
|
||||
} else {
|
||||
frm.roles_editor.show();
|
||||
|
|
@ -248,6 +253,7 @@ frappe.ui.form.on("User", {
|
|||
frm.roles_editor.show();
|
||||
}
|
||||
|
||||
frm.module_editor.disable = frm.doc.module_profile ? 1 : 0;
|
||||
frm.module_editor && frm.module_editor.show();
|
||||
|
||||
if (frappe.session.user == doc.name) {
|
||||
|
|
@ -336,7 +342,7 @@ frappe.ui.form.on("User", {
|
|||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint(__("Save API Secret: {0}", [r.message.api_secret]));
|
||||
show_api_key_dialog(r.message.api_key, r.message.api_secret);
|
||||
frm.reload_doc();
|
||||
}
|
||||
},
|
||||
|
|
@ -437,3 +443,52 @@ function get_roles_for_editing_user() {
|
|||
.map((perm) => perm.role) || ["System Manager"]
|
||||
);
|
||||
}
|
||||
|
||||
function show_api_key_dialog(api_key, api_secret) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("API Keys"),
|
||||
fields: [
|
||||
{
|
||||
label: __("API Key"),
|
||||
fieldname: "api_key",
|
||||
fieldtype: "Code",
|
||||
read_only: 1,
|
||||
default: api_key,
|
||||
},
|
||||
{
|
||||
label: __("API Secret"),
|
||||
fieldname: "api_secret",
|
||||
fieldtype: "Code",
|
||||
read_only: 1,
|
||||
default: api_secret,
|
||||
},
|
||||
],
|
||||
size: "small",
|
||||
primary_action_label: __("Download"),
|
||||
primary_action: () => {
|
||||
frappe.tools.downloadify(
|
||||
[
|
||||
["api_key", "api_secret"],
|
||||
[api_key, api_secret],
|
||||
],
|
||||
"System Manager",
|
||||
"frappe_api_keys"
|
||||
);
|
||||
|
||||
dialog.hide();
|
||||
},
|
||||
secondary_action_label: __("Copy token to clipboard"),
|
||||
secondary_action: () => {
|
||||
const token = `${api_key}:${api_secret}`;
|
||||
frappe.utils.copy_to_clipboard(token);
|
||||
dialog.hide();
|
||||
},
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
dialog.show_message(
|
||||
__("Store the API secret securely. It won't be displayed again."),
|
||||
"yellow",
|
||||
1
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -601,6 +601,7 @@
|
|||
"unique": 1
|
||||
},
|
||||
{
|
||||
"description": "<a href=\"https://docs.frappe.io/framework/user/en/api/rest#1-token-based-authentication\" target=\"_blank\">\n Click here to learn about token-based authentication\n</a>",
|
||||
"fieldname": "generate_keys",
|
||||
"fieldtype": "Button",
|
||||
"label": "Generate Keys",
|
||||
|
|
|
|||
|
|
@ -590,6 +590,13 @@ class User(Document):
|
|||
note.remove(row)
|
||||
note.save(ignore_permissions=True)
|
||||
|
||||
# Unlink user from all of its invitation docs
|
||||
invites = frappe.db.get_all("User Invitation", filters={"email": self.name}, pluck="name")
|
||||
for invite in invites:
|
||||
invite_doc = frappe.get_doc("User Invitation", invite)
|
||||
invite_doc.user = None
|
||||
invite_doc.save(ignore_permissions=True)
|
||||
|
||||
def before_rename(self, old_name, new_name, merge=False):
|
||||
# if merging, delete the old user notification settings
|
||||
if merge:
|
||||
|
|
@ -1343,7 +1350,7 @@ def generate_keys(user: str):
|
|||
user_details.api_secret = api_secret
|
||||
user_details.save()
|
||||
|
||||
return {"api_secret": api_secret}
|
||||
return {"api_key": user_details.api_key, "api_secret": api_secret}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -24,13 +24,9 @@ Define user invitation hooks in your app's `hooks.py` file. An example is shown
|
|||
|
||||

|
||||
|
||||
- `only_for`
|
||||
|
||||
Roles that are allowed to invite users to your app.
|
||||
|
||||
- `allowed_roles`
|
||||
|
||||
Roles that are allowed to be invited to your app.
|
||||
A map of `only_for` roles to a list of roles that are allowed to be invited to your app.
|
||||
|
||||
- `after_accept`
|
||||
|
||||
|
|
@ -104,3 +100,4 @@ Cancels a specific pending invitation associated with an installed Framework app
|
|||
- Once an invitation document is created from Desk, all of the fields are immutable except the `Redirect To Path` field which is mutable only when the invitation status is `Pending`.
|
||||
- To manually mark an invitation as expired, you can use the `expire` method on the invitation document.
|
||||
- To manually cancel an invitation, you can use the `cancel_invite` method on the invitation document.
|
||||
- Disabled users cannot be invited. Trying to invite a disabled user from the Desk will generate an error and the whitelisted functions will ignore emails associated with disabled users.
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 73 KiB |
|
|
@ -20,6 +20,7 @@ emails = [
|
|||
"test_user_invite3@example.com",
|
||||
"test_user_invite4@example.com",
|
||||
"test_user_invite5@example.com",
|
||||
"test_user_invite6@example.com",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -138,8 +139,7 @@ class IntegrationTestUserInvitation(IntegrationTestCase):
|
|||
redirect_to_path="/abc",
|
||||
app_name="frappe",
|
||||
).insert()
|
||||
invitation.status = "Accepted"
|
||||
invitation.save()
|
||||
invitation.accept()
|
||||
self.assertEqual(len(self.get_email_names(False)), 1)
|
||||
pending_invite_email = emails[2]
|
||||
frappe.get_doc(
|
||||
|
|
@ -156,10 +156,35 @@ class IntegrationTestUserInvitation(IntegrationTestCase):
|
|||
roles=["System Manager"],
|
||||
redirect_to_path="/xyz",
|
||||
)
|
||||
self.assertSequenceEqual(res["disabled_user_emails"], [])
|
||||
self.assertSequenceEqual(res["accepted_invite_emails"], [accepted_invite_email])
|
||||
self.assertSequenceEqual(res["pending_invite_emails"], [pending_invite_email])
|
||||
self.assertSequenceEqual(res["invited_emails"], [email_to_invite])
|
||||
self.assertEqual(len(self.get_email_names(False)), 3)
|
||||
user = frappe.get_doc("User", invitation.email)
|
||||
IntegrationTestUserInvitation.delete_invitation(invitation.name)
|
||||
frappe.delete_doc("User", user.name)
|
||||
|
||||
def test_invite_by_email_api_disabled_user(self):
|
||||
user = frappe.new_doc("User")
|
||||
user.first_name = "Random"
|
||||
user.last_name = "User"
|
||||
user.email = emails[5]
|
||||
user.append_roles("System Manager")
|
||||
user.insert()
|
||||
user.reload()
|
||||
user.enabled = 0
|
||||
user.save()
|
||||
res = invite_by_email(
|
||||
emails=user.email,
|
||||
roles=["System Manager"],
|
||||
redirect_to_path="/xyz",
|
||||
)
|
||||
self.assertSequenceEqual(res["disabled_user_emails"], [user.email])
|
||||
self.assertSequenceEqual(res["accepted_invite_emails"], [])
|
||||
self.assertSequenceEqual(res["pending_invite_emails"], [])
|
||||
self.assertSequenceEqual(res["invited_emails"], [])
|
||||
frappe.delete_doc("User", user.email)
|
||||
|
||||
def test_accept_invitation_api_pass_redirect(self):
|
||||
invitation = frappe.get_doc(
|
||||
|
|
|
|||
|
|
@ -84,13 +84,22 @@ class UserInvitation(Document):
|
|||
self._validate_roles()
|
||||
self._validate_email()
|
||||
if frappe.db.get_value(
|
||||
"User Invitation", filters={"email": self.email, "status": "Accepted", "app_name": self.app_name}
|
||||
"User Invitation",
|
||||
filters={
|
||||
"email": self.email,
|
||||
"status": "Accepted",
|
||||
"app_name": self.app_name,
|
||||
"user": ["is", "set"],
|
||||
},
|
||||
):
|
||||
frappe.throw(title=_("Error"), msg=_("invitation already accepted"))
|
||||
frappe.throw(title=_("Error"), msg=_("Invitation already accepted"))
|
||||
if frappe.db.get_value(
|
||||
"User Invitation", filters={"email": self.email, "status": "Pending", "app_name": self.app_name}
|
||||
):
|
||||
frappe.throw(title=_("Error"), msg=_("invitation already exists"))
|
||||
frappe.throw(title=_("Error"), msg=_("Invitation already exists"))
|
||||
user_enabled = frappe.db.get_value("User", self.email, "enabled")
|
||||
if user_enabled is not None and user_enabled == 0:
|
||||
frappe.throw(title=_("Error"), msg=_("User is disabled"))
|
||||
|
||||
def _after_insert(self):
|
||||
key = frappe.generate_hash()
|
||||
|
|
@ -150,13 +159,21 @@ class UserInvitation(Document):
|
|||
def _validate_app_name(self):
|
||||
UserInvitation.validate_app_name(self.app_name)
|
||||
|
||||
def _get_allowed_roles(self):
|
||||
user_invitation_hook = frappe.get_hooks("user_invitation", app_name=self.app_name)
|
||||
if not isinstance(user_invitation_hook, dict):
|
||||
return []
|
||||
res = set[str]()
|
||||
allowed_roles_mp = user_invitation_hook.get("allowed_roles") or dict()
|
||||
only_for = set(allowed_roles_mp.keys())
|
||||
for role in only_for & set(frappe.get_roles()):
|
||||
res.update(allowed_roles_mp[role])
|
||||
return list(res)
|
||||
|
||||
def _validate_roles(self):
|
||||
if self.app_name == "frappe":
|
||||
return
|
||||
user_invitation_hook = frappe.get_hooks("user_invitation", app_name=self.app_name)
|
||||
allowed_roles: list[str] = []
|
||||
if isinstance(user_invitation_hook, dict):
|
||||
allowed_roles = user_invitation_hook.get("allowed_roles") or []
|
||||
allowed_roles = self._get_allowed_roles()
|
||||
for r in self.roles:
|
||||
if r.role in allowed_roles:
|
||||
continue
|
||||
|
|
@ -175,7 +192,7 @@ class UserInvitation(Document):
|
|||
@staticmethod
|
||||
def validate_app_name(app_name: str):
|
||||
if app_name not in frappe.get_installed_apps():
|
||||
frappe.throw(title=_("Invalid app"), msg=_("application is not installed"))
|
||||
frappe.throw(title=_("Invalid app"), msg=_("Application is not installed"))
|
||||
|
||||
@staticmethod
|
||||
def validate_role(app_name: str) -> None:
|
||||
|
|
@ -183,9 +200,7 @@ class UserInvitation(Document):
|
|||
user_invitation_hook = frappe.get_hooks("user_invitation", app_name=app_name)
|
||||
only_for: list[str] = []
|
||||
if isinstance(user_invitation_hook, dict):
|
||||
only_for = user_invitation_hook.get("only_for") or []
|
||||
if "System Manager" not in only_for:
|
||||
only_for.append("System Manager")
|
||||
only_for = list((user_invitation_hook.get("allowed_roles") or dict()).keys())
|
||||
frappe.only_for(only_for)
|
||||
|
||||
|
||||
|
|
@ -209,7 +224,7 @@ def get_allowed_apps(user: Document | None) -> list[str]:
|
|||
user_invitation_hooks = frappe.get_hooks("user_invitation", app_name=app)
|
||||
if not isinstance(user_invitation_hooks, dict):
|
||||
continue
|
||||
only_for = user_invitation_hooks.get("only_for") or []
|
||||
only_for = list((user_invitation_hooks.get("allowed_roles") or dict()).keys())
|
||||
if set(only_for) & user_roles:
|
||||
allowed_apps.append(app)
|
||||
return allowed_apps
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model import no_value_fields, table_fields
|
||||
from frappe.model import datetime_fields, no_value_fields, table_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
|
@ -120,6 +120,10 @@ def get_diff(old, new, for_child=False, compare_cancelled=False):
|
|||
if df.fieldtype in ("Link", "Dynamic Link"):
|
||||
old_value, new_value = cstr(old_value), cstr(new_value)
|
||||
|
||||
if df.fieldtype in datetime_fields:
|
||||
if old_value is None and new_value == "":
|
||||
new_value = None
|
||||
|
||||
if not for_child and df.fieldtype in table_fields:
|
||||
old_rows_by_name = {}
|
||||
for d in old_value:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<p>{{ data.comment }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% const getEscapedValue = (v) => v === null ? "null" : frappe.utils.escape_html(v) %}
|
||||
{% if data.changed && data.changed.length %}
|
||||
<h4>{{ __("Values Changed") }}</h4>
|
||||
<table class="table table-bordered">
|
||||
|
|
@ -18,8 +19,8 @@
|
|||
{% for item in data.changed %}
|
||||
<tr>
|
||||
<td>{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }}</td>
|
||||
<td class="diff-remove">{{ frappe.utils.escape_html(item[1]) }}</td>
|
||||
<td class="diff-add">{{ frappe.utils.escape_html(item[2]) }}</td>
|
||||
<td class="diff-remove">{{ getEscapedValue(item[1]) }}</td>
|
||||
<td class="diff-add">{{ getEscapedValue(item[2]) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -50,7 +51,7 @@
|
|||
{% for row_key in item_keys %}
|
||||
<tr>
|
||||
<td class="small">{{ row_key }}</td>
|
||||
<td class="small">{{ frappe.utils.escape_html(item[1][row_key]) }}</td>
|
||||
<td class="small">{{ getEscapedValue(item[1][row_key]) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -85,8 +86,8 @@
|
|||
<td>{{ frappe.meta.get_label(doc.ref_doctype, table_info[0]) }}</td>
|
||||
<td>{{ table_info[1] }}</td>
|
||||
<td>{{ item[0] }}</td>
|
||||
<td class="diff-remove">{{ frappe.utils.escape_html(item[1]) }}</td>
|
||||
<td class="diff-add">{{ frappe.utils.escape_html(item[2]) }}</td>
|
||||
<td class="diff-remove">{{ getEscapedValue(item[1]) }}</td>
|
||||
<td class="diff-add">{{ getEscapedValue(item[2]) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from frappe.permissions import (
|
|||
)
|
||||
from frappe.utils.user import get_users_with_role as _get_user_with_role
|
||||
|
||||
not_allowed_in_permission_manager = ["DocType", "Patch Log", "Module Def", "Transaction Log"]
|
||||
not_allowed_in_permission_manager = ["DocType", "Patch Log", "Module Def"]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Transaction Log Report"] = {
|
||||
onload: function (query_report) {
|
||||
query_report.add_make_chart_button = function () {
|
||||
//
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"add_total_row": 0,
|
||||
"creation": "2018-03-15 18:37:48.783779",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-12-27 18:10:29.785415",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Transaction Log Report",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Transaction Log",
|
||||
"report_name": "Transaction Log Report",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Administrator"
|
||||
},
|
||||
{
|
||||
"role": "System Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import hashlib
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import format_datetime
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = get_columns(filters), get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_data(filters=None):
|
||||
result = []
|
||||
logs = frappe.get_all("Transaction Log", fields=["*"], order_by="creation desc")
|
||||
|
||||
for l in logs:
|
||||
row_index = int(l.row_index)
|
||||
if row_index > 1:
|
||||
previous_hash = frappe.get_all(
|
||||
"Transaction Log",
|
||||
fields=["chaining_hash"],
|
||||
filters={"row_index": row_index - 1},
|
||||
)
|
||||
if not previous_hash:
|
||||
integrity = False
|
||||
else:
|
||||
integrity = check_data_integrity(
|
||||
l.chaining_hash, l.transaction_hash, l.previous_hash, previous_hash[0]["chaining_hash"]
|
||||
)
|
||||
|
||||
result.append(
|
||||
[
|
||||
_(str(integrity)),
|
||||
_(l.reference_doctype),
|
||||
l.document_name,
|
||||
l.owner,
|
||||
l.modified_by,
|
||||
format_datetime(l.timestamp, "YYYYMMDDHHmmss"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
result.append(
|
||||
[
|
||||
_("First Transaction"),
|
||||
_(l.reference_doctype),
|
||||
l.document_name,
|
||||
l.owner,
|
||||
l.modified_by,
|
||||
format_datetime(l.timestamp, "YYYYMMDDHHmmss"),
|
||||
]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def check_data_integrity(chaining_hash, transaction_hash, registered_previous_hash, previous_hash):
|
||||
if registered_previous_hash != previous_hash:
|
||||
return False
|
||||
|
||||
calculated_chaining_hash = calculate_chain(transaction_hash, previous_hash)
|
||||
|
||||
if calculated_chaining_hash != chaining_hash:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def calculate_chain(transaction_hash, previous_hash):
|
||||
sha = hashlib.sha256()
|
||||
sha.update(transaction_hash.encode("utf-8") + previous_hash.encode("utf-8"))
|
||||
return sha.hexdigest()
|
||||
|
||||
|
||||
def get_columns(filters=None):
|
||||
return [
|
||||
{
|
||||
"label": _("Chain Integrity"),
|
||||
"fieldname": "chain_integrity",
|
||||
"fieldtype": "Data",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Reference Doctype"),
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Data",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Reference Name"),
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Owner"),
|
||||
"fieldname": "owner",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Modified By"),
|
||||
"fieldname": "modified_by",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Timestamp"),
|
||||
"fieldname": "timestamp",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
|
|
@ -18,7 +18,7 @@ STANDARD_EXCLUSIONS = [
|
|||
"*.scss",
|
||||
"*.vue",
|
||||
"*.html",
|
||||
"*/test_*",
|
||||
"*/test_*/*",
|
||||
"*/node_modules/*",
|
||||
"*/doctype/*/*_dashboard.py",
|
||||
"*/patches/*",
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"disable_count",
|
||||
"disable_comment_count",
|
||||
"disable_sidebar_stats",
|
||||
"disable_auto_refresh",
|
||||
"allow_edit",
|
||||
"disable_sidebar_stats",
|
||||
"disable_automatic_recency_filters",
|
||||
"total_fields",
|
||||
"column_break_oany",
|
||||
"disable_comment_count",
|
||||
"disable_scrolling",
|
||||
"allow_edit",
|
||||
"section_break_evqq",
|
||||
"fields_html",
|
||||
"fields"
|
||||
],
|
||||
|
|
@ -35,12 +37,6 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Disable Auto Refresh"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_fields",
|
||||
"fieldtype": "Select",
|
||||
"label": "Maximum Number of Fields",
|
||||
"options": "\n4\n5\n6\n7\n8\n9\n10"
|
||||
},
|
||||
{
|
||||
"fieldname": "fields_html",
|
||||
"fieldtype": "HTML",
|
||||
|
|
@ -71,11 +67,25 @@
|
|||
"fieldname": "disable_automatic_recency_filters",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Automatic Recency Filters"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_scrolling",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Scrolling"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_oany",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_evqq",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [],
|
||||
"modified": "2025-03-12 16:28:46.073808",
|
||||
"modified": "2025-08-25 15:54:18.886680",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "List View Settings",
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ class ListViewSettings(Document):
|
|||
disable_automatic_recency_filters: DF.Check
|
||||
disable_comment_count: DF.Check
|
||||
disable_count: DF.Check
|
||||
disable_scrolling: DF.Check
|
||||
disable_sidebar_stats: DF.Check
|
||||
fields: DF.Code | None
|
||||
total_fields: DF.Literal["", "4", "5", "6", "7", "8", "9", "10"]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -302,6 +302,9 @@ def save_page(name, public, new_widgets, blocks):
|
|||
public = frappe.parse_json(public)
|
||||
|
||||
doc = frappe.get_doc("Workspace", name)
|
||||
if not doc.type:
|
||||
doc.type = "Workspace"
|
||||
|
||||
doc.content = blocks
|
||||
|
||||
save_new_widget(doc, name, blocks, new_widgets)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ def add(args=None, *, ignore_permissions=False):
|
|||
else:
|
||||
from frappe.utils import nowdate
|
||||
|
||||
description = str(args.get("description", ""))
|
||||
description = args.get("description") or ""
|
||||
has_content = strip_html(description) or "<img" in description
|
||||
if not has_content:
|
||||
args["description"] = _("Assignment for {0} {1}").format(args["doctype"], args["name"])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import frappe
|
||||
from frappe.model import no_value_fields, table_fields
|
||||
from frappe.utils.caching import http_cache
|
||||
from frappe.www.printview import set_title_values_for_link_and_dynamic_link_fields
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -42,12 +43,16 @@ def get_preview_data(doctype, docname):
|
|||
"preview_title": preview_data.get(title_field),
|
||||
"name": preview_data.get("name"),
|
||||
}
|
||||
if meta.show_title_field_in_link and meta.title_field:
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
set_title_values_for_link_and_dynamic_link_fields(meta, doc)
|
||||
|
||||
for key, val in preview_data.items():
|
||||
if val and meta.has_field(key) and key not in [image_field, title_field, "name"]:
|
||||
formatted_preview_data[meta.get_field(key).label] = frappe.format(
|
||||
val,
|
||||
meta.get_field(key).fieldtype,
|
||||
value=val,
|
||||
doc=doc,
|
||||
df=meta.get_field(key),
|
||||
translated=True,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
|||
? frappe.last_response.setup_wizard_failure_message
|
||||
: __("Failed to complete setup");
|
||||
|
||||
this.update_setup_message(__("Could not start up: ") + fail_msg);
|
||||
this.update_setup_message(__("Could not start up:") + " " + fail_msg);
|
||||
|
||||
this.$working_state.find(".title").html(__("Setup failed"));
|
||||
|
||||
|
|
|
|||
|
|
@ -67,8 +67,9 @@ def get_count() -> int | None:
|
|||
|
||||
# args.limit is specified to avoid getting accurate count.
|
||||
if not args.limit:
|
||||
args.fields = [f"count({fieldname}) as total_count"]
|
||||
return execute(**args)[0].get("total_count")
|
||||
args.fields = [fieldname]
|
||||
partial_query = execute(**args, run=0)
|
||||
return frappe.db.sql(f"select count(*) from ( {partial_query} ) p")[0][0]
|
||||
|
||||
args.fields = [fieldname]
|
||||
partial_query = execute(**args, run=0)
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter
|
|||
user_perms = frappe.utils.user.UserPermissions(frappe.session.user)
|
||||
user_perms.build_permissions()
|
||||
can_read = user_perms.can_read
|
||||
from frappe import _
|
||||
from frappe.modules import load_doctype_module
|
||||
|
||||
com_doctypes = []
|
||||
|
|
@ -96,7 +97,15 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter
|
|||
d[0] for d in frappe.db.get_values("DocType", {"issingle": 0, "istable": 0, "hide_toolbar": 0})
|
||||
]
|
||||
|
||||
return [[dt] for dt in com_doctypes if txt.lower().replace("%", "") in dt.lower() and dt in can_read]
|
||||
results = []
|
||||
txt_lower = txt.lower().replace("%", "")
|
||||
|
||||
for dt in com_doctypes:
|
||||
if dt in can_read:
|
||||
if txt_lower in dt.lower() or txt_lower in _(dt).lower():
|
||||
results.append([dt])
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def sendmail(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
"default_incoming",
|
||||
"column_break_uynb",
|
||||
"attachment_limit",
|
||||
"last_synced_at",
|
||||
"last_received_at",
|
||||
"mailbox_settings",
|
||||
"use_imap",
|
||||
"use_ssl",
|
||||
|
|
@ -257,7 +257,7 @@
|
|||
{
|
||||
"default": "250",
|
||||
"depends_on": "use_imap",
|
||||
"description": "Total number of emails to sync in initial sync process ",
|
||||
"description": "Total number of emails to sync in initial sync process",
|
||||
"fieldname": "initial_sync_count",
|
||||
"fieldtype": "Select",
|
||||
"hide_days": 1,
|
||||
|
|
@ -497,7 +497,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "For more information, <a class=\"text-muted\" href=\"https://erpnext.com/docs/user/manual/en/setting-up/email/linking-emails-to-document\">click here</a>.",
|
||||
"documentation_url": "https://docs.frappe.io/erpnext/user/manual/en/linking-emails-to-document",
|
||||
"fieldname": "enable_automatic_linking",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
|
|
@ -661,12 +661,6 @@
|
|||
"fieldname": "column_break_uynb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.service == \"Frappe Mail\"",
|
||||
"fieldname": "last_synced_at",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Last Synced At"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.service == \"Frappe Mail\" && doc.auth_method != \"Basic\" && !doc.__islocal && !doc.__unsaved)",
|
||||
"fieldname": "validate_frappe_mail_settings",
|
||||
|
|
@ -707,13 +701,19 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "Always BCC Address",
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.service == \"Frappe Mail\"",
|
||||
"fieldname": "last_received_at",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Last Received At"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-inbox",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-03-10 14:22:11.021118",
|
||||
"modified": "2025-08-20 11:35:14.540578",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class EmailAccount(Document):
|
|||
imap_folder: DF.Table[IMAPFolder]
|
||||
incoming_port: DF.Data | None
|
||||
initial_sync_count: DF.Literal["100", "250", "500"]
|
||||
last_synced_at: DF.Datetime | None
|
||||
last_received_at: DF.Datetime | None
|
||||
login_id: DF.Data | None
|
||||
login_id_is_different: DF.Check
|
||||
no_failed: DF.Int
|
||||
|
|
@ -646,9 +646,9 @@ class EmailAccount(Document):
|
|||
try:
|
||||
if self.service == "Frappe Mail":
|
||||
frappe_mail_client = self.get_frappe_mail_client()
|
||||
messages = frappe_mail_client.pull_raw(last_synced_at=self.last_synced_at)
|
||||
messages = frappe_mail_client.pull_raw(last_received_at=self.last_received_at)
|
||||
process_mail(messages)
|
||||
self.db_set("last_synced_at", messages["last_synced_at"], update_modified=False)
|
||||
self.db_set("last_received_at", messages["last_received_at"], update_modified=False)
|
||||
else:
|
||||
email_sync_rule = self.build_email_sync_rule()
|
||||
email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
"depends_on": "eval:doc.use_html",
|
||||
"fieldname": "response_html",
|
||||
"fieldtype": "Code",
|
||||
"label": "Response ",
|
||||
"label": "Response",
|
||||
"options": "Jinja"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import math
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from urllib.parse import urljoin
|
||||
|
|
@ -8,6 +10,8 @@ from frappe import _
|
|||
from frappe.frappeclient import FrappeClient, FrappeOAuth2Client
|
||||
from frappe.utils import convert_utc_to_system_timezone, get_datetime, get_system_timezone
|
||||
|
||||
CHUNK_SIZE = 5 * 1024 * 1024 # 5MB
|
||||
|
||||
|
||||
class FrappeMail:
|
||||
"""Class to interact with the Frappe Mail API."""
|
||||
|
|
@ -83,6 +87,7 @@ class FrappeMail:
|
|||
headers=headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return self.client.post_process(response)
|
||||
|
||||
|
|
@ -98,23 +103,47 @@ class FrappeMail:
|
|||
) -> None:
|
||||
"""Sends an email using the Frappe Mail API."""
|
||||
|
||||
session_id = str(uuid.uuid4())
|
||||
endpoint = "/api/method/mail.api.outbound.send_raw"
|
||||
data = {"from_": sender, "to": recipients, "is_newsletter": is_newsletter}
|
||||
self.request("POST", endpoint=endpoint, data=data, files={"raw_message": message})
|
||||
|
||||
def pull_raw(self, limit: int = 50, last_synced_at: str | None = None) -> dict[str, str | list[str]]:
|
||||
"""Pulls emails for the email using the Frappe Mail API."""
|
||||
if isinstance(message, str):
|
||||
message = message.encode("utf-8")
|
||||
|
||||
total_size = len(message)
|
||||
total_chunks = math.ceil(total_size / CHUNK_SIZE)
|
||||
|
||||
for i in range(total_chunks):
|
||||
start = i * CHUNK_SIZE
|
||||
end = start + CHUNK_SIZE
|
||||
chunk = message[start:end]
|
||||
|
||||
files = {"raw_message": ("raw_message.eml", chunk)}
|
||||
data = {
|
||||
"from_": sender,
|
||||
"to": recipients,
|
||||
"is_newsletter": is_newsletter,
|
||||
"uuid": session_id,
|
||||
"chunk_index": i,
|
||||
"total_chunk_count": total_chunks,
|
||||
"chunk_byte_offset": start,
|
||||
}
|
||||
self.request("POST", endpoint=endpoint, data=data, files=files)
|
||||
|
||||
def pull_raw(
|
||||
self, mailbox: str = "inbox", limit: int = 50, last_received_at: str | None = None
|
||||
) -> dict[str, str | list[str]]:
|
||||
"""Pull emails for the account using the Frappe Mail API."""
|
||||
|
||||
endpoint = "/api/method/mail.api.inbound.pull_raw"
|
||||
if last_synced_at:
|
||||
last_synced_at = add_or_update_tzinfo(last_synced_at)
|
||||
if last_received_at:
|
||||
last_received_at = add_or_update_tzinfo(last_received_at)
|
||||
|
||||
data = {"email": self.email, "limit": limit, "last_synced_at": last_synced_at}
|
||||
data = {"mailbox": mailbox, "limit": limit, "last_received_at": last_received_at}
|
||||
headers = {"X-Site": frappe.utils.get_url()}
|
||||
response = self.request("GET", endpoint=endpoint, data=data, headers=headers)
|
||||
last_synced_at = convert_utc_to_system_timezone(get_datetime(response["last_synced_at"]))
|
||||
last_received_at = convert_utc_to_system_timezone(get_datetime(response["last_received_at"]))
|
||||
|
||||
return {"latest_messages": response["mails"], "last_synced_at": last_synced_at}
|
||||
return {"latest_messages": response["mails"], "last_received_at": last_received_at}
|
||||
|
||||
|
||||
def add_or_update_tzinfo(date_time: datetime | str, timezone: str | None = None) -> str:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import frappe
|
|||
import frappe.sessions
|
||||
import frappe.utils
|
||||
from frappe import _, is_whitelisted, ping
|
||||
from frappe.core.doctype.file.utils import find_file_by_url
|
||||
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
|
||||
from frappe.monitor import add_data_to_monitor
|
||||
from frappe.permissions import check_doctype_permission
|
||||
|
|
@ -230,8 +231,8 @@ def download_file(file_url: str):
|
|||
Endpoints : download_file, frappe.core.doctype.file.file.download_file
|
||||
URL Params : file_name = /path/to/file relative to site path
|
||||
"""
|
||||
file: File = frappe.get_doc("File", {"file_url": file_url})
|
||||
if not file.is_downloadable():
|
||||
file = find_file_by_url(file_url)
|
||||
if not file:
|
||||
raise frappe.PermissionError
|
||||
|
||||
frappe.local.response.filename = os.path.basename(file_url)
|
||||
|
|
|
|||
|
|
@ -575,5 +575,7 @@ persistent_cache_keys = [
|
|||
]
|
||||
|
||||
user_invitation = {
|
||||
"only_for": ["System Manager"],
|
||||
"allowed_roles": {
|
||||
"System Manager": [],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,6 +333,7 @@ def install_app(name, verbose=False, set_as_patched=True, force=False):
|
|||
for after_sync in app_hooks.after_sync or []:
|
||||
frappe.get_attr(after_sync)() #
|
||||
|
||||
frappe.clear_cache()
|
||||
frappe.client_cache.erase_persistent_caches()
|
||||
frappe.flags.in_install = False
|
||||
|
||||
|
|
@ -347,6 +348,7 @@ def add_to_installed_apps(app_name, rebuild_website=True):
|
|||
post_install(rebuild_website)
|
||||
|
||||
frappe.get_single("Installed Applications").update_versions()
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def remove_from_installed_apps(app_name):
|
||||
|
|
@ -416,6 +418,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
|
|||
remove_from_installed_apps(app_name)
|
||||
frappe.get_single("Installed Applications").update_versions()
|
||||
frappe.db.commit()
|
||||
frappe.clear_cache()
|
||||
|
||||
for after_uninstall in app_hooks.after_uninstall or []:
|
||||
frappe.get_attr(after_uninstall)()
|
||||
|
|
@ -428,6 +431,9 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
|
|||
click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
|
||||
frappe.flags.in_uninstall = False
|
||||
|
||||
if not dry_run:
|
||||
frappe.clear_cache()
|
||||
|
||||
|
||||
def _delete_modules(modules: list[str], dry_run: bool) -> list[str]:
|
||||
"""Delete modules belonging to the app and all related doctypes.
|
||||
|
|
|
|||
|
|
@ -4,21 +4,16 @@
|
|||
frappe.ui.form.on("Connected App", {
|
||||
refresh: (frm) => {
|
||||
frm.add_custom_button(__("Get OpenID Configuration"), async () => {
|
||||
if (!frm.doc.openid_configuration) {
|
||||
frappe.msgprint(__("Please enter OpenID Configuration URL"));
|
||||
} else {
|
||||
try {
|
||||
const response = await fetch(frm.doc.openid_configuration);
|
||||
const oidc = await response.json();
|
||||
frm.set_value("authorization_uri", oidc.authorization_endpoint);
|
||||
frm.set_value("token_uri", oidc.token_endpoint);
|
||||
frm.set_value("userinfo_uri", oidc.userinfo_endpoint);
|
||||
frm.set_value("introspection_uri", oidc.introspection_endpoint);
|
||||
frm.set_value("revocation_uri", oidc.revocation_endpoint);
|
||||
} catch (error) {
|
||||
frappe.msgprint(__("Please check OpenID Configuration URL"));
|
||||
}
|
||||
}
|
||||
frm.call("get_openid_configuration").then(({ message: oidc }) => {
|
||||
frm.set_value("authorization_uri", oidc.authorization_endpoint);
|
||||
frm.set_value("token_uri", oidc.token_endpoint);
|
||||
frm.set_value("userinfo_uri", oidc.userinfo_endpoint);
|
||||
frm.set_value("introspection_uri", oidc.introspection_endpoint);
|
||||
frm.set_value("revocation_uri", oidc.revocation_endpoint);
|
||||
|
||||
frm.fields_dict.authorization_uri.section.collapse(false); // Un-collapse
|
||||
frappe.show_alert(__("OpenID Configuration fetched successfully!"));
|
||||
});
|
||||
});
|
||||
|
||||
if (!frm.is_new()) {
|
||||
|
|
@ -26,7 +21,7 @@ frappe.ui.form.on("Connected App", {
|
|||
frappe.call({
|
||||
method: "initiate_web_application_flow",
|
||||
doc: frm.doc,
|
||||
callback: function (r) {
|
||||
callback: (r) => {
|
||||
window.open(r.message, "_blank");
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from requests_oauthlib import OAuth2Session
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.utils import make_get_request
|
||||
from frappe.model.document import Document
|
||||
|
||||
if any((os.getenv("CI"), frappe.conf.developer_mode, frappe.conf.allow_tests)):
|
||||
|
|
@ -47,6 +48,12 @@ class ConnectedApp(Document):
|
|||
in a Token Cache.
|
||||
"""
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_openid_configuration(self):
|
||||
if not self.openid_configuration:
|
||||
frappe.throw(_("Please enter OpenID Configuration URL"))
|
||||
return make_get_request(self.openid_configuration)
|
||||
|
||||
def validate(self):
|
||||
base_url = frappe.utils.get_url()
|
||||
callback_path = (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ frappe.ui.form.on("Google Settings", {
|
|||
refresh: function (frm) {
|
||||
frm.dashboard.set_headline(
|
||||
__("For more information, {0}.", [
|
||||
`<a href='https://erpnext.com/docs/user/manual/en/erpnext_integration/google_settings'>${__(
|
||||
`<a href='https://erpnext.com/docs/user/manual/en/google_settings'>${__(
|
||||
"Click here"
|
||||
)}</a>`,
|
||||
])
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@
|
|||
"description": "Do not create new user if user with email does not exist in the system",
|
||||
"fieldname": "do_not_create_new_user",
|
||||
"fieldtype": "Check",
|
||||
"label": "Do Not Create New User "
|
||||
"label": "Do Not Create New User"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"label": "API Secret"
|
||||
},
|
||||
{
|
||||
"description": "Enabling this will register your site on a central relay server to send push notifications for all installed apps through Firebase Cloud Messaging. This server only stores user tokens and error logs, and no messages are saved. ",
|
||||
"description": "Enabling this will register your site on a central relay server to send push notifications for all installed apps through Firebase Cloud Messaging. This server only stores user tokens and error logs, and no messages are saved.",
|
||||
"fieldname": "section_break_qgjr",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Relay Settings"
|
||||
|
|
|
|||
|
|
@ -89,6 +89,17 @@ class SocialLoginKey(Document):
|
|||
frappe.throw(
|
||||
_("Please enter Client Secret before social login is enabled"), exc=ClientSecretNotSetError
|
||||
)
|
||||
if self.auth_url_data:
|
||||
try:
|
||||
json.loads(self.auth_url_data)
|
||||
except json.JSONDecodeError:
|
||||
frappe.throw(_("Auth URL data should be valid JSON"))
|
||||
|
||||
if self.api_endpoint_args:
|
||||
try:
|
||||
json.loads(self.api_endpoint_args)
|
||||
except json.JSONDecodeError:
|
||||
frappe.throw(_("API Endpoint Args should be valid JSON"))
|
||||
|
||||
def set_icon(self):
|
||||
icon_map = {
|
||||
|
|
|
|||
2242
frappe/locale/ar.po
2242
frappe/locale/ar.po
File diff suppressed because it is too large
Load diff
2264
frappe/locale/bs.po
2264
frappe/locale/bs.po
File diff suppressed because it is too large
Load diff
2226
frappe/locale/cs.po
2226
frappe/locale/cs.po
File diff suppressed because it is too large
Load diff
31637
frappe/locale/da.po
Normal file
31637
frappe/locale/da.po
Normal file
File diff suppressed because it is too large
Load diff
2347
frappe/locale/de.po
2347
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
2260
frappe/locale/eo.po
2260
frappe/locale/eo.po
File diff suppressed because it is too large
Load diff
2260
frappe/locale/es.po
2260
frappe/locale/es.po
File diff suppressed because it is too large
Load diff
2423
frappe/locale/fa.po
2423
frappe/locale/fa.po
File diff suppressed because it is too large
Load diff
2242
frappe/locale/fr.po
2242
frappe/locale/fr.po
File diff suppressed because it is too large
Load diff
2266
frappe/locale/hr.po
2266
frappe/locale/hr.po
File diff suppressed because it is too large
Load diff
2619
frappe/locale/hu.po
2619
frappe/locale/hu.po
File diff suppressed because it is too large
Load diff
2276
frappe/locale/id.po
2276
frappe/locale/id.po
File diff suppressed because it is too large
Load diff
4279
frappe/locale/it.po
4279
frappe/locale/it.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
31863
frappe/locale/nb.po
Normal file
31863
frappe/locale/nb.po
Normal file
File diff suppressed because it is too large
Load diff
2226
frappe/locale/nl.po
2226
frappe/locale/nl.po
File diff suppressed because it is too large
Load diff
2228
frappe/locale/pl.po
2228
frappe/locale/pl.po
File diff suppressed because it is too large
Load diff
2239
frappe/locale/pt.po
2239
frappe/locale/pt.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
2255
frappe/locale/ru.po
2255
frappe/locale/ru.po
File diff suppressed because it is too large
Load diff
2372
frappe/locale/sr.po
2372
frappe/locale/sr.po
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue